
안녕하세요, 성조입니다.
오늘은 간단하게 ListView라는 개념에 대해 정리 포스팅을 진행해보려 해요.
ListView class란?

플러터 공식 문서를 따르면 'A scrollalbe list of widgets arranged linearly.'라는 설명이 나온다.
해석하면 위젯들이 한 방향(선형)으로 나열되어 있으며, 스콜이 가능한 리스트.라고 해석 할 수 있다.
정의를 이해하기 위해 조금 설명을 개별적으로 조금씩 풀어보면 다음과 같다.
scrollabe -> 디바이스의 크기에서 화면을 넘어서면 스크롤이 가능해진다는 의미이다.
list of widgets -> 아이템이 단순 데이터가 아니라 위젯 단위가 된다는 의미이다.
arranged linearly -> 격자(Grid)가 아니라 한 방향(세로 또는 가로)으로 배치해서 사용한다는 의미이다.
즉, 위젯들을 하나의 직선 방향으로 배치했으며, 스크롤이 가능한 목록 정도로 정의해서 볼 수 있다는 게 종합적인 의미가 될텐데 ListView가 Column/Row 의 방향으로 스크롤(Scroll) 기능이 결합된 개념이라 이해하면 된다.
또한, ListView class를 분리해보면 내부적으로 CustomScrollView + SliverList 1개로 구성된 구조가 된다.
ListView 생성 방법
리스트뷰는 공식 문서에서 4가지 방식의 생성 방법이 있다고 설명한다.

4가지 설명 위에 내용은 위에 설명했던 ListView는 가장 일반적으로 사용되는 스크롤 위젯임을 이야기한다. 또한, 하나씩 반대 축(cross axis) 방향에서는 자식 위젯이 ListView의 크기를 가득 채워야 한다고 이야기한다.
1) 기본 생성자 방법
List<widget> 형태의 자식 위젯 리스트를 직접 전달하며, 자식 수가 적은 경우에 권장된다.
리스트에 들어갈 모든 자식 위젯을 미리 생성하기 때문에, 자식의 자식 위젯들까지 화면에 불필요하게 렌더링 되더라도 실제로 화면에 보이지 않는 위젯까지 모두 생성하게 되면서 디바이스 성능을 저하시킬 수 있는 문제 점을 인지하고 사용하는 것이 좋다.
즉, 리스트 값이 내부 한정적인 데이터 타입들을 해당 사항에서 맞춰서 사용하는 것이 좋을 수 있다는 이야기가 된다.
기본 생성자는 ListView(children: [...])의 형태로 데이터를 전달한한다.
ListView(
padding: const EdgeInsets.all(10),
children: <Widget>[
Container(
height: 50,
color: Colors.black,
child: const Center(child: Text('A')),
),
Container(
height: 50,
color: Colors.black,
child: const Center(child: Text('B')),
),
Container(
height: 50,
color: Colors.black,
child: const Center(child: Text('C')),
),
],
);
위와 같은 형식으로 생성 활용하며, 자식 위젯들도 모두 가져오는 것을 인지해야 된다.
2) ListView.builder
IndexedWidgetBuilder를 받아서 필요한 시점에만 위젯을 생성하는 방법으로 실무에서 자주 접하게 되는 문법이다. 실제 데이터가 필요한 시점에만 위젯을 생성하기 때문에 부모 위젯에서 자식 위젯으로 데이터가 필요한 시점에서만 요청하게 되며, 실제 데이터를 전달 받는 시점이 특정 스크롤 위치 영역마다 호출하게 되는 렌더링 조건을 걸어주면 무한 스크롤뷰를 구성하게 된다는 것을 인지할 수 있게 된다.
위 생성자의 경우 데이터를 1천개 또는 1만개 데이터를 한번에 불러올 때 한명의 경우 괜찮지만 동시 접속자들이 대량의 트래픽을 요청하면서 서버에 접근하게 되면 치명적인 문제가 된다.
무한 스크롤 구조로 내보내는 것을 권장하는 게 좋다.
예제 코드는 다음과 같다.
final List<String> entries = <String>['A', 'B', 'C'];
final List<int> colorCodes = <int>[600, 500, 100];
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.all(10),
itemCount: entries.length,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 50,
color: Colors.amber[colorCodes[index]],
child: Center(child: Text('Entry ${entries[index]}')),
);
},
);
}
3) ListView.separated
구분선 또는 간격이 기본 요구사항으로 두 개의 값이 들어오는 경우 itemBuilder로 separatorBuilder로 아이템 사이에 separator를 생성해서 사용한다.
이 생성자로 값을 사용하는 경우 고정 길이(fixed number)의 리스트에 적합한 방식이다. 이 방식은 Divider, spacing 등을 깔끔하게 관리할 수 있는 리스트 방법으로 고정 정돈하는 UI 파트에서 리스트 뷰가 필요할 때 불러오면 좋다.
예제 코드는 다음과 같다.
final List<String> entries = <String>['A', 'B', 'C'];
final List<int> colorCodes = <int>[600, 500, 100];
Widget build(BuildContext context) {
return ListView.separated(
padding: const EdgeInsets.all(10),
itemCount: entries.length,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 50,
color: Colors.amber[colorCodes[index]],
child: Center(child: Text('Entry ${entries[index]}')),
);
},
separatorBuilder: (BuildContext context, int index) => const Divider(),
);
}
4) ListView.custom
SliverChildDelegate를 직접 전달하는 생성자 방식이다.
자식 위젯 모델의 동작 방식을 세밀하게 제어하는 경우 커스텀 생성자 방식을 선택해서 사용하면 좋다.
아직 화면에 보이지 않는 자식들의 크기나 나오는 애니메이션 동작 등에 대한 배치 등을 작업하기 좋고, AI 시대에서 많이 사용되는 코사인 유사도 값으로 추론하는 알고리즘을 구성했을 때 추정 값들을 불러오는 부분에서도 어떻게 추정 제어할 것인지 선택할 수 있기 때문에 디테일한 단위에서 커스텀이 필요한 경우 추천된다.
초기 스크롤 위치 지정하는 방법
ListView에서는 스크롤 레이어 값이 나오는 위치를 제어할 수 있는데 ScrollController 메서드를 제공하고, 해당 값에서 ScrollController.initialScrollOffset를 지정해서 값을 설정해주면 된다.
[예시 코드]
final controller = ScrollController(initialScrollOffset: 200); // 위치
ListView.builder(
controller: controller,
itemCount: 100,
itemBuilder: (_, i) => ListTile(title: Text('Item $i')),
);
itemExtent와 prototypeitem 성능 개선 더 하기
ListView 공식 문서에는 성능 문제로 인해 itemExtent와 prototypeItem 두 가지 방법에 대해서도 다루는데 정리하면 다음과 같다.
1) itemExtent -> null이 아니면, 스크롤 방향으로 자식의 크기를 강제하는 것을 권장한다.
2) prototypeitem -> null이 아니면, 제공된 위젯과 동일한 extent를 갖도록 강제하는 것을 권장한다.
공식 문서 피셜 아이템 크기를 미리 알면 스크롤 엔진이 foreknowledge(사전 지식)을 인식하고 활용해서 스크롤 위치가 크게 바뀌는 상황 등에서 불필요한 렌더링 작업을 줄일 수 있어서 더 효율적으로 연산된다는 것이다.
참고로 itemExtent와 protypeItem은 둘이 동시에 사용할 수 없고 하나만 사용하거나, 아예 사용하지 않는 구조로만 사용된다.
ListView의 Child Lifecycle과 상태 관리 전략 이해하기
공식 문서에서는 리스트뷰 클래스의 생명주기 관련해서도 정리를 진행한다.
1) ListView 자식 위젯의 생명주기
2) 스크롤 시 발생하는 생명 주기의 생성과 파괴(정리) 동작
3) 상태 유지를 위한 가이드 해결 전략
4) ListView에서 CustomScrollView로 전환하는 기준과 매핑 관계
1. ListView Child Elements(자식 위젯)의 Lifecycle(생명주기)
1-1) Creation
문서에서는 ListView는 리스트를 레이아웃하는 과정에서 화면에 실제로 보이는 자식 위젯들만 생성하는 것으로 정의한다.
다만, 위에 첫 생성자에서 얘기했던 것처럼 한 번에 생성하는 생성자도 있으니 생성자에 다르다고 보는 것이 맞다고 생각한다.
생성 과정에서 생겨나는 것은 다음의 3가지 속성 값이 있다.
- Element
- State
- RenderObject
이 생성 과정은 lazy 방식(지연 생성)으로 동작한다.
- ListView(children: ...) -> 이미 존재하는 위젯을 필요할 때만 생성하는 지연 생성하는 방식
- ListView.builder(...) -> bulider를 통해 필요한 시점에 위젯을 제공받아 지연 생성하는 방식
여기서 기본 생성자는 위에서 모두 한번에 생성한다고 정의하지 않았는지에 대해 의문을 품을 수 있다.
중요한 지점은 기본 생성자는 위젯 객체를 생성하는 것이라 하위 속성 3가지 값을 생성하는 것이 아니라는 점을 인지해야 된다.
테이블로 정리하면 다음과 같이 정리할 수 있다.
| 구분 | 기본 생성자 | ListView.builder |
| Widget 객체 | lazy 생성이 아님 (모두 생성해버림) | O - lazy |
| Element | O - lazy | O - lazy |
| State | O - lazy | O - lazy |
| RenderObject | O - lazy | O - lazy |
| 대량 데이터 송출에 적합한가? | X | O |
기본 생성자의 경우 데이터 수가 많으면 lazy 생성을 지원하지 않는다.
또한, 위젯 객체 수 자체가 많아지면서 [GC, 메모리] 값들을 통해 자원을 많이 소비하기 때문에 트래픽이 작은 요소는 괜찮지만 큰 요소에서는 비권장하게 되는 것이다.
1 - 2) Destruction (파괴 (종료(?)))
리스트 아이템의 값들이 스크롤되어 디바이스 화면 밖으로 벗어나면, 해당 아이템과 연결된 모든 요소를 완전히 제거(destroy) 하게 만드는 단계이다.
다음 항목을 제거한다.
- element subtree
- state
- render object
이후, 동일한 위치에 아이템이 다시 들어오면 새로운 [element, state, render object] 값들을 lazy하게 다시 생성하게 된다.
2. Destruction Mitigation 상태 유지 전략 (문서 참조)
스크롤로 인해 자식 위젯이 생성과 파괴되는 과정에서 상태를 유지해야 하는 경우, 공식 문서에서는 이런 방법들을 제시한다.
2 - 1) 핵심 상태를 리스트 외부로 이동하는 방식 (권장)
가장 권장되는 방식으로 중요한 상태를 리스트 아이템 밖으로 분리하는 방법이다.
예를 들면 본인이 만들고 있는 앱에서 게시물을 리스트 형식으로 불러올 때 또는 네트워크 캐시 기반의 추천 수(upvote)값들을 다루는 경우가 이런 외부로 처리하는 방법을 권장한다.
게시글 데이터와 추천 수 또는 조회수 등을 리스트 외부의 데이터 모델(source of truth) 등에 저장하고, 리스트 아이템 UI는 해당 모델의 기반으로 언제든 재생성 가능하도록 구성해주는 방법이다. 리스트 아이템 내부의 StatefulWidget은 일시적인 UI 상태만 관리하게 된다는 것이다.
이런 방식은 ListView의 lazy 호출 특성과 잘 맞으면서 상태 초기화 문제를 구조적으로 해결하는 방식이 된다.
상태 관리는 BLoC, Riverpod, Provider 등의 관리 패턴들을 말하는 것이다.
2 - 2) KeepAlive 위젯 사용 (무조건 유지 사용)
유지하고 싶은 리스트 아이템의 루트 위젯으로 KeepAlive를 사용하는 방법이다.
KeepAlive는 해당 자식의 최상위 render object를 keep-alive 대상으로 표시하는 방식이며, 스크롤 화면 밖에 나가서도 destroy되지 않기에 캐시 리스트에서 해당 값을 보관해서 항시 유지되어야 하는 값들이 여기에서 사용된다.
서비스 속도 향상을 위한 캐싱에 가깝다고 생각한다.
단, 다음 조건들을 만족해야 된다.
addAutomaticKeepAlives == false
addRepaintBoundaries == false
두 옵션들이 true 값을 가져가면 ListView가 자식을 다른 위젯으로 감싸기 때문에 위 조건들을 충족해야 기능을 활용할 수 있다.
2 - 3) AutomaticKeepAlive
조건부 유지를 진행하는 권장 방식이다.
플러터 공식 문서는 종종 자동 할당 부분을 생각보다 권장하는 경우가 있다.
해당 방식은 addAutomaticKeepAlives가 true일 때 기본적으로 자동 삽입되는 keep-alive 방식이다.
EditableText 같은 텍스트 필더에 포커스가 있을 때 subtree를 keep alive로 보내고, 포커스가 없고 다른 keep-alive 요청도 없다면 스크롤 시 destroy(파괴 처리)하는 방식으로 사용되는 메서드 값이다.
보통 자식 위젯에서는 AutomaticKeepAliveClientMixin를 사용하거나, wantKeepAlive getter를 구현, updateKeepAlive() 호출하는 등 실무에서 많이 사용되는 방식이라는 말이 있다.
Transitioning to CustomScrollView (커스텀스크롤뷰로 전환) 이해하기
이전에 짧게 언급했던 리스트뷰는 커스텀뷰스크롤로 전환하는 부분에 대해서도 이야기가 나온다.
ListView class는 내부적으로 CustomScrollView와 SliverList 1개로 구성된 구조이다.
커스텀스크롤뷰가 필요한 시점은 다음과 같다.
1) [리스트 + 그리드] 구조로 조합해야 되는 경우
2) SliverAppBar와 결합해야 하는 경우
3) 복잡한 스크롤 UI 구성과 로딩 속도에 맞춰서 분리해야 되는 경우 (캐싱 서버 또는 파워에 대한 차이와 유저에게 보여줘야 하는 속도 등의 디테일한 값들을 해당 파트에서 반영한다고 보면 된다.)
ListView에서 CustomScrollView 매핑 관계
ListView의 다음 속성들은 커스텀스크롤뷰(CustomScrollVIew)의 동일한 이름의 속성과 1:1 대응으로 적용된다.
- key
- scrollDirection
- reverse
- controller
- primary
- physics
- shrinkWrap
ListView와 CustomScrollView의 차이점으로 padding 처리 방법이 다르다.
ListView는 MediaQuery.padding 값을 자동으로 회피한다.
CustomScrollView는 자동으로 처리하지 않는다.
디테일한 메서드 값들을 모두 번역 해석하기에는 투머치할 수 있기에 해당 개념 이론 정도로 파악하고 더 고도화가 필요하거나 디테일함을 공부해야 하는 경우 공식 문서를 참조하자.
감사합니다.
다음 포스팅에서 뵙겠습니다.
- 참조 주소 -
https://api.flutter.dev/flutter/widgets/ListView-class.html
ListView class - widgets library - Dart API
A scrollable list of widgets arranged linearly. ListView is the most commonly used scrolling widget. It displays its children one after another in the scroll direction. In the cross axis, the children are required to fill the ListView. If non-null, the ite
api.flutter.dev
'Flutter' 카테고리의 다른 글
| [Flutter] Flutter Padding class 이해하기 (0) | 2026.01.02 |
|---|---|
| [Flutter] SingleChildScrollView 정리하기 (0) | 2026.01.01 |
| [Flutter] AppLifecycleState 이론 한 스푼 (7) | 2025.08.04 |
| [Flutter] Bloc 상태 관리. 기초부터 심화까지 공식문서 학습하기 (1) | 2025.06.15 |
| [Flutter] StatelessWidget과 StatefulWidget의 차이점 정리 (0) | 2023.08.19 |