로그인 화면이나 프로필 페이지를 개발하다 보면 필연적으로 Column 위젯 내부에 ListView나 GridView를 배치해야 하는 순간이 옵니다. "상단에 고정된 타이틀을 두고, 아래에는 스크롤 가능한 리스트를 넣자"라는 단순한 기획을 코드로 옮기는 순간, 에뮬레이터는 시뻘건 화면과 함께 Vertical viewport was given unbounded height라는 악명 높은 에러를 뱉어냅니다. 초보 시절 저를 며칠 밤새우게 만들었던 이 에러는 위젯의 배치 순서가 아닌, Flutter의 제약 조건(Constraints) 전달 방식에 대한 오해에서 비롯됩니다.
무한(Infinite)과 무한의 충돌: 원인 분석
이 문제의 핵심은 Column과 스크롤 가능한 위젯(ListView 등)이 높이를 계산하는 방식의 충돌에 있습니다. Flutter의 레이아웃 시스템은 "부모가 제약(Constraints)을 내리고, 자식이 크기(Size)를 올린다"는 대원칙을 따릅니다.
Column 위젯은 기본적으로 Main Axis(수직 방향)로 가능한 한 모든 자식을 배치하려 하며, 자식들에게 "너희가 원하는 만큼 커져도 좋아(Unbounded)"라는 신호를 보냅니다. 반면, ListView는 스크롤 가능한 영역을 확보하기 위해 부모가 허용하는 최대 높이까지 확장하려는 성질이 있습니다.
Column(높이 제한 없음) 안에 ListView(무한히 확장 시도)를 넣으면, ListView는 자신의 끝이 어디인지 계산할 수 없게 됩니다. 결과적으로 렌더링 엔진은 높이를 Infinity로 계산하다가 RenderFlex overflowed 오류를 발생시킵니다.
The Solution: Expanded 위젯을 통한 제약 강제
가장 정석적이고 성능상 이점이 많은 해결책은 Expanded 위젯을 사용하는 것입니다. Expanded는 Column 내에서 남은 공간(Remaining Space)을 계산하여 자식 위젯에게 명확한 높이 제약(Bounded Height)을 전달합니다.
아래 코드는 에러가 발생하는 상황을 Expanded로 래핑하여 해결한 예시입니다. Flexible을 사용해도 되지만, fit: FlexFit.tight 속성을 기본으로 가진 Expanded가 UI 레이아웃을 잡는 데 더 직관적입니다.
// Bad Code: Unbounded Height Error 발생
Column(
children: [
Text("Header"),
// ListView는 여기서 무한한 높이를 가지려 시도함
ListView.builder(
itemCount: 100,
itemBuilder: (context, index) => Text("Item $index"),
),
],
)
// Good Code: Expanded로 남은 공간 계산 강제
Column(
children: [
Container(
height: 50,
color: Colors.blue,
child: Center(child: Text("Fixed Header")),
),
// Expanded가 남은 화면 높이를 계산하여 ListView에 전달
Expanded(
child: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) => ListTile(
title: Text("Safe Item $index"),
),
),
),
],
)
대안: shrinkWrap: true 사용 시 주의사항
또 다른 해결책으로 ListView의 shrinkWrap: true 옵션을 사용하는 방법이 있습니다. 이 속성은 리스트가 자신의 자식들을 모두 렌더링해본 뒤, 그 내용물의 전체 크기만큼만 자리를 차지하게 만듭니다.
shrinkWrap: true는 리스트의 모든 아이템 높이를 계산해야 하므로 비용이 비쌉니다. 아이템이 적을 때는 괜찮지만, 데이터가 많은 리스트에 사용하면 스크롤 버벅임(Jank)의 주원인이 됩니다. 가급적 Expanded를 우선 고려하십시오.
| 해결 방법 | 동작 원리 | 성능(Performance) | 추천 상황 |
|---|---|---|---|
| Expanded | 남은 공간만큼 높이 강제 할당 | 최상 (O(1)) | 화면 전체를 채우는 리스트 |
| shrinkWrap: true | 내용물 크기에 맞춰 높이 축소 | 낮음 (O(N)) | 다이얼로그 내부 등 작은 리스트 |
| SliverList | CustomScrollView 위임 | 상 (Complex) | 복잡한 스크롤 효과 필요 시 |
결론
Flutter에서 레이아웃 에러를 마주했을 때 SingleChildScrollView로 무작정 감싸는 것은 "언 발에 오줌 누기" 식의 임시방편일 뿐입니다. 특히 Column 내부에 리스트가 존재한다면 99%의 확률로 Expanded가 정답입니다. 레이아웃의 부모-자식 간 제약 조건 흐름을 이해하는 것이야말로 단순 코더에서 엔지니어로 거듭나는 첫걸음임을 명심하시기 바랍니다.
Post a Comment