Row & Column Widgets

Flutter Tutorial, Flutter 레이아웃을 구성하는 핵심 위젯

John Cho
Flutter Seoul

--

이 튜토리얼에서는 Flutter에서 레이아웃을 구성하는 핵심 개념인 Row 위젯과 Column 위젯에 대해서 다루고, 두개의 조합을 통해 어떤 식의 레이아웃을 구성할 수 있는 지 살펴본다.

아래 글의 예제들은 모두 Material App을 사용했다를 전제로 해두었다. 만약 Non Material Code라면 더 많은 코드가 필요할 것이다.

tl;dr

  • Flutter의 레이아웃은 Flex 레이아웃을 채택하고 있다.
  • 수평으로 나열해야할 때는 Row, 수직으로 나열해야할 때는 Column을 사용한다.
  • FlexRender 클래스가 Flex 정렬을 제어하는데, 이 로직을 이해하는 건 Intermediate에서 더 자세히 다루겠다.

Flex

Row와 Column 위젯은 Flex 레이아웃 모델을 따라가고 있다. 만약 CSS에 익숙한 사람이라면 display: flex 를 사용해본 경험이 있을텐데 그 모델과 동일한 모델이다.

Flex는 유연한 레이아웃 모델로, 자식 요소들을 배치할 때 부모 컨테이너 기준으로 자식을 배치시킨다.

Flex 레이아웃 모델을 가장 잘 설명하는 그림은 아래 그림이다. 아래 그림은 W3C의 Flexiblebox 스펙 문서에 있는 그림이며, Flex를 가장 잘 설명하고 있다고 생각한다.

기본적으로 주 축 (main axis)과 교차 축 (cross axis)를 가지며, 주축에 따라서 방향이 전환된다. 주축이 수평이라면 Row, 주축이 수직이라면 Column 위젯을 사용하면 된다.

Flutter의 Flex 위젯들은 스크롤이 불가능하다는 점을 항상 주의해야한다. Flex 위젯이 표현 가능한 범위를 넘어가면 에러로 간주하기 때문에, 만약 스크롤을 지원해야한다면 ListView를 사용하시길 바란다.

Example 구조

import 'package:flutter/material.dart';

class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Layout demo',
home: Scaffold(
appBar: AppBar(
title: Text('Flutter material layout demo')
),
body: Center(
child: Row() // Hands on! 여기를 아래 코드로 대체하면 된다.
)
)
);
}
}

Row Widget

기본적인 사용법은 간단하다. Row를 하나 선언하고, 자식 요소를 넣으면 된다.

Row(
children: <Widget>[
FlutterLogo(),
Text('Hello, Flutter Beginner!'),
Icon(Icons.sentiment_very_satisfied),
]
)

이렇게 하면 아래 이미지와 같은 레이아웃이 형성된다.

Flex 레이아웃 모델은 기본적으로 주 축과 교차 축으로 아이템들을 나열한다. mainAxisAlignment 속성을 사용하면 주 축을 기준으로 정렬을 수정하는 것이 가능하며, 교차 축 정렬은 crossAxisAlignment 속성을 사용할 수 있다.

Row(
mainAxisAlignment: MainAxisAlignment.center, // 주 축 기준 중앙
crossAxisAlignment: CrossAxisAlignment.center, // 교차 축 기준 중앙
children: <Widget>[
FlutterLogo(),
Text('Hello, Flutter Beginner!'),
Icon(Icons.sentiment_very_satisfied),
]
)

Row 위젯에서 crossAxisAlignment를 사용하려면 Height를 지정해줘야하는데, Height를 지정하려면 Container 위젯을 써야해서Container 위젯을 하나 추가하도록 하겠다.

Center(
child: Container(
height: 360, // 높이를 360으로 지정
child: Row(
children: <Widget>[
FlutterLogo(),
Text('Hello, Flutter Beginner!'),
Icon(Icons.sentiment_very_satisfied),
]
)
)
)

MainAxisAlignment

주 축 정렬을 수정할 때 사용하는 enum이다.

  • start — 시작 정렬
  • end — 끝정렬
  • center— 중앙정렬
  • spaceAround— 중간 여백은 동일하나, 첫번째와 마지막 여백은 중간 여백의 절반
  • spaceBetween — 양 끝은 붙이고, 중간 여백만 동일하게
  • spaceEvenly— 첫번째, 중간, 마지막 여백이 모두 동일하게

crossAxisAlignment

교차 축 정렬을 수정할 때 사용하는 enum 이다.

  • start — 시작 정렬
  • end — 끝정렬
  • center— 중앙정렬
  • stretch — 가능한 크기만큼 자식 요소의 크기를 키운다.

둘 다 중앙정렬로 해둔 예제는 다음과 같다.

만약 자식이 남아있는 영역을 차지하기를 원한다면 Expanded 위젯을 사용할 수 있다.

Row(
children: <Widget>[
FlutterLogo(),
Expanded: (
child: Text('Hello, Flutter Beginner!')
),
Icon(Icons.sentiment_very_satisfied),
]
)

Expanded 위젯을 이용하면 Row 위젯 내의 남은 공간을 Text 위젯이 차지하도록 할 수 있다. 하지만 이렇게 하면 MainAxisAlignment 는 사용이 불가능하니 주의하는 것이 좋다. (하나를 얻으면 하나를 잃는 세상이다)

예를 들어 자식 위젯이 서로 동일한 영역을 가지기를 원한다면 세 자식을 모두 Expanded 위젯으로 감싸면 된다.

Row(
children: <Widget>[
Expanded(
child: FlutterLogo()
),
Expanded(
child: Text('Hello, Flutter Beginner!')
),
Expanded(
child: Icon(Icons.sentiment_very_satisfied),
)
]
)

이렇게 하면 세개의 자식이 서로 같은 너비를 가지면서 배치된다.

만약 그 중에서 하나의 아이템이 다른 것보다 2배 크기를 가지고 싶다면 flex 속성을 추가하면 된다.

Row(
children: <Widget>[
Expanded(
child: FlutterLogo()
),
Expanded(
flex: 2,
child: Text('Hello, Flutter Beginner!')
),
Expanded(
child: Icon(Icons.sentiment_very_satisfied),
)
]
)

이런 식으로 Row Widget을 이용해서 위젯을 수평으로 배치할 수 있다.

Column Widget

Column 위젯은 Row 위젯과 거의 동일하다. 다만 주 축이 수평이 아니라 수직일 뿐이다. 대부분의 사용법은 Row와 동일하다.

Column(
children: <Widget>[
FlutterLogo(),
Text('Hello, Flutter Beginner!'),
Icon(Icons.sentiment_very_satisfied),
]
)

이렇게 하면 아이템들이 수평이 아니라 수직으로 정렬된다.

수직으로 정렬되는 아이템들을 Row와 마찬가지로 Main Axis와 Cross Axis를 이용해서 수정이 가능하나, 한가지 주의해야할 점은 이제 Main Axis가 수직을 기준으로 정렬하고 Cross Axis가 수평을 기준으로 정렬한다는 것이다. (축이 기준이니)

예를 들어 예를 들어 mainAxisAlignment를 center로 하면,

Column(
mainAxisAlignment: MainAxisAlignment.center, // 주 축 기준 중앙
children: <Widget>[
FlutterLogo(),
Text('Hello, Flutter Beginner!'),
Icon(Icons.sentiment_very_satisfied),
]
)

아래 이미지와 같은 결과물이 나온다.

따라서 Row 위젯과 Column 위젯을 사용할 때에는 주 축이 어떤 방향인 지, 어떤 식으로 활용 가능한 지 고민해가면서 작성하는 것이 중요하다.

복잡한 레이아웃 구성하기

이제 Row와 Column 에 대해서 이해했으니 이제 간단한 앱을 하나 만들어보자. Flutter 공식 가이드에서 제공하는 아래 레이아웃을 보자.

여기서부터의 내용은 Flutter 공식 가이드라인을 봐도 무관하다. 다만 Flutter문서가 그렇게 친절한 편은 아니라서 이 글에서는 조금 더 디테일하게 설명할 예정이다.

레이아웃 분석하기

주어진 레이아웃이 있을 때 이 레이아웃을 어떤 식으로 구성해야할 지 먼저 분석하는 것이 중요하다. 기본적으로는 Row와 Column 단위로, 더 단순하게 생각하자면 어느 걸 수평으로 배치하고 어느 걸 수직으로 배치할 지 결정하면 된다.

먼저 수직으로 배치해야하는 것들은 크게 네가지가 있다.

  1. 썸네일 영역
  2. 제목 영역
  3. 버튼 영역
  4. 본문 영역

일단 네개의 분리된 영역을 화면에 표시해주기 위해서 간단히 Column 위젯을 사용해보자. 아직 레이아웃에 익숙하지 않을테니 우선 제일 간단한 Text 위젯을 사용해서 화면을 보여주도록 하자.

Column(
children: <Widget>[
Text('이미지 영역'),
Text('제목 영역'),
Text('버튼 영역'),
Text('본문 영역'),
],
)

그리고 나서 수평으로 배치해야하는 것들을 찾아보자. UI 상에서 크게 제목 영역과 버튼 영역이 수평으로 배치되어있다.

제목 영역은 그 와중에 Column도 써야한다.
버튼 영역도 아이콘과 텍스트를 보여주기 위해 Column을 다시 써야한다.

제목 영역 구현하기

제목 영역에는 주제목, 부제목, 별 아이콘, 별 갯수를 나타내는 4개의 아이템이 존재하는데, 이 중에서 주제목과 부제목은 수직(Column)으로, 제목과 아이콘, 텍스트는 수평(Row)으로 배치되어야한다.

먼저 수평 제목과 아이콘, 텍스트를 수평으로 배치해보자. 위 코드에서 Text('제목영역') 이라고 되어있던 부분을 수정하면 된다.

Row(
children: <Widget>[
Text('제목영역입니다'),
Icon(Icons.star),
Text('41')
]
)

이렇게 하면 제목 영역 내에서 수평으로 레이아웃을 구성할 수 있다.

이번에는 주제목과 부제목을 나타내는 부분을 Column으로 감싸보자.

Row(
children: <Widget>[
Column(
children: <Widget>[
Text('주제목입니다'),
Text('부제목입니다')
]
),
Icon(Icons.star),
Text('41')
]
)

이렇게 하면 주제목과 부제목을 아래 코드처럼 배치할 수 있게 된다.

지금은 Style을 별도로 주지 않았기 때문에 산출물이 굉장히 투박한데, 이 글에서는 Row와 Column 활용법에만 초점을 맞추고있기 때문에 Style을 주는 법은 별도의 글을 작성하도록 하겠다.

제목 영역에서 주제목과 부제목 영역이 잔여 공간 전체를 차지하게 하려면 Expanded 위젯으로 확장하면 된다. (사실 글 작성하다가 중간에 깨달아서 버튼 영역 구현이 이미 끝나있는데 당황하지 말고 그대로 진행하면 된다.)

Row(
children: <Widget>[
Expanded(
child: Column(
children: <Widget>[
Text('주제목입니다'),
Text('부제목입니다')
]
)
),
Icon(Icons.star),
Text('41')
]
)

이렇게 하면 주제목과 부제목이 공간 전체를 차지한다.

여기서 개별 위젯들이 중앙정렬되고 있는데, Column 위젯의 crossAxisAlignment 의 기본값이 center 로 들어가있어서 그렇다. 이 부분도 수정해주자.

Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('주제목입니다'),
Text('부제목입니다')
]
)

이렇게 하면 교차 축 기준의 시작 지점 정렬이기 때문에 좌측 정렬이 이루어진다.

버튼 영역 구현하기

이번에는 버튼 영역을 구현해보자. 버튼 영역은 크게 3개의 아이콘과 3개의 텍스트로 구성되어 있다.

우선 수평으로 나열해야하는 것부터 먼저 생각해보자. 이 경우에는 애초에 아이콘과 텍스트가 하나의 Column으로 구성되어있기 때문에 Column부터 시작해도 되고, Row부터 시작해도 된다.

나는 우선 Row로 3개의 텍스트를 보여주는 것부터 시작하도록 하겠다. 위 코드에서 Text('버튼 영역') 부분의 코드를 수정하면 된다.

Row(
children: <Widget>[
Text('전화'),
Text('경로'),
Text('공유')
],
)

이렇게 하면 3개의 Text 위젯이 수평으로 나열되서 보여진다.

이제 아이콘을 추가해보도록 하자. 아이콘을 추가하기 위해서는 개별 Text 위젯과 Icon 위젯을 Column 위젯으로 감싸줘야할 필요가 있다. 만약 한번에 하기 어려우면 일단 모든 아이템에 Column 위젯을 추가하고 Text 위젯을 2개 두는 형태로 구현해도 괜찮다.

하지만 여기서는 한번에 하도록 하겠다.

Row(
children: <Widget>[
Column(
children: <Widget>[
Icon(Icons.call),
Text('전화')
],
),
Column(
children: <Widget>[
Icon(Icons.directions),
Text('경로')
],
),
Column(
children: <Widget>[
Icon(Icons.share),
Text('공유')
],
)
],
),

코드로 보면 어려워보이는데 Column 위젯을 추가하고 Icon 위젯과 Text 위젯을 각각 자식으로 넣은 코드다.

너무 왼쪽으로만 쏠려있으니, 여백을 동일하게 정렬시키는 것이 좋아보인다. mainAxisAlignment 속성을 이용해서 정렬시키도록 하자.

Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Column(
children: <Widget>[
Icon(Icons.call),
Text('전화')
],
),
Column(
children: <Widget>[
Icon(Icons.directions),
Text('경로')
],
),
Column(
children: <Widget>[
Icon(Icons.share),
Text('공유')
],
)
],
),

이렇게 해서 Row와 Column이 복합적으로 이루어진 레이아웃을 구성해보았다. 전체 예제 코드는 아래 경로에서 확인 가능하다.

마무리

원래는 조금 더 짧게 마무리하려고 했는데 글을 작성하다보니 내용이 많아져서 생각보다 더 길게 작성된 거 같다. 다음 글에서는 이번에 구현한 레이아웃에 Style을 씌우고, 이미지를 가져오는 등의 작업을 추가로 진행해보는 작업을 진행해보도록 하겠다.

--

--