Flutter를 사용하면서 리스트(List Builder)를 많이 활용하고 계시죠? 하지만 여러분은 이 리스트 빌더를 정말 제대로 사용하고 계신가요? 이번 글에서는 Flutter에서 리스트 빌더를 올바르게 사용하는 방법과, 최적화가 부족한 부분은 무엇인지에 대해 알아보겠습니다.
- Why
List Builder를 왜 사용하시나요? 리스트 안에 담긴 아이템들을 쉽게 보여주기 위해서인가요? 코드가 짧아져서 편리하다고 느끼시나요? 이러한 이유들도 있지만, 가장 중요한 점은 성능 향상에 큰 도움이 되기 때문입니다.
This constructor is appropriate for list views with a large (or infinite) number of children because the builder is called only for those children that are actually visible.
Flutter 공식문서 ListView Builder에 관한 설명입니다. 해석하면 실제로 표시되는 위젯에 한해서만 빌드가 되기 때문에 많은 양의 리스트를 보여주기에 적합하다고 볼 수 있습니다. 비슷한 것으로 안드로이드에 RecyclerView라는 게 있습니다. 이것을 증명하기 위해 아래 코드를 실행해 보겠습니다.
class ListPage extends StatelessWidget {
const ListPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemBuilder: (context, index) {
print('빌드: $index');
return _buildItem();
},
itemCount: 30,
),
);
}
Widget _buildItem() {
return Container(
width: double.infinity,
height: 150,
margin: const EdgeInsets.only(bottom: 10),
color: Colors.deepOrangeAccent,
alignment: Alignment.center,
child: const Text('Hello, World!'),
);
}
}
--- 결과 ---
flutter: 빌드: 0
flutter: 빌드: 1
flutter: 빌드: 2
flutter: 빌드: 3
flutter: 빌드: 4
flutter: 빌드: 5
flutter: 빌드: 6
itemCount가 30개지만 빌드되는 것은 7번째 아이템까지만 빌드가 되는 것을 확인할 수 있습니다. 드래그를 하면 화면에서 사라진 위젯은 정리가 되고 화면에 나타난 위젯이 다시 그려지면서 Print가 계속 찍히는 것을 보실 수 있으실 겁니다. 그래서 뭐가 문제야?라고 할 수 있습니다. 우리가 UI를 개발할 때 저렇게 리스트만 있는 페이지로 개발을 할까요? 있을 수도 있습니다. 하지만 대부분 그렇지 않죠. 아래에 잘 사용한 예제와 그렇지 못한 예제를 보도록 하겠습니다.
- Good
class ListPage extends StatelessWidget {
const ListPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
Text(
'이것은 Hello, World! 리스트 입니다.',
),
Expanded(
child: ListView.builder(
itemBuilder: (context, index) {
print('빌드: $index');
return _buildItem();
},
itemCount: 30,
),
),
],
),
),
);
}
Widget _buildItem() {
return Container(
width: double.infinity,
height: 150,
margin: const EdgeInsets.only(bottom: 10),
color: Colors.deepOrangeAccent,
alignment: Alignment.center,
child: const Text('Hello, World!'),
);
}
}
- Bad
class ListPage extends StatelessWidget {
const ListPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: SingleChildScrollView(
child: Column(
children: [
Text(
'이것은 Hello, World! 리스트 입니다.',
),
ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (context, index) {
print('빌드: $index');
return _buildItem();
},
itemCount: 30,
),
],
),
),
),
);
}
Widget _buildItem() {
return Container(
width: double.infinity,
height: 150,
margin: const EdgeInsets.only(bottom: 10),
color: Colors.deepOrangeAccent,
alignment: Alignment.center,
child: const Text('Hello, World!'),
);
}
}
Good의 코드는 타이틀이 고정되어 있고 리스트를 보여주는 영역이 Expanded로 감싸져 있어 리스트를 보여줄 영역이 정확히 지정되어 있습니다.
Bad의 코드는 타이틀이 고정되어 있지 않고 스크롤 시 같이 올라가고 그로 인해 리스트를 보여줄 영역을 지정하지 않아서 shrinkWrap 속성을 사용해서 자식 Widget의 크기만큼 알아서 영역을 잡아라! 이렇게 만들어둔 코드입니다.
뭐가 문제야? Bad의 코드를 실행하면 아래와 같은 결과가 실행하자마자 나타납니다.
--- 결과 ---
flutter: 빌드: 0
flutter: 빌드: 1
flutter: 빌드: 2
...
flutter: 빌드: 27
flutter: 빌드: 28
flutter: 빌드: 29
결과로 보면 실행하자마자 모든 위젯을 빌드시키는 것을 확인할 수 있습니다. 이것은 ListView Builder의 실제로 표시되는 위젯에 한해서만 빌드가 된다 라는 장점을 잃어버린 코드라고 볼 수 있습니다.
물론 위의 코드처럼 Container에 Text 정도만 보여주는 경우라면 크리티컬 한 문제가 아닐 수 있습니다. 하지만 만약 이미지를 표시해야 한다면, 모바일 기기가 이미지를 그리는 데 필요한 CPU 자원을 감당하지 못해 튕겨버리는 경우가 발생할 수 있습니다.
- Why?
그렇다면 왜 Bad의 코드는 저런 현상이 나타나는 것 일 까요? Good의 코드와 같이 리스트를 그리는 영역이 고정적일 경우 ListView.builder는 정상적으로 동작합니다. 반면, Bad의 코드와 같이 shrinkWrap 속성을 사용하면 리스트의 크기가 동적으로 조정됩니다. 이 경우, 리스트의 전체 높이를 계산하기 위해 모든 아이템을 한 번에 빌드합니다.
성능을 신경 쓴다면 shrinkWrap의 사용은 자제하는 것이 좋습니다.
- Solution
그렇다면 Bad코드와 같이 리스트 영역 위에 따로 보여줄 영역이 있어야 하고, 스크롤 시에 같이 위로 올라가야 하는 상황에서는 어떻게 해야 하는지 많은 방법 중에 제일 간단한 방법을 설명하겠습니다.
class ListPage extends StatelessWidget {
const ListPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: ListView.builder(
itemBuilder: (context, index) {
print('빌드: $index');
if (index == 0) {
return Column(
children: [
Text(
'이것은 Hello, World! 리스트 입니다.',
),
_buildItem()
],
);
}
return _buildItem();
},
itemCount: 30,
),
),
);
}
Widget _buildItem() {
return Container(
width: double.infinity,
height: 150,
margin: const EdgeInsets.only(bottom: 10),
color: Colors.deepOrangeAccent,
alignment: Alignment.center,
child: const Text('Hello, World!'),
);
}
}
위의 코드처럼 0번째 인덱스에서 타이틀을 함께 반환하는 방식으로 사용하면 ListView.builder는 정상적으로 동작합니다. 하지만 이러한 방법은 시각적으로 매력적이지 않으며, 만약 위에 보여줄 위젯들이 많아진다면 더욱 어색해질 수 있습니다. 이러한 문제를 해결하기 위해, 다음 포스트에서는 CustomScrollView를 사용해 해당 문제를 해결해 보겠습니다.
그리고 추가적으로 본인 프로젝트에서 ListView Builder는 제 역할을 하는지 한번 점검해 보시고 효율적으로 사용하시길 바라겠습니다.
'Flutter > 기본' 카테고리의 다른 글
[Flutter] Migrate to applying Gradle plugins with the declarative plugins block (1) | 2025.02.28 |
---|---|
[Flutter] Cursor로 간단하게 개발하기 (1) | 2025.01.30 |
[Flutter] Cursor로 프로젝트 세팅하기 (1) | 2025.01.30 |
[Flutter] 개발 환경 세팅 - macOS (1) | 2024.08.22 |
[Flutter] 해피톡(HappyTalk) 서비스 레퍼런스 (0) | 2024.08.21 |