# 모바일 개발자를 위한 Flutter 면접 질문 20선 > Flutter 면접에서 가장 자주 출제되는 20가지 질문을 준비하십시오. Widget, 상태 관리, Dart, 아키텍처, 모범 사례를 상세한 코드 예제와 함께 설명합니다. - Published: 2026-01-24 - Updated: 2026-04-12 - Author: SharpSkill - Tags: flutter, dart, interview, mobile development, technical interview - Reading time: 15 min --- Flutter 면접에서는 프레임워크 숙련도, Dart 언어 능력, 모바일 아키텍처 패턴에 대한 이해를 평가합니다. 본 가이드에서는 기초부터 고급 개념까지, 가장 빈번하게 출제되는 20가지 질문을 상세한 답변과 코드 예제를 통해 다룹니다. > **면접 팁** > > 면접관은 "어떻게"뿐만 아니라 "왜"를 설명할 수 있는 지원자를 높이 평가합니다. 각 개념에 대해 사용 사례와 기술적 트레이드오프를 이해하는 것이 합격과 불합격의 차이를 만듭니다. ## Flutter와 Dart 기본 질문 ### 1. StatelessWidget과 StatefulWidget의 차이점은 무엇입니까? **StatelessWidget**은 초기 설정에만 의존하는 불변 위젯을 나타냅니다. 한 번 빌드되면 변경되지 않습니다. **StatefulWidget**은 시간이 지남에 따라 변화할 수 있는 가변 상태를 유지하며, 상태 변경 시 위젯의 리빌드를 트리거합니다. ```dart // stateless_example.dart // StatelessWidget: static display, no change after construction class WelcomeMessage extends StatelessWidget { // Final parameter - never changes final String username; const WelcomeMessage({super.key, required this.username}); @override Widget build(BuildContext context) { // Build called once (unless parent rebuilds) return Text('Welcome, $username'); } } ``` ```dart // stateful_example.dart // StatefulWidget: mutable state, can rebuild itself class LikeButton extends StatefulWidget { const LikeButton({super.key}); @override State createState() => _LikeButtonState(); } class _LikeButtonState extends State { // Mutable local state int _likeCount = 0; void _incrementLike() { // setState triggers rebuild with new state setState(() { _likeCount++; }); } @override Widget build(BuildContext context) { return ElevatedButton( onPressed: _incrementLike, child: Text('Likes: $_likeCount'), ); } } ``` 원칙은 명확합니다. 기본적으로 StatelessWidget을 사용하고, 위젯 로컬 상태가 필요한 경우에만 StatefulWidget을 사용합니다. ### 2. Flutter의 위젯 트리는 어떻게 동작합니까? Flutter는 인터페이스를 세 개의 상호 연결된 트리로 구성합니다. **Widget Tree**(불변 선언), **Element Tree**(생명주기 및 바인딩), **Render Tree**(레이아웃 및 페인팅)가 그것입니다. ```dart // widget_tree_example.dart // Declarative structure - widgets describe the UI class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { // Each widget creates an Element in the Element Tree return MaterialApp( home: Scaffold( // Scaffold creates multiple RenderObjects body: Center( // Center modifies its child's layout child: Column( children: [ // Each Text has its own RenderParagraph Text('First'), Text('Second'), ], ), ), ), ); } } ``` `setState`가 호출되면 Flutter는 이전 위젯 트리와 새 위젯 트리를 비교하여 변경된 엘리먼트만 리빌드합니다. 이 비교 과정은 키(Key)와 위젯 타입에 의존합니다. ### 3. const 생성자란 무엇이며 왜 사용합니까? **const 생성자**는 런타임이 아닌 컴파일 타임에 위젯을 생성합니다. Flutter는 이러한 인스턴스를 재사용할 수 있어 불필요한 리빌드를 방지하고 성능을 향상시킵니다. ```dart // const_example.dart class OptimizedScreen extends StatelessWidget { const OptimizedScreen({super.key}); @override Widget build(BuildContext context) { return Column( children: [ // ✅ const: same instance reused on each build const Icon(Icons.star, size: 48), const SizedBox(height: 16), const Text('Static title'), // ❌ Non-const: new instance on each build Icon(Icons.star, color: Theme.of(context).primaryColor), ], ); } } // Custom widget with const constructor class StaticCard extends StatelessWidget { final String title; // All fields must be final for const const StaticCard({super.key, required this.title}); @override Widget build(BuildContext context) { return Card(child: Text(title)); } } ``` Flutter 분석기는 `prefer_const_constructors` 린트를 통해 놓친 최적화 기회를 감지합니다. ### 4. Flutter에서 Key의 종류를 설명하십시오 **Key**는 위젯 재구성 시 상태를 보존하는 역할을 합니다. Key가 없으면 Flutter는 트리 내 위치에 의존하게 되며, 리스트 재정렬 시 버그가 발생할 수 있습니다. ```dart // keys_example.dart class TodoList extends StatelessWidget { final List todos; const TodoList({super.key, required this.todos}); @override Widget build(BuildContext context) { return ListView.builder( itemCount: todos.length, itemBuilder: (context, index) { final todo = todos[index]; // ✅ ValueKey: preserves state if order changes return TodoTile( key: ValueKey(todo.id), todo: todo, ); }, ); } } // Different key types for different contexts class KeyExamples extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ // ValueKey: based on a unique value Container(key: ValueKey('unique-id')), // ObjectKey: based on object identity Container(key: ObjectKey(myObject)), // UniqueKey: new key on each build Container(key: UniqueKey()), // GlobalKey: access state from outside Form(key: _formKey), ], ); } } ``` GlobalKey는 외부에서 위젯의 상태에 접근할 수 있게 해주지만, 사용을 최소화하는 것이 권장됩니다. ## Dart 질문 ### 5. Dart에서 final과 const의 차이점은 무엇입니까? **final**은 한 번만 할당 가능한 변수를 정의하며 런타임에 평가됩니다. **const**는 컴파일 타임 상수를 생성하며, 불변이고 실행 전에 평가됩니다. ```dart // final_const_example.dart class DateExample { // final: value assigned at runtime final DateTime createdAt = DateTime.now(); // const: value known at compile time static const int maxItems = 100; static const String appName = 'MyApp'; // ❌ Error: DateTime.now() not const (runtime value) // static const DateTime timestamp = DateTime.now(); } void demonstrateDifference() { // final: each call can have different value final timestamp1 = DateTime.now(); final timestamp2 = DateTime.now(); print(timestamp1 == timestamp2); // false (different) // const: same instance reused const list1 = [1, 2, 3]; const list2 = [1, 2, 3]; print(identical(list1, list2)); // true (same instance) } ``` Flutter에서는 정적 위젯에 const를, 계산된 값에 final을 사용하는 것이 권장됩니다. ### 6. Future와 async/await는 어떻게 동작합니까? **Future**는 나중에 사용할 수 있는 값을 나타냅니다. **async/await**는 중첩된 콜백 없이 비동기 작업을 처리하기 위한 가독성 높은 구문을 제공합니다. ```dart // async_example.dart class UserRepository { final ApiClient _client; UserRepository(this._client); // Future: promise of a future value Future fetchUser(String id) async { try { // await suspends execution until resolution final response = await _client.get('/users/$id'); return User.fromJson(response); } catch (e) { // Errors propagate normally with async/await throw UserNotFoundException(id); } } // Parallel execution with Future.wait Future> fetchUsers(List ids) async { // All requests start simultaneously final futures = ids.map((id) => fetchUser(id)); // Wait for all to complete return Future.wait(futures); } // Sequential processing Future processSequentially(List ids) async { for (final id in ids) { // Each request waits for the previous one await fetchUser(id); } } } ``` 병렬 처리에는 `Future.wait`를, 순차 처리에는 async 루프를 사용합니다. > **FutureBuilder vs Riverpod** > > FutureBuilder는 단순한 경우에 적합하지만, Riverpod(AsyncValue)는 복잡한 애플리케이션에서 캐시 관리, 오류 처리, 새로고침 기능 면에서 더 우수한 성능을 제공합니다. ### 7. Stream과 그 활용법을 설명하십시오 **Stream**은 비동기 값의 시퀀스를 나타내며, 실시간 이벤트(WebSocket, 센서 데이터, 사용자 인터랙션) 처리에 적합합니다. ```dart // stream_example.dart class MessageService { // StreamController manages creation and broadcasting final _messageController = StreamController.broadcast(); // Expose only the Stream (not the Sink) Stream get messages => _messageController.stream; void addMessage(Message message) { _messageController.sink.add(message); } void dispose() { _messageController.close(); } } // Usage with StreamBuilder class MessageList extends StatelessWidget { final MessageService service; const MessageList({super.key, required this.service}); @override Widget build(BuildContext context) { return StreamBuilder( stream: service.messages, builder: (context, snapshot) { // Handle all possible states if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); } if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } if (!snapshot.hasData) { return const Text('No messages'); } return MessageCard(message: snapshot.data!); }, ); } } ``` `.broadcast()` Stream은 다수의 리스너를 허용하며, 단일 구독 Stream과 구별됩니다. ## 아키텍처와 상태 관리 질문 ### 8. 상태 관리 솔루션 비교: Provider, Riverpod, Bloc 각 솔루션은 서로 다른 요구사항을 충족합니다. **Provider**는 간결성과 네이티브 통합을 제공합니다. **Riverpod**는 타입 안전성과 테스트 용이성을 갖추고 있습니다. **Bloc**은 엄격한 이벤트 기반 아키텍처를 강제합니다. ```dart // riverpod_example.dart // Riverpod: declarative and type-safe approach final userProvider = FutureProvider.autoDispose((ref) async { final repository = ref.watch(userRepositoryProvider); return repository.fetchCurrentUser(); }); class UserProfile extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // AsyncValue handles loading/error/data final userAsync = ref.watch(userProvider); return userAsync.when( loading: () => const CircularProgressIndicator(), error: (error, stack) => Text('Error: $error'), data: (user) => Text(user.displayName), ); } } ``` ```dart // bloc_example.dart // Bloc: explicit events/states separation abstract class AuthEvent {} class LoginRequested extends AuthEvent { final String email; final String password; LoginRequested(this.email, this.password); } abstract class AuthState {} class AuthInitial extends AuthState {} class AuthLoading extends AuthState {} class AuthSuccess extends AuthState { final User user; AuthSuccess(this.user); } class AuthFailure extends AuthState { final String error; AuthFailure(this.error); } class AuthBloc extends Bloc { AuthBloc() : super(AuthInitial()) { on(_onLoginRequested); } Future _onLoginRequested( LoginRequested event, Emitter emit, ) async { emit(AuthLoading()); try { final user = await _repository.login(event.email, event.password); emit(AuthSuccess(user)); } catch (e) { emit(AuthFailure(e.toString())); } } } ``` Riverpod는 현대적인 API와 테스트 용이성 덕분에 신규 프로젝트에 권장됩니다. ### 9. Flutter에서 클린 아키텍처란 무엇입니까? **클린 아키텍처**는 코드를 독립적인 계층으로 분리합니다. Domain(비즈니스 로직), Data(데이터 소스), Presentation(UI)이 그것입니다. 이러한 분리는 테스트와 유지보수를 용이하게 합니다. ```dart // domain/entities/user.dart // Entity: pure business object, no framework dependency class User { final String id; final String email; final String name; const User({ required this.id, required this.email, required this.name, }); } // domain/repositories/user_repository.dart // Interface: abstract contract, implementation in Data abstract class UserRepository { Future getUser(String id); Future updateUser(User user); } // domain/usecases/get_user_usecase.dart // Use case: isolated business logic class GetUserUseCase { final UserRepository _repository; GetUserUseCase(this._repository); Future call(String userId) { return _repository.getUser(userId); } } ``` ```dart // data/repositories/user_repository_impl.dart // Concrete implementation with data sources class UserRepositoryImpl implements UserRepository { final UserRemoteDataSource _remoteDataSource; final UserLocalDataSource _localDataSource; UserRepositoryImpl(this._remoteDataSource, this._localDataSource); @override Future getUser(String id) async { try { // Try local cache first final cachedUser = await _localDataSource.getUser(id); if (cachedUser != null) return cachedUser; } catch (_) {} // Fallback to API final user = await _remoteDataSource.fetchUser(id); await _localDataSource.cacheUser(user); return user; } } ``` Domain 계층은 어떤 것에도 의존하지 않으며, Data는 Domain에 의존하고, Presentation은 두 계층 모두에 의존합니다. ### 10. 의존성 주입은 어떻게 구현합니까? **의존성 주입**은 컴포넌트의 의존성을 외부에서 제공하여 결합도를 낮춥니다. Riverpod의 프로바이더 시스템은 이 패턴에 탁월한 성능을 발휘합니다. ```dart // di_example.dart // Provider definitions (dependencies) final apiClientProvider = Provider((ref) { return ApiClient(baseUrl: Environment.apiUrl); }); final userRepositoryProvider = Provider((ref) { // Automatically injects ApiClient final client = ref.watch(apiClientProvider); return UserRepositoryImpl(client); }); final getUserUseCaseProvider = Provider((ref) { final repository = ref.watch(userRepositoryProvider); return GetUserUseCase(repository); }); // Usage in a widget class UserScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final getUserUseCase = ref.watch(getUserUseCaseProvider); // Use the use case... } } // Tests: easy dependency override void main() { testWidgets('displays user', (tester) async { await tester.pumpWidget( ProviderScope( overrides: [ // Replace with mock for tests userRepositoryProvider.overrideWithValue(MockUserRepository()), ], child: const MyApp(), ), ); }); } ``` 가장 큰 장점은 테스트 시 모든 의존성을 모의 객체(mock)로 대체할 수 있다는 점입니다. ## 성능 질문 ### 11. 리빌드 성능을 어떻게 최적화합니까? 리빌드를 최소화하면 성능이 향상됩니다. 핵심 기법으로는 const 위젯, 세분화된 분할, Riverpod 셀렉터가 있습니다. ```dart // rebuild_optimization.dart // ❌ Bad: everything rebuilds on each change class BadExample extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final user = ref.watch(userProvider); return Column( children: [ Text(user.name), Text(user.email), const ExpensiveWidget(), // Rebuilds unnecessarily ], ); } } // ✅ Good: selector to rebuild only if name changes class GoodExample extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // select: rebuilds only if name changes final name = ref.watch(userProvider.select((u) => u.name)); return Text(name); } } // ✅ Good: splitting into smaller widgets class OptimizedScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ const Header(), // Static, never rebuilds const UserNameWidget(), // Rebuilds if name changes const UserEmailWidget(), // Rebuilds if email changes const Footer(), // Static ], ); } } ``` DevTools Performance를 활용하여 과도한 리빌드를 식별하는 것이 권장됩니다. ### 12. 긴 리스트를 어떻게 최적화합니까? 긴 리스트에는 ListView.builder를 사용한 **레이지 로딩**이 필수입니다. 20개 이상의 항목에 직접 children을 사용하는 ListView는 지양해야 합니다. ```dart // list_optimization.dart class OptimizedList extends StatelessWidget { final List items; const OptimizedList({super.key, required this.items}); @override Widget build(BuildContext context) { // ✅ ListView.builder: builds on demand return ListView.builder( // Fixed height improves scrolling itemExtent: 72, itemCount: items.length, itemBuilder: (context, index) { return ItemTile( // Key to preserve state during scroll key: ValueKey(items[index].id), item: items[index], ); }, ); } } // List widget with infinite loading class InfiniteList extends ConsumerStatefulWidget { @override ConsumerState createState() => _InfiniteListState(); } class _InfiniteListState extends ConsumerState { final _scrollController = ScrollController(); @override void initState() { super.initState(); _scrollController.addListener(_onScroll); } void _onScroll() { // Load more data near the end if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { ref.read(itemsProvider.notifier).loadMore(); } } @override Widget build(BuildContext context) { final items = ref.watch(itemsProvider); return ListView.builder( controller: _scrollController, itemCount: items.length, itemBuilder: (context, index) => ItemTile(item: items[index]), ); } } ``` 1000개 이상의 항목이 있는 매우 긴 리스트의 경우 `ListView.separated` 또는 `scrollable_positioned_list`와 같은 전문 패키지를 고려하십시오. > **이미지 캐싱** > > 이미지가 포함된 리스트에서는 `cached_network_image`를 사용하여 재로딩을 방지해야 합니다. 캐싱되지 않은 이미지는 스크롤 시 버벅임의 원인이 됩니다. ### 13. Impeller 렌더링 엔진의 동작 원리를 설명하십시오 **Impeller**는 Flutter 3.16 이후 Skia를 대체하는 기본 렌더링 엔진입니다. 셰이더를 사전 컴파일하여 첫 화면 표시 시 발생하는 "쟁크(jank)"를 제거합니다. ```dart // impeller_benefits.dart // Complex animations benefit from Impeller class SmoothAnimation extends StatefulWidget { @override State createState() => _SmoothAnimationState(); } class _SmoothAnimationState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 2), vsync: this, )..repeat(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, child) { // Impeller: no runtime shader compilation return Transform.rotate( angle: _controller.value * 2 * 3.14159, child: child, ); }, child: const FlutterLogo(size: 100), ); } } ``` Impeller는 iOS에서 기본 활성화되어 있습니다. Android에서는 `flutter run --enable-impeller`로 호환성을 확인할 수 있습니다. ## 내비게이션과 폼 질문 ### 14. 딥 링킹을 사용한 내비게이션은 어떻게 처리합니까? **딥 링킹**은 URL을 통해 앱의 특정 화면을 직접 열 수 있게 합니다. GoRouter는 이 기능을 네이티브로 지원합니다. ```dart // deep_linking.dart final router = GoRouter( routes: [ GoRoute( path: '/products/:productId', builder: (context, state) { // Extract URL parameter final productId = state.pathParameters['productId']!; return ProductScreen(productId: productId); }, ), GoRoute( path: '/search', builder: (context, state) { // Query parameters (?query=flutter) final query = state.uri.queryParameters['query'] ?? ''; return SearchScreen(initialQuery: query); }, ), ], ); // Programmatic navigation void navigateToProduct(BuildContext context, String id) { // go: replaces navigation stack context.go('/products/$id'); // push: adds to stack (allows back) context.push('/products/$id'); // pushNamed with extra for complex data context.pushNamed( 'productDetail', pathParameters: {'productId': id}, extra: ProductData(id: id), ); } ``` 시스템 딥 링킹을 활성화하려면 네이티브 파일(AndroidManifest.xml, Info.plist)을 구성해야 합니다. ### 15. 폼을 효과적으로 검증하는 방법은 무엇입니까? 검증에는 Form, TextFormField, 커스텀 검증자를 조합하여 사용합니다. `formz`와 같은 Zod 스타일 패키지가 구조화를 돕습니다. ```dart // form_validation.dart class RegistrationForm extends StatefulWidget { @override State createState() => _RegistrationFormState(); } class _RegistrationFormState extends State { final _formKey = GlobalKey(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _confirmController = TextEditingController(); @override Widget build(BuildContext context) { return Form( key: _formKey, // Real-time validation autovalidateMode: AutovalidateMode.onUserInteraction, child: Column( children: [ TextFormField( controller: _emailController, decoration: const InputDecoration(labelText: 'Email'), keyboardType: TextInputType.emailAddress, validator: _validateEmail, ), TextFormField( controller: _passwordController, decoration: const InputDecoration(labelText: 'Password'), obscureText: true, validator: _validatePassword, ), TextFormField( controller: _confirmController, decoration: const InputDecoration(labelText: 'Confirm'), obscureText: true, validator: _validateConfirmPassword, ), ElevatedButton( onPressed: _submit, child: const Text('Create account'), ), ], ), ); } String? _validateEmail(String? value) { if (value == null || value.isEmpty) { return 'Email required'; } final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); if (!emailRegex.hasMatch(value)) { return 'Invalid email format'; } return null; } String? _validatePassword(String? value) { if (value == null || value.length < 8) { return 'Minimum 8 characters'; } if (!value.contains(RegExp(r'[A-Z]'))) { return 'At least one uppercase letter required'; } if (!value.contains(RegExp(r'[0-9]'))) { return 'At least one number required'; } return null; } String? _validateConfirmPassword(String? value) { if (value != _passwordController.text) { return 'Passwords do not match'; } return null; } void _submit() { if (_formKey.currentState!.validate()) { // Form is valid, proceed } } } ``` `AutovalidateMode.onUserInteraction` 모드는 사용자 상호작용 후에만 오류를 표시하여 최상의 UX를 제공합니다. ## 테스트 질문 ### 16. Flutter 위젯을 어떻게 테스트합니까? 위젯 테스트는 디바이스에 의존하지 않고 UI를 검증합니다. `flutter_test` 패키지가 필요한 유틸리티를 제공합니다. ```dart // widget_test.dart import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; void main() { testWidgets('displays welcome message', (tester) async { // Arrange: build the widget await tester.pumpWidget( const ProviderScope( child: MaterialApp( home: WelcomeScreen(username: 'Alice'), ), ), ); // Assert: verify content expect(find.text('Welcome, Alice'), findsOneWidget); }); testWidgets('button increments counter', (tester) async { await tester.pumpWidget( const MaterialApp(home: CounterScreen()), ); // Initial state expect(find.text('0'), findsOneWidget); // Act: tap the button await tester.tap(find.byType(ElevatedButton)); await tester.pump(); // Rebuild after setState // Assert: counter incremented expect(find.text('1'), findsOneWidget); }); testWidgets('form validates email', (tester) async { await tester.pumpWidget( const MaterialApp(home: LoginForm()), ); // Enter invalid email await tester.enterText( find.byKey(const Key('email-field')), 'invalid-email', ); await tester.tap(find.byType(ElevatedButton)); await tester.pump(); // Verify error message expect(find.text('Invalid email format'), findsOneWidget); }); } ``` `pump()`은 한 프레임을 진행시키고, `pumpAndSettle()`은 모든 애니메이션이 완료될 때까지 대기합니다. ### 17. Riverpod 프로바이더를 어떻게 테스트합니까? Riverpod는 `ProviderContainer`와 오버라이드를 통해 테스트를 용이하게 합니다. ```dart // provider_test.dart import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; // Repository mock class MockUserRepository extends Mock implements UserRepository {} void main() { late MockUserRepository mockRepository; late ProviderContainer container; setUp(() { mockRepository = MockUserRepository(); container = ProviderContainer( overrides: [ userRepositoryProvider.overrideWithValue(mockRepository), ], ); }); tearDown(() { container.dispose(); }); test('loads user from repository', () async { // Arrange final expectedUser = User(id: '1', name: 'Test'); when(() => mockRepository.getUser('1')) .thenAnswer((_) async => expectedUser); // Act final user = await container.read(userProvider('1').future); // Assert expect(user, expectedUser); verify(() => mockRepository.getUser('1')).called(1); }); test('handles repository error', () async { when(() => mockRepository.getUser(any())) .thenThrow(Exception('Network error')); expect( () => container.read(userProvider('1').future), throwsException, ); }); } ``` 프로바이더 테스트는 UI 렌더링이 필요하지 않으므로 실행 속도가 빠릅니다. ## 배포 질문 ### 18. 다양한 환경(dev, staging, prod)은 어떻게 관리합니까? 환경 설정은 `.env` 파일 또는 `--dart-define`을 사용한 컴파일 타임 상수를 통해 구성됩니다. ```dart // environment.dart enum Environment { dev, staging, prod } class AppConfig { final Environment environment; final String apiUrl; final bool enableAnalytics; const AppConfig._({ required this.environment, required this.apiUrl, required this.enableAnalytics, }); // Predefined configurations static const dev = AppConfig._( environment: Environment.dev, apiUrl: 'https://api-dev.example.com', enableAnalytics: false, ); static const staging = AppConfig._( environment: Environment.staging, apiUrl: 'https://api-staging.example.com', enableAnalytics: true, ); static const prod = AppConfig._( environment: Environment.prod, apiUrl: 'https://api.example.com', enableAnalytics: true, ); // Read from --dart-define static AppConfig fromEnvironment() { const env = String.fromEnvironment('ENV', defaultValue: 'dev'); switch (env) { case 'prod': return prod; case 'staging': return staging; default: return dev; } } } ``` ```bash # terminal # Launch with specific environment flutter run --dart-define=ENV=staging # Production build flutter build apk --dart-define=ENV=prod --release ``` ### 19. 다중 앱 변형을 위한 플레이버링은 어떻게 구현합니까? **플레이버링**은 서로 다른 구성(아이콘, 이름, API)을 가진 다수의 앱 변형(client1, client2)을 생성합니다. ```dart // main_client1.dart import 'package:flutter/material.dart'; import 'config/flavor_config.dart'; import 'app.dart'; void main() { FlavorConfig( flavor: Flavor.client1, name: 'App Client 1', apiUrl: 'https://api.client1.com', primaryColor: Colors.blue, ); runApp(const App()); } // config/flavor_config.dart enum Flavor { client1, client2, internal } class FlavorConfig { final Flavor flavor; final String name; final String apiUrl; final Color primaryColor; // Singleton for global access static FlavorConfig? _instance; static FlavorConfig get instance => _instance!; FlavorConfig({ required this.flavor, required this.name, required this.apiUrl, required this.primaryColor, }) { _instance = this; } bool get isProduction => flavor != Flavor.internal; } ``` 각 플레이버에 대해 Android(build.gradle)와 iOS(xcconfig)의 네이티브 파일을 구성해야 합니다. ### 20. Flutter 보안 모범 사례는 무엇입니까? 보안은 민감한 데이터 저장, 입력값 검증, 역공학 방지를 포괄합니다. ```dart // security_example.dart import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class SecureStorage { // Encrypted storage for sensitive data final _storage = const FlutterSecureStorage( aOptions: AndroidOptions(encryptedSharedPreferences: true), iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock), ); Future saveToken(String token) async { await _storage.write(key: 'auth_token', value: token); } Future getToken() async { return _storage.read(key: 'auth_token'); } Future deleteToken() async { await _storage.delete(key: 'auth_token'); } } // Input validation class InputValidator { // Injection prevention static String sanitize(String input) { return input .replaceAll(RegExp(r'[<>"\']'), '') .trim(); } // Length validation static bool isValidLength(String input, int min, int max) { return input.length >= min && input.length <= max; } } // SSL pinning protection class SecureApiClient { Dio createSecureClient() { final dio = Dio(); // Add certificate pinning (dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () { final client = HttpClient(); client.badCertificateCallback = (cert, host, port) { // Verify certificate fingerprint return _isValidCertificate(cert); }; return client; }; return dio; } } ``` 코드에 비밀 정보를 저장해서는 안 됩니다. 토큰에는 `flutter_secure_storage`를, 프로덕션 빌드에는 `--obfuscate`를 사용하십시오. ## 결론 본 가이드에서 다룬 20가지 질문은 Flutter 면접의 핵심 영역을 포괄합니다. 프레임워크 기초, Dart 숙련도, 아키텍처 패턴, 프로덕션 모범 사례가 그것입니다. 합격의 열쇠는 단순한 구문 암기가 아닌 기저 메커니즘에 대한 깊은 이해에 있습니다. ### 준비 체크리스트 - ✅ StatelessWidget/StatefulWidget의 차이점과 사용 사례 숙지 - ✅ 위젯 트리와 리빌드 최적화 원리 이해 - ✅ async/await, Future, Stream 연습 문제 풀기 - ✅ Riverpod를 활용한 상태 관리 프로젝트 구현 - ✅ GoRouter와 딥 링킹 학습 - ✅ 유닛 테스트와 위젯 테스트 작성 - ✅ 다중 환경 구성 방법 이해 개인 프로젝트를 통한 꾸준한 실습이 이 지식을 공고히 하는 가장 효과적인 방법입니다. 본 가이드에서 다룬 각 질문은 Flutter 프레임워크의 미묘한 차이를 완전히 파악하기 위해 실제 코드로 더 깊이 탐구할 가치가 있습니다. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/flutter/flutter-interview-questions