본격적인 프로젝트 개발에 앞서 플러터의 필수 개념에 대해 알아보자.
플러터 아키텍쳐

플러터 프로젝트의 구조를 나름 비유하자면 다음과 같다. 무한하게 상상할 수 있는 Framework가 특정 규칙에 따라 상상력을 전기적 신호로 표현할 수 있는 Engine이라는 뇌로 실체화되고, Embedder를 통해 Native까지 전달된 뇌의 전기적 신호가 실제 물리적 움직임으로 구체화된다...
추상화된 하나의 뇌로 네이티브라는 서로 천차만별인 신체를 적절하게 제어하기 위해서는 뇌의 신경이 각 신체 전용 제어 구조로 연결돼야 한다. 이때 Embedder가 각 몸뚱아리 전용 제어 틀을 이 추상화된 뇌에게 제공하는 역할을 한다.
Embedder

플러터로 만든 앱이 기기에서 실행되려면 각 네이티브 환경의 인터페이스 규칙을 따르도록 설계되어야 한다. 임베더는 플러터 엔진이라는 뇌를 크로스 플랫폼으로 동작시키기 위해 네이티브와의 연결부를 담당한다. 영화 엑스맨에 나오는 세레브로라는 기계처럼 뇌와 기기를 연결하는 틀을 제공한다고 볼 수 있다.
임베더는 각 OS별 네이티브 프로젝트 구조를 제공하여 플러터 엔진이 각 OS 런타임 위에서 문제없이 실행될 수 있게 한다.
- Android: Gradle, manifest, Java/Kotlin 코드
- iOS: Xcode 프로젝트, Info.plist, Swift/Obj-C 코드
플러터 엔진은 이 디렉토리에서 정의된 규칙과 구조를 통해 Dart 코드로 네이티브 코드와 통신할 수 있고, OS가 제공하는 네이티브 API를 호출할 수 있다. 플러터로 만든 앱이 임베더를 매개로 네이티브 앱으로서 실행되고, 그 런타임 내부에서 플러터 엔진을 작동시킴으로써 OS를 통해 기기(카메라, 블루투스, GPS, 센서 등)를 제어하고 그 결과를 다시 전달받아 처리할 수 있다.
Engine
플러터 엔진은 다음과 같은 역할을 담당한다.
- Skia 자체 렌더링 (화가 역할)
플러터 엔진은 각 기기의 OS가 제공하는 네이티브 UI를 빌려 쓰는 것이 아니라 스키아(Skia) 또는 임펠러(Impeller)라는 강력한 그래픽 엔진을 자체적으로 탑재하고 있다. 따라서 부드러운 화면을 직접 그려내며 OS와 상관없이 동일한 UI/UX를 보장한다. - 다트 런타임 (통역사 역할)
lib에 작성된 Dart 코드를 실시간으로 실행하고 관리한다. 개발 환경에서 핫 리로드(Hot Reload)를 지원해 수정한 코드를 즉시 화면에 반영하고 배포 시에는 기계어로 변환해 네이티브 앱에 가까운 고성능을 보장한다. - 플랫폼 채널 관리 (통로 역할)
lib의 코드와 네이티브 환경 사이에서 데이터를 전달하는 통로(플랫폼 채널)를 관리한다. lib에 작성된 카메라, 블루투스, 사진 접근과 같은 명령을 각 OS에 바이너리로 직렬화하여 전달하고, 네이티브로부터 전달받는 데이터를 Dart로 역직렬화여 lib로 전달한다. - 파일, 네트워크 IO (문지기 역할)
플러터 엔진은 앱에서 발생하는 파일 읽기·쓰기, 네트워크 요청과 같은 입출력 작업을 관리한다.- 파일 I/O: 로컬 저장소(앱 내부, 외부 저장소 등)에서 데이터를 읽고 쓰는 작업을 처리한다. 예를 들어, 앱 설정 저장, 이미지 다운로드 후 캐싱 등은 플러터 엔진이 OS별 파일 시스템 접근을 중재하여 안전하게 수행한다.
- 네트워크 I/O: HTTP 요청, 소켓 통신 등 네트워크 연동을 담당한다. Dart 코드에서 API 호출을 하면 플러터 엔진이 OS 네트워크 스택과 연결하여 데이터를 주고받고, 응답을 다시 Dart 코드로 전달한다.
위젯
위젯은 화면 표시에 있어서 가장 기본적인 단위로 StatelessWidget, StatefulWidget, InheritedWidget 세 종류가 존재한다. 아주 대표적인 앱의 홈 화면에서 각 위젯울 찾아보고 어떤 느낌인지 감을 잡자.

StatelessWidget

StatefulWidget

InheritedWidget

이 위젯은 화면의 알맹이가 되는 정보 전달용 위젯으로 statefulWidget이 담는 컨텐츠(데이터)를 뿌려주는 데이터 바구니라고 생각하면 편하다.
위 위젯은 이런 구조로 구현할 수 있을 것이다.
CartProvider(InheritedWidget) ← 장바구니 데이터 뿌려주기
└─ CartIcon (StatelessWidget)
└─ ItemCountBadge (StatefulWidget) ← 장바구니 숫자 받아오기
Inherited를 통한 State의 전달
상위 StatefulWidget (State 보유)
└── MyInheritedWidget (State 포함)
└── 하위 StatefulWidget (context 통해 State 접근)
하위 StatefulWidget가 받는 State 변경 정보에 대한 데이터 소스는 다음과 같다.
- 위젯 필드 이용(부모가 전달) - 생성자에서 부모 필드에 직접 접근해서 받아옴 : didUpdateWidget()으로 감지 : 자동으로 리빌드
- InheritedWidget 이용(트리 기반 공유) - context 기반 전역, 상위 상태를 감지해서 받아옴 : 자동으로 리빌드
- 일반 객체 (가장 흔함) - animation 같이 변경될 수 있는 값을 가진 위젯이 아닌 객체가 State에 존재 : 자동 rebuild 안 됨 → setState를 따로 호출해야 반영됨
InheritedWidget이 상위 위젯 트리에서 데이터를 받아오는 거면 어차피 부모-자식으로 전달되는 위젯 필드로 충분히 전달 가능한데 InheritedWidget이 굳이 왜 필요한지 의문이 들 수 있다.
= 어차피 상위 트리에서 내려오는 거면 공통 부모에서 받아서 위젯 필드로 내려주면 되는 거 아닌가
물론 충분히 가능하지만 규모가 커지면 다음과 같은 Prop Drilling 문제가 생긴다.

State를 보유한 상위 StatefulWidget이 InheritedWidget에 담아 State를 전달하고 하위 StatefulWidget에서 해당 State를 참조하는 구조는 분명 직관적이다. 그러나 이런 구조는 View를 담당하는 StatefulWidget이 Model에 해당하는 State 변경 로직까지 책임진다는 점에서 UI와 비즈니스 로직이 강하게 결합되어있다고 볼 수 있다. 이는 위젯 중심의 선언형 UI로 MVVC를 지향하는 플러터의 컨셉과 배치된다. 이에 따라 Model과 View의 책임 분리를 제공하는 Provider 구조가 등장했다.
Provider
기본적으로 Provider는 위젯 간 State 전달을 담당하는 라이브러리이다. 앞서 언급한 InheritedWidget의 데이터 전달 구조와의 차이점은 다음과 같다.

플러터에는 Provider를 포함하여 Bloc, Riverpod, getX 등 상태 관리와 전달을 담당하는 여러 패턴, 라이브러리가 존재한다. 이에 관해선 다음 시간에 더 자세히 알아보자.
라이프사이클
라이프사이클은 위젯이 생성되고 삭제, 종료되는 과정에서 발생하는 여러가지 이벤트들의 호출 순서이다. 기본적으로 화면을 담당하는 Widget은 애플리케이션 내내 발생하는 여러 이벤트들에 따라 충분히 변경되거나 삭제, 재생성될 수 있고, 이 결과로 화면은 바뀌거나 안 바뀔 수도 있다.
StatelessWidget
StatelessWidget은 기본적으로 상태가 존재하지 않으므로 생성 ~ 삭제 사이에 변경이 없다(immutable). 따라서 처음에 한 번 빌드되면 이후 재빌드 시 UI 변화가 없는 경우가 많다.
라이프사이클
- StatelessWidget 생성자 : immutable → final
- createElement() : 렌더링 메타데이터 생성(build때 쓰임)
- build() : 자식 위젯트리 반환 = ui 렌더링
StatefulWidget
상태를 담고있는 StatefulWidget은 기본적으로 생성 ~ 삭제 사이에 상태가 쉴 새 없이 변경된다. 따라서 State라는 별도 객체(위젯이 아님)로 상태를 분리한 채로 존재하며 빌드를 포함한 상태 관리는 전부 State에 위임한다.
- StatefulWidget 클래스: 상태를 가지는 위젯
- State 클래스: 위젯의 상태를 관리하는 메서드를 제공하는 클래스 (위젯이 아님)
Widget 내부에는 immutable한 최소 값만 남기고 변경될 값들은 따로 State 객체에 빼둠으로써 build()함수가 State에 존재한다. 따라서 StatefulWidget은 부모 위젯이 재빌드될 때에나 생명주기에 영향이 가고, 상태 변경은 위젯의 생명주기와 독립적으로 이뤄진다.(State의 setSate() 함수에 의존)
그러나 여기서 놓치기 쉬운 점이 있는데 StatefulWidget이 State를 보유하며 setState 직접 호출이 가능하긴 하지만 State 인스턴스 자체는 완전히 독립적으로 존재하며 서로 참조하는 구조라는 것이다. 즉 기존 인스턴스가 사라지고 새로운 위젯 인스턴스가 생성되더라도 기존 State 인스턴스는 유지될 수 있다.

위 구조로 다음을 알 수가 있다.
- State가 유지되는 동안 참조하는 Widget 인스턴스는 여러 번 바뀔 수 있다 - State내부 widget 필드의 값만 변경
- StatefulWidget 내부의 key, runtimeType이라는 두 개 필드가 참조할 State를 유일하게 결정하고 이 필드는 불변이다 - 위젯 인스턴스가 유지되는 동안 한 State만 가질 수 있음
라이프사이클
- StatefulWidget 생성자 : immutable → final (statefulwidget에 생성자로 전달받는 immutable한 필드는 위젯트리 계산에 사용)
- createState() - 한 번만 : 위젯의 상태를 구성하는 값들을 관리하는 State 객체 생성
- initState() - 한 번만 : State 객체의 필드 초기화 - context 접근 불가능 = inheritedWidget의 데이터 사용 불가
- 변수 초기화
- AnimationController 생성
- Stream 구독 시작
- API 호출 시작
- didChangeDependencies() - 한 번만 : context 의존성 반영 - context 접근 가능
- Theme (앱의 전체 디자인 설정) 변경 반영
- 다크모드 ↔ 라이트모드
- primaryColor 변경
- MediaQuery(디바이스 환경 정보) 변경 반영
- 화면 회전 (가로/세로)
- 키보드 올라옴
- 화면 크기 변경
- 시스템 폰트 크기 변경
- Provider 값 변경 반영
- 구독 중인 InheritedWidget의 데이터
- Theme (앱의 전체 디자인 설정) 변경 반영
- build() - 여러 번 : 변경된 설계도로 화면 렌더링 - 그려야하는 데이터가 변하면 무조건 호출해서 화면에 반영
- 생성 직후 호출
- 부모 변경 시 호출
- setState 이후 호출
- 의존성 변경 시 호출
- setState() - 여러 번 : State 인스턴스의 필드 값을 변경하고 다음 프레임에 build를 실행시키는 트리거 함수
- 위젯과 State 인스턴스 자체는 그대로 유지, 값만 변경
- 상태변경 로직을 안에 넣어서 사용 권장
- 라이프사이클에 포함되지 않음
- didUpdateWidget() - 여러 번 : 새 위젯 인스턴스가 생성된 상황에서 State의 기존 인스턴스는 유지하며 참조하는 위젯만 변경
- AnimationController를 새 설정값으로 재구성할 때
- TextEditingController를 부모로부터 동기화할 때
- 부모 리빌드에 의해 외부에서 전달받은 값이 바뀌었을 때 setState()로 내부 상태 업데이트 필요
- dispose()- 한번만 : 메모리 누수 방지하며 위젯 종료
- 진행 중인 작업 취소
- 리소스 해제
- 상태 정리
didUpdateWidget() 예시


위와 같은 캘린더 화면을 분석해보자.
- StatefulWidget: 일정 정보를 보여주는 위젯
- State: 제목, 시작/종료 시간 같은 상태 데이터를 보유
- InheritedWidget: 일정 정보를 갖고 있는 데이터 소스
다음은 23일에서 27일로 날짜를 옮겼을 때 화면이 변경되는 과정이다.
1. 날짜별 일정 정보를 담고 있는 InheritedWidget이 바뀜 (23일 → 27일)
2. 27일에 대한 새 StatefulWidget 인스턴스가 생성됨
- 새 Widget 인스턴스가 메모리에 만들어짐
- Flutter 엔진이 새 인스턴스의 runtimeType과 key를 기존 Widget과 비교
3. State 인스턴스는 제목, 시작/종료 시간으로 동일 형식 = runtimeType과 key가 기존 위젯과 동일 → 기존 State 객체 유지
- 기존 State 객체와 새 Widget 연결
- State 객체의 widget 필드가 새 Widget 참조로 갱신- 이전 Widget 인스턴스는 oldWidget으로 전달
4. didUpdateWidget(oldWidget) 호출
- oldWidget = 이전 StatefulWidget
- widget = 새 StatefulWidget
5. setState() 호출
- State 내부 필드 값(일정 정보) 갱신
6. build() 호출- 새 Widget 데이터 + 기존 State 구조를 사용해 UI 갱신
레이아웃 위젯
각 위젯에서 인식하는 제스처를 기준으로 분류하면 다음과 같다.
- 터치/클릭 : Container, Row&Column, Expanded, Stack, Positioned, SizedBox, TabBar
- 스크롤 : ListView, GridView
- 스와이프/슬라이드 : PageView
터치/클릭
Container
사각형 상자
다음과 같은 옵션이 존재한다.
padding: 내부 여백
height : 높이
width: 너비
color: 배경색
child: 자식 위젯에 적용될 설정
decoration: 컨테이너의 외형(모양, 배경, 테두리 등)
Row & Column
Row : 가로 레이아웃
Column : 세로 레이아웃
다음과 같은 옵션이 존재한다.
mainAxisAlignment : 각 레이아웃 방향으로 정렬
crossAxisAlignment : 각 레이아웃 수직 방향으로 정렬 (부모 위젯의 높이, 너비 기준- 설정 필요)
children : 정렬될 요소들을 리스트 형태로 화면에 출력 ( children : List.generate(…) )
Expanded
Row와 Column 안에서 남은 공간을 채우도록 확장하는 자식 위젯
- Expanded = Row/Column의 자식
- 기존 자식 레이아웃은 변경되지 않음
- 부모 위젯(Row/Column)의 공간을 분할/채움
- flex 속성으로 비율 조절 가능
Stack
여러 위젯이 서로 겹쳐 배치된 위젯
다음과 같은 옵션이 존재한다.
fit:
- loose : 자식 위젯의 크기를 스택 위젯 크기에 맞게 자유롭게 배치 가능
- expand: 자식 위젯이 스택 위젯과 동일한 크기 고정
- passthrough : 자식 위젯이 스택 위젯의 크기와 위치를 무시하고 자유롭게 배치 가능
children:
- children= [ widget1, widget2 ….] 로 정의 = List<Widget> 타입
- 선언되는 순서로 z-index 조절
- 스택: 밑에서부터 쌓아올림 = 먼저 선언된 자식 위젯이 밑에 배치
Positioned
스택 내부에서 하위 위젯의 위치 설정을 제공하는 위젯
부모 위젯을 기준으로 절대 위치를 설정하는 다음 옵션이 존재한다.
left
top
right
bottom
SizedBox
고정 크기 상자
아이템 간 간격 설정에 용이
(Figma에서 그루핑하면 간격 일정하게 조절 가능한게 SizedBox로 선택되는 거 같음)
TabBar
탭 메뉴를 통한 화면 이동을 제공하는 위젯

세 개의 객체로 구성
- TabBar : 화면 이동을 제공하는 탭 메뉴
- TabBarView : 탭바를 통해 이동 시 전환되는 각각의 화면
- TabBarController : 둘을 연결
옵션
controller: _tabController로 컨트롤러에서 설정한 length만큼의 탭 메뉴 등록
tabs: const [Widget1, Widget2, Widget3 …] 형식으로 TabBar에 보일 탭 메뉴 등록
labelColor - 현재 선택된 메뉴의 색상
unselectedLabelColor - 현재 선택되지 않은 메뉴의 색상 설정
labelPadding - 메뉴 간 간격 조정
indicatorWeight - 선택된 메뉴의 인디케이터 두께 조절
labelStyle, unselectedLabelStyle - 선택된 메뉴와 선택되지 않은 메뉴의 텍스트 스타일 설정
TabController
초기값 설정 필요
옵션
initialIndex: 초기 “탭-페이지” 설정
animationDuration: 탭 메뉴를 눌렀을 때 인디케이터 애니메이션 효과 설정
length: 메뉴(탭) 개수 설정
vsync: 애니메이션을 장치 디스플레이의 수직 동기화와 맞추는 역할
- with TickerProviderStateMixin로 클래스 추가 필요
- 화면 새로고침 타이밍에 맞춰 프레임마다 알맞게 tick 이벤트를 전달
- 애니메이션 시작/중지/재생 속도 제어
- vsync 없으면 표시 안되는 프레임의 애니메이션을 계속 계산 → 배터리/CPU 낭비
TabBarView
controller: _tabController로 컨트롤러에서 설정한 length만큼의 뷰 등록
children: [Widget1, Widget2, Widget3] 형식으로 각 탭 메뉴에 대응하는 화면 정의
스크롤
ListView
스크롤 가능 항목을 목록으로 배치
옵션
scrollDirection: 스크롤방향
- 세로(디폴트 설정) : axis.vertical, 가로 : axis.horizental
reverse: 스크롤 방향과 스크롤 시작 위치를 뒤집음
- ListView는 원래 위에서 시작해서 밑으로 렌더링하는데 reverese가 켜지면 밑에서 시작해서 위로 렌더
- reverse = false) 스크롤 시작 위치 : 맨 위, 스크롤 방향 : 내려서 최신 컨텐츠 조회
- reverse = true) 스크롤 시작위치 : 맨 밑, 스크롤 방향 : 올려서 예전 컨텐츠 조회
- 콘텐츠 순서 자체를 바꾸는 건 아님, 화면 상 렌더링 시작 위치와 스크롤 진행 방향만 반대로- 채팅창 구현에 적용
controller: 스크롤 제어, 이벤트 처리 제공
- initState()와 연계하면 매 상태 생성 시 스크롤 위치 0부터 시작하도록 구현 가능
→ 스크롤 끝에 도달 확인 후 새로운 데이터 불러오기(새로운 자식 위젯 생성)로 무한 스크롤 구현
pysics: 스크롤 동작 엔진 설정
- BouncingScrollPhysics - 스크롤 끝에서 튕기는 효과(ios)
- ClampingScrollPhysics - 스크롤 끝에서 멈추는 효과(android)
- FixedExtentScrollPhysics - 스크롤 이동 단위 균일
- NeverScrollableScrollPhysics - 스크롤 비활성화
padding: 아이템 간 간격 조정
cacheExtent: 스크롤로 화면 이동 시 기존 화면에서 추가로 캐시해 둘 영역 설정
- 반응 속도와 초기로딩 속도 간의 트레이드오프
GridView
스크롤 가능 위젯을 그리드로 배치
인스타 피드처럼 동일 유형 위젯(이미지)을 모아보는 화면에 적용
gridDelegate: 그리드 레이아웃 정의
- (열 수, 크기, 열 사이 간격 등) → 한 줄에 몇 개 보여줄지 정하면 크기에서 나눠서 행 결정
- SliverGridDelegateWithFixedCrossAxisCount-> crossAxisCount: 타일 크기와 관계없이 열 수 고정 (타일의 가로가 길 경우 양옆으로 화면 넘어갈 수 O)
scrollDirection
- SliverGridDelegateWithMaxCrossAxisExtent
-> maxCrossAxisExtent: 타일 최대 너비를 고정 (화면 가로 크기 / 타일 너비로 열 수 계산)
- mainAxisSpacing, crossAxisSpacing : 그리드 셀 간 간격
reverse
controller
padding: GridView 내부 간격 설정 (테두리 간격)
- 영화관 좌석처럼 GridView에 내부 간격을 두면 해당 여백을 잡고 스크롤하기에 UX적으로 유용
스와이프/슬라이드
PageView
스와이프 제스처를 인식해서 페이징을 제공하는 위젯
슬라이드 배너 구현에 유용
옵션
children: 슬라이드로 등장할 자식 위젯, List<Widget> 형태로 구성
scrollDirection
controller
pageSnapping: 스와이프 시 자동으로 페이지가 완전히 전환되도록 하는 효과
onPageChanged: 페이지 완전 이동 시 콜백 함수 등록, 페이지 이동 단위로 특정 이벤트 수행 가능

Controller가 제공하는 addListener가 있는데 굳이 onPageChanged옵션으로 콜백 함수를 등록하는 이유는 다음과 같다.
| 구분 | addListener | onPageChanged |
| 호출 시점 | 스크롤 중 계속 | 페이지 전환 완료 |
| 값 | float (진행률) | int (페이지 index) |
| 용도 | 애니메이션 동기화, parallax, 스크롤 진행 상태 | 페이지 인덱스 변경 후 처리, indicator, 상태 업데이트 |
addListener = 진행 상태 연속 추적
onPageChanged = 페이지 전환 완료 시점 알림
구현 편의상 페이지 단위 전환 전용으로 onPageChanged를 제공하는 것
애니메이션
애니메이션은 시각적으로 흥미로운 피드백 제공하는 효과이다. 애니메이션은 투명도가 변하든, 위치나 크기가 변하든, 기본적으로 시간에 따라 값이 변하는 과정 그 자체이므로 당연히 StatefulWidget의 형태로 제공된다. 이 때문에 헷갈리기 쉬운데 앞서 설명했듯이 이 애니메이션 위젯에게 변화된 값을 뿌리는 데이터 소스는 별도 애니메이션 객체(AnimationController)이고 위젯이 내부에서 참조 중이다.
플러터에서 제공하는 애니메이션은 암시적, 명시적 두 종류가 있다. 애니메이션 위젯이 혼자 애니메이션 효과를 그리는 것이 아님을 유념하고 플러터의 애니메이션 위젯을 알아보자.
애니메이션이 그려지는 과정
1. 초기값 → 결과값 코드 실행
2. Tween 생성
- Tween : 값 보간 공식
3. Animation<double> 생성
- Animation : 시간에 따른 값 변화를 저장
4. controller.forward()
- controller : 시간 진행 제어
5. 매 프레임마다 listener 실행
- listener : 값 변경 감지
6. setState() 호출로 값 변경
- 새 값으로 갱신, 빌드 호출
7. build() 호출로 화면 반영
- 변경된 화면 표시
Tween이 값 범위를 보간하고
Animation이 시간에 따라 값을 만들고
Controller가 시간을 움직이고
listener가 rebuild를 트리거한다.
Flutter는 값이 바뀌었는지 감시하는 게 아니라 rebuild 신호가 왔는지를 감지하여 화면을 그리는 엔진이고,
애니메이션은 프레임마다 rebuild 신호를 발생시키는 방식으로 구현된다.
암시적 애니메이션
플러터는 내부에 AnimationController가 이미 정의된 애니메이션 위젯을 제공한다. 개발자가 직접 AnimationController를 정의하고 중간 동작을 커스텀한 명시적 애니메이션을 만들 수도 있지만 여기선 암시적 애니메이션에 대해 알아보자.
공통 옵션
- duration: 애니메이션의 실행 시간 설정 = 값의 변화가 일어나는 시간
- Curve: 값이 변하는 속도를 동적으로 설정 = (AnimationController가 0→1로 진행되는 과정을 시간별로 어떻게 보간할지)
- 0→1 변하는 과정을 x축을 시간으로하는 함수 개형으로 표현하면
- Curves.linear : 선형 함수
- Curves.ease : sigmoid 함수
- Curves.easeIn : 지수 함수
- Curves.easeOut : 로그 함수
- Curves.easeInOut : sigmoid 함수
- Curves.fastOutSlowIn : tan 함수
- 기타 곡선 효과
- Curves.bounceIn : 시작에 바운스
- Curves.bounceOut : 끝에 바운스
- Curves.elasticIn : 시작 부분에 탄성
- Curves.elasticOut : 끝 부분에 탄성
- onEnd: 애니메이션 끝날때 호출되는 콜백 함수 = 애니메이션 종료 후 특정 이벤트 처리하도록 설정
- 암시적 애니메이션 = 내부에서 매프레임 자동 setState → onEnd에서 별도 setState 불필요
- 명시적 애니메이션 = AnimationController 직접 정의 → Animation listener에서 매 프레임 setState를 직접 호출 필요
AnimatedOpacity
하위 위젯에 페이드 인, 페이드 아웃 애니메이션을 제공하는 위젯
opcaity는 double 타입 변수로 위젯의 불투명도(선명도) 값을 조절한다.
opacity : (flag ? 0.2 : 1)
-> duration 동안 불투명도가 0.2부터 1까지 tween에 따라 증가
AnimatedPositioned
Positioned 위젯의 위치와 크기를 애니메이션으로 변경하는 위젯
Positioned 위젯에 애니메이션을 입힌거라 스택의 하위 위젯으로 존재한다.
left : (flag ? 0 : 150)
-> duration 동안 Positioned의 x좌표가 부모 위젯 기준으로 0부터 150까지 tween에 따라 증가
AnimatedContainer
Container 위젯의 width, height, color, padding, alignment을 애니메이션으로 변경하는 위젯
width : (opacity ? 100 : 150),
height : (opacity ? 100 : 150)
-> duration 동안 Container의 너비와 높이가 100부터 150까지 tween에 따라 증가
라우팅
라우팅에서 등장하는 주요 개념은 다음과 같다.
- MaterialApp
- 앱 전체를 감싸는 최상위 위젯
- 앱을 실행하면 가장 처음 렌더링 되는 첫 위젯
- 내부에서 Navigator, Theme, Locale 등 전역 상태를 관리
- WidgetBuilder
- Widget의 생성자 함수를 호출하는 함수타입
- routes 옵션에서 위젯의 이름(String)과 위젯의 생성자(WidgetBuilder) 쌍으로 등록
- 화면 전환 시 페이지 단위로 위젯 빌더 호출
- Navigator
- 화면 전환을 담당하는 스택 구조 객체
- 들어갔던 화면(Route 객체)을 스택에 쌓아두고 관리
- push/pop으로 새 화면 이동/이전 화면 복원
- Route
- Navigator 스택에 들어가는 화면 단위 객체로 push/pop의 대상
- 내부적으로 builder: (context) => MyPage()같은 함수를 통해 위젯을 생성

이름 기반 명령형 라우팅 (Navigator 1.0 방식)
이름을 전달받은 네비게이터가 Map<String, WidgetBuilder>으로 builder를 찾아 Route를 생성하고 스택에 직접 push/pop함으로써 화면 전환
MaterialApp (최상위 위젯)
└─ Navigator 생성(routes 라우팅 정보 전달)
└─ pushNamed 호출 ('/second')
└─ Navigator가 route name 수신 ('/second')
└─ routes(Map<String, WidgetBuilder>)에서 builder 찾음
└─ 그 builder를 감싸는 Route 객체 생성 (MaterialPageRoute)
└─ Navigator 스택에 push
└─ Route가 builder 실행
└─ Widget 생성 (SecondPage)
└─ build() 실행 → 화면 렌더링
Page 기반 선언형 라우팅 (Navigator 2.0 방식)
Page 기반에서는 push 호출 없이 pages의 상태 변경만으로 화면 전환 구현
MaterialApp (최상위 위젯)
└─ Navigator(pages: List<Page> 전달)
└─ 앱 상태 변경 (pages 리스트에 새 Page 객체 추가)
└─ setState()로 pages 리스트 변경
└─ Navigator 리빌드
└─ Navigator가 기존 pages와 새 pages diff 비교
└─ 새 Page에 대해 createRoute(context) 호출
└─ Route 객체 생성 (MaterialPageRoute)
└─ Navigator가 Route를 Overlay에 추가
└─ Route가 buildPage() 실행
└─ Widget 생성 (SecondPage)
└─ build() 실행 → 화면 렌더링
위 선언형 라우팅 방식은 웹(Web)과 상태 기반 앱 아키텍처를 제대로 지원하기 위해서 최근에 업데이트됐다.
왜 더 복잡한 구조로 업데이트 됐는지에 대한 이유는 다음과 같다고 한다..




| Navigator 1.0 | Navigator 2.0 |
| 모바일 중심 | 웹 + 모바일 |
| 명령형 | 선언형 |
| push/pop 기록 | 상태 기반 |
| URL 무관 | URL 동기화 가능 |
그러나 내가 참고하는 책은 스택을 이용한 명령형 라우팅 기준으로 쓰여 있으므로 우선 Navigator 1.0 방식을 학습한다.
Navigator 이동 방식
Navigator.push, Navigator.pushNamed
새로운 페이지로 이동
1. 앱에서의 페이지 전환 = 새로운 화면 = 네비게이터 스택에 push
2. 새로운 화면 = 새로운 위젯 생성 → 위젯 생성자 인터페이스는 Widget build(BuildContext context)
= context를 인자로 전달하며 새 위젯 생성자를 호출
이 두 가지를 고려하면 화면 이동을 담당하는 다음 두 함수가 직관적으로 이해된다.
Navigator.push( // Route 객체를 직접 지정해서 화면 전환
context,
MaterialPageRoute(
builder: (context) => SecondPage()
)
);
Navigator.pushNamed(context, '/second'); //이름 기반 라우팅
| 메서드 | 전달 인자 | 특징 |
| push | Route 객체 | 이름 없이 직접 Route/Widget 지정해서 생성자 호출 |
| pushNamed | 이름 | MaterialApp.routes에 들러서 이름에 대응하는 위젯 생성자 찾아서 호출 |
aliasing 차이만 있을 뿐 사실상 같은 구조로 동작함을 알 수 있다.
Navigator.pop
현재 페이지 지우고 직전 페이지로 이동
initialRoute: '/' //홈화면
Navigator.pushNamed(context, '/second'); //홈화면 위에 두번째 화면
Navigator.pop(context); // 두번째 화면을 지움 = 이전화면(홈화면)이 top
Navigator.pushReplacementNamed
현재 페이지 지우고 새 페이지로 이동
initialRoute: '/' //홈화면
Navigator.pushNamed(context, '/second'); //홈화면 위에 두번째 화면
Navigator.pushReplacementNamed(context, '/third');
// 현재화면(두번째 화면)을 지우고 세번째 화면 푸시 = 홈화면 위에 세번째 화면
Navigator.popUntil
현재 페이지부터 특정 페이지 사이의 모든 페이지를 지우고 원하는 특정 페이지로 이동
Navigator.popUntil(context, ModalRoute.withName("/"))
→ 현재 화면부터 이름이 `/`인 화면까지 모든 화면을 pop
회원 가입처럼 여러 단계에 걸친 동작 종료 후 새 화면으로 이동하도록 구현할 때 활용
Navigator.pushNamed(context, '/home'); //홈화면
Navigator.pushNamed(context, '/first'); // 회원가입 첫번째 단계
Navigator.pushNamed(context, '/second'); // 회원가입 두번째 단계
Navigator.pushNamed(context, '/third'); // 회원가입 세번째 단계
Navigator.popUntil(context, ModalRoute.withName("/home"));
//회원가입 완료 후 회원가입 단계 페이지 다 지우고 홈화면으로 복귀
'dev > app' 카테고리의 다른 글
| flutter - 플러터로 크로스 플랫폼 앱 개발하기(3) (0) | 2026.03.06 |
|---|---|
| flutter - 플러터로 크로스 플랫폼 앱 개발하기(4) (0) | 2026.02.28 |
| flutter - 플러터로 크로스 플랫폼 앱 개발하기(1) (0) | 2026.02.22 |