일부 내용은 나름대로 공부해서 제가 이해한 것을 바탕으로 기술 했습니다.
잘못된 내용이 있을 수 있습니다. 잘못된 내용이 있다면 이메일 또는 댓글 달아 주시면 수정 하겠습니다.
[Height 와 Width]
Constraint에 대해 알아보기 전에 맛보기로 widget 사이즈를 정하는 기본 속성인 height 와 width 에 대해 알아 보겠습니다.
플루터에서 화면에 출력되는 대부분의 widget에 height 와 width 속성을 줄 수 있습니다.
import 'package:flutter/material.dart';
void main(){
runApp(
MaterialApp(
title: "My App",
home: Container(
color: Colors.yellow,
alignment: Alignment.center,
height: 200.0,
width: 400.0,
child: Text("Container constraint test"),
),
)
);
}
위 코드를 수행 하면, width, height 값이 있음에도 Container가 전체 화면 영역을 차자하는 것을 알 수 있습니다.
어떤 widget은 자신의 크기를 자식 위젯에게 강제하는 것이 있습니다.
MaterialApp 이 그렇습니다. 비록 Container가 width와 height 를 갖고 있지만, MaterialApp이 parent 이므로, MaterialApp의 크기를 가져가게 됩니다.
이런경우에는 Center widget과 같이 자신의 자식 위젯을 align 하는 widget을 이용하면 좋습니다.
import 'package:flutter/material.dart';
void main(){
runApp(
MaterialApp(
title: "My App",
home: Center(
child: Container(
color: Colors.yellow,
alignment: Alignment.center,
height: 200.0,
width: 300.0,
child: Text("Container constraint test"),
),
)
)
);
}
위의 코드를 수행해보면 비로소 Container widget이 본인의 크기대로 보여지는 것을 알 수 있습니다.
[constraint 란?]
플러터에서 위젯은 자신의 렌더박스에 그려집니다. 렌더박스의 크기는 부모 위젯으로부터 받은 제약조건(constraint)에 의해 결정됩니다. (이후 부터는 제약조건이라 해석하지 않고 constraint 라 표현 하겠습니다. 그게 더 쉽습니다. )
constraint는 minWidth, maxWidth, minHeight, maxHeight로 구성됩니다.
size는 width 와 height 로 구성 됩니다.
일반적으로 constraint를 다루는 방식에 따라 3종류의 box가 있습니다.
- 가능한 최대 크기로 하고 싶은 경우. (e.g. Center 및 ListView)
- 자식 요소들과 같은 크기로 만들려는 경우. (e.g. Transform 및 Opacity)
- 개별적인 크기를 갖게 하려는 경우. (e.g. Image 및 Text)
- Special conditions:
– parent로 부터 받은 constraint에 의해 달라짐 (e.g. Row & Column) , (Flex 섹션에서 설명 됩니다.)
– 생성자 parameter에 의해 달라짐(e.g. container) (아래에 좀 더 자세히 설명 합니다.)
Container widget 의 constraint 특성은 좀 복잡합니다. 좀 더 자세히 살펴 보겠습니다. (절대적인 규칙은 아니니 참고 하세요.)
Container에 아무런 size 속성을 주지 않으면 가능한 큰 사이즈를 갖습니다. size를 주면 주어진 constraint 내에서 적정 사이즈를 갖습니다.
아래 표는 Container widget size 테스트 결과 입니다.
Container constraint 예제, constraint(minWidth=300,minHeight=300,maxWidth=400, maxHeight=400) 가 주어진 상황:
(leaf widget의 경우 size 가 자동으로 조정되는 특성을 가진 Text widget 사용)
no __ | Container (width,height) | Child widget(width,height) | 결과: Container(width,height) | 비고 |
---|---|---|---|---|
1 | None | No chid | (400,400) | constraint max 사이즈 |
2 | (200,200) | No chid | (300,300) | constraint min 사이즈 |
3 | (350,350) | (200,200) | (350,350) | 자신(container)의 사이즈 |
4 | (320,320) | (350,350) | (320,320) | 자신(container)의 사이즈 |
5 | None | (350,350) | (350,350) | child widget의 사이즈 |
6 | (500,500) | (500,500) | (400,400) | constraint max 사이즈 |
7 | (300,300) | (500,500) | (300,300) | 자신(container)의 사이즈 |
8 | (200,200) | (500,500) | (300,300) | constraint min 사이즈 |
constraint 속성은 Dart DevTool 을 이용해서 확인 할 수 있습니다.
DevTool은 editor의 오른쪽 하단 메뉴를 클릭하면 열립니다.(VSCode 기준)
아래 소스코드를 첨부 하니 참고 하세요.
Center가 최대 크기를 가져가는것 확인
ListView가 최대 크기를 가져가는것 확인
Container constraint 확인
1) widget 렌더링 과정
constraint를 이해하기 위해서는 flutter에서 widget이 어떻게 그려지는지에 대해 좀 알아야 합니다.
flutter는 widget을 렌더링 할때 tree 구조를 이용합니다.
1) root widget 부터 제일 마지막 widget(leaf widget)까지 내려 갑니다. tree를 타고 내려가는 동안 parent는 자신의 constraint를 자식에게 전달 합니다. 그럼 자식은 손자에게 전달하는 형태로 leaf 까지 전달 합니다.
2) 다시left widget(마지막 widget)부터 tree를 타고 돌아 옵니다. 돌아오면서 constraint 값을 기준으로 size를 계산 합니다. 그리고 계산된 size 값을 parent에 전달 합니다. 이 과정은 root widget에 도달할때까지 반복 됩니다.
3) 최종적으로 flutter는 모든 widget의 size를 알게 됩니다.
[Unbounded constraints(한정되지 않은 제약조건)]
constraint가 엄격하게 주어지는 경우에는, render box는 정해진 size 대로 그리게 됩니다. (엄격하게 주어진다는 의미는 maxWidth=minWidth, maxHeight=minHeight 인 것을 말합니다.)
어떤 경우에는, box에 주어지는 constraint가 unbounded 또는 infinite 이기도 합니다. 이말은 말그대로 max widht, max height 가 infinity로 설정됨을 말합니다.
일반적으로 box가 unbounded constraint 로 쓰이는 경우는 Flex box(Row,Column) 와 스크롤view(ListView,GridView,ScrollView) 내부에서 입니다.
먼소린지 잘 와닿지가 않습니다.
아래 표를 보면 좀 더 쉽게 이해 하실 수 있습니다.
maxWidth __ | maxHeight __ | constraint | layout |
---|---|---|---|
Number | Number | Bounded | 제한된 값 내에서 가능한 큰 영역을 차지하려 한다. |
Number | ∞ | Unbounded Height | 가로영역은 제한된 값 내에서 가능한 크게 가져가고, 세로 영역은 가능한 작게 가져간다. |
∞ | Number | Unbounded Width | 가로영역은 가능한 작게 가져가고, 세로 영역은 제한된 값 내에서 가능한 크게 가져간다. |
∞ | ∞ | Unbounded | 가로 세로 모두 가능한 작게 가져간다. |
예제로 검증 해보겠습니다.
1) Listview (vertical scrolling) 밑에 unbounded widget
ListView의 child로 Card widget과 ListTile widget 을 사용했습니다.
일반적으로 자주 사용하는 조합 입니다.
DevTool 로 보면 Card 와 Listtile의 constraint 가 Width=Screen size, Hiehgt=infinity 로 되어 있는것을 알 수 있습니다.
이 말은 Card와 ListTile의 height는 가능한 작게 설정된다는 것입니다.
ListView를 Column으로 변경해도 동일한 결과 입니다.


예제소스: ListView 에서 unbounded constraint 사용
1) unbounded constraints widget 을 중첩해서 사용하지 마세요.
ListView 같은 스크롤 가능한 위젯들은 crossAxis 방향으로 크기를 확장하려는 constraint를 가집니다. 예를들어, 세로로 스크롤 되는 ListView가 있다면, 이 ListView는 가로 사이즈를 가능한 크게 가져갑니다.
만약에 가로로 스크롤되는 ListView 내에 세로로 스크롤 퇴는 ListView를 넣으면, 세로로 스크롤 되는 ListView는 crossAxis인 가로 방향으로 최대한 커지려 할 것입니다.
가로로 스크롤되는 parent 위젯을 가졌으므로 가로 방향으로 최대한 커지려 한다는 것은 무제한으로 커진다는 의미가 됩니다. 이런 모양은 부적절 합니다. 실제로 수행해 보면 에러가 발생합니다.
예제를 살펴 보겠습니다.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main(){
debugPaintSizeEnabled=true;
runApp(
MaterialApp(
title: "My App",
home: ListView(
// 스크롤 방향 설정. 수평적으로 스크롤되도록 설정
scrollDirection: Axis.horizontal,
children: <Widget>[
Container(
width: 160.0,
color: Colors.red,
),
Container(//Container없이 ListView를 바로 사용하면 에러 발생 함.
color: Colors.blue,
child: ListView(
scrollDirection: Axis.vertical,
children: <Widget>[
Text('1', textAlign: TextAlign.center),
Text('2', textAlign: TextAlign.center),
Text('3', textAlign: TextAlign.center),
Text('4', textAlign: TextAlign.center),
Text('5', textAlign: TextAlign.center),
Text('6', textAlign: TextAlign.center),
],
),
),
Container(
width: 160.0,
color: Colors.green,
),
Container(
width: 160.0,
color: Colors.yellow,
),
Container(
width: 160.0,
color: Colors.orange,
)
],
)
)
);
}
위 예제처럼 가로로 스크롤되는 ListView 에 세로로 스코롤되는 ListView를 중첩해서 사용하면 에러가 납니다.
unbounded constraint 인 ListView는 crossAxis 방향으로 가능한 커지려는 속성이 있습니다.
먼저, 첫번째 ListView는 가로로 스크롤되기 때문에 가로 영역은 무제한 입니다.
그 안에 세로로 스크롤되는 ListView를 넣으면 이 위젯은 가로로 무제한으로 늘어나려 할 것입니다. 이미 스크롤이 되기 때문에 멈추지 않고 늘어나겠죠. 그렇기 때문에 에러가 나는 것입니다.
이를 방지 하기위해 Container에 width 를 줘서 가로 사이즈를 제한 하면 어떨까요?
에러가 나지 않고 잘 동작 할 것입니다.
(이미지에서 화살표는 스크롤 방향을 나타냅니다.)
[Flex]
Flex box 기반의 Row, Column 위젯은 주어진 constraint 가 bounded 또는 unbounded 인가에 따라 다르게 동작합니다.
constraint가 bounded일 때 자신의 mainAxis 방향으로 가능한 한 크게 확장합니다. unbounded일 때는 자식 위젯들의 크기에 맞춰집니다.
unbounded constraint의 경우, 해당 방향으로 자식 요소들을 딱 맞추려 하는데요. 이 경우, 자식 요소에 flex를 0 이외의 다른 것으로 설정할 수 없습니다.
이는 Flex box가 또다른 Flex box나 스크롤 박스 안에 있을 때 Expanded를 사용할 수 없다는 걸 의미하는데요. 만약 그렇게 하면, 예외 메세지가 나타날 겁니다.
교차 방향, 즉 Column의 너비(vertical flex)와 Row의 높이(horizontal flex)는 제한되면 안되며, 아닐 경우 자식 요소들을 제대로 정렬할 수 없습니다.
1) Flex 내에 unbounded constraints widget 을 배치하지 마세요.
Column은 Flex 입니다. ListView는 unbounded constraints widget 이죠.
Column 내에 ListView를 배치하면 어떻게 되는지 예제를 통해 알아 보겠습니다.
void main(){
runApp(
MaterialApp(
title: "My App",
home: Column(
children: <Widget>[
ListView.builder(
itemBuilder: (context,index){
return Text('this is test')
},
itemCount: 3,
)
]
),
)
);
}
위 코드를 실행하면 error 가 발생 합니다.
왜냐하면 Column은 flex 이고 ListView는 세로로 가능한 모든 영역을 다 가져갑니다. column은 자식 위젯인 ListView에 영역을 얼마나 줘야 하는지 모르는 것입니다.
이 소스코드를 동작하게 하려면 ListView의 세로 영역에 대한 속성을 shrinkWrap = true 로 주면 됩니다.
이렇게 하면 세로사이즈가 가능한 작게 설정됩니다. 자식 위젯이 차지하는 영역이 있으니만큼 가능한 작은 세로 사이즈는 자식 위젯의 영역 만큼이 될 것입니다.
이제 Column은 자식위젯인 ListView의 크기를 얼마로 해야 할지 알 수 있습니다.
void main(){
runApp(
MaterialApp(
title: "My App",
home: Column(
children: <Widget>[
ListView.builder(
shrinkWrap : true,
itemBuilder: (context,index){
return Text('this is test')
},
itemCount: 3,
)
]
),
)
);
}
위 코드를 debugPaintSizeEnabled=true; 속성을 주고 실행해 보면 아래와 같은 결과를 얻을 수 있습니다.
layout 영역을 눈으로 확인 할 수 있습니다.
(이미지에서 화살표는 스크롤 방향을 나타냅니다.)