[Fultter] 커리어톡 앱 모작 / Sliver 구조 안에 커스텀 TabBar 넣기

서회정's avatar
Oct 21, 2025
[Fultter] 커리어톡 앱 모작 / Sliver 구조 안에 커스텀 TabBar 넣기

🚨
Sliver 구조 안에 있는 TabBar를 만들어보자
notion image
커리어톡의 현재 메인화면 구조는 첨부한 이미지와 같다.
가장 상단에 SliverToBoxAdaptor구조 안에 페이지가 넘어가는 배너가 있다.
그 아래로 SliverPersistentHeader에 필터 메뉴들이 있다.
필터 메뉴 가운데서도 탭바 구조를 하고있는 메뉴를 구현해보자.
 

 

1. 샘플링

💡
구조를 짜기 전 해당 레이아웃에 사용되는 위젯을 찾아 샘플링해보자
notion image
import 'package:flutter/material.dart'; /// Flutter code sample for [TabBar]. void main() => runApp(const TabBarApp()); class TabBarApp extends StatelessWidget { const TabBarApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp(home: TabBarExample()); } } class TabBarExample extends StatefulWidget { const TabBarExample({super.key}); @override State<TabBarExample> createState() => _TabBarExampleState(); } /// [AnimationController]s can be created with `vsync: this` because of /// [TickerProviderStateMixin]. class _TabBarExampleState extends State<TabBarExample> with TickerProviderStateMixin { late final TabController _tabController; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('TabBar Sample'), bottom: TabBar( controller: _tabController, tabs: const <Widget>[ Tab(icon: Icon(Icons.cloud_outlined)), Tab(icon: Icon(Icons.beach_access_sharp)), Tab(icon: Icon(Icons.brightness_5_sharp)), ], ), ), body: TabBarView( controller: _tabController, children: const <Widget>[ Center(child: Text("It's cloudy here")), Center(child: Text("It's rainy here")), Center(child: Text("It's sunny here")), ], ), ); } }
notion image
 
플러터 문서에서 Tabbar 위젯의 사용 방법 및 샘플 코드를 확인할 수 있다. 프로젝트에 적용하기 전 샘플코드를 실행해보고 내 프로젝트에 맞게 커스텀해보자.
 

1) 테스트해보기

notion image
테스트 코드 입력 시 문제 없이 동작한다.
여기서 문제가 있다면 내 모작 프로젝트의 경우에는 appbar에서 사용되는게 아닌 Sliver 구조 내에서 사용된다는 것이다.
 

 

2. Sliver 구조 안으로 적용

여기서부터는 도저히 감이 잡히지 않아 지피티의 도움을 받았다.
 

1) SliverPersistentHeader의 델리게이트 만들기

  • TabBar의 헤더를 올려둘 핀고정 헤더이다.
 
class _TabsHeaderDelegate extends SliverPersistentHeaderDelegate { _TabsHeaderDelegate({required this.child, required this.height}); final Widget child; final double height; @override double get minExtent => height; @override double get maxExtent => height; @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { return child; } @override bool shouldRebuild(covariant _TabsHeaderDelegate old) { return old.child != child || old.height != height; } }
 
 

2) 샘플을 Sliver 구조로 리팩터링

  • AppBar에 있던 TabBarSliverPersistentHeader로 이동
  • 본문은 NestedScrollView.bodyTabBarView
  • 상단(회색)영역은 나중에 SingleChildScrollView로 바꾸기 쉽게 SliverToBoxAdapter로 감싸둠
 
import 'package:flutter/material.dart'; class DummyPage extends StatelessWidget { const DummyPage({super.key}); @override Widget build(BuildContext context) { return const MaterialApp(home: TabBarExample()); } } class TabBarExample extends StatefulWidget { const TabBarExample({super.key}); @override State<TabBarExample> createState() => _TabBarExampleState(); } class _TabBarExampleState extends State<TabBarExample> with TickerProviderStateMixin { late final TabController _tabController; final tabs = const [ Tab(text: '🔥 인기급상승'), Tab(text: '채용소식'), Tab(text: '기업정보'), Tab(text: '직무정보'), // 필요시 더 추가 ]; @override void initState() { super.initState(); _tabController = TabController(length: tabs.length, vsync: this, initialIndex: 0); } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { const activeColor = Colors.black; final inactiveColor = Colors.grey.shade500; return Scaffold( body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) => [ // (회색) 상단 컨텐츠: 나중에 SingleChildScrollView로 갈아끼우기 쉬움 SliverToBoxAdapter( child: Container( height: 160, color: Colors.grey.shade300, alignment: Alignment.center, child: const Text('여기 상단 헤더(배너/필터 등)'), ), ), // 탭바가 들어가는 핀고정 헤더 SliverPersistentHeader( pinned: true, delegate: _TabsHeaderDelegate( height: 44, child: Material( color: Colors.white, child: TabBar( controller: _tabController, isScrollable: true, tabs: tabs, labelColor: activeColor, unselectedLabelColor: inactiveColor, labelStyle: const TextStyle(fontWeight: FontWeight.w600), unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.w500), indicator: const UnderlineTabIndicator( borderSide: BorderSide(width: 3, color: Colors.black), insets: EdgeInsets.symmetric(horizontal: 14), ), indicatorSize: TabBarIndicatorSize.label, labelPadding: const EdgeInsets.symmetric(horizontal: 12), splashFactory: NoSplash.splashFactory, overlayColor: WidgetStatePropertyAll(Colors.transparent), ), ), ), ), ], // 탭별 콘텐츠 body: TabBarView( controller: _tabController, children: [ _buildList('인기급상승'), _buildList('채용소식'), _buildList('기업정보'), _buildList('직무정보'), ], ), ), ); } // 샘플 콘텐츠(탭 안 리스트) Widget _buildList(String title) { return ListView.separated( padding: const EdgeInsets.symmetric(vertical: 8), itemCount: 30, itemBuilder: (_, i) => ListTile(title: Text('$title - Item $i')), separatorBuilder: (_, __) => const Divider(height: 1), ); } }
 

 

⚠️ TabBar 사용 시 index 0번 앞에 발생하는 여백문제

 
notion image
샘플링 코드에 내 코드를 적용했을 때, 첫 탭에 앞에 알 수 없는 여백이 발생했다. 공식 문서를 뒤져보았으나 원인을 찾기 어려웠고 같은 문제를 겪어 인프런에 질문을 올린 글을 찾을 수 있었다.
 
notion image
TabBar의 속성 중 tabAlignment을 통해 이를 해결할 수 있다는 해답도 발견했고 내 프로젝트에 적용해보았다. 적용 후 완성 본은 다음과 같다.
notion image
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Share article

clubnerdy