인문주의 사피엔스

Flutter의 제약(constraint) 이해하기 본문

프로그래밍/Flutter

Flutter의 제약(constraint) 이해하기

인문주 2022. 3. 5. 20:40
반응형

Flutter에서 UI 구성을 위해 위젯(widget)을 배치(layout)하는 방식은 위젯의 다양성, 유연성, 확장성 면에서 볼 때 매우 훌륭합니다. 위젯을 제대로 활용하려면 Flutter UI 프로그래밍의 핵심 주제의 하나인 제약(constraint)을 제대로 이해해야 합니다. Flutter 문서가 위젯의 배치와 제약을 상세히 설명하고 있음에도 불구하고 그 내용을 프로그래밍에 적용할 때는 적잖은 시행착오를 겪게 됩니다. 그런 시행착오의 결과로서 위젯의 배치와 제약에 대해 정리해 보기로 했습니다. 다음 공식 웹페이지의 내용을 함께 보면 Flutter의 위젯 배치 방식을 이해하는 데 많은 도움이 될 것입니다.

 

https://docs.flutter.dev/development/ui/layout/constraints

 

Understanding constraints

Flutter's model for widget constraints, sizing, positioning, and how they interact.

docs.flutter.dev

 

Flutter 문서는 위젯의 배치 과정을 세 단계로 나누어 설명합니다. 하지만 이해를 돕는 데 좀 더 낫다는 판단에서 여기서는 다음와 같이 두 단계로 요약하였습니다.

 

  1. 부모 위젯이 자식 위젯에게 제약(constraint)을 부여한다.
  2. 자식 위젯은 그 제약 내에서 자신의 배치 방식을 결정한다.

 

여기서 말하는 제약은 크기의 제약입니다. 크기의 제약은 구체적으로 위젯의 너비(width)와 높이(height) 각각의 최소값과 최대값을 의미합니다. 부모 위젯은 자식 위젯에게 너비와 높이의 제약 조건, 즉 허용 범위를 정해줍니다. 

 

허용 범위를 부여받은 위젯은 그 범위 내에서 크기와 위치 등 자신의 배치 방식을 결정합니다. 배치 방식을 결정한다는 말은 같은 제약 조건 내에서라도 경우에 따라 배치 방식이 달라질 수 있다는 것을 뜻합니다. 구체적으로 말해 어떤 위젯은 가능한 한 자신의 크기를 키우는 반면 또다른 위젯은 가능한 한 크기를 줄일 수도 있습니다. 위젯의 배치 방식은 용도와 목적에 따라 달라질 수 있습니다.

 

부여받은 제약 조건 내에서 배치 방식을 결정하는 과정은 자식 위젯으로 내려가면서 똑같이 반복됩니다. 부모 위젯으로부터 제약을 부여받은 위젯은 자신의 배치 방식을 결정합니다. 그리고 스스로 부모 위젯이 되어 자신의 자식 위젯에게 제약을 부여합니다. 그 제약은 위로부터 부여받은 그대로 전달된 것일 수도 있고, 새롭게 만든 것일 수도 있습니다. 

 

요약하면 위젯의 배치 과정은 두 단계로 이루어집니다. 먼저 부모 위젯으로부터 제약 조건을 부여받습니다. 그런 다음 그 제약 내에서 자신의 배치 방식을 결정합니다.

 

제약의 구체적인 내용은 다음의 BoxConstraints 클래스로 정의됩니다.

 

const BoxConstraints({
  this.minWidth = 0.0,
  this.maxWidth = double.infinity,
  this.minHeight = 0.0,
  this.maxHeight = double.infinity,
})

 

BoxConstraints는 ConstrainedBox의 매개변수로서 다양한 위젯에서 직간접적으로 사용됩니다. 클래스 생성자를 보면 기본값이 정의되어 있음을 알 수 있습니다. 기본값을 풀어서 말하면 0에서 무한대까지의 모든 크기를 허용한다는 뜻입니다. 만약 minWidth, maxWidth, minHeight, maxHeight의 값을 바꾸어 입력하면 그에 따라 제약의 내용과 성격도 달라진다는 뜻입니다.

 

팽팽(tight)하거나 느슨(loose)하거나

제약은 팽팽한 것과 느슨한 것으로 나눌 수 있습니다. 그 사실을 알아야 다음 그림의 의미를 이해할 수 있습니다.

 

 

다음은 소스코드입니다.

 

 

위의 예제는 원래 의도된 내용과 결과가 다릅니다. 의도된 내용은 크기가 300인 검은색 Container 안에 크기가 100인 FlutterLogo를 표시하는 것입니다. 그러나 의도와 달리 FlutterLogo의 크기는 100이 아닌 300이 되었습니다. 왜 그럴까요? 그 이유를 알기 위해서는 FlutterLogo가 부여받은 제약 조건을 확인해야 합니다.

 

검은색 Container가 FlutterLogo에게 부여한 제약 조건은 width=300, height=300입니다. BoxConstraints의 매개변수로 표현하면 minWidth=300, maxWidth=300, minHeight=300, maxHeight=300이 됩니다. 이것을 팽팽한 제약이라고 부릅니다. 너비와 높이 각각의 최소값과 최대값이 동일한 경우입니다. 팽팽한 제약을 부여받은 FlutterLogo는 자신의 크기를 원하는 값으로 결정할 여유가 없습니다.

 

팽팽한 제약

팽팽한 제약은 minWidth=maxWidth, minHeight=maxHeight인 경우를 의미합니다. 풀어서 말하면 위젯의 너비와 높이 각각의 최소값과 최대값이 같은 경우입니다. 간단히 말해 위젯의 크기가 특정 값으로 고정된 경우입니다. 

 

팽팽한 제약이 사용되는 대표적인 경우는 앱의 최상위 위젯입니다. 일반적으로 앱의 최상위 위젯보다 위에 있는 것은 기기의 스크린 뿐입니다. 스크린이 앱의 최상위 위젯에 부여하는 제약은 스크린 자체의 크기입니다. 따라서 앱의 최상위 위젯은 스크린 크기에 팽팽하게 고정됩니다. 팽팽한 제약을 부여받은 앱의 최상위 위젯은 자신의 크기를 다른 값으로 바꿀 여유가 없습니다.

 

흔히 SizedBox나 Container를 사용할 때 팽팽한 제약에 대한 오해가 생깁니다. 그 오해는 앞에 나온 FlutterLogo의 경우처럼 자식 위젯의 크기를 지정하는 데 실패할 때 생깁니다. 자식 위젯에 제약을 부여하는 데 실패한 것으로 착각하기 때문입니다. 하지만 그것은 제약을 적용하는 데 실패한 것이 아니라 그 상황에 적절한 제약을 사용하지 않은 것입니다.

 

자식 위젯이 크기를 변경할 자유를 주고 싶다면 팽팽한 제약이 아니라 느슨한 제약을 적용해야 합니다.

 

느슨한 제약

느슨한 제약은 minWidth=0, maxWidth>0, minHeight=0, maxHeight>0인 경우입니다. 풀어서 말하면 너비와 높이 각각의 최소값이 0이고 최대값은 무한대가 아닌 유한의 값을 가지는 경우입니다. 느슨한 제약을 부여받은 위젯은 자신의 크기를 변경할 수 있습니다. 물론 허용된 범위 내에서 그렇습니다.

 

느슨한 제약을 사용되는 대표적인 위젯은 Center입니다. Center 위젯이 자식 위젯에 부여하는 것이 느슨한 제약입니다. 그 제약은 최소 0에서부터 최대 Center 위젯의 크기까지의 범위입니다. Center 위젯에 포함된 자식 위젯은 허용된 범위 내에서 자신의 크기를 자유롭게 변경할 수 있습니다.

 

다음은 앞에 나온 FlutterLogo와 Container 사이에 Center를 끼워넣은 소스코드입니다.

 

 

다음 그림은 ‘case 1’의 실행 결과입니다.

 

 

‘case 1’에 구현된 FlutterLogo의 배치 과정을 다음과 같이 설명할 수 있습니다.

 

  1. Container가 Center에게 크기 300의 팽팽한 제약을 부여한다.
  2. 크기 300의 팽팽한 제약 내에서 Center의 크기가 300으로 고정된다.
  3. Center가 FlutterLogo에게 크기 0에서 300까지의 느슨한 제약을 부여한다.
  4. 크기 0에서 300까지의 느슨한 제약 내에서 FlutterLogo가 자신의 크기를 100으로 결정한다.

 

다음은 FlutterLogo의 크기를 600으로 바꾼 ‘case 2’의 실행 결과입니다.

 

 

여기서 FlutterLogo의 크기가 600이 아닌 300이 된 이유는 Center로부터 부여받은 제약의 최대값이 300이기 때문입니다.

 

유한(bounded)하거나 무한(unbounded)하거나

제약은 유한한 것과 무한한 것으로 나눌 수도 있습니다.

 

유한한 제약

유한한 제약은 maxWidth<double.infinity, maxHeight<double.infinity인 경우를 의미합니다. 풀어서 말하면 크기의 최대값이 무한대가 아닌 유한값인 경우입니다. 앞에 나온 팽팽한 제약과 느슨한 제약 모두 유한한 제약에 해당됩니다. 

 

무한한 제약

무한한 제약은 maxWidth=double.infinity, maxHeight=double.infinity인 경우를 의미합니다. 풀어서 말하면 크기의 최대값이 무한대인 경우입니다. 제약이 없는(unconstrained) 상태로 표현할 수도 있습니다.

 

무한한 제약의 대표적인 경우는 ListView의 스크롤 영역에 적용되는 제약입니다. ListView는 자식 위젯들이 스크롤되는 방향으로 무한대의 크기를 허용합니다. 따라서 이론상 무한 개의 위젯이 포함될 수 있습니다. 하지만 그것이 개별 위젯의 무한대 크기를 허용한다는 의미는 아닙니다. 만약 무한한 제약 내에 있는 개별 위젯이 무한대 확장을 시도한다면 당연히 Flutter는 그 시도를 거부할 수밖에 없습니다. 그런 경우에 의도했던 결과는 제대로 표시되지 않고 디버그 창에는 오류 메시지가 출력됩니다.

 

다음의 소스코드는 ListView의 스크롤 영역에 또 다른 ListView를 배치한 것입니다.

 

 

다음 그림은 ‘case 3’의 실행 결과입니다. (Flutter 버전에 따라 결과가 표시되지 않을 수도 있습니다.)

 

 

LayoutBuilder를 사용하면 ListView의 스크롤 영역에 적용되는 제약의 내용을 확인할 수 있습니다. 위에서는 가로축 방향으로 너비 300의 팽팽한 제약이, 세로축 방향으로 무한한 제약이 적용되고 있다는 것을 알 수 있습니다. 그리고 디버그 창에는 다음과 같은 오류 메시지가 표시됩니다.

 

 

ListView의 배치 방식은 부모 위젯으로부터 부여받은 제약에 상관없이 무조건 자신의 크기를 키우는 것입니다. 바깥쪽 ListView는 부모 위젯인 Container의 크기 300까지 팽팽하게 커지게 됩니다. 그리고 자식 위젯들이 스크롤되는 세로축 방향으로 무한대의 크기를 허용합니다.

 

안쪽 ListView 역시 자신의 크기를 최대로 키우려고 시도합니다. 하지만 안쪽 ListView에 부여되는 제약이 세로축 방향의 무한한 제약이기 때문에 그 시도가 거부되고 오류 메시지가 표시되었습니다. 문제를 해결하려면 안쪽 ListView에 유한한 제약을 부여해야 합니다. 다음 그림은 ‘case 4’에서 SizedBox를 사용해 안쪽 ListView에 유한한 제약을 부여한 결과입니다.

 

 

무한한 제약의 또 다른 경우는 Flex, Row, Column 같은 굴곡(flex) 위젯의 내부 영역에 적용되는 제약입니다. 다음 소스코드는 Column의 내부 영역에 ListView를 배치한 것입니다.

 

 

다음 그림은 ‘case 5’의 실행 결과입니다. (Flutter 버전에 따라 결과가 표시되지 않을 수도 있습니다.)

 

 

LayoutBuilder를 통해 Column의 내부 영역에 적용되는 제약의 내용을 확인할 수 있습니다. 위에서는 가로축 방향으로 너비 300의 느슨한 제약이, 세로축 방향으로 무한한 제약이 적용되고 있다는 것을 알 수 있습니다. 그리고 디버그 창에는 다음과 같은 오류 메시지가 표시됩니다.

 

 

이 경우 역시 ListView에 유한한 제약을 부여함으로써 문제를 해결할 수 있습니다. 다음 그림은 ‘case 6’에서 Expanded를 사용해 ListView에 유한한 제약을 부여한 결과입니다.

 

 

팽팽한 제약, 느슨한 제약, 유한한 제약, 무한한 제약 등의 제약을 부여하는 데 사용되는 위젯의 종류는 다양합니다. 다음은 UnconstrainedBox, OverflowBox, ConstrainedBox, LimitedBox의 사용 예를 보여주는 소스코드입니다.

 

 

UnconstrainedBox

기본적으로 대부분의 위젯은 부모 위젯의 영역 내에서만 표시됩니다. 부모 위젯의 영역보다 크기가 큰 위젯은 크기가 강제로 줄어들거나 부모 위젯의 영역 바깥쪽이 사라져 안 보이게 됩니다. 그 위젯을 원래 크기대로 부모 위젯의 영역 바깥에도 표시하려면 UnconstrainedBox를 사용할 수 있습니다. 다음 그림은 UnconstrainedBox를 사용해 크기가 300인 Container에 크기가 600인 FlutterLogo를 표시한 결과입니다.

 

 

그런데 UnconstrainedBox는 위 그림에서처럼 자식 위젯이 자신보다 클 때 디버그 모드용 overflow 경고 메시지를 표시합니다.

 

OverflowBox

디버그 모드용 overflow 경고 메시지를 없애고 싶다면 OverflowBox를 사용할 수 있습니다. 다음 그림은 OverflowBox를 사용해 크기가 300인 Container에 크기가 600인 FlutterLogo를 표시한 결과입니다.

 

 

ConstrainedBox

ConstrainedBox를 사용하면 자식 위젯에 대해 추가적인 제약을 부여할 수 있습니다. 다음 그림은 위의 결과에 ConstrainedBox를 적용해서 FlutterLogo의 크기를 100으로 제한한 결과입니다.

 

 

LimitedBox

LimitedBox를 사용해도 같은 결과를 얻을 수 있습니다.

 

 

그런데 LimitedBox에 유한한 제약을 부여하면 결과가 달라집니다.

 

 

결과가 달라지는 이유는 LimitedBox가 무한한 제약 내에 있을 때만 크기를 제한하는 속성을 갖기 때문입니다. 유한한 제약 내에서 LimitedBox의 크기는 자식 위젯의 크기에 맞춰집니다.

 

반응형

 

위젯의 배치 방식

LimitedBox의 경우처럼 모든 위젯은 부모 위젯으로부터 부여받은 제약에 따라 자신의 배치 방식을 결정합니다. 그 배치 방식은 위젯의 용도와 목적에 따라 몇 가지로 분류됩니다.

 

  • 가능한 한 큰 크기로
  • 가능한 한 작은 크기로
  • 자식 위젯의 크기로
  • 특정 크기로

 

위젯의 배치 방식은 다양하지만 그 결정 과정에는 공통적인 경향이 있습니다. 그것은 대부분의 위젯이 "유한한 제약 내에서는 가능한 한 자신의 크기를 키우고 무한한 제약 내에서는 가능한 한 줄이는" 경향입니다.

 

Center

Center는 그런 경향에 맞는 대표적인 위젯입니다. Center의 크기는 유한한 제약 내에서는 가능한 한 커지고 무한한 제약 내에서는 가능한 한 작아집니다. 

 

Container

Container도 같은 경향을 보이기는 하지만 child, alignment, width, height, constraints 등의 입력값에 따라 복잡하게 결정됩니다. 

 

Row, Column

Row와 Column의 크기도 유한한 제약 내에서는 가능한 한 커지지만 무한한 제약 내에서는 자식 위젯들의 크기에 맞춰집니다. 

 

ScrollView

ListView와 GridView의 기본 클래스인 ScrollView의 크기는 제약에 상관없이 무조건 커집니다. 따라서 무한한 제약 내에 놓인 ScrollView는 오류를 발생시킵니다.

 

다음은 Column과 ListView 안에 놓인 Container의 다양한 반응을 보기 위해 작성된 소스코드입니다.

 

 

다음 그림은 소스코드의 실행 결과입니다.

 

 

 

빨간색 Container의 경우 무한한 제약 내에서 자식 위젯이 없을 때 가능한 한 크기가 작아지는 속성을 공통적으로 보여주고 있습니다. 

녹색 Container의 결과가 서로 다른 이유는 가로축 방향으로 적용되는 제약이 다르기 때문입니다. Column의 경우는 느슨한 제약이 부여되지만 ListView의 경우는 팽팽한 제약이 부여되고 있음을 LayoutBuilder의 결과로부터 확인할 수 있습니다. 

파란색 Container도 녹색의 경우와 비슷해 보이지만 느슨한 제약과 팽팽한 제약에 대한 FlutterLogo의 반응에서 약간 다르다는 것을 알 수 있습니다. 

노란색 Container가 느슨한 제약 내에서 보이는 반응이 파란색의 경우와 다른 이유는 alignment 때문입니다. alignment의 값이 입력될 경우 Container는 크기가 가능한 한 커지는 속성을 가지게 됩니다.

 

Transform, Opacity

Transform, Opacity 등의 위젯은 자식 위젯과 같은 크기에 맞춰지는 속성을 가집니다. 

 

Image, Text

Image, Text 등의 위젯은 이미지와 텍스트가 갖는 특정 크기에 맞춰지는 속성을 가집니다.

 

다음은 Transform, Opacity, Text의 속성을 보여주기 위해 작성된 소스코드입니다.

 

 

소스코드의 실행 결과는 다음과 같습니다.

 

 

위의 결과는 다음과 같이 설명될 수 있습니다.

 

  1. Center가 Transform에게 최대 크기 300의 느슨한 제약을 부여한다.
  2. Transform이 크기를 Opacity에 맞추고 300의 느슨한 제약을 부여한다.
  3. Opacity가 크기를 Container에 맞추고 300의 느슨한 제약을 부여한다.
  4. Container가 크기를 200으로 정하고 Text에게 200의 팽팽한 제약을 부여한다.
  5. Text는 부여받은 200의 팽팽한 제약에 상관없이 크기를 텍스트에 맞춘다.

 

다음은 전체 소스코드입니다.

 

 

반응형
Comments