Flutter 완벽 가이드 | 크로스플랫폼 앱·Dart·Widgets·State·실전 활용
이 글의 핵심
Flutter 완벽 가이드에 대해 정리한 개발 블로그 글입니다. Flutter로 iOS/Android/Web 앱을 개발하는 완벽 가이드입니다. Widgets, State Management, Navigation, API, 배포까지 실전 예제로 정리했고, 위젯·엘리먼트·렌더 트리,… 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있도록 구성했습니다. 관련 키워드:…
이 글의 핵심
Flutter로 iOS/Android/Web 앱을 개발하는 완벽 가이드입니다. Widgets, State Management, Navigation, API, 배포까지 실전 예제로 정리했고, 위젯·엘리먼트·렌더 트리, 빌드/레이아웃/페인트 파이프라인, 플랫폼 채널, 상태 관리 내부, 프로덕션 패턴 등 엔진·프레임워크 관점의 심화 내용을 포함했습니다.
실무 경험 공유: Flutter로 iOS/Android/Web 앱을 동시에 출시하면서, 일관된 UI와 60fps 성능을 제공할 수 있었던 경험을 공유합니다.
들어가며: “일관된 UI가 필요해요”
실무 문제 시나리오
시나리오 1: 플랫폼마다 UI가 달라요
React Native는 Native 컴포넌트를 사용합니다. Flutter는 자체 렌더링으로 일관됩니다. 시나리오 2: 성능이 중요해요
Bridge 오버헤드가 있습니다. Flutter는 Native로 컴파일됩니다. 시나리오 3: 웹도 지원하고 싶어요
별도 개발이 필요합니다. Flutter는 웹도 지원합니다.
1. Flutter란?
핵심 특징
Flutter는 Google의 크로스플랫폼 UI 프레임워크입니다. 주요 장점:
- 크로스플랫폼: iOS, Android, Web, Desktop
- 빠른 성능: Native 컴파일
- 일관된 UI: 자체 렌더링 엔진
- Hot Reload: 즉시 반영
- 풍부한 Widgets: Material + Cupertino
2. 설치 및 프로젝트 생성
설치
# macOS
brew install flutter
# Windows
# Flutter SDK 다운로드 및 PATH 추가
프로젝트 생성
flutter create my_app
cd my_app
flutter run
3. 기본 Widgets
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Hello Flutter!', style: TextStyle(fontSize: 24)),
SizedBox(height: 16),
ElevatedButton(
onPressed: () {
print('Button pressed');
},
child: Text('Click me'),
),
],
),
),
);
}
}
4. Stateful Widget
class CounterScreen extends StatefulWidget {
@override
_CounterScreenState createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Count:', style: TextStyle(fontSize: 18)),
Text('$_counter', style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold)),
SizedBox(height: 16),
ElevatedButton(
onPressed: _incrementCounter,
child: Text('Increment'),
),
],
),
),
);
}
}
심화: 위젯 트리·엘리먼트 트리·렌더 트리
Flutter UI는 겉으로는 위젯 트리 하나처럼 보이지만, 프레임워크 내부에서는 설정(Widget), 수명·연결(Element), 픽셀·레이아웃(RenderObject) 이 분리된 세 층으로 동작합니다. 이 분리가 Hot Reload와 선언형 UI를 가능하게 하는 핵심입니다.
위젯(Widget) 은 불변(immutable) 설명서입니다. build()가 매번 새 Widget 인스턴스를 만들어도 비용이 낮은 이유는, 실제 트리를 “통째로 교체”하지 않고 기존 엘리먼트가 새 위젯과 타입·키를 비교해 갱신하기 때문입니다. StatelessWidget/StatefulWidget은 모두 이 불변 계층에 속합니다.
엘리먼트(Element) 는 가변 계층으로, 위젯과 렌더 객체 사이를 잇습니다. Element는 위젯 수명 주기(마운트/언마운트), 부모-자식 연결, InheritedWidget 의존성 등을 담당합니다. StatefulElement는 State 객체를 붙잡고 setState 시 해당 서브트리만 다시 빌드할 수 있게 스케줄링합니다. 같은 런타임 타입과 Key가 유지되면 엘리먼트와 State가 재사용되고, 타입이 바뀌거나 키가 달라지면 기존 서브트리를 버리고 새로 붙입니다.
렌더 객체(RenderObject) 는 레이아웃 제약(Constraints), 크기(Size), 페인트, 히트 테스트를 담당하는 렌더 트리의 노드입니다. RenderObjectWidget(예: Padding, Text)이 대응하는 RenderObject를 만들거나 갱신합니다. 렌더 트리는 위젯 트리와 1:1이 아니며, RenderObjectElement 등을 통해 필요한 만큼만 연결됩니다.
실무에서 이 구조를 염두에 두면 다음이 명확해집니다. (1) 불필요한 위젯 타입 변경은 엘리먼트 재생성을 유발하고, (2) Key 남용/누락은 리스트 재정렬 시 상태가 엉키는 원인이 되며, (3) 무거운 작업을 build 안에 두면 매 프레임 위젯 재구성과 겹쳐 jank가 납니다.
심화: 빌드·레이아웃·페인트 파이프라인
한 프레임에서 Flutter는 대략 빌드 → 레이아웃 → 페인트 순으로 진행됩니다. 각 단계는 서로 다른 트리에서 일어나며, 스케줄러(SchedulerBinding)가 VSYNC에 맞춰 작업을 묶습니다.
1) 빌드(Build)
setState, notifyListeners, 라우트 전환 등으로 “다시 그려야 함”이 표시되면, 해당 Element 서브트리에서 Widget.build가 호출되어 새 위젯 트리 조각이 만들어집니다. 이때 레이아웃이나 페인트는 아직 확정되지 않습니다. 빌드는 “무엇을 그릴지”를 결정하는 단계입니다.
2) 레이아웃(Layout)
부모가 자식에게 제약(Constraints) 을 전달하고, 자식은 그 안에서 크기(Size) 를 결정해 올립니다(제약은 아래로, 크기는 위로). Flex, Row, Column 등은 이 규칙 위에서 동작합니다. 레이아웃이 바뀌면 해당 RenderObject와 필요 시 자식까지 마크되어 이후 레이아웃 패스에서 다시 계산됩니다.
3) 페인트(Paint) 및 합성
레이아웃이 끝나면 RenderObject.paint가 호출되어 레이어(Layer) 트리에 그리기 명령이 쌓입니다. RepaintBoundary는 페인트 경계를 나눠 불필요한 전체 재페인트를 줄입니다. 스크롤·애니메이션·오버레이는 레이어 분리와 밀접합니다.
성능 관점에서 기억할 점은 다음과 같습니다. 빌드는 자주 일어나도 되지만 가볍게, 레이아웃/페인트는 덜 자주 일어나게 설계하는 것이 유리합니다. const 생성자로 빌드 결과를 안정화하고, 큰 리스트는 ListView.builder로 뷰포트 밖 빌드를 피하며, 무거운 연산은 Isolate나 플랫폼 쪽으로 넘깁니다.
심화: 플랫폼 채널(Platform Channel) 메커니즘
Flutter 엔진은 Dart VM과 플랫폼(iOS/Android 등) 사이를 비동기 메시지로 연결합니다. Dart의 MethodChannel·BasicMessageChannel·EventChannel은 모두 이 바이너리 메시지 파이프 위에 올라간 API입니다.
MethodChannel은 요청-응답 패턴에 가깝습니다. Dart에서 invokeMethod를 호출하면 직렬화된 메시지가 엔진을 거쳐 플랫폼 쪽 핸들러로 전달되고, 결과가 다시 Dart로 돌아옵니다. 메서드 이름과 인자는 코덱(예: StandardMethodCodec, JSONMethodCodec)에 따라 인코딩됩니다.
BasicMessageChannel은 방향성 있는 메시지 교환에 쓰이고, EventChannel은 스트림(지속 이벤트)을 플랫폼에서 Dart로 흘려보낼 때 자주 쓰입니다(예: 센서, 배터리, 네이티브 콜백 스트림).
주의할 내부적 사실은 다음과 같습니다. 채널은 기본적으로 비동기이며, 플랫폼 스레드와 UI 스레드 제약이 플랫폼마다 다릅니다. 네이티브에서 UI를 건드릴 때는 플랫폼 메인 스레드 규칙을 지켜야 하고, Dart 쪽에서는 WidgetsBinding과의 재진입을 피하는 편이 안전합니다. 고빈도·대용량 데이터를 매 프레임 주고받는 설계는 채널 오버헤드가 커지므로, 가능하면 배치 처리, 공유 버퍼, 또는 dart:ffi로 대체하는 방안을 검토합니다.
간단한 예시는 다음과 같습니다(실제 네이티브 등록 코드는 플랫폼별로 추가).
import 'package:flutter/services.dart';
class BatteryChannel {
static const _channel = MethodChannel('com.example.app/battery');
Future<int?> getLevel() async {
try {
final level = await _channel.invokeMethod<int>('getBatteryLevel');
return level;
} on PlatformException catch (e) {
// 에러 코드·메시지는 플랫폼과 사전 약속
throw Exception('Battery: ${e.code} ${e.message}');
}
}
}
심화: 상태 관리의 내부 동작
setState는 단순히 변수를 바꾸는 것이 아니라, Element에 “빌드가 필요함” 을 표시하고 프레임에 빌드 작업을 스케줄합니다. BuildOwner는 dirty 엘리먼트를 모아 한 프레임 안에서 정리합니다.
InheritedWidget은 “위에서 아래로 전달되는 데이터”를 엘리먼트 트리의 의존성 그래프로 구현합니다. 자식이 dependOnInheritedElement를 통해 의존을 등록하면, 상위 InheritedWidget이 갱신될 때 의존한 자식만 다시 빌드됩니다. Theme, MediaQuery 등이 이 패턴입니다.
Provider(및 Listenable/ChangeNotifier)는 보통 InheritedWidget + Listenable 조합으로, Consumer는 InheritedNotifier 계열을 통해 listen하여 notifyListeners 시 필요한 위젯만 리빌드합니다. Riverpod은 InheritedWidget 대신 컴파일 타임 안전성과 스코프를 강화한 별도의 프로바이더 그래프를 둔 것으로 이해하면 내부 철학이 잡힙니다.
요약하면, 상태 관리 프레임워크의 차이는 “상태를 어디에 두느냐”보다 누가 어떤 Element 서브트리를 다시 build하게 하느냐, 의존성 추적을 어떻게 하느냐의 설계 차이입니다. 디버깅 시 Flutter DevTools의 Rebuild Highlights로 과도한 리빌드를 잡는 것이 실전에서 가장 효과적입니다.
심화: 프로덕션 Flutter 패턴
아키텍처
화면별 로직만으로는 커지면 한계가 있으므로, 레이어드 구조(presentation / domain / data)나 feature 단위 모듈로 경계를 나눕니다. 상태는 화면 단위 ChangeNotifier/Notifier부터 시작해 도메인이 커지면 Bloc, Riverpod, get_it + injectable 등 팀 규모에 맞게 도입합니다.
성능·안정성
const생성자와 불변 모델로 빌드 비용 절감- 긴 리스트·그리드는 빌더 위젯과 캐시 전략
- 이미지는
cacheWidth/cacheHeight, 적절한 포맷(WebP 등) FlutterError.onError,runZonedGuarded로 비동기 예외까지 수집하고, Sentry/Crashlytics로 연결- 테스트:
widget_test, Golden test(픽셀 회귀), 통합 테스트는 팀에 맞게 최소 세트 고정
빌드·배포
--dart-define / --dart-define-from-file로 환경 분리, flavor(Android productFlavors, iOS scheme)로 스테이징·프로덕션 분기. CI에서는 flutter test, flutter analyze, dart format을 게이트로 두고, 코드 서명·스토어 제출은 파이프라인 문서화합니다.
플랫폼·웹
모바일과 웹은 렌더러·스크롤·라우팅 차이가 있으므로 kIsWeb 분기나 ResponsiveFramework 등으로 레이아웃을 나눕니다. 접근성(semantics), 국제화(intl), 오른쪽-왼쪽 언어는 초기에 넣을수록 비용이 적습니다.
5. Navigation
다음은 Dart 예제 코드입니다.
// 화면 이동
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailsScreen()),
);
// 뒤로가기
Navigator.pop(context);
// 데이터 전달
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailsScreen(userId: 1),
),
);
Named Routes
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomeScreen(),
'/details': (context) => DetailsScreen(),
},
);
// 사용
Navigator.pushNamed(context, '/details');
6. State Management (Provider)
위 「심화: 상태 관리의 내부 동작」 에서 setState, InheritedWidget, 리빌드 스케줄링을 엔진 관점에서 다룹니다. 여기서는 대표적인 선언적 API인 Provider 사용 예입니다.
flutter pub add provider
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Counter with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => Counter(),
child: MyApp(),
),
);
}
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Count:'),
Consumer<Counter>(
builder: (context, counter, child) {
return Text('${counter.count}', style: TextStyle(fontSize: 48));
},
),
ElevatedButton(
onPressed: () {
context.read<Counter>().increment();
},
child: Text('Increment'),
),
],
),
),
);
}
}
7. API 호출
flutter pub add http
import 'package:http/http.dart' as http;
import 'dart:convert';
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'],
);
}
}
Future<List<User>> fetchUsers() async {
final response = await http.get(Uri.parse('https://api.example.com/users'));
if (response.statusCode == 200) {
List<dynamic> body = jsonDecode(response.body);
return body.map((json) => User.fromJson(json)).toList();
} else {
throw Exception('Failed to load users');
}
}
class UserListScreen extends StatefulWidget {
@override
_UserListScreenState createState() => _UserListScreenState();
}
class _UserListScreenState extends State<UserListScreen> {
late Future<List<User>> futureUsers;
@override
void initState() {
super.initState();
futureUsers = fetchUsers();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Users')),
body: FutureBuilder<List<User>>(
future: futureUsers,
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
final user = snapshot.data![index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
);
},
);
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
return Center(child: CircularProgressIndicator());
},
),
);
}
}
8. Form
class LoginForm extends StatefulWidget {
@override
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
void _handleSubmit() {
if (_formKey.currentState!.validate()) {
print('Email: ${_emailController.text}');
print('Password: ${_passwordController.text}');
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
decoration: InputDecoration(labelText: 'Email'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter email';
}
return null;
},
),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter password';
}
return null;
},
),
SizedBox(height: 16),
ElevatedButton(
onPressed: _handleSubmit,
child: Text('Submit'),
),
],
),
);
}
}
정리 및 체크리스트
핵심 요약
- Flutter: 크로스플랫폼 UI 프레임워크
- Dart: 프로그래밍 언어
- 빠른 성능: Native 컴파일
- 일관된 UI: 자체 렌더링(Skia/Impeller 등 렌더 백엔드)
- Hot Reload: 즉시 반영(위젯은 불변, 엘리먼트가 갱신)
- 세 트리: Widget(설정) / Element(수명·연결) / RenderObject(레이아웃·페인트)
- 한 프레임: 빌드 → 레이아웃(제약↓ 크기↑) → 페인트·합성
- 플랫폼 연동: MethodChannel 등 비동기 메시지 + 코덱
구현 체크리스트
- Flutter 설치
- 프로젝트 생성
- 기본 Widgets 사용
- (심화) 트리·파이프라인 이해 후 const/Key/리스트 최적화
- State Management
- Navigation 구현
- API 호출
- Form 구현
- (필요 시) Platform Channel 또는 FFI로 네이티브 연동
- 배포(flavor·
dart-define·CI)
같이 보면 좋은 글
이 글에서 다루는 키워드
Flutter, Dart, Mobile, iOS, Android, Cross-platform, Web
내부 동작과 핵심 메커니즘
이 글의 주제는 「Flutter 완벽 가이드 | 크로스플랫폼 앱·Dart·Widgets·State·실전 활용」입니다. 앞선 튜토리얼을 구현·런타임 관점에서 다시 압축합니다. 구성 요소 간 책임 분리와 관측 가능한 지점을 기준으로 “입력이 어디서 검증되고, 핵심 연산이 어디서 일어나며, 부작용(I/O·네트워크·디스크)·동시성이 어디서 터지는가”를 한 장면으로 그리면 장애 분석이 빨라집니다.
처리 파이프라인(개념도)
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
경계에서의 지연·실패(시퀀스 관점)
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(프로세스·런타임·게이트웨이) participant D as 의존성(외부 API·DB·큐) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
알고리즘·프로토콜·리소스 관점 체크포인트
- 불변 조건(Invariant): 각 단계가 만족해야 하는 조건(버퍼 경계, 프로토콜 상태, 트랜잭션 격리, 파일 디스크립터 상한)을 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 동일 입력에 동일 출력이 보장되는 순수 층과, 시간·네트워크·스레드 스케줄에 의해 달라질 수 있는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화/역직렬화, 문자 인코딩, syscall 횟수, 락 경합, GC·할당, 캐시 미스처럼 누적 비용을 의심 목록에 넣습니다.
- 백프레셔: 생산자가 소비자보다 빠를 때(소켓 버퍼, 큐 깊이, 스트림) 어디서 어떤 신호로 속도를 줄일지 정의합니다.
프로덕션 운영 패턴
실서비스에서는 기능과 함께 관측·배포·보안·비용·규제가 동시에 요구됩니다.
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율/지연 분위수(p95/p99), 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시 계층·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션 호환성·플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·파일 디스크립터·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 가능한 한 프로덕션에 가깝게 맞추는 것이 재현율을 높입니다.
확장 예시: 엔드투엔드 미니 시나리오
「Flutter 완벽 가이드 | 크로스플랫폼 앱·Dart·Widgets·State·실전 활용」을 실제 배포·운영 흐름으로 옮긴 체크리스트형 시나리오입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드 표를 API 또는 이벤트 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 한 화면(로그+메트릭+트레이스)에서 추적한다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지(또는 피처 플래그) 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값이 기대 범위인지 본다.
의사코드 스케치(프레임워크 무관)
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request) // 경계에서 거절
authorize(validated, ctx) // 권한·테넌트
result = domainCore(validated) // 순수에 가까운 규칙
persistOrEmit(result, idempotentKey) // I/O: 멱등·재시도 정책
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성 불안정, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정이 로컬과 다름 | 프로필·시크릿·기본값, 지역 리전 | 단일 소스(예: 스키마 검증된 설정)와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
자주 묻는 질문 (FAQ)
Q. React Native와 비교하면 어떤가요?
A. Flutter가 더 빠르고 일관된 UI를 제공합니다. React Native는 JavaScript 생태계를 활용할 수 있습니다.
Q. 학습 곡선은 어떤가요?
A. Dart를 배워야 해서 초반에는 어렵지만, 익숙해지면 생산적입니다.
Q. 웹도 지원하나요?
A. 네, Flutter Web으로 웹 앱도 만들 수 있습니다.
Q. 프로덕션에서 사용해도 되나요?
A. 네, Google, Alibaba, BMW 등 대기업에서 사용하고 있습니다.
Q. 위젯 트리와 렌더 트리는 같은가요?
A. 아닙니다. 위젯은 불변 설정이고, 엘리먼트가 수명을 관리하며, 렌더 객체가 레이아웃·페인트를 담당합니다. 같은 화면이라도 구조가 1:1로 대응하지 않을 수 있습니다.
Q. 네이티브 기능은 어떻게 연동하나요?
A. MethodChannel 등 플랫폼 채널로 Dart와 네이티브 코드가 비동기 메시지를 주고받습니다. 고성능·공유 메모리가 필요하면 dart:ffi를 검토합니다.