Flutter에서 여러 화면을 탭으로 전환하는 UI는 TabBar(상단 탭) + TabBarView(탭별 내용) + TabController(둘을 연결) 조합으로 만듭니다. 이 글에서는 기본 구조와 동작 예제, 그리고 스크롤 가능한 탭·상태 유지 같은 실전 옵션까지 정리합니다.
🧩 핵심 위젯 3가지 #
- TabBar — 상단에 탭 목록을 배치하는 위젯. 사용자가 선택할 항목(
Tab)들을 나열합니다. (“RootTab"이라고 부르는 상단 탭 구성이 이것입니다.) - TabBarView — 각 탭에 대응하는 내용 화면을 표시하는 위젯. 자식 위젯 리스트를 받아 선택된 탭의 콘텐츠를 보여줍니다.
- TabController — TabBar와 TabBarView의 선택 상태를 동기화하는 컨트롤러.
1️⃣ 기본 예제 — DefaultTabController #
가장 간단한 방법은 DefaultTabController로 컨트롤러를 자동 제공하는 것입니다. 별도 패키지 없이 material.dart만으로 충분합니다.
1flutter create my_app
2cd my_applib/main.dart:
1import 'package:flutter/material.dart';
2
3void main() {
4 runApp(MyApp());
5}
6
7class MyApp extends StatelessWidget {
8 @override
9 Widget build(BuildContext context) {
10 return MaterialApp(
11 title: 'Flutter Tab Example',
12 home: HomeScreen(),
13 );
14 }
15}
16
17class HomeScreen extends StatelessWidget {
18 @override
19 Widget build(BuildContext context) {
20 return DefaultTabController(
21 length: 3, // 탭의 개수
22 child: Scaffold(
23 appBar: AppBar(
24 title: Text('Tab Example'),
25 bottom: TabBar(
26 tabs: [
27 Tab(text: 'Tab 1'),
28 Tab(text: 'Tab 2'),
29 Tab(text: 'Tab 3'),
30 ],
31 ),
32 ),
33 body: TabBarView(
34 children: [
35 Center(child: Text('Content for Tab 1')),
36 Center(child: Text('Content for Tab 2')),
37 Center(child: Text('Content for Tab 3')),
38 ],
39 ),
40 ),
41 );
42 }
43}1flutter run코드 설명 #
- DefaultTabController: 하위 트리에 TabController를 제공하며,
length로 탭 개수를 지정합니다. - Scaffold / AppBar: 기본 레이아웃과 상단 영역을 구성합니다.
- TabBar:
tabs에Tab위젯 리스트를 넣어 상단 탭을 만듭니다. - TabBarView:
children의 각 위젯이 탭 선택에 따라 표시됩니다. 탭 전환 애니메이션은 기본 제공됩니다.
2️⃣ TabController 직접 제어 #
탭 인덱스 감지·프로그래밍적 전환이 필요하면 TabController를 직접 만듭니다. SingleTickerProviderStateMixin을 함께 사용합니다.
1class HomeScreen extends StatefulWidget {
2 @override
3 State<HomeScreen> createState() => _HomeScreenState();
4}
5
6class _HomeScreenState extends State<HomeScreen>
7 with SingleTickerProviderStateMixin {
8 late final TabController _tab;
9
10 @override
11 void initState() {
12 super.initState();
13 _tab = TabController(length: 3, vsync: this);
14 }
15
16 @override
17 void dispose() {
18 _tab.dispose();
19 super.dispose();
20 }
21
22 @override
23 Widget build(BuildContext context) {
24 return Scaffold(
25 appBar: AppBar(
26 bottom: TabBar(controller: _tab, tabs: const [
27 Tab(icon: Icon(Icons.home), text: 'Home'),
28 Tab(icon: Icon(Icons.search), text: 'Search'),
29 Tab(icon: Icon(Icons.person), text: 'Profile'),
30 ]),
31 ),
32 body: TabBarView(controller: _tab, children: const [
33 Center(child: Text('Home')),
34 Center(child: Text('Search')),
35 Center(child: Text('Profile')),
36 ]),
37 );
38 }
39}💡
Tab(icon: ..., text: ...)처럼 아이콘과 텍스트를 함께 줄 수 있습니다.
3️⃣ 실전 옵션 #
스크롤 가능한 탭 — isScrollable
#
탭이 많아 가로 폭을 넘으면 isScrollable: true로 스크롤되게 합니다(기본값 false).
1TabBar(
2 isScrollable: true,
3 tabs: [ /* 많은 Tab들 */ ],
4)탭 상태 유지 — AutomaticKeepAliveClientMixin
#
기본적으로 탭을 전환하면 각 탭의 상태가 초기화됩니다. 스크롤 위치·입력값 등을 유지하려면 탭 콘텐츠 위젯에 AutomaticKeepAliveClientMixin을 적용하고 wantKeepAlive를 true로 둡니다.
1class KeepAlivePage extends StatefulWidget {
2 @override
3 State<KeepAlivePage> createState() => _KeepAlivePageState();
4}
5
6class _KeepAlivePageState extends State<KeepAlivePage>
7 with AutomaticKeepAliveClientMixin {
8 @override
9 bool get wantKeepAlive => true; // 상태 유지
10
11 @override
12 Widget build(BuildContext context) {
13 super.build(context); // 필수 호출
14 return const Center(child: Text('상태가 유지되는 탭'));
15 }
16}⚠️ TabBarView에 내장 keep-alive 옵션은 (2025년 기준) 아직 없습니다. 위 mixin 방식이 현재 표준 해법입니다.
긴 콘텐츠 #
탭 내용이 길면 각 탭 위젯을 SingleChildScrollView/ListView로 감싸 세로 스크롤되게 합니다.
❓ 자주 묻는 질문 #
Q. DefaultTabController와 TabController 직접 생성, 무엇을 쓰나요?
단순 탭 UI는 DefaultTabController로 충분합니다. 현재 탭 인덱스 감지나 코드로 탭 이동이 필요하면 TabController를 직접 만드세요.
Q. 탭을 바꾸면 입력값이 사라져요.
AutomaticKeepAliveClientMixin + wantKeepAlive = true로 해당 탭의 상태를 유지하세요.
Q. 탭이 화면 폭을 넘어가요.
TabBar(isScrollable: true)로 가로 스크롤을 켜면 됩니다.