2025. 3. 4. 09:49ㆍKnowledge/Flutter
※ 아래의 '개념'에 대한 포스트를 읽고 오시면 이해하기 더 쉽습니다.
https://kwonputer.tistory.com/572
[ Flutter ] 면접 질문을 통해 알아보는 플러터 (2) - 개념
https://kwonputer.tistory.com/573 [ Flutter ] 면접 질문을 통해 알아보는 플러터 (3) - Flutter 기술 & 경험※ 아직 포스트를 작성하는 중입니다. ※ 아래의 '개념'에 대한 포스트를 읽고 오시면 이해하기 더
kwonputer.tistory.com
안녕하세요!
이번 포스트에서는 'Flutter 기술'에 대한 면접 질문에 대해서 다루겠습니다.
실제 면접이라고 생각하고 포스트를 작성해 보겠습니다.
주관적인 생각을 바탕으로 작성한 포스트라서 참고만 부탁드립니다.
정확한 정보전달이 아닐 수도 있습니다.
아래의 목차를 참고해 주세요.
1. Flutter의 장점과 단점에 대해 말해주세요.
2. Stateless & Stateful Widget의 차이점을 말해주세요.
3. Flutter에서 상태 관리는 어떻게 하는지 말해주세요.
4. Riverpod에 대해 말해주세요.
5. Bloc에 대해 말해주세요.
6. Provider에 대해 말해주세요.
7. GetX에 대해 말해주세요.
8. 위젯 트리 구조에 대해 말해주세요.
9. Flutter에서 비동기 프로그래밍을 하는 방법에 대해 말해주세요.
10. Flutter에서 API 호출을 하는 방법에 대해 말해주세요.
11. Hot Reload와 Hot Restart의 차이점을 말해주세요.
12. Flutter에서 Native 코드와의 통합 방법에 대해 말해주세요.
13. Flutter에서 라우팅과 네비게이션을 처리하는 방법에 대해 말해주세요.
14. Flutter의 Form 위젯과 Form 검증 방법에 대해 말해주세요.
15. Build Context의 context의 역할에 대해 말해주세요.
16. Flutter의 Key에 대해 말해주세요.
17. Flutter의 Stream과 Stream 유형에 대해 말해주세요.
18. FutureBuilder와 StreamBuilder의 차이점에 대해 말해주세요.
19. Flutter의 3가지 테스트(단위 테스트, 위젯 테스트, 통합 테스트)에 대해 말해주세요.
20. WidgetsApp과 MaterialApp의 차이점에 대해 말해주세요.
21. Abstract (extends) & Interface (implements) & Mixin (with)에 대해 말해주세요.
1. Flutter의 장점과 단점에 대해 말해주세요.
└ Flutter는 Google이 개발한 크로스 플랫폼 프레임워크로, 단일 코드베이스로 여러 플랫폼에서 고성능 앱을 개발할 수 있지만, 앱 크기가 상대적으로 크고 일부 플랫폼별 기능에 제한이 있을 수 있습니다.
◎ Flutter의 주요 장점
1. 크로스 플랫폼 개발: 하나의 코드베이스로 AOS, IOS, 웹, 데스크톱 앱을 모두 개발할 수 있어서 개발 시간과 비용을 절감할 수 있습니다.
2. 핫리로드: 코드 변경 사항을, 앱을 재시작하지 않고도 즉시 확인할 수 있어 개발 속도가 빠릅니다.
3. 풍부한 위젯 라이브러리: Material Design과 Cupertino 스타일의 다양한 내장 위젯을 제공합니다.
4. 자체 렌더링 엔진: [1]Skia 그래픽 엔진을 사용하여 플랫폼과 관계없이 일관된 UI를 제공합니다.
5. 우수한 성능: [2]JIT(Just-In-Time) & AOT(Ahead-Of-Time) 컴파일을 모두 지원하여 개발 및 배포 환경에서 최적의 성능을 제공합니다.
◎ Flutter의 주요 단점
1. 앱 크기: 기본 앱 크기가 네이티브 앱보다 상대적으로 큽니다.
2. 플랫폼별 기능 제한: 일부 플랫폼 특화 기능은 별도의 플러그인이 필요합니다.
3. 학습 곡선: Dart 언어와 위젯 기반 프로그래밍 패러다임에 익숙해지는 데에 시간이 필요할 수 있습니다.
4. 새로운 플랫폼 업데이트 지연: 새로운 네이티브 플랫폼 기능이 Flutter에 적용되기까지 시간이 걸릴 수 있습니다.
* [1]Skia 그래픽 엔진
※ Skia란?
● Skia는 Google이 개발하고 관리하는 오픈 소스 2D 그래픽 라이브러리입니다.
● Flutter뿐만 아니라 Chrome & Android & Firefox & Google Photos... 등 다양한 제품에서 사용됩니다.
※ Flutter에서의 역할
● Flutter는 Skia를 내장하여 플랫폼에 독립적인 렌더링 시스템을 구현합니다.
● 네이티브 UI 컴포넌트가 아닌 직접 화면에 픽셀을 그리는 방식으로 동작합니다.
● 이를 통해 Flutter는 AOS/IOS... 등 다양한 플랫폼에서 완전히 동일한 UI 렌더링이 가능합니다.
※ 장점
● 플랫폼 간 일관된 UI 표현이 가능합니다.
● 플랫폼 네이티브 위젯에 의존하지 않아 OS 버전에 따른 UI 차이가 없습니다.
● 복잡한 커스텀 애니메이션이나 그래픽 효과를 모든 플랫폼에서 동일하게 구현할 수 있습니다.
● 60fps(초당 60프레임)의 부드러운 애니메이션과 전환 효과를 제공합니다.
* [2]JIT(Just-In-Time) & AOT(Ahead-Of-Time) 컴파일
Flutter는 개발과 배포 환경에서 서로 다른 컴파일 방식을 사용하여 최적의 개발 경험과 런타임 성능을 제공합니다.
※ JIT(Just-In-Time) 컴파일
● 개발 환경에서 사용: Flutter 개발 중에 주로 사용됩니다.
● 작동 방식: 코드가 실행되는 시점에 필요한 부분만 실시간으로 컴파일합니다.
● 장점
1. 핫 리로드 가능: 코드 변경 후, 전체 재컴파일 없이 변경된 부분만 갱신하여 실시간으로 결과를 확인할 수 있습니다.
2. 빠른 개발 주기: 변경 사항을 즉시 확인할 수 있어 개발 속도가 향상됩니다.
3. 디버깅 정보 유지: 런타임에 더 많은 정보를 유지하여 디버깅이 용이합니다.
● 단점
1. 초기 실행 속도가 상대적으로 느리고, 런타임 오버헤드가 있습니다.
※ AOT(Ahead-Of-Time) 컴파일
● 배포 환경에서 사용: 앱 릴리스 빌드 시 사용됩니다.
● 작동 방식: 앱이 실행되기 전에 모든 코드를 미리 네이티브 코드로 컴파일합니다.
● 장점
1. 빠른 실행 속도: 앱 시작 및 실행 속도가 빠릅니다.
2. 낮은 메모리 사용량: 실행 시, 추가 컴파일 과정이 필요 없어서 메모리 효율이 좋습니다.
3. 최적화된 성능: 런타임에 추가 분석이나 컴파일 없이 최적화된 네이티브 코드를 실행합니다.
4. 예측 가능한 성능: 런타임 컴파일로 인한 지연이 없습니다.
● 단점
1. 핫 리로드가 불가능하며, 코드 변경 시에 전체 앱을 다시 컴파일해야 합니다.
※ Flutter에서의 활용
● 개발 모드(debug): JIT 컴파일러를 사용합니다. 핫 리로드로 빠른 개발 경험을 제공합니다.
● 릴리스 모드(release): AOT 컴파일러를 사용합니다. 최적의 성능과 작은 앱 크기를 제공합니다.
● 프로필 모드(profile): AOT 컴파일을 사용하되 일부 디버깅 기능을 유지합니다.
이러한 이중 컴파일 전략은 Flutter의 큰 장점 중 하나로, 개발자는 개발 중에는 빠른 피드백 루프를 경험하면서도 최종 사용자에게는 네이티브에 가까운 성능의 앱을 제공할 수 있습니다.
2. Stateless & Stateful Widget의 차이점을 말해주세요.
└ Stateless Widget은 내부 상태를 가지지 않고, 한 번 빌드되면 변경되지 않지만, Stateful Widget은 내부 상태를 관리하고 상태가 변경될 때마다 재빌드됩니다.
◎ Stateless Widget
● 불변(Immutable)하며 한 번 생성되면 변경되지 않습니다.
● 내부 상태를 가지지 않으며, 외부에서 주입받은 데이터만 사용합니다.
● build 메서드는 위젯이 생성될 때 한번만 호출됩니다. (부모 위젯이 재빌드될 때는 제외)
● 메모리 사용이 적고 성능이 좋습니다.
● 주로 정적인 UI 요소나 데이터 표시를 위해 사용됩니다. (ex. Text & Icon & Image... 등의 정적 위젯)
◎ Stateful Widget
● 내부 상태(State 객체)를 가지며, 이 상태가 변경될 때 위젯이 재빌드 됩니다.
● 위젯 클래스와 상태 클래스(State) 두 개의 클래스로 구성됩니다.
● setState 메서드를 통해 상태 변경을 알리고 UI를 업데이트합니다.
● 다양한 수명 주기 메서드(initState, dipose... 등)를 가집니다.
● 사용자 입력 & 애니메이션 & 데이터 변경과 같은 동적 요소에 적합합니다. (ex. Checkbox & TextField & AnimatedContainer... 등의 동적 위젯)
※ 적절한 사용
● 데이터가 변경되지 않는 UI 요소에는 Stateless Widget을 사용합니다.
● 사용자 상호 작용이나 데이터 변경에 따라 UI가 변경되어야 하는 경우 Stateful Widget을 사용합니다.
● 성능 최적화를 위해 가능한 작은 부분만 Stateful로 유지하는 것이 좋습니다.
3. Flutter에서 상태 관리는 어떻게 하는지 말해주세요.
└ Flutter에서 단순한 앱은 setState로, 복잡한 앱은 Provider & Bloc & Riverpod & GetX... 등의 상태 관리 라이브러리를 사용하여 효율적으로 상태를 관리합니다.
※ 기본 상태 관리
● setState: Stateful Widget 내에서 로컬 상태를 관리하는 가장 기본적인 방법으로, 간단한 앱에 적합합니다.
● [1]InheritedWidget: 위젯 트리 아래로 데이터를 효율적으로 전달하는 방법으로, Flutter의 많은 상태 관리 솔루션의 기반이 됩니다.
※ 중간 규모 앱 상태 관리
● Provider: InheritedWidget을 기반으로 한 인기 있는 상태 관리 라이브러리로, 간단하면서도 강력합니다.
● [2]Scoped Model: 모델 클래스를 통해 상태를 관리하고 UI와 로직을 분리합니다.
※ 대규모 앱 상태 관리
● Bloc(Business Logic Component): 이벤트와 상태를 분리하여 Stream 기반으로 상태를 관리하는 패턴입니다.
● Riverpod: Provider의 개선된 버전으로, 컴파일 타임 안전성과 테스트 용이성을 제공합니다.
● GetX: 상태 관리 & 라우팅 & 종속성 주입을 제공하는 경량 솔루션입니다.
● [3]Redux: 단방향 데이터 흐름과 예측할 수 있는 상태 변경을 제공하는 패턴입니다.
● [4]MobX: 관찰할 수 있는 상태와 반응형 프로그래밍 패러다임을 사용합니다.
* [1]InheritedWidget
// 데이터를 담을 InheritedWidget 정의
class CounterInheritedWidget extends InheritedWidget {
final int counter;
final Function incrementCounter;
const CounterInheritedWidget({
Key? key,
required this.counter,
required this.incrementCounter,
required Widget child,
}) : super(key: key, child: child);
// 데이터 접근 헬퍼 메서드
static CounterInheritedWidget of(BuildContext context) {
final result = context.dependOnInheritedWidgetOfExactType<CounterInheritedWidget>();
assert(result != null, 'No CounterInheritedWidget found in context');
return result!;
}
@override
bool updateShouldNotify(CounterInheritedWidget oldWidget) {
return counter != oldWidget.counter;
}
}
// 상태를 관리하는 상위 위젯
class CounterProvider extends StatefulWidget {
final Widget child;
const CounterProvider({Key? key, required this.child}) : super(key: key);
@override
_CounterProviderState createState() => _CounterProviderState();
}
class _CounterProviderState extends State<CounterProvider> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return CounterInheritedWidget(
counter: _counter,
incrementCounter: _incrementCounter,
child: widget.child,
);
}
}
// 사용 예시 - 데이터 소비 위젯
class CounterDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counterData = CounterInheritedWidget.of(context);
return Text('카운터: ${counterData.counter}');
}
}
class CounterButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counterData = CounterInheritedWidget.of(context);
return ElevatedButton(
onPressed: () => counterData.incrementCounter(),
child: Text('증가'),
);
}
}
// 앱 구조
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CounterProvider(
child: Scaffold(
appBar: AppBar(title: Text('InheritedWidget 예시')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CounterDisplay(),
CounterButton(),
],
),
),
),
),
);
}
}
InheritedWidget은 Flutter의 기본 클래스로, 위젯 트리를 통해 데이터를 효율적으로 하위 위젯에 전달하는 메커니즘입니다. 위젯 트리의 상위에서 데이터를 정의하고, 하위 위젯에서는 'context.dependOnInheritedWidgetOfExactType<T>()'를 통해 해당 데이터에 접근할 수 있습니다.
◎ 주요 특징
● 위젯 트리 아래로 데이터를 전파합니다.
● 데이터가 변경되면 해당 데이터에 의존하는 위젯만 다시 빌드됩니다.
● Provider & Riverpod... 등 많은 상태 관리 라이브러리의 기반 기술입니다.
● BuildContext를 통해 데이터에 접근합니다.
* [2]Scoped Model
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
// 1. Model 클래스 정의
class CounterModel extends Model {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
// 상태 변경을 알림
notifyListeners();
}
}
// 2. 앱에서 ScopedModel 사용
class MyApp extends StatelessWidget {
final CounterModel model = CounterModel();
@override
Widget build(BuildContext context) {
return ScopedModel<CounterModel>(
model: model,
child: MaterialApp(
home: CounterPage(),
),
);
}
}
// 3. 모델 데이터 사용
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Scoped Model 예시')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('버튼을 누른 횟수:'),
// ScopedModelDescendant로 모델 데이터 접근
ScopedModelDescendant<CounterModel>(
builder: (context, child, model) {
return Text(
'${model.counter}',
style: Theme.of(context).textTheme.headline4,
);
},
),
],
),
),
floatingActionButton: ScopedModelDescendant<CounterModel>(
builder: (context, child, model) {
return FloatingActionButton(
onPressed: model.increment,
tooltip: 'Increment',
child: Icon(Icons.add),
);
},
),
);
}
}
Scoped Model은 InheritedWidget을 기반으로 만들어진 더 간단한 상태 관리 패턴으로, 모델 클래스를 통해 상태를 관리하고 UI와 로직을 분리합니다. 지금은 Provider... 등 다른 라이브러리가 더 많이 사용되지만, 여전히 간단한 앱에서는 유용합니다.
◎ 주요 특징
● Model & ScopedModel & ScopedModelDescendant의 세 가지 주요 클래스로 구성됩니다.
● 데이터 변경 시, notifyListeners를 호출하여 관련 위젯만 다시 빌드합니다.
● Provider보다 단순하지만 유연성은 다소 떨어집니다.
* [3]Redux
import 'package:flutter/material.dart';
import 'package:redux/redux.dart';
import 'package:flutter_redux/flutter_redux.dart';
// 1. 앱 상태 정의
class AppState {
final int counter;
AppState({required this.counter});
AppState copyWith({int? counter}) {
return AppState(counter: counter ?? this.counter);
}
}
// 2. 액션 정의
class IncrementAction {}
// 3. 리듀서 정의
AppState reducer(AppState state, dynamic action) {
if (action is IncrementAction) {
return state.copyWith(counter: state.counter + 1);
}
return state;
}
// 4. 메인 앱
void main() {
// 스토어 생성
final store = Store<AppState>(
reducer,
initialState: AppState(counter: 0),
);
runApp(MyApp(store: store));
}
class MyApp extends StatelessWidget {
final Store<AppState> store;
const MyApp({Key? key, required this.store}) : super(key: key);
@override
Widget build(BuildContext context) {
return StoreProvider<AppState>(
store: store,
child: MaterialApp(
home: CounterPage(),
),
);
}
}
// 5. 카운터 페이지
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Redux 예시')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('버튼을 누른 횟수:'),
// 상태 구독
StoreConnector<AppState, String>(
converter: (store) => store.state.counter.toString(),
builder: (context, count) {
return Text(
count,
style: Theme.of(context).textTheme.headline4,
);
},
),
],
),
),
floatingActionButton: StoreConnector<AppState, VoidCallback>(
// 디스패치 함수 변환
converter: (store) {
return () => store.dispatch(IncrementAction());
},
builder: (context, callback) {
return FloatingActionButton(
onPressed: callback,
tooltip: 'Increment',
child: Icon(Icons.add),
);
},
),
);
}
}
Redux는 단방향 데이터 흐름을 가진 예측 가능한 상태 관리 라이브러리로, JavaScript 생태계에서 유래했습니다. 모든 상태 변화는 액션(Action)을 통해 이루어지며, 리듀서(Reducer)에서 현재 상태와 액션을 기반으로 새 상태를 생성합니다.
◎ 주요 개념
● Store: 앱의 모든 상태를 저장하는 단일 객체입니다.
● Action: 상태 변경을 트기러하는 이벤트로, type과 payload를 가집니다.
● Reducer: 현재 상태와 액션을 받아 새 상태를 반환하는 순수 함수입니다.
● Middleware: 액션이 리듀서에 도달하기 전에 처리하는 함수로, 비동기 작업... 등에 사용됩니다.
* [4]MobX
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
// MobX 코드 생성을 위한 부분
part 'counter_store.g.dart';
// 1. Store 클래스 정의
class CounterStore = _CounterStore with _$CounterStore;
abstract class _CounterStore with Store {
@observable
int counter = 0;
@computed
String get counterString => '현재 카운트: $counter';
@action
void increment() {
counter++;
}
}
// 2. 메인 앱
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// Store 인스턴스 생성
final CounterStore counterStore = CounterStore();
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CounterPage(counterStore: counterStore),
);
}
}
// 3. 카운터 페이지
class CounterPage extends StatelessWidget {
final CounterStore counterStore;
const CounterPage({Key? key, required this.counterStore}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('MobX 예시')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Observer 위젯으로 반응형 UI 구현
Observer(
builder: (_) => Text(
counterStore.counterString,
style: Theme.of(context).textTheme.headline4,
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: counterStore.increment,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
MobX는 관찰 가능한(Observable) 상태와 반응형 프로그래밍 패러다임을 사용하는 상태 관리 라이브러리입니다. 상태 변경을 자동으로 추적하고 관련 UI를 업데이트합니다.
◎ 주요 개념
● Observable: 관찰할 수 있는 상태 값으로, 변경을 추적합니다.
● Action: 상태를 수정하는 함수로, [5]트랜잭션으로 처리됩니다.
● Computed: 다른 Observable에서 파생된 값으로, 캐시됩니다.
● Observer: Observable 값의 변화에 반응하는 Widget입니다.
* [5]트랜잭션
트랜잭션은 '하나의 논리적 작업 단위'를 의미합니다. 이 작업 단위는 완전히 수행되거나 아예 수행되지 않아야 합니다. 중간 상태는 유효하지 않습니다.
트랜잭션을 일상생활로 비유하면 이해하기 쉽습니다.
트랜잭션은 "모 아니면 도"라는 원칙으로 작동하는 하나의 작업 묶음입니다.
※ 은행 송금
1. 내 계좌에서 10만원이 빠져나감
2. 친구 계좌에 10만원이 들어감
위의 두 단계를 반드시 함께 성공하거나 함께 실패해야 합니다. 만약 내 계좌에서만 돈이 빠져나가고 친구 계좌에 들어가지 않는다면, 돈이 증발하는 심각한 문제가 생깁니다.
트랜잭션은 위의 두 단계를 하나로 묶어서 "전체가 성공하거나 아니면 아무 일도 없었던 것처럼" 만들어 줍니다.
※ 온라인 쇼핑
1. 상품 재고 감소
2. 고객 돈 결제
3. 배송 정보 등록
위의 모든 과정이 트랜잭션으로 묶여 있어야 합니다. 결제만 되고 재고는 안 줄어들면, 상품 과잉 판매 문제가 발생합니다. 또한, 결제가 실패했는데 재고만 줄어들면, 판매자의 손해가 발생합니다.
// 트랜잭션 없이 상태 변경 (각각 UI 업데이트 발생)
userStore.name = "홍길동"; // UI 업데이트 1번 발생
userStore.age = 30; // UI 업데이트 2번 발생
userStore.isActive = true; // UI 업데이트 3번 발생
// 트랜잭션으로 묶은 상태 변경 (한 번의 UI 업데이트만 발생)
runInAction(() {
userStore.name = "홍길동";
userStore.age = 30;
userStore.isActive = true;
// 이 모든 변경 후 UI 업데이트는 딱 1번만!
});
[4]MobX에서 말한 트랜잭션은 여러 상태 변화를 하나로 묶어주는 것을 뜻합니다.
4. Riverpod에 대해 말해주세요.
└ Riverpod는 Provider의 한계를 극복한 개선된 상태 관리 라이브러리로, 컴파일 타임 안전성 & 코드 재사용성 & 테스트 용이성을 제공합니다.
※ Provider의 한계 극복
● Provider는 InheritedWidget의 한계로 인해 전역에서 Provider를 읽을 수 없고, 컴파일 타임에 오류를 잡기 어렵습니다.
● Riverpod는 이러한 제약 없이 BuildContext 외부에서도 Provider에 접근할 수 있습니다.
◎ 주요 특징
1. 컴파일 타임 안정성: 타입 시스템을 활용해 컴파일 시점에서 많은 오류를 잡아냅니다.
2. Provider 재정의: 테스트와 개발 환경에서 Provider를 쉽게 오버라이드할 수 있습니다.
3. 자동 캐싱과 메모리 관리: 사용되지 않는 상태를 자동으로 폐기합니다.
4. 의존성 주입: 여러 Provider 간의 의존성을 쉽게 관리할 수 있습니다.
5. 코드 공유: 여러 Provider 간에 상태와 로직을 쉽게 공유할 수 있습니다.
◎ 장점
1. BuildContext 없이도 Provider에 접근 할 수 있습니다.
2. 테스트 용이성이 매우 높습니다.
3. 의존성 오버라이드가 쉽습니다.
4. 자동 캐싱 및 메모리 관리가 가능합니다.
5. 성능 최적화(선택적 재빌드)를 할 수 있습니다.
◎ 단점
1. Provider에 비해 초기 학습 곡선이 다소 높습니다.
2. 보일러플레이트 코드가 약간 더 많을 수 있습니다.
* Riverpod의 주요 Provider 유형
1. Provider(기본 제공자)
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 단순한 값 제공
final nameProvider = Provider<String>((ref) {
return '홍길동';
});
// API 클라이언트와 같은 서비스 인스턴스 제공
final apiClientProvider = Provider<ApiClient>((ref) {
return ApiClient('https://api.example.com');
});
// 사용 예시
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final name = ref.watch(nameProvider);
return Text('이름: $name');
}
}
● 단순한 값이나 객체를 제공하는 가장 기본적인 Provider입니다. 읽기 전용이며, 변경되지 않는 값에 적합합니다.
2. StateProvider(간단한 상태 제공자)
// 카운터 상태 관리
final counterProvider = StateProvider<int>((ref) {
return 0; // 초기값
});
// 드롭다운 선택 값 관리
final selectedCategoryProvider = StateProvider<String>((ref) {
return '전체'; // 기본 선택값
});
// 사용 예시
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.watch(counterProvider);
return Column(
children: [
Text('카운터: ${counter.value}'),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: Text('증가'),
),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).state = 0,
child: Text('초기화'),
),
],
);
}
}
● 간단한 상태를 관리하는데 사용됩니다. 상태를 직접 변경할 수 있어서 간단한 상황에 적합합니다.
3. StateNotifierProvider(복잡한 상태 제공자)
// 할 일 항목 클래스
class Todo {
final String id;
final String title;
final bool completed;
Todo({
required this.id,
required this.title,
this.completed = false,
});
Todo copyWith({String? title, bool? completed}) {
return Todo(
id: this.id,
title: title ?? this.title,
completed: completed ?? this.completed,
);
}
}
// 할 일 목록 상태 관리자
class TodosNotifier extends StateNotifier<List<Todo>> {
TodosNotifier() : super([]); // 초기값: 빈 배열
// 할 일 추가
void addTodo(String title) {
state = [
...state,
Todo(id: DateTime.now().toString(), title: title),
];
}
// 할 일 완료 상태 토글
void toggleTodo(String id) {
state = state.map((todo) {
if (todo.id == id) {
return todo.copyWith(completed: !todo.completed);
}
return todo;
}).toList();
}
// 할 일 삭제
void removeTodo(String id) {
state = state.where((todo) => todo.id != id).toList();
}
}
// StateNotifierProvider 정의
final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
return TodosNotifier();
});
// 사용 예시
class TodoListScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(todosProvider);
return Scaffold(
appBar: AppBar(title: Text('할 일 목록')),
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
title: Text(todo.title),
leading: Checkbox(
value: todo.completed,
onChanged: (_) => ref.read(todosProvider.notifier).toggleTodo(todo.id),
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => ref.read(todosProvider.notifier).removeTodo(todo.id),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 간단한 예시이므로 실제로는 대화상자를 표시하여 제목 입력 받기
ref.read(todosProvider.notifier).addTodo('새 할 일');
},
child: Icon(Icons.add),
),
);
}
}
● 복잡한 상태를 관리하기 위한 Provider로, StateNotifier 클래스와 함께 사용됩니다. 상태 변경 로직을 캡슐화하는데 적합합니다.
4. FutureProvider(비동기 데이터 제공자)
// 사용자 데이터 모델
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
name: json['name'],
email: json['email'],
);
}
}
// 사용자 API 서비스
class UserService {
Future<User> fetchUser(int id) async {
// 실제로는 HTTP 요청을 보냄
await Future.delayed(Duration(seconds: 2)); // 네트워크 지연 시뮬레이션
// 예시 데이터
return User.fromJson({
'id': id,
'name': '홍길동',
'email': 'hong@example.com',
});
}
}
// API 서비스 Provider
final userServiceProvider = Provider<UserService>((ref) {
return UserService();
});
// 사용자 데이터를 가져오는 FutureProvider
final userProvider = FutureProvider.family<User, int>((ref, userId) async {
final userService = ref.read(userServiceProvider);
return userService.fetchUser(userId);
});
// 사용 예시
class UserProfileScreen extends ConsumerWidget {
final int userId;
UserProfileScreen({required this.userId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider(userId));
return Scaffold(
appBar: AppBar(title: Text('사용자 프로필')),
body: userAsync.when(
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('오류 발생: $error')),
data: (user) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('이름: ${user.name}', style: TextStyle(fontSize: 24)),
SizedBox(height: 8),
Text('이메일: ${user.email}'),
],
),
),
);
}
}
● 비동기 작업의 결과를 제공하는 Provider입니다. API 호출이나 데이터베이스 쿼리에 적합합니다.
5. StreamProvider(스트림 데이터 제공자)
// 메시지 모델
class Message {
final String id;
final String text;
final String sender;
final DateTime timestamp;
Message({
required this.id,
required this.text,
required this.sender,
required this.timestamp,
});
}
// 채팅 서비스
class ChatService {
// 실제로는 Firebase 같은 서비스와 연결
Stream<List<Message>> getMessages(String chatId) {
// 예시: 메시지 스트림 시뮬레이션
return Stream.periodic(Duration(seconds: 1), (count) {
// 새 메시지 추가 시뮬레이션
return List.generate(count + 1, (index) {
return Message(
id: 'msg_$index',
text: '메시지 $index',
sender: index % 2 == 0 ? '나' : '상대방',
timestamp: DateTime.now().subtract(Duration(minutes: index)),
);
});
}).take(10); // 예시를 위해 10개로 제한
}
}
// 채팅 서비스 Provider
final chatServiceProvider = Provider<ChatService>((ref) {
return ChatService();
});
// 메시지 StreamProvider
final messagesProvider = StreamProvider.family<List<Message>, String>((ref, chatId) {
final chatService = ref.read(chatServiceProvider);
return chatService.getMessages(chatId);
});
// 사용 예시
class ChatScreen extends ConsumerWidget {
final String chatId = 'chat_123';
@override
Widget build(BuildContext context, WidgetRef ref) {
final messagesAsync = ref.watch(messagesProvider(chatId));
return Scaffold(
appBar: AppBar(title: Text('채팅')),
body: messagesAsync.when(
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('오류 발생: $error')),
data: (messages) => ListView.builder(
reverse: true,
itemCount: messages.length,
itemBuilder: (context, index) {
final message = messages[index];
return ListTile(
title: Text(message.text),
subtitle: Text(message.sender),
trailing: Text(
'${message.timestamp.hour}:${message.timestamp.minute}',
),
);
},
),
),
);
}
}
● Stream 데이터를 제공하는 Provider입니다. 실시간 업데이트나 이벤트 스트림 관리에 적합합니다.
6. ChangeNotifierProvider(ChangeNotifier 기반 제공자)
import 'package:flutter/foundation.dart';
// ChangeNotifier를 사용한 카트 모델
class CartModel extends ChangeNotifier {
List<Product> _items = [];
List<Product> get items => _items;
double get totalPrice => _items.fold(0, (sum, item) => sum + item.price);
void addProduct(Product product) {
_items.add(product);
notifyListeners();
}
void removeProduct(Product product) {
_items.remove(product);
notifyListeners();
}
void clearCart() {
_items = [];
notifyListeners();
}
}
class Product {
final String id;
final String name;
final double price;
Product({required this.id, required this.name, required this.price});
}
// ChangeNotifierProvider 정의
final cartProvider = ChangeNotifierProvider<CartModel>((ref) {
return CartModel();
});
// 사용 예시
class CartScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final cart = ref.watch(cartProvider);
return Scaffold(
appBar: AppBar(
title: Text('장바구니'),
actions: [
IconButton(
icon: Icon(Icons.delete),
onPressed: () => ref.read(cartProvider).clearCart(),
),
],
),
body: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: cart.items.length,
itemBuilder: (context, index) {
final product = cart.items[index];
return ListTile(
title: Text(product.name),
subtitle: Text('₩${product.price}'),
trailing: IconButton(
icon: Icon(Icons.remove_circle),
onPressed: () => ref.read(cartProvider).removeProduct(product),
),
);
},
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('총액:', style: TextStyle(fontSize: 18)),
Text('₩${cart.totalPrice}', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
],
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 예시용 상품 추가
ref.read(cartProvider).addProduct(
Product(id: DateTime.now().toString(), name: '테스트 상품', price: 10000),
);
},
child: Icon(Icons.add),
),
);
}
}
● Flutter의 ChangeNotifier를 사용하여 상태를 관리하는 Provider입니다. Provider 패키지에서 마이그레이션 하는 경우에 유용합니다.
이러한 Provider들을 조합하여 복잡한 애플리케이션 상태를 효율적으로 관리할 수 있습니다.
Riverpod의 강점은 이러한 Provider들을 서로 결합하고 참조할 수 있다는 점입니다.
// 인증 상태 관리
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
return AuthNotifier();
});
// 현재 사용자 정보 - authProvider에 의존
final currentUserProvider = FutureProvider<User?>((ref) async {
final authState = ref.watch(authProvider);
// 로그인 상태에 따라 사용자 정보 제공
if (authState is AuthAuthenticated) {
final userService = ref.read(userServiceProvider);
return userService.fetchUser(authState.userId);
}
return null; // 로그인되지 않은 경우
});
이처럼 Riverpod는 다양한 종류의 Provider를 제공하며, 이들을 조합하여 앱의 상태를 체계적으로 관리할 수 있습니다.
5. Bloc에 대해 말해주세요.
└ Bloc(Business Logic Component)은 이벤트와 상태를 명확히 분리하고 Stream을 활용하여 예측 가능한 상태 관리를 제공하는 아키텍처 패턴입니다.
※ 기본 원리
● 관심사 분리: UI와 비즈니스 로직을 명확하게 분리합니다.
● 단방향 데이터 흐름: 이벤트 => Bloc => 상태의 일관된 흐름을 따릅니다.
● 반응형 프로그래밍: Stream과 StreamBuilder를 활용하여 상태 변화에 반응합니다.
※ Bloc의 주요 컴포넌트
● 이벤트(Event): 사용자 액션이나 시스템 이벤트를 나타내는 클래스입니다.
● 상태(State): UI의 현재 상태를 나타내는 불변(immutable) 클래스입니다.
● Bloc 클래스: 이벤트를 받아 처리하고 새로운 상태를 생성하는 클래스입니다.
※ 작동 방식
● UI에서 이벤트가 발생하면 Bloc에 이벤트를 전달합니다.
● Bloc는 이벤트를 처리하고 필요한 비즈니스 로직을 실행합니다.
● 처리 결과에 따라 새로운 상태(State)를 생성하여 Stream으로 내보냅니다.
● UI는 상태 Stream을 구독하고 있다가 새로운 상태가 오면 화면을 업데이트합니다.
◎ 장점
1. 테스트 용이성이 높습니다.
2. 코드 구조화와 유지 관리가 쉽습니다.
3. 비즈니스 로직과 UI의 명확한 분리를 유도합니다.
4. 디버깅과 상태 추적이 쉽습니다.
5. 예측 가능한 단방향 데이터 흐름을 제공합니다.
◎ 단점
1. 작은 앱에는 과도한 보일러플레이트 코드가 생길 수 있습니다.
2. 초기 학습 곡선이 다소 가파를 수 있습니다.
3. Stream 기반 프로그래밍에 익숙해져야 합니다.
* 간단한 카운터 앱 Bloc 예제
// 카운터 Bloc
// counter_event.dart - 이벤트 정의
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}
class ResetEvent extends CounterEvent {}
// counter_state.dart - 상태 정의
class CounterState {
final int count;
CounterState(this.count);
}
// counter_bloc.dart - BLoC 구현
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterState(0)) {
// 이벤트 핸들러 등록
on<IncrementEvent>((event, emit) {
emit(CounterState(state.count + 1));
});
on<DecrementEvent>((event, emit) {
emit(CounterState(state.count - 1));
});
on<ResetEvent>((event, emit) {
emit(CounterState(0));
});
}
}
// 카운터 UI
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CounterBloc(),
child: CounterView(),
);
}
}
class CounterView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('BLoC 카운터 예제')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('카운터 값:'),
// BlocBuilder로 상태 변화 감지 및 UI 업데이트
BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Text(
'${state.count}',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
);
},
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
// BLoC에 이벤트 전달
context.read<CounterBloc>().add(DecrementEvent());
},
child: Icon(Icons.remove),
),
SizedBox(width: 20),
ElevatedButton(
onPressed: () {
context.read<CounterBloc>().add(ResetEvent());
},
child: Text('초기화'),
),
SizedBox(width: 20),
ElevatedButton(
onPressed: () {
context.read<CounterBloc>().add(IncrementEvent());
},
child: Icon(Icons.add),
),
],
),
],
),
),
);
}
}
6. Provider에 대해 말해주세요.
└ Provider는 InheritedWidget을 기반으로 한 간단하면서도 효과적인 상태 관리 및 의존성 주입 라이브러리로, 위젯 트리를 통해 데이터를 효율적으로 전달합니다.
※ 기본 개념과 원리
● InheritedWidget을 기반으로 하지만 훨씬 사용하기 쉽고 강력합니다.
● 위젯 트리를 통해 데이터를 효율적으로 전달하는 메커니즘을 제공합니다.
● 의존성 주입 패턴을 적용하여 앱의 다양한 부분에서 데이터에 쉽게 접근할 수 있습니다.
※ 주요 Provider 유형 (위의 4번 'Riverpod'에서 자세히 설명했기에 간단한 설명만 하겠습니다.)
● Provider: 가장 기본적인 형태로, 단순한 값 제공 (읽기 전용)
● ChangeNotifierProvider: ChangeNotifier를 사용하여 변경 가능한 상태 관리
● FutureProvider: 비동기 작업 결과 제공
● StreamProvider: Stream 데이터 제공
● MultiProvider: 여러 Provider를 한 번에 제공
● ProxyProvider: 다른 Provider에 의존하는 Provider 생성
◎ 장점
1. 코드의 가독성과 유지 관리성이 향상됩니다.
2. 불필요한 위젯 재빌드 방지로 성능을 최적화할 수 있습니다.
3. 비즈니스 로직과 UI 코드를 분리합니다.
4. 테스트 용이성이 향상됩니다.
◎ 단점
1. 복잡한 상태 관리에는 기능이 제한적일 수 있습니다.
2. BuildContext가 필요하여 모든 곳에서 접근하기 어렵습니다. (Riverpod가 이 문제를 해결한 라이브러리입니다.)
3. 깊은 중첩 구조에서는 가독성이 떨어질 수 있습니다.
7. GetX에 대해 말해주세요.
└ GetX는 상태 관리 & 라우팅 & 종속성 주입을 통합적으로 제공하는 초경량 솔루션으로, 최소한의 코드로 반응형 애플리케이션을 구축할 수 있게 합니다.
※ 세 가지 핵심 기능
1. 상태 관리: 간단하고 반응형 상태 관리를 제공합니다.
2. 라우팅 관리: 네비게이션과 라우팅을 위한 포괄적인 솔루션을 제공합니다.
3. 종속성 관리: 효율적인 의존성 주입 시스템을 제공합니다.
※ GetX 상태 관리의 특징
● 단순 상태 관리: GetBuilder를 사용하여 간단한 상태 관리를 할 수 있습니다.
● 반응형 상태 관리: Rx변수와 .obs확장을 통한 반응형 프로그래밍을 제공합니다.
● GetX Controller: 비즈니스 로직을 캡슐화하는 컨트롤러 패턴입니다.
● 메모리 관리: 사용하지 않는 컨트롤러를 자동으로 삭제합니다.
※ GetX 라우팅
● 화면 전환을 위한 간결한 구문 Get.to & Get.back... 등을 제공합니다.
● 명명된 라우트 지원 및 중첩 네비게이션을 제공합니다.
● 미들웨어 & 전환 애니메이션 & 페이지 간 데이터 전달을 할 수 있습니다.
● BuildContext 없이도 네비게이션을 할 수 있습니다.
※ 종속성 주입
● Get.put & Get.lazyPut & Get.find 메서드를 통한 간단한 의존성을 주입합니다.
● 싱글톤 & 팩토리 & 일회성 인스턴스 관리를 지원합니다.
● 바인딩을 통한 자동 의존성 관리를 제공합니다.
◎ 장점
● 코드 간결성: 보일러플레이트 코드를 최소화합니다.
● 성능 최적화: 필요한 위젯만 재빌드합니다.
● BuildContext에 의존하지 않습니다. 어디에서나 접근 할 수 있습니다.
● 통합 솔루션을 제공합니다. 별도의 라이브러리 없이도 모든 기능을 제공합니다.
● 직관적인 API와 쉬운 학습 곡선을 가지고 있습니다.
◎ 단점
● 전역 싱글톤을 많이 사용해서 테스트가 어려울 수 있습니다.
● 남용 시, 앱 구조가 복잡해질 수 있습니다.
● 지나친 간결함으로 인해 디버깅이 어려울 수 있습니다.
* 간단한 GetX 사용 예시
// 컨트롤러 정의
class CounterController extends GetxController {
var count = 0.obs; // 반응형 변수
void increment() {
count++; // 자동으로 UI 업데이트
}
}
// GetX 앱 설정
void main() {
runApp(
GetMaterialApp( // MaterialApp 대신 GetMaterialApp 사용
home: Home(),
),
);
}
// 컨트롤러 사용
class Home extends StatelessWidget {
final CounterController controller = Get.put(CounterController());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('GetX 예제')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 반응형 UI
Obx(() => Text('${controller.count}')),
ElevatedButton(
onPressed: controller.increment,
child: Text('증가'),
),
// 페이지 이동
ElevatedButton(
onPressed: () => Get.to(SecondPage()),
child: Text('다음 페이지'),
),
],
),
),
);
}
}
// 스낵바, 다이얼로그 등의 유틸리티
void showMessage() {
Get.snackbar(
'알림',
'메시지가 도착했습니다.',
snackPosition: SnackPosition.BOTTOM,
);
}
8. 위젯 트리 구조에 대해 말해주세요.
└ Flutter의 위젯 트리는 계층적 구조로 UI를 구성하며, 모든 UI 요소는 위젯 트리 내에서 부모-자식 관계를 형성하여 효율적인 렌더링과 재사용성을 제공합니다.
Flutter의 UI는 위젯 트리라고 불리는 계층적 구조로 구성됩니다. 이 트리는 루트 위젯에서 시작하여 자식 위젯들로 분기되는 형태입니다. 각 위젯은 하나의 부모와 여러 자식을 가질 수 있으며, 이는 UI 컴포넌트의 구성과 배치를 정의합니다.
※ 위젯 트리는 세 가지 주요 트리로 변환
1. [1]위젯 트리: UI의 구성을 정의합니다.
2. [2]엘리먼트 트리: 위젯의 인스턴스를 관리합니다.
3. [3]렌더 트리: 실제 화면에 그려질 요소들을 처리합니다.
Flutter의 [4]렌더링 파이프라인은 build & layout & paint라는 세 단계로 진행됩니다.
● build 단계에서 위젯 트리가 구성됩니다.
● layout 단계에서 각 요소의 크기와 위치가 결정됩니다.
● paint 단계에서 실제 픽셀이 화면에 그려집니다.
위젯 트리 구조는 코드의 재사용성과 [5]모듈화를 촉진하며, setState 호출 시, 효율적인 부분 업데이트를 가능하게 합니다. 또한, 필요한 위젯만 다시 빌드하므로 성능이 최적화됩니다.
* [1]위젯트리
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 위젯 트리 생성 부분
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('트리 구조 예제'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('이것은 Text 위젯입니다'),
SizedBox(height: 20),
Container(
width: 200,
height: 100,
color: Colors.blue,
child: Center(
child: Text(
'컨테이너 안의 텍스트',
style: TextStyle(color: Colors.white),
),
),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
print('버튼이 눌렸습니다');
},
child: Text('버튼'),
),
],
),
),
),
);
}
}
● 위젯 트리는 Flutter UI의 구조를 정의하는 불변(immutable) 객체들의 계층입니다.
● 위젯은 UI의 설계도(blueprint)로 생각할 수 있으며, 어떻게 UI가 보여야 하는지를 설명합니다.
● 상태가 변경될 때마다 새로운 위젯 트리가 생성됩니다.
* [2]엘리먼트 트리
● 엘리먼트 트리는 위젯 트리의 실제 인스턴스를 관리합니다.
● 각 위젯에 대응하는 엘리먼트가 있으며, 위젯의 수명 주기를 관리합니다.
● 엘리먼트는 위젯과 렌더 객체 사이의 중간 매개체 역할을 합니다.
● 상태가 변경될 때, Flutter는 이전 엘리먼트 트리와 새 위젯 트리를 비교하여 최소한의 변경만 적용합니다.
* [3]렌더 트리
● 렌더 트리는 화면에 실제로 그려질 객체들을 포함합니다.
● RenderObject들로 구성되며, 레이아웃 계산과 실제 페인팅을 담당합니다.
● 모든 위젯이 RenderObject를 생성하는 것은 아닙니다. RenderObjectWidget(ex. Container & Text... 등)만 렌더 객체를 생성합니다.
* [4]렌더링 파이프라인
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
// 렌더링 파이프라인 디버깅 활성화
debugPaintSizeEnabled = true; // 레이아웃 단계 시각화
debugPaintBaselinesEnabled = true; // 텍스트 기준선 표시
debugPaintLayerBordersEnabled = true; // 레이어 경계 표시
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
print('Build 메서드 호출됨'); // Build 단계
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('렌더링 파이프라인 예제')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 애니메이션으로 레이아웃 변경 보여주기
AnimatedContainer(
duration: Duration(seconds: 1),
width: _expanded ? 300.0 : 200.0,
height: _expanded ? 200.0 : 100.0,
color: _expanded ? Colors.blue : Colors.red,
// Layout 및 Paint 단계를 볼 수 있는 커스텀 위젯
child: CustomPaint(
painter: MyCustomPainter(),
child: Center(
child: Text(
'터치하세요',
style: TextStyle(color: Colors.white),
),
),
),
),
SizedBox(height: 20),
Text('상태: ${_expanded ? "확장됨" : "축소됨"}'),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
_expanded = !_expanded; // 상태 변경으로 rebuild 트리거
});
},
child: Icon(_expanded ? Icons.remove : Icons.add),
),
),
);
}
}
// Paint 단계를 시연하는 CustomPainter
class MyCustomPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
print('Paint 메서드 호출됨'); // Paint 단계
final paint = Paint()
..color = Colors.yellow.withOpacity(0.3)
..style = PaintingStyle.stroke
..strokeWidth = 5.0;
// 사각형 그리기
canvas.drawRect(
Rect.fromLTWH(10, 10, size.width - 20, size.height - 20),
paint,
);
// 대각선 그리기
canvas.drawLine(
Offset(0, 0),
Offset(size.width, size.height),
paint..color = Colors.green.withOpacity(0.5),
);
canvas.drawLine(
Offset(size.width, 0),
Offset(0, size.height),
paint,
);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
1. Build 단계
● 위젯의 build 메서드가 호출되어 위젯 트리를 구성합니다.
● 이 단계에서 UI의 구조와 속성이 정의됩니다.
● 상태가 변경되면 setState를 통해 이 단계가 다시 시작됩니다.
2. Layout 단계
● 각 렌더 객체의 크기와 위치를 결정합니다.
● 이 과정은 두 단계로 나뉩니다.
1. 부모에서 자식으로 제약 조건(constraints)을 전달(top-down)
2. 자식에서 부모로 크기 정보를 전달(bottom-up)
● 이 과정을 통해 각 요소의 정확한 크기와 위치가 계산됩니다.
3. Paint 단계
● 실제 픽셀을 화면에 그리는 단계입니다.
● 렌더 객체들은 paint 메서드를 통해 Canvas에 자신을 그립니다.
● 이 단계에서 그림자 & 효과 & 클리핑... 등이 적용됩니다.
렌더링 파이프라인 구조는 Flutter가 60fps의 부드러운 성능을 달성하는 데에 중요한 역할을 합니다. 상태 변경 시에 필요한 부분만 다시 빌드하고 레이아웃을 계산하여 효율적으로 화면을 업데이트합니다.
* [5]모듈화
class CustomButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
final Color color;
const CustomButton({
Key? key,
required this.text,
required this.onPressed,
this.color = Colors.blue,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: color,
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(text),
);
}
}
Flutter에서 모듈화란 UI와 로직을 재사용 가능하고 관리하기 쉬운 작은 단위로 분리하는 것을 의미합니다.
● 위젯 분리: 복잡한 UI를 작고 독립적인 위젯으로 분리하여 코드의 가독성과 유지보수성을 향상시킵니다.
● 재사용성: 공통 위젯을 별도의 클래스로 분리하여 여러 화면에서 재사용할 수 있습니다.
● 관심사 분리: UI 위젯 & 비즈니스 로직 & 데이터 모델을 명확히 분리하여 코드 구조를 개선합니다.
● 패키지화: 관련 기능을 패키지로 묶어 프로젝트 간에 공유하거나 pub.dev에 배포할 수 있습니다.
위의 예시는 앱 전체에서 사용되는 공통 버튼을 모듈화한 것입니다. 이렇게 모듈화된 위젯은 앱 전체에서 일관된 디자인을 유지하고, 변경 사항을 한 곳에서 관리할 수 있게 해줍니다.
9. Flutter에서 비동기 프로그래밍을 하는 방법에 대해 말해주세요.
└ Flutter에서 Future & async/await & Stream을 활용하여 비동기 작업을 효율적으로 처리할 수 있으며, 무거운 연산은 Isolate를 통해 별도 스레드에서 처리할 수 있습니다.
* Future
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: FutureExample(),
);
}
}
class FutureExample extends StatefulWidget {
@override
_FutureExampleState createState() => _FutureExampleState();
}
class _FutureExampleState extends State<FutureExample> {
String _result = "결과가 여기에 표시됩니다";
// Future를 반환하는 함수
Future<String> fetchData() async {
// 네트워크 요청을 시뮬레이션하는 지연
await Future.delayed(Duration(seconds: 2));
return "데이터 가져오기 성공!";
}
// then()을 사용한 Future 처리
void _loadDataWithThen() {
setState(() => _result = "로딩 중...");
fetchData().then((value) {
setState(() => _result = value);
}).catchError((error) {
setState(() => _result = "오류 발생: $error");
});
}
// async/await를 사용한 Future 처리
void _loadDataWithAsync() async {
setState(() => _result = "로딩 중...");
try {
final value = await fetchData();
setState(() => _result = value);
} catch (error) {
setState(() => _result = "오류 발생: $error");
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Future 예제')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_result, style: TextStyle(fontSize: 18)),
SizedBox(height: 20),
ElevatedButton(
onPressed: _loadDataWithThen,
child: Text('then()으로 데이터 로드'),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: _loadDataWithAsync,
child: Text('async/await로 데이터 로드'),
),
],
),
),
);
}
}
● Future는 미래의 어느 시점에 완료될 단일 비동기 작업을 나타냅니다.
* async/await
Future<String> fetchData() async {
// 비동기 작업
return await http.get('https://api.example.com/data');
}
● async/await 문법을 사용하면 비동기 코드를 동기 코드처럼 작성할 수 있어서 가독성이 향상됩니다. 그래서 Future.then을 사용한 콜백 방식보다 async/await를 사용하는 것이 권장됩니다.
* FutureBuilder
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: FutureBuilderExample(),
);
}
}
class FutureBuilderExample extends StatelessWidget {
// Future를 반환하는 함수
Future<List<String>> fetchItems() async {
await Future.delayed(Duration(seconds: 2));
return ["항목 1", "항목 2", "항목 3", "항목 4", "항목 5"];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('FutureBuilder 예제')),
body: Center(
child: FutureBuilder<List<String>>(
future: fetchItems(), // Future 제공
builder: (context, snapshot) {
// 상태에 따른 UI 처리
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator(); // 로딩 중
} else if (snapshot.hasError) {
return Text("오류 발생: ${snapshot.error}"); // 오류 발생
} else if (snapshot.hasData) {
// 데이터 로드 성공
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(snapshot.data![index]),
);
},
);
} else {
return Text("데이터 없음"); // 데이터가 없는 경우
}
},
),
),
);
}
}
● Flutter는 FutureBuilder 위젯을 제공하여 Future의 상태(대기 & 완료 & 오류)에 따라 UI를 쉽게 업데이트할 수 있습니다.
* Stream
import 'package:flutter/material.dart';
import 'dart:async';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: StreamExample(),
);
}
}
class StreamExample extends StatefulWidget {
@override
_StreamExampleState createState() => _StreamExampleState();
}
class _StreamExampleState extends State<StreamExample> {
// 스트림 컨트롤러 생성
final _streamController = StreamController<int>();
int _counter = 0;
late Timer _timer;
@override
void initState() {
super.initState();
// 1초마다 카운터 증가하고 스트림에 추가
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
_counter++;
_streamController.sink.add(_counter);
});
}
@override
void dispose() {
// 리소스 정리
_timer.cancel();
_streamController.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Stream 예제')),
body: Center(
child: StreamBuilder<int>(
stream: _streamController.stream,
initialData: 0,
builder: (context, snapshot) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'현재 카운트:',
style: TextStyle(fontSize: 18),
),
Text(
'${snapshot.data}',
style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
),
],
);
},
),
),
);
}
}
● Stream은 시간에 따라 여러 값을 비동기적으로 전달하는 데이터 시퀀스입니다.
* StreamBuilder
import 'package:flutter/material.dart';
import 'dart:async';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: StreamBuilderExample(),
);
}
}
class StreamBuilderExample extends StatefulWidget {
@override
_StreamBuilderExampleState createState() => _StreamBuilderExampleState();
}
class _StreamBuilderExampleState extends State<StreamBuilderExample> {
// 랜덤 색상을 생성하는 스트림
Stream<Color> colorStream() async* {
final List<Color> colors = [
Colors.red,
Colors.green,
Colors.blue,
Colors.yellow,
Colors.purple,
Colors.orange,
];
var random = DateTime.now().millisecondsSinceEpoch;
while (true) {
await Future.delayed(Duration(seconds: 1));
random = (random * 1664525 + 1013904223) % 0xFFFFFFFF;
yield colors[random % colors.length];
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('StreamBuilder 예제')),
body: Center(
child: StreamBuilder<Color>(
stream: colorStream(),
builder: (context, snapshot) {
return AnimatedContainer(
duration: Duration(milliseconds: 500),
width: 200,
height: 200,
color: snapshot.data ?? Colors.grey,
child: Center(
child: Text(
'색상 변경 중...',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
);
},
),
),
);
}
}
● StreamBuilder 위젯을 사용하면 스트림 데이터에 반응하는 UI를 쉽게 구축할 수 있습니다.
* Completer
import 'package:flutter/material.dart';
import 'dart:async';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CompleterExample(),
);
}
}
class CompleterExample extends StatefulWidget {
@override
_CompleterExampleState createState() => _CompleterExampleState();
}
class _CompleterExampleState extends State<CompleterExample> {
String _result = "결과가 여기에 표시됩니다";
// Completer를 사용해 수동으로 Future 완료하기
Future<String> performAsyncOperation() {
final completer = Completer<String>();
// 비동기 작업 시뮬레이션
Future.delayed(Duration(seconds: 2), () {
// 작업 성공 시 completer 완료
if (DateTime.now().second % 2 == 0) {
completer.complete("작업이 성공적으로 완료되었습니다!");
} else {
// 작업 실패 시 예외 발생
completer.completeError("작업 중 오류가 발생했습니다");
}
});
// Future를 즉시 반환
return completer.future;
}
void _startOperation() async {
setState(() => _result = "작업 진행 중...");
try {
final result = await performAsyncOperation();
setState(() => _result = result);
} catch (error) {
setState(() => _result = "$error");
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Completer 예제')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_result, style: TextStyle(fontSize: 18)),
SizedBox(height: 20),
ElevatedButton(
onPressed: _startOperation,
child: Text('비동기 작업 시작'),
),
],
),
),
);
}
}
● Completer는 수동으로 Future를 생성하고 완료하는 데 사용됩니다.
* Isolate
import 'package:flutter/material.dart';
import 'dart:isolate';
import 'dart:math';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: IsolateExample(),
);
}
}
class IsolateExample extends StatefulWidget {
@override
_IsolateExampleState createState() => _IsolateExampleState();
}
class _IsolateExampleState extends State<IsolateExample> {
String _result = "결과가 여기에 표시됩니다";
bool _isCalculating = false;
// Isolate에서 피보나치 수열 계산
Future<void> calculateFibonacciWithIsolate(int n) async {
setState(() {
_isCalculating = true;
_result = "계산 중...";
});
// Isolate 통신을 위한 포트 생성
final receivePort = ReceivePort();
// Isolate 시작
await Isolate.spawn(fibonacciCalculator, [receivePort.sendPort, n]);
// 결과 수신
final response = await receivePort.first;
setState(() {
_result = "피보나치($n) = $response";
_isCalculating = false;
});
}
// 메인 UI 스레드에서 직접 계산
void calculateFibonacciDirectly(int n) {
setState(() {
_isCalculating = true;
_result = "계산 중...";
});
// 의도적으로 UI 블로킹 작업 시뮬레이션
final result = fibonacci(n);
setState(() {
_result = "피보나치($n) = $result";
_isCalculating = false;
});
}
// 재귀적 피보나치 계산 (의도적으로 비효율적)
static int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// Isolate에서 실행될 함수
static void fibonacciCalculator(List<dynamic> params) {
final SendPort sendPort = params[0];
final int n = params[1];
final result = fibonacci(n);
sendPort.send(result);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Isolate 예제')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_result, style: TextStyle(fontSize: 18)),
SizedBox(height: 20),
if (_isCalculating)
CircularProgressIndicator()
else
Column(
children: [
ElevatedButton(
onPressed: () => calculateFibonacciWithIsolate(35),
child: Text('Isolate로 계산 (n=35)'),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: () => calculateFibonacciDirectly(35),
child: Text('UI 스레드에서 계산 (n=35) - 주의: UI 멈춤'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
),
],
),
],
),
),
);
}
}
● Isolate는 메인 UI 스레드를 차단하지 않고 무거운 연산을 별도의 스레드에서 실행할 수 있게 해줍니다.
위의 예제들은 Flutter에서 비동기 프로그래밍의 다양한 접근 방식을 보여주기 위해서 작성했습니다. Future와 async/await은 단일 비동기 작업을, Stream은 시간에 따른 데이터 흐름을, FutureBuilder & StreamBuilder는 비동기 데이터에 따른 UI 업데이트를, Completer는 수동 Future 완료를, Isolate는 무거운 계산 작업의 백그라운드 처리를 보여줍니다.
10. Flutter에서 API 호출을 하는 방법에 대해 말해주세요.
└ Flutter에서는 http & dio 같은 라이브러리를 사용하여 Restful API를 호출하고, JSON 데이터를 모델 객체로 변환하여 앱에서 활용합니다.
* http 라이브러리
import 'package:http/http.dart' as http;
Future<void> fetchData() async {
final response = await http.get(Uri.parse('https://api.example.com/data'));
if (response.statusCode == 200) {
// 성공적으로 데이터를 받아옴
final data = jsonDecode(response.body);
}
}
http 라이브러리는 Flutter 팀에서 제공하는 기본적인 HTTP 클라이언트로, 간단한 API 호출에 적합합니다.
* dio 라이브러리
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Dio 예제',
theme: ThemeData(primarySwatch: Colors.blue),
home: DioExample(),
);
}
}
class DioExample extends StatefulWidget {
@override
_DioExampleState createState() => _DioExampleState();
}
class _DioExampleState extends State<DioExample> {
final Dio _dio = Dio();
String _responseData = '여기에 데이터가 표시됩니다';
bool _isLoading = false;
// GET 요청 예제
Future<void> _fetchData() async {
setState(() {
_isLoading = true;
_responseData = '로딩 중...';
});
try {
// 기본 GET 요청
final response = await _dio.get('https://jsonplaceholder.typicode.com/posts/1');
setState(() {
_responseData = 'GET 응답:\n${response.data.toString()}';
});
} catch (e) {
setState(() {
_responseData = '오류 발생: ${e.toString()}';
});
} finally {
setState(() {
_isLoading = false;
});
}
}
// POST 요청 예제
Future<void> _postData() async {
setState(() {
_isLoading = true;
_responseData = '로딩 중...';
});
try {
// 헤더 및 데이터와 함께 POST 요청
final response = await _dio.post(
'https://jsonplaceholder.typicode.com/posts',
data: {
'title': 'foo',
'body': 'bar',
'userId': 1,
},
options: Options(
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
),
);
setState(() {
_responseData = 'POST 응답:\n${response.data.toString()}';
});
} catch (e) {
setState(() {
_responseData = '오류 발생: ${e.toString()}';
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Dio HTTP 예제')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: _isLoading ? null : _fetchData,
child: Text('GET 요청'),
),
ElevatedButton(
onPressed: _isLoading ? null : _postData,
child: Text('POST 요청'),
),
],
),
SizedBox(height: 20),
_isLoading
? Center(child: CircularProgressIndicator())
: Expanded(
child: SingleChildScrollView(
child: Text(_responseData),
),
),
],
),
),
);
}
}
dio 라이브러리는 더 많은 기능(인터셉터 & 취소 토큰 & 폼 데이터 & 파일 다운로드... 등)을 제공하는 강력한 HTTP 클라이언트입니다. API 응답으로 받은 JSON 데이터는 일반적으로 다음 과정을 통해 처리합니다.
1. jsonDecode를 사용하여 JSON 문자열을 Dart 객체로 파싱
2. 파싱된 데이터를 모델 클래스로 변환 (json_serializable & built_value 라이브러리 활용)
효율적인 API 통신을 위해 캐싱 전략을 구현하거나, Retrofit과 같은 라이브러리를 사용하여 API 클라이언트를 정의할 수도 있습니다. 또한, 인터넷 연결 상태를 확인하기 위해서 connectivity 라이브러리를 함께 사용하는 것을 권장 드립니다.
11. Hot Reload와 Hot Restart의 차이점을 말해주세요.
└ Hot Reload는 상태를 유지하며 UI 변경 사항만 빠르게 적용하지만, Hot Restart는 앱을 완전히 재시작하여 모든 상태를 초기화합니다.
Hot Reload와 Hot Restart는 Flutter 개발의 생산성을 높이는 핵심 기능입니다.
◎ Hot Reload
// Hot Reload 사용 방법
1. 단축키 사용
● Windows/Linux: Ctrl + S (파일 저장) 또는 Ctrl + \
● macOS: Cmd + S (파일 저장) 또는 Cmd + \
2. IDE 버튼 사용
● VS Code: 디버그 모드에서 상단 메뉴의 번개 모양 아이콘(⚡) 클릭
● Android Studio/IntelliJ: 상단 툴바의 번개 모양 아이콘(⚡) 클릭
3. Flutter CLI 사용 (터미널에서 앱 실행 중일 경우)
● 터미널에서 r 키를 누르면 Hot Reload가 실행됩니다.
● 위젯 트리를 재구성하지만, 앱의 상태(State)를 보존합니다.
● 다트 코드만 리컴파일하고 변경된 코드를 기존 다트 VM에 주입합니다.
● 매우 빠르게 실행되며(일반적으로 1초 이내), UI 변경 사항을 즉시 확인할 수 있습니다.
● initState와 같은 생명주기 메서드는 다시 실행되지 않습니다.
◎ Hot Restart
// Hot Restart 사용 방법
1. 단축키 사용
● Windows/Linux: Ctrl + Shift + \
● macOS: Cmd + Shift + \
2. IDE 버튼 사용
● VS Code: 디버그 모드에서 상단 메뉴의 재시작 아이콘(🔄) 클릭
● Android Studio/IntelliJ: 상단 툴바의 재시작 아이콘(🔄) 클릭
3. Flutter CLI 사용 (터미널에서 앱 실행 중일 경우)
● 터미널에서 R 키(대문자)를 누르면 Hot Restart가 실행됩니다.
● 앱을 완전히 재시작하고 모든 상태를 초기화합니다.
● 전체 앱을 다시 빌드하고 새로운 다트 VM 인스턴스를 생성합니다.
● Hot Reload보다 시간이 더 걸립니다. (일반적으로 몇 초 정도)
● 상태 변경 & 생명주기 메서드 수정 & 전역 변수 변경... 등 Hot Reload로 적용되지 않는 변경 사항에 사용합니다.
UI 변경 사항만 있을 때는, Hot Reload가 효율적입니다.
앱 로직 & 상태 관리 코드 & 생명주기 메서드 등을 변경했을 때는 Hot Restart가 필요합니다.
12. Flutter에서 Native 코드와의 통합 방법에 대해 말해주세요.
└ Flutter에서는 플랫폼 채널을 통해 Dart 코드와 네이티브 코드(AOS/IOS) 간의 통신을 구현하며, 플러그인 형태로 패키징하여 재사용할 수 있습니다.
Flutter에서 네이티브 코드와의 통합은 주로 플랫폼 채널(Platfrom Channels)을 통해 이루어집니다.
1. 메서드 채널(MethodChannel): 일회성 메서드 호출에 사용되며, Flutter에서 네이티브 기능을 호출하거나 결과를 받아올 때 주로 사용합니다.
2. 이벤트 채널(EventChannel): 지속적인 데이터 스트림을 처리할 때 사용하며, 센서 데이터나 연속적인 네이티브 이벤트를 Flutter로 전달할 수 있습니다.
* 메서드 채널 구현 예시
// Dart 코드
static const platform = MethodChannel('com.example.app/battery');
Future<int> getBatteryLevel() async {
try {
final int result = await platform.invokeMethod('getBatteryLevel');
return result;
} on PlatformException catch (e) {
// 오류 처리
return -1;
}
}
// 네이티브 코드(Android Kotlin)
private val CHANNEL = "com.example.app/battery"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "getBatteryLevel") {
val batteryLevel = getBatteryLevel()
result.success(batteryLevel)
} else {
result.notImplemented()
}
}
}
13. Flutter에서 라우팅과 네비게이션을 처리하는 방법에 대해 말해주세요.
└ Flutter에서는 Navigator 위젯을 사용하여 화면 간 이동을 관리하며, 명명된 라우트와 동적 라우트 모두 지원합니다.
* 기본 네비게이션
// 새 화면으로 이동
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondScreen()),
);
// 이전 화면으로 돌아가기
Navigator.pop(context);
* 명명된 라우트(Named Routes)
1. MaterialApp에서 라우트를 정의
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomeScreen(),
'/details': (context) => DetailScreen(),
'/settings': (context) => SettingsScreen(),
},
);
// 명명된 라우트로 이동
Navigator.pushNamed(context, '/details');
2. 인수 전달
// 인수와 함께 라우트 이동
Navigator.pushNamed(
context,
'/details',
arguments: {'id': 123, 'title': '상품 상세'},
);
// 인수 받기
final args = ModalRoute.of(context)!.settings.arguments as Map;
3. onGenerateRoute를 사용한 동적 라우트 생성
MaterialApp(
onGenerateRoute: (settings) {
if (settings.name == '/product') {
final args = settings.arguments as Map;
return MaterialPageRoute(
builder: (context) => ProductScreen(id: args['id']),
);
}
return null;
},
);
Go_Router 같은 라이브러리를 사용하면 더 복잡한 라우팅 시나리오 (딥 링크 & 중첩 라우팅... 등)를 쉽게 처리할 수 있습니다.
14. Flutter의 Form 위젯과 Form 검증 방법에 대해 말해주세요.
└ Flutter의 Form 위젯은 사용자 입력을 수집하고 검증하기 위한 컨테이너로, GlobalKey와 validator 함수를 사용하여 입력 값을 관리합니다.
* Form 기본 구조
final _formKey = GlobalKey<FormState>();
Form(
key: _formKey,
child: Column(
children: [
TextFormField(
// TextFormField 설정
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// 폼이 유효할 때 실행할 코드
_formKey.currentState!.save();
}
},
child: Text('제출'),
),
],
),
);
* TextFormField와 검증
TextFormField(
decoration: InputDecoration(labelText: '이메일'),
validator: (value) {
if (value == null || value.isEmpty) {
return '이메일을 입력해주세요';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return '유효한 이메일 형식이 아닙니다';
}
return null; // null 반환은 입력이 유효하다는 의미
},
onSaved: (value) {
// 값 저장 로직
},
)
* Form 상태 관리
// 폼 검증
if (_formKey.currentState!.validate()) {
// 폼 데이터 저장
_formKey.currentState!.save();
// 추가 로직 실행
}
// 폼 초기화
_formKey.currentState!.reset();
* 사용자 정의 FormField
FormField<bool>(
initialValue: false,
validator: (value) {
if (value == false) {
return '이용약관에 동의해야 합니다';
}
return null;
},
builder: (field) {
return CheckboxListTile(
title: Text('이용약관에 동의합니다'),
value: field.value,
onChanged: (newValue) {
field.didChange(newValue);
},
subtitle: field.hasError
? Text(field.errorText!, style: TextStyle(color: Colors.red))
: null,
);
},
)
* AutovalidateMode 사용
TextFormField(
autovalidateMode: AutovalidateMode.onUserInteraction,
// 사용자가 입력할 때마다 검증 실행
)
15. Build Context의 context의 역할에 대해 말해주세요.
└ BuildContext는 위젯의 위치를 위젯 트리 내에서 식별하고, 상위 위젯 & 테마 & 미디어 쿼리... 등의 정보에 접근할 수 있게 해주는 핵심 요소입니다.
BuildContext는 Flutter에서 매우 중요한 역할을 합니다.
* 위젯 트리에서의 위치 정보
@override
Widget build(BuildContext context) {
// context는 이 위젯의 위치를 나타냅니다
return Container();
}
● 각 위젯은 빌드될 때, 고유한 BuildContext를 받습니다.
● 이 context는 위젯 트리에서 해당 위젯의 위치를 나타냅니다.
● context를 통해 위젯은 자신의 상위 요소들에 접근할 수 있습니다.
* 상위 위젯에 접근하기
// 가장 가까운 Scaffold 찾기
final scaffold = Scaffold.of(context);
// 가장 가까운 Form 찾기
final form = Form.of(context);
* InheritedWidget을 통한 데이터 접근
// 테마 데이터 접근
final theme = Theme.of(context);
final primaryColor = theme.primaryColor;
// 미디어 쿼리 정보 접근
final screenWidth = MediaQuery.of(context).size.width;
// 현재 로케일 접근
final locale = Localizations.localeOf(context);
* Navigator & 라우팅
// 네비게이션에 사용
Navigator.push(context, MaterialPageRoute(builder: (context) => NextScreen()));
* Provider와 같은 상태 관리 도구 사용
// Provider를 통한 데이터 접근
final myData = Provider.of<MyDataModel>(context);
// Watch vs Read
final watchingData = context.watch<MyModel>(); // 변경 감지
final readOnlyData = context.read<MyModel>(); // 한 번만 읽기
Context는 Flutter의 모든 것의 중심에 있으며, 위젯 트리 내의 정보 & 테마 & 크기 & 의존성... 등 다양한 데이터에 접근하는 데에 필수적입니다.
16. Flutter의 Key에 대해 말해주세요.
└ Key는 위젯을 고유하게 식별하고 위젯 트리가 재구축될 때, 상태를 보존하는 데에 사용됩니다.
Flutter의 Key는 위젯의 ID 역할을 하며, 특히 동적으로 변경되는 위젯 목록에서 중요합니다.
* Key의 필요성
// Key 없이 사용 (문제 발생 가능)
ListView(
children: items.map((item) => ListTile(title: Text(item.title))).toList(),
);
// Key를 사용하여 각 항목 식별
ListView(
children: items.map((item) => ListTile(
key: ValueKey(item.id),
title: Text(item.title),
)).toList(),
);
● Flutter는 위젯 트리를 비교할 때, 동일한 타입과 위치에 있는 위젯은 같은 것으로 간주합니다.
● 동적 목록에서 항목을 추가 & 제거 & 재정렬할 때, Key가 없으면 위젯의 상태가 섞일 수 있습니다.
* 주요 Key 유형
1. ValueKey
// 문자열, 숫자 등의 값으로 키 생성
ValueKey<String>('unique-id')
2. ObjectKey
// 객체 자체를 키로 사용
ObjectKey(myObject)
3. UniqueKey
// 매번 새로운 고유 키 생성
UniqueKey()
4. GlobalKey
// 위젯의 상태에 전역적으로 접근 가능
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
Form(
key: _formKey,
// ...
);
// 다른 곳에서 접근
_formKey.currentState!.validate();
* Key 사용 사례
1. 리스트 항목 재정렬
ListView(
children: myList.map((item) => ListTile(
key: ValueKey(item.id), // id로 식별
title: Text(item.title),
)).toList(),
);
2. 위젯 상태 보존
// 키가 있는 StatefulWidget은 트리에서 위치가 변경되어도 상태 유지
MyStatefulWidget(key: ValueKey('my-widget'))
3. GlobalKey로 다른 위젯의 상태 접근
// 스크롤 컨트롤러에 접근
final GlobalKey<ScrollableState> _scrollKey = GlobalKey();
// 다른 위젯에서 스크롤 시작
_scrollKey.currentState?.position.animateTo(...);
Key를 적절히 사용하면 Flutter 앱의 성능과 사용자 경험을 크게 향상시킬 수 있습니다.
17. Flutter의 Stream과 Stream 유형에 대해 말해주세요.
└ Stream은 비동기 데이터 시퀀스를 처리하는 방법으로, 시간에 따라 발생하는 이벤트나 데이터를 처리하는 데에 이상적입니다.
* Stream 기본 개념
// 기본 스트림 구독
final subscription = myStream.listen(
(data) => print('데이터 수신: $data'),
onError: (error) => print('오류 발생: $error'),
onDone: () => print('스트림 완료'),
cancelOnError: false,
);
// 구독 취소
subscription.cancel();
● Stream은 시간이 지남에 따라, 비동기적으로 데이터가 흐르는 통로입니다.
● Future가 단일 값을 비동기적으로 반환하는 반면, Stream은 여러 값을 시간에 따라 제공합니다.
* Stream 유형
1. SingleSubscriptionStream
● 기본 Stream 유형입니다.
● 한번에 하나의 리스너만 가질 수 있습니다.
● 구독이 취소된 후 다시 구독할 수 없습니다.
2. BroadcastStream
// 브로드캐스트 스트림 생성
final broadcastStream = myStream.asBroadcastStream();
● 여러 리스너를 동시에 가질 수 있습니다.
● 구독자가 없어도 계속 이벤트를 발생시킵니다.
* StreamControlloe 사용
final controller = StreamController<int>();
// 데이터 추가
controller.sink.add(1);
controller.sink.add(2);
// 오류 추가
controller.sink.addError('오류 발생');
// 스트림 구독
controller.stream.listen((data) => print('받은 데이터: $data'));
// 완료 후 컨트롤러 닫기
controller.close();
* Stream 변환 연산자
// 맵핑
stream.map((value) => value * 2);
// 필터링
stream.where((value) => value > 5);
// 시간 제한
stream.timeout(Duration(seconds: 5));
// 중복 제거
stream.distinct();
// 스트림 병합
Stream.merge([stream1, stream2]);
Stream은 실시간 데이터 & 사용자 입력 & 센서 데이터 & 네트워크 이벤트...등 지속적으로 변화하는 데이터를 처리하는 데에 매우 유용합니다.
18. FutureBuilder와 StreamBuilder의 차이점에 대해 말해주세요.
└ FutureBuilder는 일회성 비동기 작업을 처리하지만, StreamBuilder는 연속적인 데이터 흐름을 처리하는 위젯입니다.
FutureBuilder와 StreamBuilder는 모두 Flutter에서 비동기 데이터를 UI에 쉽게 표시할 수 있게 해주는 위젯입니다.
* FutureBuilder
FutureBuilder<String>(
future: fetchData(), // 비동기 데이터 가져오기
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
return Text('Data: ${snapshot.data}');
}
},
)
● 단일 비동기 작업(Future)의 결과를 기다렸다가, 결과에 따라 UI를 구성합니다.
● 한 번 완료되면 다시 실행되지 않는, 일회성 작업에 적합합니다.
● 네트워크 요청 & 파일 로딩과 같은 단일 결과를 반환하는 작업에 주로 사용됩니다.
* StreamBuilder
StreamBuilder<int>(
stream: countStream(), // 데이터 스트림 구독
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
return Text('Count: ${snapshot.data}');
}
},
)
● 지속적으로 데이터를 방출하는 Stream을 구독하고 새 데이터마다 UI를 업데이트합니다.
● 실시간 데이터 & 연속적인 이벤트 & 지속적인 업데이트가 필요한 경우에 적합합니다.
● 실시간 채팅 & 센서 데이터 모니터링 & 주식 가격 업데이트... 등에 사용됩니다.
* 주요 차이점
● FutureBuilder는 한 번 데이터를 로드하고 완료되지만, StreamBuilder는 지속적으로 데이터 흐름을 처리합니다.
● FutureBuilder의 future는 한 번만 실행되지만, StreamBuilder의 stream은 여러 값을 시간에 걸쳐 방출할 수 있습니다.
● StreamBuilder는 사용이 끝나면 메모리 누수를 방지하기 위해 Stream을 닫아야 하지만, FutureBuilder는 이런 관리가 필요 없습니다.
19. Flutter의 3가지 테스트(단위 테스트, 위젯 테스트, 통합 테스트)에 대해 말해주세요.
└ Flutter의 테스트 피라미드는 빠른 단위 테스트 & UI 중심의 위젯 테스트 & 전체 앱 기능을 검증하는 통합 테스트로 구성됩니다.
Flutter는 앱의 품질을 보장하기 위해 세 가지 주요 테스트 방법을 제공합니다.
* 단위 테스트(Unit Test)
// 단위 테스트 예시
void main() {
test('Counter value should be incremented', () {
final counter = Counter();
counter.increment();
expect(counter.value, 1);
});
test('Counter value should be decremented', () {
final counter = Counter();
counter.decrement();
expect(counter.value, -1);
});
}
● 개별 함수 & 메서드 & 클래스의 동작을 검증합니다.
● UI와 독립적이며 가장 빠르게 실행됩니다.
● 비즈니스 로직 & 유틸리티 함수 & 모델 클래스... 등을 테스트하는 데에 적합합니다.
* 위젯 테스트 (Widget Test)
// 위젯 테스트 예시
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// 위젯 빌드
await tester.pumpWidget(MyApp());
// 초기 상태 확인
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// 버튼 탭 동작 시뮬레이션
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// 상태 변화 확인
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
● 개별 위젯이나 위젯 트리의 동작을 검증합니다.
● 실제 디바이스 없이 Flutter 엔진을 사용하여 위젯을 렌더링합니다.
● 위젯의 렌더링 & 상호 작용 & 상태 변화... 등을 테스트할 수 있습니다.
* 통합 테스트(Intergration Test)
// 통합 테스트 예시
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Complete user flow test', (WidgetTester tester) async {
// 앱 시작
await tester.pumpWidget(MyApp());
// 로그인 화면에서 사용자 정보 입력
await tester.enterText(find.byKey(Key('email')), 'test@example.com');
await tester.enterText(find.byKey(Key('password')), 'password123');
await tester.tap(find.byKey(Key('login_button')));
await tester.pumpAndSettle();
// 홈 화면으로 이동했는지 확인
expect(find.text('Welcome!'), findsOneWidget);
// 상품 목록으로 이동
await tester.tap(find.byKey(Key('products_button')));
await tester.pumpAndSettle();
// 상품 선택
await tester.tap(find.text('Product 1'));
await tester.pumpAndSettle();
// 상세 화면에 정보가 표시되는지 확인
expect(find.text('Product Details'), findsOneWidget);
});
}
● 전체 앱 또는 앱의 큰 부분의 동작을 검증합니다.
● 실제 디바이스나 에뮬레이터에서 실행됩니다.
● 여러 위젯 간의 상호 작용 & 네비게이션 & 백엔드 통신... 등을 테스트합니다.
* 각 테스트 유형의 특징
● 단위 테스트는 가장 빠르고 작성하기 쉽지만, UI와의 상호 작용은 테스트하지 않습니다.
● 위젯 테스트는 UI 컴포넌트를 효율적으로 테스트할 수 있지만, 전체 앱 흐름은 테스트하기 어렵습니다.
● 통합 테스트는 가장 현실적인 테스트를 제공하지만, 실행 속도가 느리고 설정이 복작합니다.
20. WidgetsApp과 MaterialApp의 차이점에 대해 말해주세요.
└ MaterialApp은 WidgetsApp을 확장하여 Material Design 요소와 테마를 추가한 고수준 위젯입니다.
WidgetsApp과 MaterialApp은 모두 Flutter 앱의 루트 위젯으로 사용될 수 있지만, 제공하는 기능과 디자인 철학에 차이가 있습니다.
* WidgetsApp
WidgetsApp(
color: Colors.blue,
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) =>
PageRouteBuilder<T>(
settings: settings,
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) => builder(context),
),
home: MyCustomWidget(),
)
● Flutter의 가장 기본적은 앱 구조를 제공하는 저수준 위젯입니다.
● 특정 디자인 시스템에 종속되지 않는 기본 앱 프레임워크입니다.
● 라우팅 & 위젯 바인딩 & 지역화... 등 핵심 앱 기능만 제공합니다.
● 디자인 요소나 테마는 포함하지 않습니다.
● 사용자 정의 디자인 시스템을 구축하거나 Material & Cupertino 디자인을 사용하지 않을 때, 적합합니다.
* MaterialApp
MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.system,
home: MyHomePage(),
)
● WidgetsApp을 상속하여 Google의 Material Design 시스템을 통합한 고수준 위젯입니다.
● Material 디자인 요소(AppBar & FloatingActionButton & Card... 등)와 테마를 자동으로 제공합니다.
● 내장된 테마 지원 & 네비게이션 드로어 & 앱 바... 등 추가 기능을 포함합니다.
● 대부분의 Flutter 앱에서 사용되는 표준 앱 구조입니다.
* 주요 차이점
● 상속 관계: MaterialApp은 WidgetsApp을 확장(내부적으로 사용)합니다.
● 디자인 시스템: MaterialApp은 Material Design을 기본으로 제공하지만, WidgetsApp은 어떤 디자인 시스템도 제공하지 않습니다.
● 기본 위젯: MaterialApp은 Scaffold & AppBar... 등 Material 위젯을 사용하기 쉽게 해주는 반면, WidgetsApp은 이런 편의성을 제공하지 않습니다.
● 테마: MaterialApp은 포괄적인 테마 지원(밝은/어두운 테마 & 색상 시스템)을 제공하지만, WidgetsApp은 기본 테마 지원이 없습니다.
21. Abstract (extends) & Interface (implements) & Mixin (with)에 대해 말해주세요.
└ Dart에서 추상 클래스(extends)는 기본 구현을 제공하고, 인터페이스(implements)는 계약을 정의하며, 믹스인(with)은 여러 클래스에 코드를 재사용하는 메커니즘입니다.
Dart는 객체지향 프로그래밍을 지원하며, 코드 구조화와 재사용을 위한 세 가지 주요 메커니즘을 제공합니다.
* 추상 클래스(Abstract Class) - extends
// 추상 클래스 예시
abstract class Vehicle {
// 구현된 메서드
void startEngine() {
print('Engine started');
}
// 추상 메서드 (구현 없음)
void accelerate();
// 추상 getter
int get wheels;
}
// 추상 클래스 상속
class Car extends Vehicle {
@override
void accelerate() {
print('Car is accelerating');
}
@override
int get wheels => 4;
}
● abstract 키워드로 선언된 클래스로, 직접 인스턴스화할 수 없습니다.
● 구현된 메서드와 추상 메서드(구현 없이 선언만 된 메서드)를 모두 포함할 수 있습니다.
● 하위 클래스가 extends 키워드를 사용하여 상속받습니다.
● 하위 클래스는 추상 메서드를 반드시 구현해야 합니다.
● Dart에서는 단일 상속만 지원하므로, 한 클래스는 하나의 클래스만 extends 할 수 있습니다.
* 인터페이스(Interface) - implements
// 인터페이스 역할을 하는 클래스
class DatabaseConnector {
void connect() => print('Connected to database');
void disconnect() => print('Disconnected from database');
Future<List<Map>> query(String sql) async => [];
}
// 인터페이스 구현
class MySQLConnector implements DatabaseConnector {
@override
void connect() {
print('Connected to MySQL database');
}
@override
void disconnect() {
print('Disconnected from MySQL database');
}
@override
Future<List<Map>> query(String sql) async {
print('Executing query in MySQL: $sql');
return [];
}
}
● Dart에는 interface 키워드가 없지만, 모든 클래스는 암시적으로 인터페이스를 정의합니다.
● 클래스를 implements하면 해당 클래스의 모든 메서드와 속성을 구현해야 합니다.
● 구현하는 클래스는 메서드 시그니처만 사용하며, 원본 구현은 상속받지 않습니다.
● 여러 인터페이스를 구현할 수 있어서 다중 상속의 일종을 제공합니다.
* 믹스인(Mixin) - with
// 믹스인 정의
mixin Logger {
void log(String message) {
print('LOG: $message');
}
}
mixin Analytics {
void trackEvent(String event) {
print('EVENT TRACKED: $event');
}
}
// 믹스인 사용
class UserService with Logger, Analytics {
void createUser(String username) {
log('Creating user: $username');
trackEvent('user_created');
// 사용자 생성 로직
}
}
● 여러 클래스에 코드를 재사용하기 위한 방법입니다.
● mixin 키워드로 선언되며, 인스턴스화할 수 없습니다.
● 클래스는 with 키워드를 사용하여 여러 믹스인을 포함할 수 있습니다.
● 상속 계층 구조 없이 기능을 '혼합'하는 방법을 제공합니다.
● 다중 상속의 문제([1]다이아몬드 문제)를 피하면서 코드 재사용을 가능하게 합니다.
* 세 가지 메커니즘의 비교
1. 사용 시기
● 추상 클래스(extneds): 공통 기능을 가진 관련 클래스들의 기본 구현을 제공할 때, 사용합니다.
● 인터페이스(implements): 서로 다른 클래스가 동일한 계약을 준수해야 할 때, 사용합니다.
● 믹스인(with): 독립적인 기능을 여러 클래스에 추가하고자 할 때, 사용합니다.
2. 제한 사항
● extends: 단일 상속만 가능합니다. (한 클래스는 하나의 클래스만 상속)
● implements: 여러 인터페이스를 구현할 수 있지만, 모든 메서드를 직접 구현해야 합니다.
● with: 여러 믹스인을 사용할 수 있으며, 충돌이 있을 경우 마지막으로 지정된 믹스인의 구현을 우선합니다.
* [1]다이아몬드 문제(Diamond Problem)
다이아몬드 문제는 다중 상속을 지원하는 프로그래밍 언어에서 발생할 수 있는 모호성 문제입니다. 이름이 "다이아몬드"인 이유는, 상속 계층 구조를 도식화했을 때, 형태가 다이아몬드 모양을 띠기 때문입니다.
A
/ \
B C
\ /
D
다이아몬드 문제는 다음과 같은 상황에서 발생합니다.
1. 기본 클래스 A가 있습니다.
2. 클래스 B와 C가 모두 A를 상속받습니다.
3. 클래스 D가 B와 C를 모두 상속받습니다.
※ 발생 가능한 문제
문제는 클래스 A에 someMethod라는 메서드가 있고, B와 C클래스가 이 메서드를 다르게 오버라이드한 경우에 발생합니다.
1. D클래스가 someMethod를 호출할 때, B의 버전을 사용해야 할지, 아니면 C의 버전을 사용해야 할지 모호합니다.
2. A의 속성이 B와 C에서 중복으로 상속되어 D에서 두 번 존재해야 하는지, 아니면 한 번만 존재해야 하는지 모호합니다.
※ Dart에서의 다이아몬드 문제 해결
Dart는 다중 상속을 직접적으로 지원하지 않기 때문에 전통적인 다이아몬드 문제가 발생하지 않습니다. 대신, 믹스인을 사용할 때 비슷한 충돌이 발생할 수 있지만, Dart는 명확한 규칙을 가지고 있습니다.
mixin A {
void method() {
print('A.method');
}
}
mixin B {
void method() {
print('B.method');
}
}
class C with A, B {
// B의 method()가 A의 method()를 오버라이드
}
void main() {
C().method(); // 출력: B.method
}
위의 예시에서 믹스인이 적용되는 순서에 따라 충돌이 해결됩니다. "마지막으로 지정된 믹스인(B)"의 구현이 우선됩니다.
* 믹스인을 사용한 더 복잡한 예제
class Animal {
void breathe() {
print('Animal breathing');
}
}
mixin Mammal on Animal {
void feedMilk() {
print('Feeding milk');
}
@override
void breathe() {
super.breathe();
print('Mammal breathing');
}
}
mixin Bird on Animal {
void layEggs() {
print('Laying eggs');
}
@override
void breathe() {
super.breathe();
print('Bird breathing');
}
}
// Platypus는 Animal을 상속받고, Mammal과 Bird 믹스인을 사용
class Platypus extends Animal with Mammal, Bird {
// 여기서 breathe()는 어떻게 동작할까요?
}
void main() {
Platypus().breathe();
// 출력:
// Animal breathing
// Mammal breathing
// Bird breathing
}
위의 예제에서 breathe 메서드 호출은 상속과 믹스인 순서에 따라 처리됩니다.
1. Animal.breathe 실행
2. Mammal.breathe에서 super.breathe가 Animal.breathe를 호출 후, 자신의 코드 실행
3. Bird.breathe에서 super.breathe가 Mammal.breathe를 호출 후, 자신의 코드 실행
이처럼 Dart는 믹스인을 통해 다중 상속의 이점을 제공하면서도 전통적인 다이아몬드 문제를 피할 수 있는 방법을 제공합니다.
※ 믹스인과 super 호출
Dart에서 믹스인의 super 호출은 믹스인 체인에서 이전 믹스인이나 상위 클래스를 참조합니다. 이를 통해 다이아몬드 문제가 발생할 수 있는 상황에서도 메서드 호출 순서가 명확해집니다.
이러한 설계는 다중 상속의 유연성을 제공하면서도 다이아몬드 문제로 인한 모호성을 피할 수 있게 해줍니다.
긴 글을 끝까지 읽어주셔서 감사합니다!
Flutter 면접 질문들을 정리하면서, 특히 FutureBuilder와 StreamBuilder의 차이점, 테스트 방법론, 그리고 Dart의 객체지향 프로그래밍 개념과 같은 중요한 주제들을 더 깊이 이해할 수 있었습니다.
그럼, 다음 포스트에서 인사드리겠습니다~!
'Knowledge > Flutter' 카테고리의 다른 글
[Flutter] 면접 질문을 통해 알아보는 플러터 (4) - 경험 (0) | 2025.03.04 |
---|---|
[Flutter] 면접 질문을 통해 알아보는 플러터 (2) - 개념 (0) | 2025.03.04 |
[Flutter] 면접 질문을 통해 알아보는 플러터 (1) - 목차 (1) | 2025.03.03 |