Flutter 튜토리얼 8편: 리스트와 그리드 구현
요약
핵심 요지
문서가 설명하는 범위
- ListView의 기본 사용법과 builder 패턴
- 가로 스크롤 리스트 구현
- GridView로 그리드 레이아웃 만들기
- 다양한 타입의 항목이 섞인 혼합 리스트
읽는 시간: 14분 | 난이도: 초급
참고 자료
- Use lists - 기본 리스트
- Create a horizontal list - 가로 리스트
- Create a grid list - 그리드 리스트
- Create lists with different types of items - 혼합 리스트
- Work with long lists - 긴 리스트 처리
문제 상황
앱에서 연락처, 메시지, 상품 목록처럼 많은 데이터를 표시해야 하는 경우가 많습니다.
단순히 Column에 모든 항목을 넣으면 화면을 넘어가고, 메모리 문제도 발생합니다.
목록 표시의 요구사항
- 수백, 수천 개의 항목을 표시해야 한다.- 스크롤이 부드러워야 한다.- 메모리를 효율적으로 사용해야 한다.- 세로, 가로, 그리드 등 다양한 형태가 필요하다.문제는 다음과 같습니다.
- Column은 스크롤을 지원하지 않는다.
- 모든 항목을 한 번에 생성하면 메모리가 부족하다.
- 다양한 레이아웃 요구사항에 대응하기 어렵다.
해결 방법
Flutter는 ListView와 GridView로 효율적인 스크롤 목록을 제공합니다.
builder 패턴을 사용하면 화면에 보이는 항목만 생성해 성능을 최적화합니다.
챕터 1: 기본 ListView 사용하기
Why
NOTE소수의 항목만 표시할 때는 단순한 ListView로 충분합니다.
ListTile 위젯을 함께 사용하면 일관된 목록 항목을 쉽게 만들 수 있습니다.
What
NOTE
ListView는 스크롤 가능한 목록을 표시하는 위젯입니다.
children속성에 위젯 목록을 전달하면 세로로 스크롤되는 리스트가 됩니다.
How
TIP기본 ListView
ListView(children: [ListTile(leading: Icon(Icons.map),title: Text('지도'),),ListTile(leading: Icon(Icons.photo_album),title: Text('앨범'),),ListTile(leading: Icon(Icons.phone),title: Text('전화'),),],)ListTile의 주요 속성
속성 설명 leading왼쪽 아이콘/이미지 title주요 텍스트 subtitle보조 텍스트 trailing오른쪽 위젯 onTap탭 콜백 ListTile(leading: CircleAvatar(backgroundImage: NetworkImage('https://example.com/avatar.jpg'),),title: Text('홍길동'),subtitle: Text('안녕하세요!'),trailing: Icon(Icons.chevron_right),onTap: () {print('항목 클릭');},)구분선 추가
ListView(children: [ListTile(title: Text('항목 1')),Divider(), // 구분선ListTile(title: Text('항목 2')),Divider(),ListTile(title: Text('항목 3')),],)
Watch out
WARNING기본 ListView는 모든 자식을 한 번에 생성합니다.
항목이 많으면 메모리 문제가 발생하므로ListView.builder를 사용하세요.// 비효율적: 1000개 항목을 모두 생성ListView(children: List.generate(1000, (i) => ListTile(title: Text('항목 $i'))),)// 효율적: 화면에 보이는 것만 생성ListView.builder(itemCount: 1000,itemBuilder: (context, index) => ListTile(title: Text('항목 $index')),)
결론: 소수의 항목은 기본 ListView로, 많은 항목은 ListView.builder로 처리합니다.
챕터 2: ListView.builder로 긴 리스트 처리하기
Why
NOTE수천 개의 항목을 표시할 때 모든 위젯을 미리 생성하면 메모리가 낭비됩니다.
화면에 보이는 항목만 생성하는 “지연 로딩” 방식이 효율적입니다.
What
NOTE
ListView.builder는itemBuilder콜백을 사용해 필요한 항목만 생성합니다.
스크롤할 때 화면 밖으로 나간 항목은 메모리에서 해제됩니다.
How
TIP기본 사용법
final items = List<String>.generate(10000, (i) => '항목 $i');ListView.builder(itemCount: items.length,itemBuilder: (context, index) {return ListTile(title: Text(items[index]),);},)성능 최적화: 항목 크기 지정
항목 크기를 미리 알려주면 스크롤 성능이 향상됩니다.
속성 사용 시기 prototypeItem모든 항목이 같은 크기일 때 itemExtent고정 픽셀 크기를 알 때 itemExtentBuilder항목마다 크기가 다를 때 // prototypeItem: 샘플 항목으로 크기 추론ListView.builder(itemCount: items.length,prototypeItem: ListTile(title: Text(items.first)),itemBuilder: (context, index) {return ListTile(title: Text(items[index]));},)// itemExtent: 고정 높이 지정ListView.builder(itemCount: items.length,itemExtent: 56.0, // 각 항목 높이itemBuilder: (context, index) {return ListTile(title: Text(items[index]));},)작동 방식
graph LR A[스크롤] --> B{화면에 보임?} B -->|예| C[itemBuilder 호출] B -->|아니오| D[위젯 생성 안 함] C --> E[위젯 렌더링] F[화면 밖으로 나감] --> G[위젯 폐기]
Watch out
WARNING
itemCount를 지정하지 않으면 무한 리스트가 됩니다.
데이터 소스의 길이를 반드시 전달하세요.// 위험: 무한 리스트ListView.builder(itemBuilder: (context, index) {return ListTile(title: Text('항목 $index'));},)// 안전: itemCount 지정ListView.builder(itemCount: items.length,itemBuilder: (context, index) {return ListTile(title: Text(items[index]));},)
결론: 긴 리스트는 ListView.builder와 itemExtent로 효율적으로 처리합니다.
챕터 3: 가로 스크롤 리스트
Why
NOTE이미지 갤러리, 카테고리 선택 등에서 가로 스크롤이 필요합니다.
scrollDirection을 변경하면 간단히 구현할 수 있습니다.
What
NOTEListView의
scrollDirection속성을Axis.horizontal로 설정하면 가로 스크롤이 됩니다.
How
TIP기본 가로 리스트
Container(height: 200, // 높이 제한 필수!child: ListView(scrollDirection: Axis.horizontal,children: [Container(width: 160, color: Colors.red),Container(width: 160, color: Colors.green),Container(width: 160, color: Colors.blue),Container(width: 160, color: Colors.yellow),],),)builder와 함께 사용
Container(height: 150,child: ListView.builder(scrollDirection: Axis.horizontal,itemCount: 20,itemBuilder: (context, index) {return Container(width: 100,margin: EdgeInsets.all(8),decoration: BoxDecoration(color: Colors.primaries[index % Colors.primaries.length],borderRadius: BorderRadius.circular(8),),child: Center(child: Text('$index', style: TextStyle(color: Colors.white)),),);},),)실용적인 예: 카테고리 선택
Container(height: 100,child: ListView.builder(scrollDirection: Axis.horizontal,itemCount: categories.length,itemBuilder: (context, index) {return Padding(padding: EdgeInsets.all(8),child: Column(children: [CircleAvatar(radius: 30,backgroundImage: NetworkImage(categories[index].imageUrl),),SizedBox(height: 8),Text(categories[index].name),],),);},),)
Watch out
WARNING가로 ListView는 높이가 무한하면 오류가 발생합니다.
반드시 부모에서 높이를 제한하세요.// 오류: 높이 제한 없음Column(children: [ListView(scrollDirection: Axis.horizontal, // Unbounded height!children: [...],),],)// 해결: Container나 SizedBox로 높이 지정Column(children: [SizedBox(height: 200,child: ListView(scrollDirection: Axis.horizontal,children: [...],),),],)
결론: 가로 리스트는 scrollDirection을 변경하고 높이를 제한합니다.
챕터 4: GridView로 그리드 만들기
Why
NOTE이미지 갤러리, 상품 목록, 앱 아이콘 등은 그리드 형태가 적합합니다.
GridView는 2차원 격자로 항목을 배치합니다.
What
NOTE
GridView는 항목을 격자 형태로 배치하는 스크롤 가능한 위젯입니다.
GridView.count는 열 개수로,GridView.extent는 타일 크기로 그리드를 정의합니다.
How
TIPGridView.count: 열 개수 지정
GridView.count(crossAxisCount: 2, // 2열children: List.generate(100, (index) {return Container(color: Colors.primaries[index % Colors.primaries.length],child: Center(child: Text('$index'),),);}),)GridView.extent: 타일 최대 크기 지정
GridView.extent(maxCrossAxisExtent: 150, // 타일 최대 너비 150mainAxisSpacing: 4, // 세로 간격crossAxisSpacing: 4, // 가로 간격padding: EdgeInsets.all(4),children: List.generate(100, (index) {return Container(color: Colors.primaries[index % Colors.primaries.length],);}),)GridView.builder: 많은 항목 처리
GridView.builder(gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3,mainAxisSpacing: 8,crossAxisSpacing: 8,),itemCount: 1000,itemBuilder: (context, index) {return Container(color: Colors.primaries[index % Colors.primaries.length],child: Center(child: Text('$index')),);},)GridDelegate 비교
Delegate 용도 SliverGridDelegateWithFixedCrossAxisCount열 개수 고정 SliverGridDelegateWithMaxCrossAxisExtent타일 최대 크기 지정 // 3열 고정SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3,childAspectRatio: 1.0, // 정사각형)// 타일 최대 150pxSliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 150,childAspectRatio: 1.0,)
Watch out
WARNINGGridView의 자식은 기본적으로 정사각형(1<1>1> 비율)입니다.
childAspectRatio로 비율을 조정하세요.GridView.count(crossAxisCount: 2,childAspectRatio: 16 / 9, // 가로로 넓은 타일children: [...],)
결론: GridView.count로 열 개수를, GridView.builder로 많은 항목을 효율적으로 처리합니다.
챕터 5: 혼합 리스트 만들기
Why
NOTE실제 앱에서는 헤더, 메시지, 광고 등 다양한 타입의 항목이 섞인 리스트가 필요합니다.
추상 클래스를 사용해 다형성을 구현하면 깔끔하게 처리할 수 있습니다.
What
NOTE공통 인터페이스를 정의하고, 각 항목 타입이 이를 구현하는 방식으로 혼합 리스트를 만듭니다.
How
TIP1. 공통 인터페이스 정의
abstract class ListItem {Widget buildTitle(BuildContext context);Widget buildSubtitle(BuildContext context);}2. 각 타입별 클래스 구현
// 헤더 항목class HeadingItem implements ListItem {final String heading;HeadingItem(this.heading);@overrideWidget buildTitle(BuildContext context) {return Text(heading,style: Theme.of(context).textTheme.headlineSmall,);}@overrideWidget buildSubtitle(BuildContext context) => SizedBox.shrink();}// 메시지 항목class MessageItem implements ListItem {final String sender;final String body;MessageItem(this.sender, this.body);@overrideWidget buildTitle(BuildContext context) => Text(sender);@overrideWidget buildSubtitle(BuildContext context) => Text(body);}3. 혼합 데이터 생성
final items = List<ListItem>.generate(1000, (i) {if (i % 6 == 0) {return HeadingItem('섹션 ${i ~/ 6}');} else {return MessageItem('발신자 $i', '메시지 내용 $i');}});4. ListView.builder로 렌더링
ListView.builder(itemCount: items.length,itemBuilder: (context, index) {final item = items[index];return ListTile(title: item.buildTitle(context),subtitle: item.buildSubtitle(context),);},)더 복잡한 예: 타입별 다른 위젯
ListView.builder(itemCount: items.length,itemBuilder: (context, index) {final item = items[index];if (item is HeadingItem) {return Container(color: Colors.grey[200],padding: EdgeInsets.all(16),child: Text(item.heading,style: TextStyle(fontWeight: FontWeight.bold),),);} else if (item is MessageItem) {return ListTile(leading: CircleAvatar(child: Text(item.sender[0])),title: Text(item.sender),subtitle: Text(item.body),);}return SizedBox.shrink();},)
Watch out
WARNING타입 체크 시
is연산자를 사용하세요.
runtimeType비교는 상속 관계에서 문제가 될 수 있습니다.// 권장if (item is HeadingItem) { ... }// 비권장if (item.runtimeType == HeadingItem) { ... }
결론: 추상 클래스와 다형성으로 다양한 타입의 항목을 하나의 리스트에서 처리합니다.
한계
ListView와 GridView는 기본적인 스크롤 목록에 적합합니다.
- 복잡한 스크롤 효과: 접히는 헤더, 고정 헤더 등은 CustomScrollView와 Sliver가 필요합니다.
- 무한 스크롤: 페이지네이션 로직을 직접 구현해야 합니다.
- 가변 높이 그리드: 기본 GridView는 고정 비율만 지원하며, 가변 높이는 추가 패키지가 필요합니다.
Footnotes
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!