Layout : 제약조건

Clean Code Big Poo·2022년 5월 4일
4

Flutter

목록 보기
7/38
post-thumbnail

플러터의 레이아웃 제약 조건

위젯의 크기가 어떻게 결정되는지 이해하기 위해서는 레이아웃과 제약 조건개념에 대해 알아둘 필요가 있다.
플러터는 결국 UI 라이브러리이며 렌더링 엔진이기 때문인데, 이를 이해하지 못 할수록 flutter layout infinite size(플러터 레이아웃 무한 크기)오류를 겪게 될 것이다.
이러한 에러를 고치기 위해서는 플러터가 화면의 픽셀을 어떻게 칠하고 제약 조건의 역할은 무엇인지 알 필요가 있다.

RenderObject

  • 내부에서 사용되는 클래스이기에 직접 이를 사용하는일은 드믈다.
  • RenderObject는 실제 화면에 그리는 작업을 담당한다.
  • 프레임워크 내부에서 이 클래스를 구현한다.
  • 모든 RenderObject가 모여 Render Tree를 만든다. (위젯 트리와는 다르다.)
  • RenderObject는 각자 대응하는 위젯을 가진다.
  • 렌더 객체는 상태나 로직을 포함하지 않는다.
  • 렌더 객체는 부모 렌더 객체의 일부 기본 정보를 알고 있으며 자식을 방문하는 기능을 포함한다.
  • 렌더 객체는 의시결정 능력이 없어 항상 명령을 따른다.
  • Column 위젯은 컨테이너이기에 보통 위젯트리에서 말단 RenderObjectWidget이 아니다
    • 열은 추상화된 레이아웃 개념이기에 실체를 볼 수 없다.
    • Text, Color등은 화면에 그릴 수 있는 실체가 존재하는 객체이다.
    • Column위젯은 오직 제약 조건을 제공하는역할만 담당한다.

RenderObject와 제약조건

  • 제약 조건 위젯으로 위젯의 제약 조건을 설정하면 렌더 객체가 최종적으로 프레임워크에 위젯의 실제 물리적 크기를 전달한다.
  • 제약 조건은 렌더 객체로 전달되며 주어진 제약 조건을 고려해 위젯의 크기와 위치를 결정한다.
  • 제약 조건은 minWidth, minHeight, maxWidth, maxHeight등의 프로퍼티르 설정한다.
  • RenderBox는 렌더 객체 서브클래스로 데카르트 좌표계를 기반으로 위젯 크기를 계산한다.
    • Center위젯: 최대 공간 차치
    • Opacity 위젯: 자식과 같은 크기의 공간 차지
    • Image 위젯: 특정 크기의 공간 차지

RenderBox와 레이아웃 오류

위에서 언급한 flutter layout infinite size 오류는 위젯이 수평, 수직으로 무한 크기를 갖도록 제약조건이 설정되었을 때 발생한다. 즉, 이 오류는 렌더 객체가 전달된 제약 조건을 처리하는 과정에서 발생한다.
레이아웃 위젯중 Row, Column 처럼 스크롤할 수 있는 위젯에서 길이(너비)는 이론적으로는 무한할 수 있다. 하지만 컴퓨터의 연산능력, 시간의 제약 등등 때문에 정말 무한해질수는 없다.

Row, Column 레이아웃 위젯은 플렉스 상자로 이들의 렌더 객체는 위에 설명한 세 가지 렌더 객체 동작 유형에 속하지 않고, 부모가 전달한 제약 조건에 따라 다른 동작을 수행한다.
그렇기에 이들은 부모가 한정된 제약 조건을 가지면 그 한정된 제약 조건 내에서 최대한의 공간을 차지한다.

그렇기에 이미지가 여러개인 Column이라면 가장 높아가 큰 이미지의 높이가 Column의 높이가 된다. 그럼 생각해 볼 이슈가 있다. Column 위젯은 자식이 원하는 크기를 갖도록 하는데, 자식이 제약없이 허용된 최대 크기를 선택한다면 오류가 발생한다.

child: Column(children: <Widget>[
          Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Expanded(
                  child: Text("..."),
                )
              ])
        ]),

위 코드에서 Column은 자식이 원하는 크기만큼 갖도록 하며, 자식에서는 Expanded로 부모가 허락하는 만큼 최대한이라는 제약조건이 두개가 겹치며 오류를 발생시킨다.

위젯은 위처럼 제약 조건을 트리 아래로 전달하기에 중복된 플렉스 상자를 어느정도 분리해야 한다. 개발을 하다보면 Column안에 Row를 갖거나 반대의 경우와 같은 플렉시블 위젯을 중첩해서 사용하는 경우가 잦다.
우리는 플렉시블 위젯 동작을 최대한 이해해 이 문제를 쉽게 해결할 수 있도록 해야 한다.

회피 기동

The Advanced Layout Rule Even Beginners Must Know

Center 안에 넣은 위젯의 widget:100이 왜 100px이 아닌지, FittedBox가 가끔 오작동하는지, Column이 넘쳐흐르는지(overflowing), 혹은 IntrinsicWidth 는 어떻게 작동해야 하는 지...

Flutter의 레이아웃은 규칙을 모르면 이해할 수 없으므로 모두 모두 일찍 배워 놓도록 하자!

👉 Constraints go down. Sizes go up. Positions are set by parents.
제약이 줄어든다. 사이즈는 올라간다. 위치는 부모가 설정한다.

  • 위젯은 부모로부터 고유한 제약조건(Constraints)를 받는다. 제약조건은 최소 및 최대 너비, 최소 및 최대 높이의 4개의 이중 집합이다.
  • 위젯은 자신의 자식 목록을 검토한다. 위젯은 하나씩 자식에게 제약조건(각 자식마다 다를 수 있음.)이 무엇인지 알려주고, 각 자식에게 원하는 크기를 묻는다.
  • 그런 다음 위젯은 자식을 하나씩 배치한다.
    • x측에서 수평으로 y측에서 수직으로
  • 마지막으로 위젯은 부모에게 자신의 크기을 알려준다.
    • 원래의 제약조건 내에서!

예를들어 아래의 위젯이 패딩이 있고, 두개의 자식을 레이아웃하고 싶다면...?
[하이, 나는 배치되고 싶은 위젯]

위젯 — 부모님, 제 제약 조건은 무엇입니까?
상위 — 너비는 90~ 300픽셀 이어야 하고 높이는 30~85이여야 합니다.
위젯 — 흠, 5픽셀의 패딩을 원하기 때문에 내 아이들은 최대 290픽셀의 넓이와 75픽셀의 높이를 가질 수 있습니다.
위젯 — Hey first child, 당신은 0~290 넓이, 0~75의 높이를 가져야 합니다.
첫 번째 자식 — 오키. 그러면 너비는 290픽셀로, 높이는 20픽셀로 지정하겠습니다.
위젯 — 흠, 두 번째 아이를 첫 번째 아이 이후에 놓고 싶기 때문에 두 번째 아이를 위한 높이는 55픽셀만 남습니다.
위젯 — hey second child, 당신은 0~290의 넓이를 가지고 0~55의 높이를 가져야 합니다.
두 번째 자식 — 오키, 140픽셀의 넓이 이고 싶고 높이는 30픽셀이 되고 싶어요.
위젯 — 아주 좋습니다. 첫째 아이를 x: 5 and y: 5에, 둘째 아이를 x: 80 and y: 25에 넣겠습니다.
위젯 — 안녕하세요 부모님, 제 크기는 300픽셀의 너비이고 60픽셀의 높이로 결정했습니다.

제한사항

  • 위젯은 부모가 지정한 제약조건 내에서만 자신의 크기를 결정할 수 있다. 위젯이 일반적으로 원하는 크기를 가질 수 없다는 것을 의미한다 .
  • 위젯은 자신의 위치를 결정하는 것이 위젯의 부모이기 때문에 스스로의 위치를 결정하거나 알 수 없다. 화면에서 자신의 위치를 결정하지 않는다 .
  • 부모의 크기와 위치는 차례로 자신의 부모에 따라 달라지므로 전체 트리를 고려하지 않고 위젯의 크기와 위치를 정확하게 정의하는 것은 불가능하다.

예시로 알아보자

Container #1

Container(color: Colors.red)

container의 부모 화면이다. 빨간 container가 화면과 같은 사이즈가 되도록 강제한다. 그래서 화면이 전부 빨갛게 된다.

Container #2

Container(width: 100, height: 100, color: Colors.red)

빨간 container가 100*100 사이즈로 정의되었다. 하지만 안되쥬? 빨간 container가 화면과 같은 사이즈가 되도록 강제한다.

Center

Center(
   child: Container(width: 100, height: 100, color: Colors.red)
)

screen 은 center를 화면과 같은 크기로 강제한다. 그래서 center가 화면을 꽉 채운다.

center는 container가 원하는 사이즈가 되도록 할 수 있다. 단, 화면보다 클 수는 없다. 이제 container는 100*100이 될 수 있다.

Align

Align(
   alignment: Alignment.bottomRight,
   child: Container(width: 100, height: 100, color: Colors.red),
)

center대신 align을 사용했다는 점에서 앞의 예제와 다르다.

align은 container가 원하는 사이즈가 되도록 할 수 있지만, alignment.bottomRight에 따라 오른쪽 하단에 container를 배치한다.

Container in Center(infinity)

Center(
   child: Container(
      color: Colors.red,
      width: double.infinity,
      height: double.infinity,
   )
)

screen 은 center를 화면과 같은 크기로 강제한다. 그래서 center가 화면을 꽉 채운다.

center는 container가 원하는 사이즈가 되도록 할 수 있다. 단, 화면보다 클 수는 없다. container는 infinit한 사이즈를 가지고 싶지만, 화면보다 커질 수 없다.

그래서 화면을 꽉 채울 것이다.

Container in Center #1

Center(child: Container(color: Colors.red))

screen 은 center를 화면과 같은 크기로 강제한다. 그래서 center가 화면을 꽉 채운다.

center는 container가 원하는 사이즈가 되도록 할 수 있다. 단, 화면보다 클 수는 없다. container는 사이즈가 결정된 child가 없기 때문에 가질 수 있는 가장 큰 사이즈를 가지게 된다.

그래서 화면을 꽉 채울 것입니다.

왜 container는 화면을 꽉 채울까? 간단히 말해 container 위젯을 생성한 사람의 디자인적 결정이었기 떄문이다. (다른 방식으로 생성되었을 수도 있다.) 그러므로 container 도큐먼트를 읽고 container가 동작하는 방식을 이해해야 한다.

Container in Center #2

Center(
   child: Container(
      color: Colors.red,
      child: Container(color: Colors.green, width: 30, height: 30),
   )
)

screen 은 center를 화면과 같은 크기로 강제한다. 그래서 center가 화면을 꽉 채운다.

center는 container가 원하는 사이즈가 되도록 할 수 있다. 단, 화면보다 클 수는 없다. container는 사이즈가 지정되어 있지 않지만 child가 있기 때문에, child의 크기와 같은 사이즈가 된다.

child container와 부모 container는 모두 30*30이 되었다.
child container가 부모 container보다 위에 있기 때문에, 빨간 색을 표시 되지 않는다.

Container in Center with padding

Center(
   child: Container(
     color: Colors.red,
     padding: const EdgeInsets.all(20.0),
     child: Container(color: Colors.green, width: 30, height: 30),
   )
)

빨간 색 container는 자식 크기에 맞게 크기가 조정되자만 자체 패딩을 고려한다. 따라서 70*70이 되었다.

ConstrainedBox

ConstrainedBox(
   constraints: BoxConstraints(
      minWidth: 70, 
      minHeight: 70,
      maxWidth: 150, 
      maxHeight: 150,
   ),
   child: Container(color: Colors.red, width: 10, height: 10),
)

Containerrk 70~150픽셀 사이가 되어야 한다고 생각 하겠지만 틀렸다! ConstrainedBox는 부모로부터 받은 것보다 추가 제약을 부과한다 .

screen 은 ConstrainedBox가 화면과 같은 크기로 강제한다.
child Container도 화면과 같은 크기라 가정하므로 매개변수를 무시한다.

ConstrainedBox in Center #1

Center(
   child: ConstrainedBox(
      constraints: BoxConstraints(
         minWidth: 70, 
         minHeight: 70,
         maxWidth: 150, 
         maxHeight: 150,
      ),
      child: Container(color: Colors.red, width: 10, height: 10),
   )    
)

screen 은 center를 화면과 같은 크기로 강제한다. 그래서 center가 화면을 꽉 채운다.

center는 container가 원하는 사이즈가 되도록 할 수 있다. 단, 화면보다 클 수는 없다. constrainedBox는 추가 제약조건을 부과한다.
따라서 container는 70~150 픽셀 사이여야 한다.

ConstrainedBox in Center #2

Center(
  child: ConstrainedBox(
     constraints: BoxConstraints(
        minWidth: 70, 
        minHeight: 70,
        maxWidth: 150, 
        maxHeight: 150,
        ),
     child: Container(color: Colors.red, width: 1000, height: 1000),
  )  
)

center는 constrainedBox이 화면 사이즈내의 모든 사이즈를 허용한다.
constrainedBox는 추가 제약조건을 부과한다.

따라서 1000픽셀 크기가 되고 싶은 container는 150 픽셀이 된다.

ConstrainedBox in Center #3

Center(
   child: ConstrainedBox(
      constraints: BoxConstraints(
         minWidth: 70, 
         minHeight: 70,
         maxWidth: 150, 
         maxHeight: 150,
      ),
      child: Container(color: Colors.red, width: 100, height: 100),
   ) 
)

center는 constrainedBox이 화면 사이즈내의 모든 사이즈를 허용한다.
constrainedBox는 추가 제약조건을 부과한다.

따라서 1000픽셀 크기가 되고 싶은 container는 100 픽셀이 된다.
(원하는 크기를 가짐)

Container in UnconstrainedBox #1

UnconstrainedBox(
   child: Container(color: Colors.red, width: 20, height: 50),
)

screen은 unconstrainedBox가 화면과 같은 크기로 강제한다. 그러나 unconstrainedBox는 container 가 어떤 사이즈든 허용한다.

Container in UnconstrainedBox #2

UnconstrainedBox(
   child: Container(color: Colors.red, width: 4000, height: 50),
);

screen은 unconstrainedBox가 화면과 같은 크기로 강제한다. 그리고 unconstrainedBox는 container 가 어떤 사이즈든 허용한다.

안타깝게도 container는 4000픽셀이 되고 싶다. unconstrainedBox안에 맞기엔 너무 크다. 그래서 unconstrainedBox는 'overflow warning'을 표시하게 된다.

OverflowBox

OverflowBox(
   minWidth: 0.0,
   minHeight: 0.0,
   maxWidth: double.infinity,
   maxHeight: double.infinity,   
   child: Container(color: Colors.red, width: 4000, height: 50),
);

screen은 OverflowBox가 화면과 같은 크기로 강제한다. 그러나 OverflowBox는 container 가 어떤 사이즈든 허용한다.

OverflowBox는 unconstrainedBox와 비슷하지만 공간이 맞지 않아도 경고를 표시하지 않는 다는 점에서 차이점이 있다.

container는 4000픽셀이 되고 싶고 overflowBox안에 맞기에 너무 크지만, 가능한 것으로 표시된다.(경고 표시되지 않습니다.)

Container in UnconstrainedBox(infinity)

UnconstrainedBox(
   child: Container(
      color: Colors.red, 
      width: double.infinity, 
      height: 100,
   )
)

이 경우에 아무것도 렌더링하지 않으면서 console에 에러가 표시된다.
unconstrainedBox는 container 가 어떤 사이즈든 허용하지만 container는 무한한 사이즈를 원한다.

flutter는 무한한 사이즈를 렌더링 할 수 없으므로, 에러가 표시된다.
error message : BoxConstraints forces an infinite width.

LimitedBox in UnconstrainedBox

UnconstrainedBox(
   child: LimitedBox(
      maxWidth: 100,
      child: Container( 
         color: Colors.red,
         width: double.infinity, 
         height: 100,
      )
   )
)

이렇게 하면 더이상 에러가 발생하지 않는다. 왜냐하면 LimitedBox는 무한한 사이즈를 제공받지만, child에게는 maxWidth 100을 전달하게 된다.

Note. 만약에 unconstrainedBox를 center로 변경한다면 LimitedBox는 limit을 더이상 적용하지 못한다.무한한 제약조건에 대해서만 적용이 가능하지 때문이다. 고로 container는 100이상의 width를 가질 수 있게 된다.
이것이 LimitedBox와 constrainedBox의 차이점을 만든다.

FittedBox

FittedBox(
   child: Text('Some Example Text.'),
)

screen 은 FittedBox를 화면과 같은 크기로 강제한다. text는 텍스트의 양, 글꼴, 사이즈 등에 의해 자연스럽게 크기가 결정된다.

fittedBox는 text에게 원하는 사이즈를 가지도록 허용하지만, text의 크기가 지정된 후에는 fittedBox는 사용가능한 모든 너비를 채울 때까지 크기를 조정한다.

FittedBox in Center

Center(
   child: FittedBox(
      child: Text('Some Example Text.'),
   )
)

fittedBox가 center안에 있으면 어떻게 될까? center는 fittedBox가 원하는 크기를 갖도록 허용한다.

fittedBox는 사이즈가 지정되어 있지 않지만 child가 있기 때문에, child의 크기와 같은 사이즈가 된다.
(크기 조정이 일어나지 않음!)

FittedBox with long text

Center(
   child: FittedBox(
      child: Text('This is some very very very large text that is too big to fit a regular screen in a single line.'),
   )
)

만약에 fittedBox가 center에 있고, text가 너무 큰 사이즈이면 어떻게 될까?

fittedBox는 text사이즈가 되도록 시도하지만 화면보다 커질 수 없다. text를 화면에 맞는 사이즈로 리사이즈 하게된다.

Text with long text

Center(
   child: Text('This is some very very very large text that is too big to fit a regular screen in a single line.'),
)

위에서 fittedBox를 제거하면, 화면의 최대 너비에 맞도록 줄을 끊는다.

FittedBox(infinity)

FittedBox(
   child: Container(
      height: 20.0, 
      width: double.infinity,
   )
)

Note. fittedBox는 경계가 있는 위젯만 크기조정이 가능합니다.(non infinite width and height)

이 경우에 아무것도 렌더링하지 않으면서 console에 에러가 표시된다.

Container in Row

Row(
   children:[
      Container(color: Colors.red, child: Text('Hello!')),
      Container(color: Colors.green, child: Text('Goodbye!)),
   ]
)

screen 은 row가 화면과 같은 크기로 강제한다.

unconstrainedBox와 마찬가지로 row는 제약조건을 child에게 전달하지 않는다. 그대신 원하는 사이즈를 허용한다. row는 나란히 배치하고 남은 공간을 비어 있게 된다.

Container in Row with long text

Row(
   children:[
      Container(color: Colors.red, child: Text('This is a very long text that won’t fit the line.')),
      Container(color: Colors.green, child: Text('Goodbye!')),
   ]
)

자식에게 제약을 가하지 않기 때문에 row의 child가 너비에 맞지 않을 정도로 클 수도 있다.

그래서 row는 'overflow warning'을 표시하게 된다.

Expanded(single one) in Row

Row(
   children:[
      Expanded(
         child: Container(color: Colors.red, child: Text('This is a very long text that won’t fit the line.'))
      ),
      Container(color: Colors.green, child: Text('Goodbye!')),
   ]
)

row의 child가 expand로 감싸지게 되면 row는 child의 width를 정의하지 못하게 된다.
그대신 다른 자식에 의해 정해진 expand의 width를 정의하게 될 것이다.

즉, expand를 사용하면 원래 child의 width는 무의미해지고 무시된다.

Expanded

Row(
   children:[
      Expanded(
         child: Container(color: Colors.red, child: Text(‘This is a very long text that won’t fit the line.)),
      ),
      Expanded(
         child: Container(color: Colors.green, child: Text(‘Goodbye!),
      ),
   ]
)

만약에 모든 row의 child들이 expand로 감싸지게 되면, 각각의 expand는 균형잡힌 사이즈를 가지게 된다.

즉, expand를 사용하면 원래 child의 선호 사이즈를 무시한다.

Flexible

Row(children:[
  Flexible(
    child: Container(color: Colors.red, child: Text('This is a very long text that won’t fit the line.'))),
  Flexible(
    child: Container(color: Colors.green, child: Text(‘Goodbye!))),
  ]
)

Flexible과 expand의 차이점은 단 한가지이다. Flexible는 자식이 자기보다 작거나 같은 width를 가지게 하는 반면 expand는 정확하게 자기와 같은 사이즈를 강제한다.

Flexible와 expand 모두 크기를 조정할 때 자식의 크기를 무시한다.

Note. 크기에 비례하여 row의 child를 확장하는 것이 불가능하다. row은 확장 또는 유연성을 사용할 때 정확한 child을 사용하거나 완전히 무시한다.

Scaffold

Scaffold(
   body: Container(
      color: blue,
      child: Column(
         children: [
            Text('Hello!'),
            Text('Goodbye!'),
         ]
      )))

screen은 Scaffold가 화면과 같은 크기로 강제한다. Scaffold는 화면을 꽉 채우게 된다.

Scaffold는 container가 화면 크기내에서 원하는 사이즈를 허용한다.

Note. 위젯이 자신의 자식에게 어떤 사이즈보다 작아야 한다고 말한다면, 이것은 '느슨한 제약'이라고 말한다. 아래에서 후술한다.

SizedBox.expand

Scaffold(
   body: SizedBox.expand(
      child: Container(
         color: blue,
         child: Column(
            children: [
               Text('Hello!'),
               Text('Goodbye!'),
            ],
         ))))

만약에 Scaffold의 자식에게 Scaffold와 같은 사이즈여야 한다고 말한다면 sizedBox.expand로 감쌀 수 있다.

Note. 위젯이 자신의 자식에게 어떤 사이즈와 같아야 한다고 말한다면, 이것은 '엄격한 제약'이라고 말한다. 아래에서 후술한다.

엄격한 vs 느슨한 제약

제약조건이 엄격하다, 느슨하다는 것은 매우 일반적이므로 알아둘 가치가 있다.

  • 엄격한 제약조건을 정확한 크기(단일 가능성)을 제공한다. 즉 엄격한 제약조건의 최대 최소 너비를 동일하다.
    flutter의 box.dart로 이동하여 BoxConstraints생성자를 검색하면 다음을 찾을 수 있다.
BoxConstraints.tight(Size size)
   : minWidth = size.width,
     maxWidth = size.width,
     minHeight = size.height,
     maxHeight = size.height;

위의 두번째 예제(Container #2)를 확인해보면 빨간 container가 화면와 동일한 사이즈를 가지게 강제한다고 말했었다. 이를 container에게 엄격한 제약조건을 적용한 것이다.

  • 느슨한 제약조건은 최대 너비/높이를 설정하지만 위젯을 원하는 만큼 작게 설정할 수도 있다. 즉 느슨한 제약조건의 최소 너비/높이는 0이다.
BoxConstraints.tight(Size size)
   : minWidth = size.width,
     maxWidth = size.width,
     minHeight = size.height,
     maxHeight = size.height;

세번째 예제(Center)를 확인해보면 center는 container가 원하는 사이즈가 되도록 할 수 있다. 단, 화면보다 클 수는 없고 하였다.
center는 container에게 느슨한 제약조건을 전달한다.

궁극적으로 center는 screen(부모)에게 받은 엄격한 제약조건을 container(자식)에게 느슨한 제약조건으로 변환하는 역할을 한다.

0개의 댓글