Flutter 튜토리얼 6편: 레이아웃 기초

요약#

핵심 요지#

  • 문제 정의: 위젯을 원하는 위치에 배치하려면 레이아웃 규칙을 이해해야 한다.
  • 핵심 주장: Flutter의 레이아웃은 Row1, Column2, 정렬3, 크기 조절4의 네 가지 개념으로 구성된다.
  • 주요 근거: 모든 레이아웃 위젯은 단일 자식 또는 다중 자식을 가지며, 위젯 조합으로 복잡한 UI를 구성한다.
  • 실무 기준: Row와 Column을 중첩하고, Expanded5로 공간을 분배하며, mainAxisAlignment로 정렬한다.
  • 한계: 무한 제약(Unbounded Constraints) 오류가 자주 발생하므로 주의가 필요하다.

문서가 설명하는 범위#

  • 레이아웃의 기본 원리와 제약 조건 흐름
  • Row와 Column을 사용한 배치
  • 정렬과 크기 조절 방법
  • 실전 레이아웃 예제

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


참고 자료#


문제 상황#

위젯을 화면에 배치할 때 위치와 크기를 정확히 제어하기 어렵습니다.
CSS의 Flexbox나 네이티브의 LayoutManager와 달리 Flutter만의 레이아웃 규칙이 있기 때문입니다.
이 규칙을 모르면 원하는 대로 배치할 수 없습니다.

레이아웃 관련 질문들#

- 위젯을 가로로 나란히 배치하려면?
- 남은 공간을 균등하게 나누려면?
- 위젯을 중앙에 정렬하려면?
- 화면 크기에 따라 레이아웃을 바꾸려면?

문제는 다음과 같습니다.

  • Flutter의 레이아웃 규칙을 모르면 예상과 다른 결과가 나온다.
  • “Unbounded constraints” 오류가 자주 발생한다.
  • 위젯 간 공간 분배가 직관적이지 않다.

해결 방법#

Flutter의 레이아웃은 위젯 조합으로 구성됩니다.
Row와 Column으로 배치하고, 정렬과 크기 조절로 미세 조정합니다.
이 네 가지만 알면 대부분의 레이아웃을 만들 수 있습니다.

챕터 1: 레이아웃의 기본 원리#

Why#

NOTE

Flutter에서 레이아웃을 이해하려면 제약 조건(Constraints)을 알아야 합니다.
제약 조건이란 무엇일까요?
부모가 자식에게 “이 범위 안에서 크기를 정해”라고 알려주는 것입니다.

flowchart LR A[부모] -->|"최소 100, 최대 200"| B[자식] B -->|"150으로 결정"| A A -->|"여기에 배치"| C[화면]

What#

NOTE

Flutter의 레이아웃은 세 단계로 작동합니다.
첫째, 부모가 자식에게 제약을 전달합니다.
둘째, 자식이 크기를 결정해서 부모에게 알립니다.
셋째, 부모가 자식의 위치를 결정합니다.

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

SizedBoxContainer로 크기를 지정해도 부모의 제약이 우선합니다. 부모가 허용하는 범위 내에서만 크기가 적용됩니다.

// 부모가 최대 100px만 허용하면
SizedBox(
width: 200, // 요청한 크기
// 실제로는 100px만 적용됨
)

결론: Flutter 레이아웃은 부모-자식 간 제약 협상으로 작동합니다.


챕터 2: Row와 Column 사용하기#

Why#

NOTE

위젯을 가로나 세로로 배치하는 것은 가장 기본적인 레이아웃입니다.
Row는 자식을 가로로, Column은 세로로 배열합니다.
이 두 가지만 알아도 많은 레이아웃을 만들 수 있습니다.

What#

NOTE

Row는 자식을 수평으로, Column은 수직으로 배치합니다.
둘 다 여러 자식을 받을 수 있습니다.
children 속성에 위젯 목록을 전달하면 됩니다.

How#

TIP

Row: 가로 배치

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#

WARNING

Row나 Column의 자식이 주축 방향으로 무한한 공간을 차지하려 하면 오류가 발생합니다.

// 오류: Row 안에 스크롤되지 않는 ListView
Row(
children: [
ListView(...), // ListView가 무한 너비를 요청!
],
)
// 해결: Expanded로 감싸기
Row(
children: [
Expanded(child: ListView(...)),
],
)

결론: Row는 가로, Column은 세로 배치이며, 중첩해서 복잡한 레이아웃을 구성합니다.


챕터 3: 정렬하기#

Why#

NOTE

Row나 Column에 자식을 배치하면 기본적으로 시작점에 몰립니다.
균등하게 배치하거나 중앙에 정렬하려면 어떻게 해야 할까요?
정렬 속성을 사용하면 됩니다.

What#

NOTE

mainAxisAlignment는 주축 방향 정렬, crossAxisAlignment는 교차축 방향 정렬을 제어합니다.

How#

TIP

MainAxisAlignment 옵션

옵션설명
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#

NOTE

Row나 Column에서 자식들이 공간을 어떻게 나눌지 제어해야 합니다.
한 위젯이 남은 공간을 모두 차지하거나, 비율대로 나누는 상황이 있죠.
Expanded와 flex 속성으로 이를 제어할 수 있습니다.

What#

NOTE

Expanded는 자식이 남은 공간을 차지하게 합니다.
flex 속성으로 비율을 조절할 수 있습니다.
Flexible은 비슷하지만 자식이 원하는 크기보다 작을 수도 있습니다.

How#

TIP

Expanded: 남은 공간 채우기

Row(
children: [
Container(width: 100, color: Colors.red),
Expanded(
child: Container(color: Colors.blue), // 남은 공간 전체
),
],
)

flex로 비율 조절

Row(
children: [
Expanded(
flex: 1, // 1/4
child: Container(color: Colors.red),
),
Expanded(
flex: 2, // 2/4
child: Container(color: Colors.green),
),
Expanded(
flex: 1, // 1/4
child: Container(color: Colors.blue),
),
],
)

시각적 표현

flex: 1, 2, 1
[ red | green | blue ]
25% 50% 25%

Flexible vs Expanded

속성ExpandedFlexible
최소 크기남은 공간 전체자식이 원하는 크기
최대 크기남은 공간 전체남은 공간까지
사용 상황공간 균등 분배자식 크기 유지하면서 확장 허용
// Flexible: 자식이 원하는 만큼만
Row(
children: [
Flexible(
child: Container(
width: 50, // 이 크기 유지 가능
color: Colors.blue,
),
),
],
)

Watch out#

WARNING

Expanded나 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#

NOTE

Row와 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#

WARNING

Container는 강력하지만 무겁습니다. 단순한 여백만 필요하면 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#

TIP

SingleChildScrollView: 단일 콘텐츠 스크롤

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#

WARNING

Column 안에 ListView를 넣으면 “Unbounded constraints” 오류가 발생합니다. Expanded나 SizedBox로 높이를 제한해야 합니다.

// 오류: Column 안의 ListView
Column(
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 {
@override
Widget 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#

  1. Row(로우): 자식 위젯들을 가로 방향으로 배치하는 레이아웃 위젯이다.

  2. Column(컬럼): 자식 위젯들을 세로 방향으로 배치하는 레이아웃 위젯이다.

  3. Alignment(정렬): mainAxisAlignment와 crossAxisAlignment로 위젯 배치를 제어하는 속성이다.

  4. Sizing(크기 조절): Expanded, Flexible, SizedBox 등으로 위젯 크기를 제어하는 방법이다.

  5. Expanded(익스팬디드): Row나 Column에서 자식이 남은 공간을 채우도록 확장하는 위젯이다.

공유

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

Flutter 튜토리얼 6편: 레이아웃 기초
https://moodturnpost.net/posts/flutter/flutter-layout-basics/
작성자
Moodturn
게시일
2026-01-08
Moodturn

목차