Flutter 튜토리얼 78편: Flutter 내부 구조 이해

요약#

핵심 요지#

  • Flutter는 세 가지 트리(Widget, Element, RenderObject)를 사용하여 UI를 관리합니다.
  • 적극적인 합성(Aggressive Composability)으로 작은 위젯들을 조합해 복잡한 UI를 구성합니다.
  • 선형 이하(Sublinear) 알고리즘으로 레이아웃과 빌드 성능을 최적화합니다.
  • 무한 스크롤 리스트는 뷰포트 인식 레이아웃과 주문형 위젯 빌드로 구현됩니다.

문서가 설명하는 범위#

Flutter 공식 문서의 Architectural overviewInside Flutter를 기반으로 Flutter의 계층 구조, 세 가지 트리 시스템, 성능 최적화 기법을 설명합니다.

참고 자료#

문제 상황#

Flutter 앱을 개발하다 보면 여러 의문이 생깁니다.

  • Widget을 아무리 많이 만들어도 왜 성능이 괜찮을까요?
  • setState()를 호출하면 내부에서 무슨 일이 일어날까요?
  • Hot Reload는 어떻게 상태를 유지하면서 UI를 업데이트할까요?
  • 무한 스크롤 리스트는 어떻게 수천 개의 아이템을 부드럽게 처리할까요?

이러한 질문에 답하려면 Flutter의 내부 구조를 이해해야 합니다.

해결 방법#

챕터 1: Flutter 아키텍처 계층#

Why: 왜 계층 구조가 중요한가요?#

Flutter는 계층화된 아키텍처1로 설계되어 있습니다. 각 계층이 독립적이므로 필요한 부분만 교체하거나 확장할 수 있습니다.

What: Flutter의 계층 구조#

Flutter는 크게 세 가지 계층으로 나뉩니다.

1. Framework (Dart)

개발자가 주로 사용하는 계층입니다.

┌─────────────────────────────────────────┐
│ Material / Cupertino │ ← 디자인 시스템
├─────────────────────────────────────────┤
│ Widgets │ ← UI 구성 요소
├─────────────────────────────────────────┤
│ Rendering │ ← 레이아웃, 페인팅
├─────────────────────────────────────────┤
│ Foundation │ ← 기본 유틸리티
└─────────────────────────────────────────┘

2. Engine (C++)

Flutter의 핵심 런타임입니다.

  • Skia: 2D 그래픽 엔진
  • Dart VM: Dart 코드 실행
  • Text rendering: 텍스트 렌더링

3. Embedder (Platform-specific)

각 플랫폼(iOS, Android, Web 등)에 Flutter를 연결합니다.

How: 계층 구조 활용하기#

일반적인 앱 개발에서는 Widget 계층만 사용해도 충분합니다.

// Widget 계층에서 작업
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Text('Hello, Flutter!'),
),
),
);
}
}

커스텀 렌더링이 필요하면 Rendering 계층에 접근합니다.

// CustomPainter로 직접 그리기
class MyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = Colors.blue;
canvas.drawCircle(
Offset(size.width / 2, size.height / 2),
50,
paint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

Watch out: 주의할 점#

  • 대부분의 경우 Widget 계층만으로 충분합니다.
  • Rendering 계층을 직접 다루면 복잡도가 급격히 증가합니다.
  • 플랫폼별 기능이 필요하면 Platform Channel을 사용하세요.

챕터 2: 세 가지 트리 시스템#

Why: 왜 트리가 세 개나 필요한가요?#

Flutter는 Widget, Element, RenderObject라는 세 가지 트리를 사용합니다. 각 트리가 서로 다른 역할을 담당하여 성능과 유연성을 모두 확보합니다.

What: 각 트리의 역할#

Widget Tree - 설계도

위젯은 불변(immutable)2 객체입니다. UI가 어떻게 보여야 하는지 설명하는 청사진 역할을 합니다.

// Widget은 불변 - 매번 새로 생성
Container(
color: Colors.blue,
child: Text('Hello'),
)

Element Tree - 관리자

Element는 Widget과 RenderObject 사이를 연결합니다. Widget의 생명주기를 관리하고 상태(State)를 보관합니다.

Widget (설계도) → Element (관리자) → RenderObject (실제 작업자)

RenderObject Tree - 실제 작업자

실제 레이아웃 계산과 화면 그리기를 담당합니다.

// RenderObject가 하는 일
// 1. 레이아웃: 크기와 위치 계산
// 2. 페인팅: 화면에 그리기
// 3. 히트 테스트: 터치 이벤트 처리

How: 세 트리의 상호작용#

setState() 호출 시 일어나는 일을 살펴봅시다.

class Counter extends StatefulWidget {
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int count = 0;
void increment() {
setState(() { // 1. Element를 dirty로 표시
count++;
});
}
@override
Widget build(BuildContext context) { // 2. 새 Widget 생성
return Text('Count: $count');
}
}

실행 순서는 다음과 같습니다.

  1. setState()가 Element를 dirty로 표시합니다.
  2. 다음 프레임에서 dirty Element의 build()를 호출합니다.
  3. 새 Widget과 기존 Widget을 비교합니다.
  4. 변경된 부분만 RenderObject를 업데이트합니다.

Watch out: 주의할 점#

  • Widget은 매 빌드마다 새로 생성되지만, Element와 RenderObject는 재사용됩니다.
  • const 생성자를 사용하면 동일한 Widget 인스턴스를 재사용할 수 있습니다.
  • Key를 적절히 사용하면 Element 재사용을 제어할 수 있습니다.

챕터 3: 적극적인 합성(Aggressive Composability)#

Why: 왜 작은 위젯으로 나누나요?#

Flutter는 모든 것을 위젯으로 만듭니다. Padding도 위젯이고, Center도 위젯입니다.

// 다른 프레임워크에서는 속성
<div style="padding: 16px">...</div>
// Flutter에서는 위젯
Padding(
padding: EdgeInsets.all(16),
child: ...,
)

이렇게 하면 조합의 자유도가 높아지고 재사용성이 증가합니다.

What: 합성의 장점#

1. 단일 책임

각 위젯이 하나의 역할만 수행합니다.

// 각 위젯은 하나의 책임만
Center( // 중앙 정렬
child: Padding( // 여백 추가
padding: EdgeInsets.all(16),
child: Container( // 배경색, 크기
color: Colors.blue,
width: 100,
height: 100,
),
),
)

2. 유연한 조합

필요한 기능만 선택적으로 조합할 수 있습니다.

// Padding 없이 Center만
Center(child: Text('Hello'))
// Center 없이 Padding만
Padding(
padding: EdgeInsets.all(16),
child: Text('Hello'),
)

3. 테스트 용이성

작은 단위로 나뉘어 있어 테스트하기 쉽습니다.

How: 효율적인 합성 패턴#

재사용 가능한 위젯을 만드세요.

// 재사용 가능한 카드 위젯
class InfoCard extends StatelessWidget {
final String title;
final String subtitle;
final VoidCallback? onTap;
const InfoCard({
super.key,
required this.title,
required this.subtitle,
this.onTap,
});
@override
Widget build(BuildContext context) {
return Card(
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 4),
Text(subtitle, style: Theme.of(context).textTheme.bodySmall),
],
),
),
),
);
}
}

Watch out: 주의할 점#

  • 위젯이 많아도 성능 문제는 거의 없습니다(최적화된 알고리즘 덕분).
  • 하지만 불필요한 중첩은 코드 가독성을 해칩니다.
  • 3단계 이상 중첩되면 별도 위젯으로 추출하는 것을 고려하세요.

챕터 4: 선형 이하(Sublinear) 알고리즘#

Why: 왜 성능이 좋은가요?#

Flutter는 O(N) 미만의 알고리즘을 사용합니다. 위젯 수가 늘어나도 성능이 선형적으로 증가하지 않습니다.

What: 최적화 기법들#

1. 레이아웃 최적화

Flutter의 레이아웃은 한 패스(single pass)로 완료됩니다.

부모 → 자식: 제약 조건 전달
자식 → 부모: 크기 반환
┌─────────────────────┐
│ Constraints ↓ │
│ ┌───────────────┐ │
│ │ Child │ │
│ │ Widget │ │
│ └───────────────┘ │
│ Size ↑ │
└─────────────────────┘

2. 빌드 최적화

dirty로 표시된 Element만 다시 빌드합니다.

class Parent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
const Header(), // 변경 안 됨 - 스킵
DynamicContent(), // dirty - 리빌드
const Footer(), // 변경 안 됨 - 스킵
],
);
}
}

3. 빠른 비교

Widget의 동일성은 참조 비교로 확인합니다.

// const를 사용하면 같은 인스턴스 재사용
const Icon(Icons.star) // 항상 같은 인스턴스

How: 성능 최적화 적용#

const 생성자 활용

// 좋은 예
const Text('Hello')
const SizedBox(height: 16)
const Icon(Icons.home)
// 나쁜 예 - 매번 새 인스턴스 생성
Text('Hello')
SizedBox(height: 16)

불필요한 리빌드 방지

class OptimizedList extends StatelessWidget {
final List<String> items;
const OptimizedList({super.key, required this.items});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
// 각 아이템만 필요할 때 빌드
return ListTile(title: Text(items[index]));
},
);
}
}

Watch out: 주의할 점#

  • const를 사용할 수 있는 곳에서는 항상 사용하세요.
  • ListView.builder는 보이는 항목만 빌드합니다.
  • 불필요한 상위 위젯의 setState()는 피하세요.

챕터 5: 무한 스크롤 구현 원리#

Why: 어떻게 무한 리스트가 가능한가요?#

ListView에 10만 개의 아이템이 있어도 Flutter는 부드럽게 동작합니다. 이는 뷰포트 인식 레이아웃과 주문형 빌드 덕분입니다.

What: Sliver 시스템#

Sliver3는 스크롤 가능한 영역의 일부를 나타냅니다.

// Sliver를 사용한 커스텀 스크롤 뷰
CustomScrollView(
slivers: [
SliverAppBar( // 접히는 앱바
expandedHeight: 200,
flexibleSpace: FlexibleSpaceBar(
title: Text('My App'),
),
),
SliverList( // 리스트
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('Item $index')),
childCount: 100000, // 10만 개도 문제없음
),
),
],
)

How: 주문형 빌드 활용#

ListView.builder 사용

ListView.builder(
itemCount: items.length, // 또는 null로 무한
itemBuilder: (context, index) {
// 화면에 보일 때만 호출됨
return buildItem(index);
},
)

GridView.builder 사용

GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: items.length,
itemBuilder: (context, index) {
return buildGridItem(index);
},
)

무한 스크롤 구현

class InfiniteList extends StatefulWidget {
@override
State<InfiniteList> createState() => _InfiniteListState();
}
class _InfiniteListState extends State<InfiniteList> {
final List<String> items = [];
bool isLoading = false;
@override
void initState() {
super.initState();
loadMore();
}
Future<void> loadMore() async {
if (isLoading) return;
setState(() => isLoading = true);
// API 호출 시뮬레이션
await Future.delayed(const Duration(seconds: 1));
setState(() {
items.addAll(List.generate(20, (i) => 'Item ${items.length + i}'));
isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: (notification) {
if (notification is ScrollEndNotification) {
if (notification.metrics.pixels >=
notification.metrics.maxScrollExtent - 200) {
loadMore(); // 끝에 가까워지면 더 로드
}
}
return false;
},
child: ListView.builder(
itemCount: items.length + (isLoading ? 1 : 0),
itemBuilder: (context, index) {
if (index >= items.length) {
return const Center(child: CircularProgressIndicator());
}
return ListTile(title: Text(items[index]));
},
),
);
}
}

Watch out: 주의할 점#

  • ListView(children: [...]) 대신 ListView.builder를 사용하세요.
  • children 방식은 모든 위젯을 한 번에 생성합니다.
  • 아이템이 20개 이상이면 반드시 builder 패턴을 사용하세요.

챕터 6: Element 재사용과 Key#

Why: 왜 Key가 필요한가요?#

Flutter는 Element를 재사용하여 성능을 최적화합니다. 하지만 리스트 순서가 바뀌면 잘못된 Element가 재사용될 수 있습니다.

What: Key의 종류#

ValueKey

고유한 값으로 위젯을 식별합니다.

ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return ListTile(
key: ValueKey(item.id), // 고유 ID 사용
title: Text(item.name),
);
},
)

ObjectKey

객체 참조로 위젯을 식별합니다.

ListTile(
key: ObjectKey(item), // 객체 자체를 키로 사용
title: Text(item.name),
)

GlobalKey

앱 전체에서 고유한 키입니다.

final formKey = GlobalKey<FormState>();
Form(
key: formKey,
child: TextFormField(...),
)
// 어디서든 접근 가능
formKey.currentState?.validate();

How: Key 올바르게 사용하기#

순서가 바뀔 수 있는 리스트

// 드래그 앤 드롭으로 순서 변경
ReorderableListView(
onReorder: (oldIndex, newIndex) {
setState(() {
final item = items.removeAt(oldIndex);
items.insert(newIndex, item);
});
},
children: items.map((item) {
return ListTile(
key: ValueKey(item.id), // Key 필수!
title: Text(item.name),
);
}).toList(),
)

StatefulWidget 리스트

// 각 아이템이 상태를 가질 때
ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
return TodoItem(
key: ValueKey(todos[index].id), // Key로 상태 유지
todo: todos[index],
);
},
)

Watch out: 주의할 점#

  • 인덱스를 Key로 사용하지 마세요. 순서가 바뀌면 문제가 생깁니다.
  • GlobalKey는 꼭 필요할 때만 사용하세요. 비용이 큽니다.
  • StatelessWidget만 있는 리스트에서는 Key가 필수는 아닙니다.

한계#

  • 이 문서는 Flutter의 개념적인 구조를 설명합니다. 실제 소스 코드는 더 복잡합니다.
  • Engine(C++) 계층의 상세 동작은 다루지 않습니다.
  • 플랫폼별 Embedder 구현은 별도 학습이 필요합니다.

Footnotes#

  1. 계층화된 아키텍처(Layered Architecture)는 소프트웨어를 독립적인 레이어로 분리하여 각 레이어가 특정 책임만 담당하도록 설계하는 패턴입니다.

  2. 불변(Immutable)은 객체가 생성된 후 상태를 변경할 수 없음을 의미합니다. Flutter의 Widget은 불변이므로 UI 변경 시 새 Widget을 생성합니다.

  3. Sliver는 스크롤 가능한 영역의 일부분을 의미합니다. “조각”, “얇은 조각”이라는 뜻으로, 뷰포트 내에서 보이는 부분만 렌더링합니다.

공유

이 글이 도움이 되었다면 다른 사람과 공유해주세요!

Flutter 튜토리얼 78편: Flutter 내부 구조 이해
https://moodturnpost.net/posts/flutter/flutter-architecture/
작성자
Moodturn
게시일
2026-01-08
Moodturn

목차