Flutter 튜토리얼 8편: 리스트와 그리드 구현

요약#

핵심 요지#

  • 문제 정의: 많은 데이터를 효율적으로 화면에 표시하려면 스크롤 가능한 목록이 필요하다.
  • 핵심 주장: Flutter의 ListView1GridView2로 다양한 형태의 목록을 구현한다.
  • 주요 근거: ListView.builder3는 화면에 보이는 항목만 생성해 메모리를 절약한다.
  • 실무 기준: 항목이 많으면 반드시 builder 생성자를 사용하고, 항목 크기를 미리 지정한다.
  • 한계: 복잡한 스크롤 동작이 필요하면 CustomScrollView와 Sliver를 사용해야 한다.

문서가 설명하는 범위#

  • ListView의 기본 사용법과 builder 패턴
  • 가로 스크롤 리스트 구현
  • GridView로 그리드 레이아웃 만들기
  • 다양한 타입의 항목이 섞인 혼합 리스트

읽는 시간: 14분 | 난이도: 초급


참고 자료#


문제 상황#

앱에서 연락처, 메시지, 상품 목록처럼 많은 데이터를 표시해야 하는 경우가 많습니다.
단순히 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.builderitemBuilder 콜백을 사용해 필요한 항목만 생성합니다.
스크롤할 때 화면 밖으로 나간 항목은 메모리에서 해제됩니다.

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#

NOTE

ListView의 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#

TIP

GridView.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, // 타일 최대 너비 150
mainAxisSpacing: 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, // 정사각형
)
// 타일 최대 150px
SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 150,
childAspectRatio: 1.0,
)

Watch out#

WARNING

GridView의 자식은 기본적으로 정사각형(1<1> 비율)입니다.
childAspectRatio로 비율을 조정하세요.

GridView.count(
crossAxisCount: 2,
childAspectRatio: 16 / 9, // 가로로 넓은 타일
children: [...],
)

결론: GridView.count로 열 개수를, GridView.builder로 많은 항목을 효율적으로 처리합니다.


챕터 5: 혼합 리스트 만들기#

Why#

NOTE

실제 앱에서는 헤더, 메시지, 광고 등 다양한 타입의 항목이 섞인 리스트가 필요합니다.
추상 클래스를 사용해 다형성을 구현하면 깔끔하게 처리할 수 있습니다.

What#

NOTE

공통 인터페이스를 정의하고, 각 항목 타입이 이를 구현하는 방식으로 혼합 리스트를 만듭니다.

How#

TIP

1. 공통 인터페이스 정의

abstract class ListItem {
Widget buildTitle(BuildContext context);
Widget buildSubtitle(BuildContext context);
}

2. 각 타입별 클래스 구현

// 헤더 항목
class HeadingItem implements ListItem {
final String heading;
HeadingItem(this.heading);
@override
Widget buildTitle(BuildContext context) {
return Text(
heading,
style: Theme.of(context).textTheme.headlineSmall,
);
}
@override
Widget buildSubtitle(BuildContext context) => SizedBox.shrink();
}
// 메시지 항목
class MessageItem implements ListItem {
final String sender;
final String body;
MessageItem(this.sender, this.body);
@override
Widget buildTitle(BuildContext context) => Text(sender);
@override
Widget 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#

  1. ListView(리스트뷰): 스크롤 가능한 목록을 표시하는 위젯이다.

  2. GridView(그리드뷰): 항목을 2차원 격자 형태로 배치하는 스크롤 가능한 위젯이다.

  3. ListView.builder: 화면에 보이는 항목만 동적으로 생성하는 효율적인 ListView 생성자다.

공유

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

Flutter 튜토리얼 8편: 리스트와 그리드 구현
https://moodturnpost.net/posts/flutter/flutter-lists-grids/
작성자
Moodturn
게시일
2026-01-08
Moodturn

목차