본문 바로가기
Flutter

[Flutter] Constraints를 이해하고 layout만들기

by 아마도개발자 2024. 1. 13.

 

나 같은 플러터 초보자들이 UI를 그릴 때 가장 에러를 많이 일으키는 가장 큰 원인은 잘못된 constraints의 사용일 것이다. 처음 플러터를 학습할 때는 Container, Text, Column 그리고 Row같은 위젯을 사용하여 단순한 layout을 만들어 보게 된다. 직관적인 선언형UI 덕분에 처음에는 빠르게 플러터에 적응하여 화면을 그려나갔을 것이다. 하지만 점점 위젯들이 중첩되고, 스크롤 위젯 등 다양한 위젯들을 활용하게 되었을 때, 어디서 부터 잘못된 것인지 파악하기도 힘든 상황들이 발생하게 된다. 

 

대부분의 위젯 관련 에러는 Flutter의 Widget constraints에 대한 이해의 부족으로 발생한다. constraints에 대한 이해가 부족한 채 개발을 진행하게 되면, 쓸모 없는 위젯을 사용하여 누더기가 된 코드로 인해 수정이 힘들어지거나 필요없는 위젯들이 사용되어 리소스를 낭비하게 되기 쉽상이다.

 

그렇다면 constraints의 이해는 어디서부터 시작해야 하는 것일까?

 

우선 Flutter의 layout구성에서 핵심은 제약조건(constraints)부모 위젯이라는 것을 꼭 인지하고 있어야 한다. 모든 위젯들은 constraints와 부모 위젯으로부터 자유로울 수 없다.이것을 Flutter공식문서에서는 Constraints go down. Sizes go up. Parent sets position로 정의하였고, 가장 기본적인 규칙이라 말한다. 이 규칙을 이해하지 못한다면 Flutter layout을 이해할 수 없으며, 이 규칙에 대한 설명은 다음과 같다.

 

1. 위젯은 부모 위젯으로부터 고유한 제약조건을 받는다. 이 제약조건은 최소 너비와 최대 너비, 최소 높이와 최대높이 4가지로 이루어진다.

2. 이후, 위젯은 자신의 자식 위젯 목록을 확인한다. 위젯은 자신의 자식 위젯의 constraints를 각각 지정해준 뒤, 각각의 위젯 크기를 묻는다.

3. 이후 자식 위젯들을 하나씩 배치한다(가로 x축, 세로 y축).

4. 마지막으로 자신의 크기를 부모 위젯에게 전달한다.

 

즉, 하나의 위젯은 생성될 때마다 부모에게서 제약조건을 받고, 자식에게 제약조건을 전달하며 다시 자식에게 크기를 전달받고, 부모에게 크기를 전달하는 과정을 거치는 것이다. 위젯들은 모두 이와같은 과정을 동일하게 거치게 되나, 위젯별로 렌더링될 때 차이점이 발생하게 된다.

 

Flutter에서 위젯은 기본적으로 RenderBox객체에 의해 렌더링된다. Flutter의 많은 RenderBox들 중 특히 하나의 자식 위젯을 가지는 것들은 그들의 자식 위젯에게 제약 조건을 전달하는데, 일반적으로 제약 조건을 처리하는 방법에 따라 RenderBox는 세가지 종류로 나뉘어진다.

 

1. 제약조건 내에서 최대크기를 가지려고 하는 것. ex) Center, ListView 

2. 자식 위젯과 같은 크기를 가지려고 하는 것. ex) Transform, Opacity

3. 특정 크기를 가지려고 하는 것. ex) Image, Text

 

+ 생성자 인수에 따라 1~3번 중 유형을 선택하는 것. ex) Container

* Container 생성자가 기본 값일 때는 가능한 큰 크기가 되려고 하지만 크기를 지정하면 해당 값의 크기가 되려고 한다. 자식 위젯이 있을 경우, 자식 위젯의 크기를 따라간다.

 

또한, Flutter layout에는 몇 가지 제한 사항이 있다. 

 

1. 위젯은 부모 위젯의 제약 조건 내에서만 자신의 크기를 결정한다.
2. 위젯의 위치는 부모 위젯이 결정한다.
3. 부모 위젯의 크기와 위치도 부모 위젯의 부모 위젯에 따라 달라지기 때문에 전체적으로 트리를 고려하지 않고 위젯의 크기와 위치를 정의하는 것은 불가능하다.

4. 자식 위젯이 부모 위젯과 다른 크기를 원할 때, 부모가 자식을 배치할 정보가 충분하지 않으면 자녀의 크기가 무시될 수 있다.

 

앞서 학습한 내용들을 직접 확인할 수 있는 예제를 살펴보자.

 

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.blue,
    );
  }
}

 

이렇게 아무런 속성이 없는 Container를 하나 선언해보자. 현재 선언한 Container의 부모는 Screen이다. 이 screen이 화면과 같은 크기로 Container의 크기를 강제하고 있어 화면 전체가 파란색으로 물든 것을 볼 수 있다.

 

이번에는 width와 height를 지정해보자.

 

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      color: Colors.blue,
    );
  }
}

 

 

width와 height에 100을 줬음에도, 화면을 꽉 채우고 있는 것을 확인 할 수 있다. Container에 크기를 지정했지만, 아직 부모 위젯인 screen이 자신과 같은 크기를 갖도록 강제하고 있기 때문이다. 이것은 LayoutBuilder를 통해 확인할 수 있다.

 

 

위젯은 부모 위젯의 제약 조건 내에서만 자신의 크기를 결정한다는 1번 제한사항에 의해 Container의 크기는 무시되고, screen을 가득채우게 되는 것이다. Container의 width, height를 적용시키려면 어떻게 해야할까? 바로 자식 위젯대신 Constraints를 받을 Widget으로 자식 위젯을 감싸버리는 것이다.

 

 

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return Center(
      child: LayoutBuilder(
        builder: (context, constraints) {
          print(constraints.minHeight);
          print(constraints.minWidth);
          print(constraints.maxWidth);
          print(constraints.maxHeight);
          return Container(
            width: 100,
            height: 100,
            color: Colors.blue,
          );
        },
      ),
    );
  }
}

 

위 코드를 보면 Container 위젯을 Center로 감싸주었다. Center위젯은 Container 대신 Screen의 크기로 강제될 것이다. 하지만 Center는 자식 위젯에게 제약조건을 강제하지 않기 때문에 아래와 같이 0 부터 screen의 최대 크기를 Container가 가질 수 있게 되는 것이다.

 

 

 

 

그렇다면, 아래와 같은 경우는 어떻게 렌더링될까? 직접 실행시켜보지 않아도 constraints에 대한 이해가 생겼다면 알 수 있을 것이다. 코드에 따른 렌더링 순서를 유추해보자.

 

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

 

 

예상 실행 순서

 

1. Center는 부모 위젯(screen)에 의해 screen과 같은 크기를 가지게 됨.

2. Center에서 빨간색 컨테이너 constraints를 전달(0 ~ screen의 크기)

3. 빨간색 컨테이너에서 초록색 컨테이너로 constraints를 전달( screen을 초과하지 않는 원하는 크기)

4. 초록색 컨테이너의 크기는 30x30으로 특정되어 있음.

5. 빨간색 컨테이너는 크기 속성이 없고, 자식 위젯이 있기 때문에 자식 위젯의 크기와 같은 크기를 가지게 됨. (30x30)

6. 초록색 컨테이너가 빨간색 컨테이너를 덮어 빨간색 컨테이너가 보이지 않음.

 

실행 화면

처음 언급했었던 Constraints go down. Sizes go up. Parent sets position 규칙에 의거하여 렌더링이 이루어지는 것을 확인할 수 있다. 이와 같은 예제는 다양한 조건의 위젯들의 중첩으로 무한한 경우의 수를 갖게 될 것이다. 하지만 아무리 복잡한 layout이더라도 위 규칙을 제대로 이해한다면, 결국 constraints와 size가 어떻게 서로 전달되는지 알 수있기 때문에 충분히 본인이 원하는 화면을 그려낼 수 있을 것이라 생각한다.

 

 

 

참고

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