Flutter 튜토리얼 7편: 제약 조건(Constraints) 이해하기

요약#

핵심 요지#

  • 문제 정의: 위젯 크기가 예상과 다르게 렌더링되는 이유를 이해해야 한다.
  • 핵심 주장: Flutter의 레이아웃은 Constraints go down, Sizes go up, Parent sets position1 규칙을 따른다.
  • 주요 근거: Tight Constraints2Loose Constraints3의 차이를 이해하면 레이아웃 문제를 해결할 수 있다.
  • 실무 기준: 위젯이 원하는 크기로 렌더링되지 않으면 부모의 제약 조건을 먼저 확인한다.
  • 한계: 전체 위젯 트리를 고려해야 정확한 크기와 위치를 알 수 있다.

문서가 설명하는 범위#

  • Constraints 시스템의 작동 원리
  • Tight와 Loose Constraints의 차이
  • Widget, Element, RenderObject의 관계
  • 흔한 레이아웃 문제 해결법

읽는 시간: 16분 | 난이도: 중급


참고 자료#


문제 상황#

Flutter에서 위젯에 width: 100을 설정했는데 100픽셀로 렌더링되지 않는 경우가 있습니다.
이런 “예상과 다른 레이아웃” 문제는 Constraints 시스템을 이해하지 못해서 발생합니다.

자주 겪는 문제들#

// 문제 1: Container 크기가 무시됨
Container(
width: 100,
height: 100,
color: Colors.red,
) // 화면 전체를 차지함!
// 문제 2: Column 안의 위젯이 넓어지지 않음
Column(
children: [
Container(color: Colors.blue), // 높이가 0!
],
)
// 문제 3: Unbounded height 오류
ListView(
children: [
Column(children: [...]), // 오류 발생!
],
)

문제는 다음과 같습니다.

  • 위젯이 설정한 크기를 항상 가질 수 없다.
  • 부모가 전달하는 제약 조건이 자식의 크기를 결정한다.
  • Constraints 규칙을 모르면 디버깅이 어렵다.

해결 방법#

Flutter의 레이아웃 시스템을 이해하면 이런 문제를 예측하고 해결할 수 있습니다.
핵심은 세 가지 규칙입니다.

챕터 1: 레이아웃의 세 가지 규칙#

Why#

NOTE

Flutter 레이아웃이 다른 프레임워크와 다르게 작동하는 이유는 성능 최적화 때문입니다.
제약 기반 레이아웃은 O(n) 시간 복잡도로 매우 효율적입니다.

What#

NOTE

Flutter 레이아웃의 핵심 규칙은 다음과 같습니다.

  1. Constraints go down (제약은 아래로)
  2. Sizes go up (크기는 위로)
  3. Parent sets position (부모가 위치 결정)

How#

TIP

레이아웃 과정을 그림으로 보기

flowchart TB subgraph "1단계: 제약 전달 ⬇️" A[부모] -->|"최대 300×100"| B[위젯] B -->|"최대 290×90"| C[자식] end subgraph "2단계: 크기 보고 ⬆️" D[자식] -->|"100×50 원함"| E[위젯] E -->|"110×60 결정"| F[부모] end subgraph "3단계: 위치 결정" G[부모가 위젯을<br/>x:10, y:20에 배치] end

3단계 정리

단계방향내용
1. Constraints부모 → 자식”너는 최대 이 크기까지만 가능해”
2. Size자식 → 부모”저는 이 크기로 할게요”
3. Position부모 결정”넌 여기에 배치할게”

중요한 제한사항

규칙설명
크기 제한위젯은 제약 범위 내에서만 크기 결정 가능
위치 무지위젯은 자신의 화면 위치를 알 수 없음
전체 의존정확한 크기/위치는 전체 트리에 의존

Watch out#

WARNING

위젯에 width: 100을 설정해도 부모의 제약이 우선합니다.
부모가 “최소 200픽셀”이라고 하면 100픽셀은 무시됩니다.

// 부모가 tight constraints를 전달하면
// Container의 크기 설정이 무시됨
SizedBox.expand(
child: Container(
width: 100, // 무시됨
height: 100, // 무시됨
color: Colors.red,
),
)

결론: 위젯 크기는 자신의 설정과 부모의 제약 중 더 엄격한 것에 따릅니다.


챕터 2: Tight와 Loose Constraints#

Why#

NOTE

같은 위젯이 어떤 부모 아래에서는 원하는 크기로 렌더링되고, 다른 부모 아래에서는 그렇지 않습니다.
이 차이는 부모가 전달하는 제약의 종류 때문입니다.

What#

NOTE

Tight Constraints는 정확한 크기를 강제하고, Loose Constraints는 최대 크기만 제한합니다.

How#

TIP

Tight Constraints (긴밀한 제약)

최소값과 최대값이 같은 제약입니다.
자식은 정확히 그 크기여야 합니다.

// Tight Constraints 예시
BoxConstraints.tight(Size(200, 100))
// minWidth: 200, maxWidth: 200
// minHeight: 100, maxHeight: 100

Loose Constraints (느슨한 제약)

최소값은 0이고 최대값만 있는 제약입니다.
자식은 0부터 최대값 사이에서 원하는 크기를 선택합니다.

// Loose Constraints 예시
BoxConstraints.loose(Size(200, 100))
// minWidth: 0, maxWidth: 200
// minHeight: 0, maxHeight: 100

차이점 비교

구분TightLoose
최소/최대같음다름 (최소=0)
자식 자유도없음있음
사용 위젯Scaffold body, SizedBox.expandCenter, Align

실제 예제

// 예제 1: Container가 화면 전체를 차지
// Scaffold의 body는 tight constraints 전달
Scaffold(
body: Container(
width: 100, // 무시됨
height: 100, // 무시됨
color: Colors.red,
),
)
// 예제 2: Container가 원하는 크기로 렌더링
// Center는 loose constraints 전달
Scaffold(
body: Center(
child: Container(
width: 100, // 적용됨!
height: 100, // 적용됨!
color: Colors.red,
),
),
)

왜 Center가 해결책인가?

graph TD A[Scaffold body] -->|Tight: 화면 전체| B[Container] B --> C[화면 전체 크기] D[Scaffold body] -->|Tight: 화면 전체| E[Center] E -->|Loose: 0~화면 전체| F[Container] F --> G[100x100 크기]

Watch out#

WARNING

Center, Align 같은 위젯은 자식에게 loose constraints를 전달합니다.
하지만 자식이 크기를 지정하지 않으면 0 크기가 될 수 있습니다.

Center(
child: Container(
// width, height 없음
color: Colors.red,
),
)
// Container 크기가 0x0!

결론: 위젯이 원하는 크기로 렌더링되지 않으면 Center나 Align으로 감싸서 loose constraints를 전달합니다.


챕터 3: ConstrainedBox와 UnconstrainedBox#

Why#

NOTE

때로는 부모의 제약을 수정하거나 무시해야 합니다.
ConstrainedBox는 추가 제약을 적용하고, UnconstrainedBox는 제약을 제거합니다.

What#

NOTE

ConstrainedBox4는 자식에게 추가 제약을 적용합니다.
UnconstrainedBox5는 부모의 제약을 무시하고 자식이 원하는 크기를 허용합니다.

How#

TIP

ConstrainedBox: 추가 제약 적용

Center(
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(
width: 10, // 최소값 70이 적용됨
height: 10, // 최소값 70이 적용됨
color: Colors.red,
),
),
)
// 결과: 70x70 빨간 Container

ConstrainedBox의 규칙

ConstrainedBox는 부모 제약과 자신의 제약 중 더 엄격한 것을 적용합니다.

// 부모 제약: maxWidth 100
// ConstrainedBox: maxWidth 150
// 결과: maxWidth 100 (더 엄격한 것)
// 부모 제약: minWidth 0
// ConstrainedBox: minWidth 70
// 결과: minWidth 70 (더 엄격한 것)

UnconstrainedBox: 제약 무시

ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 200,
maxHeight: 200,
),
child: UnconstrainedBox(
child: Container(
width: 300, // 200 제약 무시!
height: 50,
color: Colors.red,
),
),
)
// 결과: 300x50 Container (overflow 경고 발생)

OverflowBox: 오버플로우 허용

SizedBox(
width: 100,
height: 100,
child: OverflowBox(
maxWidth: 300,
child: Container(
width: 300,
height: 50,
color: Colors.red,
),
),
)
// 결과: 300x50 Container가 부모 밖으로 넘침

Watch out#

WARNING

UnconstrainedBox로 제약을 무시하면 overflow 경고가 발생할 수 있습니다.
화면 밖으로 위젯이 넘치면 노란색/검은색 줄무늬로 표시됩니다.

// overflow 경고 예시
Row(
children: [
UnconstrainedBox(
child: Container(width: 1000), // 화면보다 큼!
),
],
)

결론: ConstrainedBox로 추가 제약을, UnconstrainedBox로 제약 해제를 조절합니다.


챕터 4: Widget, Element, RenderObject#

Why#

NOTE

Flutter의 효율적인 렌더링을 이해하려면 세 가지 트리 구조를 알아야 합니다.
Widget은 설계도, Element는 인스턴스, RenderObject는 실제 렌더링을 담당합니다.

What#

NOTE

Flutter는 Widget Tree6, Element Tree7, RenderObject Tree8 세 가지 트리를 유지합니다.

How#

TIP

세 가지 트리의 역할

트리역할특징
WidgetUI 설계도불변, 매 프레임 재생성 가능
ElementWidget 인스턴스프레임 간 유지, 캐시 역할
RenderObject실제 렌더링레이아웃과 페인팅 수행

관계도

graph TD subgraph Widget Tree W1[Container] --> W2[Row] W2 --> W3[Text] W2 --> W4[Image] end subgraph Element Tree E1[ContainerElement] --> E2[RowElement] E2 --> E3[TextElement] E2 --> E4[ImageElement] end subgraph RenderObject Tree R1[RenderDecoratedBox] --> R2[RenderFlex] R2 --> R3[RenderParagraph] R2 --> R4[RenderImage] end W1 -.-> E1 E1 -.-> R1

성능 최적화: Element 캐싱

Widget이 변경되어도 Element는 재사용됩니다.

// 이전 프레임
Text('Hello')
// 현재 프레임
Text('World')
// 동작:
// 1. 새 Text Widget 생성
// 2. 기존 TextElement 재사용
// 3. RenderParagraph만 업데이트

RenderBox와 제약

RenderObject 중 대부분은 RenderBox입니다. RenderBox는 BoxConstraints를 받아 레이아웃을 계산합니다.

// RenderBox가 받는 제약
BoxConstraints(
minWidth: 0,
maxWidth: 300,
minHeight: 0,
maxHeight: 200,
)
// RenderBox가 결정하는 크기
Size(200, 150) // 제약 범위 내

Watch out#

WARNING

Widget은 불변이므로 속성을 변경하면 새 Widget이 생성됩니다.
하지만 Element와 RenderObject는 가능하면 재사용되어 성능을 유지합니다.

// 비효율적: 매번 새 Widget 타입
condition ? Text('A') : Container(child: Text('B'))
// 효율적: 같은 Widget 타입, 속성만 변경
Text(condition ? 'A' : 'B')

결론: Flutter는 Widget-Element-RenderObject 구조로 효율적인 렌더링을 달성합니다.


챕터 5: 흔한 레이아웃 문제 해결#

Why#

NOTE

Constraints를 이해해도 실제 개발에서는 다양한 문제를 만납니다.
흔한 문제들의 원인과 해결책을 알아둡니다.

What#

NOTE

Unbounded constraints, overflow, 크기 무시 등의 문제는 대부분 제약 조건 관련입니다.

How#

TIP

문제 1: Container 크기가 무시됨

// 문제
Scaffold(
body: Container(
width: 100,
height: 100,
color: Colors.red,
),
)
// Container가 화면 전체 차지
// 해결: Center로 감싸기
Scaffold(
body: Center(
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
)

문제 2: Column 안의 Container가 안 보임

// 문제
Column(
children: [
Container(color: Colors.blue), // 높이 0!
],
)
// 해결 1: 높이 지정
Column(
children: [
Container(
height: 100,
color: Colors.blue,
),
],
)
// 해결 2: Expanded 사용
Column(
children: [
Expanded(
child: Container(color: Colors.blue),
),
],
)

문제 3: Unbounded height 오류

// 문제: Column 안에 ListView
Column(
children: [
ListView(...), // 오류!
],
)
// 해결 1: Expanded로 감싸기
Column(
children: [
Expanded(
child: ListView(...),
),
],
)
// 해결 2: SizedBox로 높이 제한
Column(
children: [
SizedBox(
height: 200,
child: ListView(...),
),
],
)

문제 4: Row 안의 Text가 넘침

// 문제
Row(
children: [
Text('아주 긴 텍스트가 화면을 넘어갑니다...'), // overflow!
],
)
// 해결: Expanded나 Flexible 사용
Row(
children: [
Expanded(
child: Text('아주 긴 텍스트가 화면을 넘어갑니다...'),
),
],
)

문제 해결 체크리스트

증상원인해결책
위젯이 화면 전체Tight constraintsCenter/Align 사용
위젯이 안 보임크기 0높이/너비 지정 또는 Expanded
Unbounded 오류무한 제약Expanded 또는 SizedBox
Overflow 경고부모보다 큼Expanded, Flexible, 스크롤

Watch out#

WARNING

flutter analyze나 DevTools의 Widget Inspector를 사용하면 제약 조건을 확인할 수 있습니다.
문제가 발생하면 해당 위젯의 constraints를 직접 확인하세요.

LayoutBuilder(
builder: (context, constraints) {
print('Constraints: $constraints');
return Container(...);
},
)

결론: 레이아웃 문제는 대부분 제약 조건 관련이며, Center, Expanded, SizedBox로 해결합니다.


한계#

Constraints 시스템은 강력하지만 복잡합니다.

  • 직관적이지 않음: CSS나 다른 프레임워크와 다르게 작동합니다.
  • 전체 트리 의존: 단일 위젯만 보고는 최종 크기/위치를 알 수 없습니다.
  • 디버깅 어려움: 어떤 부모가 제약을 전달했는지 추적이 필요합니다.

Footnotes#

  1. Constraints go down, Sizes go up, Parent sets position: Flutter 레이아웃의 핵심 규칙이다. 제약은 부모에서 자식으로, 크기는 자식에서 부모로 전달되며, 위치는 부모가 결정한다.

  2. Tight Constraints(긴밀한 제약): 최소값과 최대값이 같은 제약이다. 자식은 정확히 그 크기여야 한다.

  3. Loose Constraints(느슨한 제약): 최소값은 0이고 최대값만 있는 제약이다. 자식이 원하는 크기를 선택할 수 있다.

  4. ConstrainedBox: 자식에게 추가 제약 조건을 적용하는 위젯이다.

  5. UnconstrainedBox: 부모의 제약을 무시하고 자식이 원하는 크기를 허용하는 위젯이다.

  6. Widget Tree(위젯 트리): UI를 선언하는 불변 객체들의 트리이다.

  7. Element Tree(엘리먼트 트리): Widget의 인스턴스를 나타내며 프레임 간 유지되는 트리이다.

  8. RenderObject Tree(렌더 오브젝트 트리): 실제 레이아웃과 페인팅을 수행하는 트리이다.

공유

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

Flutter 튜토리얼 7편: 제약 조건(Constraints) 이해하기
https://moodturnpost.net/posts/flutter/flutter-constraints/
작성자
Moodturn
게시일
2026-01-08
Moodturn

목차