Flutter 튜토리얼 6편: 레이아웃 기초
요약
핵심 요지
문서가 설명하는 범위
- 레이아웃의 기본 원리와 제약 조건 흐름
- Row와 Column을 사용한 배치
- 정렬과 크기 조절 방법
- 실전 레이아웃 예제
읽는 시간: 18분 | 난이도: 초급
참고 자료
- Layout fundamentals - 레이아웃 핵심 개념
- Layouts in Flutter - 레이아웃 위젯 개요
- Building layouts - 레이아웃 튜토리얼
문제 상황
위젯을 화면에 배치할 때 위치와 크기를 정확히 제어하기 어렵습니다.
CSS의 Flexbox나 네이티브의 LayoutManager와 달리 Flutter만의 레이아웃 규칙이 있기 때문입니다.
이 규칙을 모르면 원하는 대로 배치할 수 없습니다.
레이아웃 관련 질문들
- 위젯을 가로로 나란히 배치하려면?- 남은 공간을 균등하게 나누려면?- 위젯을 중앙에 정렬하려면?- 화면 크기에 따라 레이아웃을 바꾸려면?문제는 다음과 같습니다.
- Flutter의 레이아웃 규칙을 모르면 예상과 다른 결과가 나온다.
- “Unbounded constraints” 오류가 자주 발생한다.
- 위젯 간 공간 분배가 직관적이지 않다.
해결 방법
Flutter의 레이아웃은 위젯 조합으로 구성됩니다.
Row와 Column으로 배치하고, 정렬과 크기 조절로 미세 조정합니다.
이 네 가지만 알면 대부분의 레이아웃을 만들 수 있습니다.
챕터 1: 레이아웃의 기본 원리
Why
NOTEFlutter에서 레이아웃을 이해하려면 제약 조건(Constraints)을 알아야 합니다.
제약 조건이란 무엇일까요?
부모가 자식에게 “이 범위 안에서 크기를 정해”라고 알려주는 것입니다.flowchart LR A[부모] -->|"최소 100, 최대 200"| B[자식] B -->|"150으로 결정"| A A -->|"여기에 배치"| C[화면]
What
NOTEFlutter의 레이아웃은 세 단계로 작동합니다.
첫째, 부모가 자식에게 제약을 전달합니다.
둘째, 자식이 크기를 결정해서 부모에게 알립니다.
셋째, 부모가 자식의 위치를 결정합니다.
How
TIP레이아웃 대화 과정
sequenceDiagram participant Parent as 부모 Widget participant Child as 자식 Widget Parent->>Child: 제약 조건 전달 (min/max 크기) Child->>Child: 제약 내에서 크기 결정 Child->>Parent: 결정된 크기 보고 Parent->>Parent: 자식 위치 설정위젯의 세 가지 크기 동작
동작 설명 예시 최대한 크게 가능한 모든 공간 차지 Center, ListView 자식과 같은 크기 자식 크기에 맞춤 Transform, Opacity 특정 크기 고유 크기 사용 Image, Text // Center: 가능한 모든 공간을 차지하고 자식을 중앙에Center(child: Text('Hello'), // Text는 자신의 크기만 사용)// Container with size: 특정 크기 지정Container(width: 200,height: 100,child: Text('Hello'),)
Watch out
WARNING
SizedBox나Container로 크기를 지정해도 부모의 제약이 우선합니다. 부모가 허용하는 범위 내에서만 크기가 적용됩니다.// 부모가 최대 100px만 허용하면SizedBox(width: 200, // 요청한 크기// 실제로는 100px만 적용됨)
결론: Flutter 레이아웃은 부모-자식 간 제약 협상으로 작동합니다.
챕터 2: Row와 Column 사용하기
Why
NOTE위젯을 가로나 세로로 배치하는 것은 가장 기본적인 레이아웃입니다.
Row는 자식을 가로로, Column은 세로로 배열합니다.
이 두 가지만 알아도 많은 레이아웃을 만들 수 있습니다.
What
NOTE
Row는 자식을 수평으로,Column은 수직으로 배치합니다.
둘 다 여러 자식을 받을 수 있습니다.
children속성에 위젯 목록을 전달하면 됩니다.
How
TIPRow: 가로 배치
Row(children: [Icon(Icons.star),Icon(Icons.star),Icon(Icons.star),],)Column: 세로 배치
Column(children: [Text('첫 번째'),Text('두 번째'),Text('세 번째'),],)Row와 Column 중첩
Row(children: [Column(children: [Icon(Icons.star),Text('Dash 1'),],),Column(children: [Icon(Icons.star),Text('Dash 2'),],),Column(children: [Icon(Icons.star),Text('Dash 3'),],),],)축(Axis) 개념
위젯 주축(Main Axis) 교차축(Cross Axis) Row 수평 (→) 수직 (↓) Column 수직 (↓) 수평 (→) graph LR subgraph Row A["← Cross Axis ↓"] B["Main Axis →"] end subgraph Column C["← Cross Axis →"] D["Main Axis ↓"] end
Watch out
WARNINGRow나 Column의 자식이 주축 방향으로 무한한 공간을 차지하려 하면 오류가 발생합니다.
// 오류: Row 안에 스크롤되지 않는 ListViewRow(children: [ListView(...), // ListView가 무한 너비를 요청!],)// 해결: Expanded로 감싸기Row(children: [Expanded(child: ListView(...)),],)
결론: Row는 가로, Column은 세로 배치이며, 중첩해서 복잡한 레이아웃을 구성합니다.
챕터 3: 정렬하기
Why
NOTERow나 Column에 자식을 배치하면 기본적으로 시작점에 몰립니다.
균등하게 배치하거나 중앙에 정렬하려면 어떻게 해야 할까요?
정렬 속성을 사용하면 됩니다.
What
NOTE
mainAxisAlignment는 주축 방향 정렬,crossAxisAlignment는 교차축 방향 정렬을 제어합니다.
How
TIPMainAxisAlignment 옵션
옵션 설명 start시작점에 정렬 (기본값) end끝점에 정렬 center중앙에 정렬 spaceBetween양끝에 붙이고 사이 균등 spaceAround각 요소 주변에 균등 간격 spaceEvenly모든 간격이 동일 Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly,children: [Icon(Icons.star),Icon(Icons.star),Icon(Icons.star),],)시각적 비교
start: [★★★ ]end: [ ★★★]center: [ ★★★ ]spaceBetween:[★ ★ ★]spaceAround: [ ★ ★ ★ ]spaceEvenly: [ ★ ★ ★ ]CrossAxisAlignment 옵션
옵션 설명 start교차축 시작점 정렬 end교차축 끝점 정렬 center교차축 중앙 정렬 (기본값) stretch교차축 방향으로 늘리기 baseline텍스트 기준선 정렬 Row(crossAxisAlignment: CrossAxisAlignment.start,children: [Container(height: 50, child: Text('작음')),Container(height: 100, child: Text('큼')),],)
Watch out
WARNING
crossAxisAlignment: CrossAxisAlignment.stretch를 사용하면 자식이 교차축 방향으로 늘어납니다. 자식에 고정 크기가 있으면 충돌할 수 있습니다.Row(crossAxisAlignment: CrossAxisAlignment.stretch,children: [Container(height: 50), // height가 무시되고 늘어남],)
결론: mainAxisAlignment와 crossAxisAlignment로 위젯 배치를 정밀하게 제어합니다.
챕터 4: 크기 조절하기
Why
NOTERow나 Column에서 자식들이 공간을 어떻게 나눌지 제어해야 합니다.
한 위젯이 남은 공간을 모두 차지하거나, 비율대로 나누는 상황이 있죠.
Expanded와 flex 속성으로 이를 제어할 수 있습니다.
What
NOTE
Expanded는 자식이 남은 공간을 차지하게 합니다.
flex속성으로 비율을 조절할 수 있습니다.
Flexible은 비슷하지만 자식이 원하는 크기보다 작을 수도 있습니다.
How
TIPExpanded: 남은 공간 채우기
Row(children: [Container(width: 100, color: Colors.red),Expanded(child: Container(color: Colors.blue), // 남은 공간 전체),],)flex로 비율 조절
Row(children: [Expanded(flex: 1, // 1/4child: Container(color: Colors.red),),Expanded(flex: 2, // 2/4child: Container(color: Colors.green),),Expanded(flex: 1, // 1/4child: Container(color: Colors.blue),),],)시각적 표현
flex: 1, 2, 1[ red | green | blue ]25% 50% 25%Flexible vs Expanded
속성 Expanded Flexible 최소 크기 남은 공간 전체 자식이 원하는 크기 최대 크기 남은 공간 전체 남은 공간까지 사용 상황 공간 균등 분배 자식 크기 유지하면서 확장 허용 // Flexible: 자식이 원하는 만큼만Row(children: [Flexible(child: Container(width: 50, // 이 크기 유지 가능color: Colors.blue,),),],)
Watch out
WARNINGExpanded나 Flexible은 Row, Column, Flex의 직접 자식이어야 합니다. 다른 위젯으로 감싸면 오류가 발생합니다.
// 오류: Expanded가 Row의 직접 자식이 아님Row(children: [Padding(padding: EdgeInsets.all(8),child: Expanded(...), // 오류!),],)// 해결: Expanded를 바깥으로Row(children: [Expanded(child: Padding(padding: EdgeInsets.all(8),child: ...,),),],)
결론: Expanded와 flex로 공간 분배를 제어하고, 직접 자식 규칙을 지킵니다.
챕터 5: 단일 자식 레이아웃 위젯
Why
NOTERow와 Column 외에도 단일 자식을 다루는 레이아웃 위젯이 있습니다.
Container, Center, Padding 등이 대표적입니다.
이 위젯들로 여백을 주거나 크기를 지정할 수 있습니다.
What
NOTE단일 자식 레이아웃 위젯은
child속성으로 하나의 자식만 받습니다.
자식을 정렬하거나, 크기를 조정하거나, 장식하는 역할을 합니다.
여러 개의 자식이 필요하면 Row나 Column을 사용합니다.
How
TIP주요 단일 자식 위젯
위젯 역할 Center자식을 중앙에 배치 Padding자식 주변에 여백 추가 Container크기, 여백, 배경, 테두리 설정 SizedBox고정 크기 지정 Align자식을 특정 위치에 배치 FractionallySizedBox부모 대비 비율로 크기 지정 Container 활용
Container(width: 200,height: 100,padding: EdgeInsets.all(16),margin: EdgeInsets.all(8),decoration: BoxDecoration(color: Colors.blue,borderRadius: BorderRadius.circular(8),boxShadow: [BoxShadow(color: Colors.black26,blurRadius: 4,offset: Offset(2, 2),),],),child: Text('Hello'),)SizedBox 활용
// 고정 크기 박스SizedBox(width: 100,height: 50,child: Text('Fixed'),)// 빈 공간 (간격용)SizedBox(height: 16), // 세로 간격SizedBox(width: 8), // 가로 간격Align 활용
Align(alignment: Alignment.topRight,child: Text('오른쪽 상단'),)// Alignment 옵션// topLeft, topCenter, topRight// centerLeft, center, centerRight// bottomLeft, bottomCenter, bottomRight
Watch out
WARNINGContainer는 강력하지만 무겁습니다. 단순한 여백만 필요하면 Padding을, 크기만 필요하면 SizedBox를 사용하세요.
// 불필요하게 무거움Container(padding: EdgeInsets.all(8),child: Text('Hello'),)// 가벼운 대안Padding(padding: EdgeInsets.all(8),child: Text('Hello'),)
결론: 목적에 맞는 단일 자식 위젯을 선택하면 코드가 간결해집니다.
챕터 6: 스크롤 레이아웃
Why
NOTE콘텐츠가 화면보다 크면 어떻게 될까요?
스크롤이 필요합니다.
ListView나 SingleChildScrollView로 스크롤을 구현할 수 있습니다.
What
NOTE스크롤 위젯은 자식이 화면을 넘어갈 때 자동으로 스크롤을 제공합니다.
ListView는 많은 항목이 있을 때 사용합니다.
SingleChildScrollView는 단일 콘텐츠에 적합합니다.
How
TIPSingleChildScrollView: 단일 콘텐츠 스크롤
SingleChildScrollView(child: Column(children: [Text('긴 콘텐츠...'),Image.asset('large_image.png'),Text('더 많은 콘텐츠...'),],),)ListView: 목록 스크롤
ListView(children: [ListTile(title: Text('항목 1')),ListTile(title: Text('항목 2')),ListTile(title: Text('항목 3')),],)ListView.builder: 동적 목록
많은 항목이 있을 때 화면에 보이는 것만 생성합니다.
ListView.builder(itemCount: 100,itemBuilder: (context, index) {return ListTile(title: Text('항목 $index'),);},)스크롤 방향 변경
ListView(scrollDirection: Axis.horizontal, // 가로 스크롤children: [...],)
Watch out
WARNINGColumn 안에 ListView를 넣으면 “Unbounded constraints” 오류가 발생합니다. Expanded나 SizedBox로 높이를 제한해야 합니다.
// 오류: Column 안의 ListViewColumn(children: [ListView(...), // 오류!],)// 해결 1: Expanded 사용Column(children: [Expanded(child: ListView(...)),],)// 해결 2: SizedBox로 높이 지정Column(children: [SizedBox(height: 200,child: ListView(...),),],)
결론: 스크롤이 필요하면 ListView나 SingleChildScrollView를 사용하고, 높이 제한에 주의합니다.
챕터 7: 실전 레이아웃 예제
Why
NOTE지금까지 배운 개념을 조합해서 실제 앱 화면을 만들어 봅시다.
이미지, 제목, 버튼, 설명이 있는 상세 화면을 구성합니다.
실전 예제를 통해 레이아웃의 감을 익힐 수 있습니다.
What
NOTE실전 레이아웃은 여러 위젯을 계층적으로 조합해서 구성합니다. 먼저 레이아웃을 그림으로 분석하고, 위젯으로 변환합니다.
How
TIP레이아웃 분석
┌─────────────────────────┐│ [이미지] │├─────────────────────────┤│ 제목 ★ 41 ││ 위치 │├─────────────────────────┤│ 📞 CALL 🗺️ ROUTE 📤 SHARE │├─────────────────────────┤│ 설명 텍스트... │└─────────────────────────┘코드 구현
class DetailScreen extends StatelessWidget {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('레이아웃 예제')),body: SingleChildScrollView(child: Column(children: [// 이미지 섹션Image.asset('images/lake.jpg',height: 240,width: double.infinity,fit: BoxFit.cover,),// 제목 섹션_buildTitleSection(),// 버튼 섹션_buildButtonSection(context),// 설명 섹션_buildTextSection(),],),),);}Widget _buildTitleSection() {return Padding(padding: EdgeInsets.all(32),child: Row(children: [Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: [Text('호수 캠핑장',style: TextStyle(fontWeight: FontWeight.bold),),SizedBox(height: 8),Text('스위스, 칸더슈테그',style: TextStyle(color: Colors.grey[500]),),],),),Icon(Icons.star, color: Colors.red),SizedBox(width: 4),Text('41'),],),);}Widget _buildButtonSection(BuildContext context) {final color = Theme.of(context).primaryColor;return Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly,children: [_buildButtonColumn(color, Icons.call, 'CALL'),_buildButtonColumn(color, Icons.near_me, 'ROUTE'),_buildButtonColumn(color, Icons.share, 'SHARE'),],);}Widget _buildButtonColumn(Color color, IconData icon, String label) {return Column(mainAxisSize: MainAxisSize.min,children: [Icon(icon, color: color),SizedBox(height: 8),Text(label,style: TextStyle(fontSize: 12, color: color),),],);}Widget _buildTextSection() {return Padding(padding: EdgeInsets.all(32),child: Text('호수 캠핑장은 아름다운 알프스 산맥에 위치하고 있습니다. ''맑은 호수와 신선한 공기, 그리고 멋진 하이킹 코스가 있습니다.',softWrap: true,),);}}레이아웃 구조도
graph TD A[Scaffold] --> B[SingleChildScrollView] B --> C[Column] C --> D[Image] C --> E[TitleSection - Row] C --> F[ButtonSection - Row] C --> G[TextSection - Padding] E --> H[Expanded - Column] E --> I[Icon + Text] F --> J[ButtonColumn 1] F --> K[ButtonColumn 2] F --> L[ButtonColumn 3]
Watch out
WARNING레이아웃 코드가 길어지면 위젯을 별도 메서드나 클래스로 분리하세요. 재사용성과 가독성이 좋아집니다.
// 메서드로 분리Widget _buildTitleSection() { ... }// 또는 별도 클래스로 분리class TitleSection extends StatelessWidget { ... }
결론: 레이아웃을 분석하고, 위젯을 조합하고, 필요하면 분리해서 관리합니다.
한계
Flutter 레이아웃에는 주의할 점이 있습니다.
처음에는 어려울 수 있지만 연습하면 익숙해집니다.
- Unbounded Constraints: Row/Column 안에 ListView를 넣는 등의 상황에서 자주 발생합니다.
- 중첩 깊이: 복잡한 레이아웃은 위젯 중첩이 깊어져 코드 가독성이 떨어질 수 있습니다.
- 학습 곡선: CSS Flexbox와 비슷하지만 완전히 같지 않아 적응이 필요합니다.
Footnotes
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!