파트 1 — Stateless 위젯을 사용하여 도그 앱 만들기
빠르게 진행하기 위해 여기서는 기본적인 앱으로 시작하겠습니다. 이 앱은 Scaffold 위젯, AppBar 위젯, 필자의 반려견인 록키(누렁이 래브라도)에 대한 정보를 표시하는 두 개의 Text 위젯을 포함합니다.
위
젯은 Flutter 앱의 기본 빌딩 블록입니다. 각 위젯은 사용자 인터페이스의 어느 한 측면의 불변적 선언으로, 많은 작업을 맡을 수 있습니다.
예를 들면 다음과 같은 위젯이 있습니다.
- 구조적 위젯 — 예: 버튼 또는 메뉴
- 글꼴이나 색 구성표를 전파하는 스타일 위젯
- 레이아웃 관련 위젯 — 예: 여백
- 기타 등등
기존 위젯에서 새 위젯을 작성할 수도 있으므로, 끝없이 조합할 수 있습니다. 그게 어떤 의미인지 보여드리겠습니다.
반려견 이름에 어떤 색을 나타내는 의미를 숨기고 싶다고 해봅시다.
Text 위젯을 DecoratedBox로 래핑하면 됩니다.
import 'package:flutter/material.dart';
void main() {
runApp(new DogApp());
}
class DogApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My Dog App',
home: Scaffold(
appBar: AppBar(
title: Text('Yellow Lab'),
),
body: Center(
child: DecoratedBox( // here is where I added my DecoratedBox
decoration: BoxDecoration(color: Colors.lightBlueAccent),
child: Text('Rocky'),
),
),
),
);
}
}
그러면 Text 위젯에 배경색이 생깁니다.
텍스트 주변에 여백을 두고 싶을 수도 있을 것입니다.
그러면 여백 위젯을 추가하면 됩니다. 록키(Rocky)라는 이름 주변에 논리 픽셀을 8로 지정하여 여백을 주겠습니다.
import 'package:flutter/material.dart';
void main() {
runApp(new DogApp());
}
class DogApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My Dog App',
home: Scaffold(
appBar: AppBar(
title: Text('Yellow Lab'),
),
body: Center(
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.lightBlueAccent),
child: Padding(
padding: const EdgeInsets.all(8.0)
child: Text('Rocky'),
),
),
),
),
);
}
이제 여백이 생겼습니다.
이처럼 위젯을 합치는 프로세스를 '합성'이라고 합니다. 필자는 지금 간단한 위젯을 조합하여 인터페이스를 합성하는 중인데, 각각의 위젯이 한 가지 특정한 작업을 처리합니다. 즉, Padding은 여백을 주고 DecoratedBox는 상자를 꾸미는 등의 방식으로 구성됩니다.
자, 이제 필자가 정말 좋아하는 누렁이 래브라도 한 쌍을 새로 맞이하러 동물보호소로 간다고 해봅시다. Center 위젯 내부에 Column 위젯을 추가하고 새로 만난 반려견의 이름을 추가할 수 있습니다.
import 'package:flutter/material.dart';
void main() {
runApp(new DogApp());
}
class DogApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My Dog App',
home: Scaffold(
appBar: AppBar(
title: Text('Yellow Lab'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DecoratedBox(
decoration: BoxDecoration(color: Colors.lightBlueAccent),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text('Rocky'),
),
),
DecoratedBox(
decoration: BoxDecoration(color: Colors.lightBlueAccent),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text('Spot'),
),
),
DecoratedBox(
decoration: BoxDecoration(color: Colors.lightBlueAccent),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text('Fido'),
),
),
],
),
),
),
);
}
}
SizedBox라는 위젯을 사용하여 이름과 이름 사이에 빈 공간을 추가하여 다음과 같은 결과를 얻습니다.
하지만 이 세 개의 이름 상자에서 반복 코드( 상용구라고도 함)를 많이 사용했다는 걸 알 수 있습니다. 이름만 하나 고르면 나머지 세부 사항을 알아서 처리해주는 나만의 위젯을 만들 수 있다면 정말 멋지지 않을까요?
네, 그렇게 할 수 있습니다.
StatelessWidget을 만들고 DogName이라 부르도록 하겠습니다. Stateless 위젯은 하위 요소로 구성되고(그래서 build() 메서드를 포함함) 추적할 필요가 있는 가변적 상태는 전혀 포함하지 않는 위젯입니다. 가변적 상태란 시간이 흐르면서 변하는 속성을 의미합니다. 사용자가 업데이트하는 문자열이나 도착/출발 표시를 업데이트하는 데이터 스트림을 포함하는 텍스트 상자를 예로 들 수 있습니다.
class DogName extends StatelessWidget {
@override
Widget build(BuildContext context) {
}
}
이 위젯에는 그런 속성이 전혀 없습니다. 이 위젯에는 변하지 않을 이름을 나타내는 문자열만 필요하므로, StatelessWidget이 제격입니다. 이 문자열을 최종 상태로 만들 수도 있습니다.
class DogName extends StatelessWidget {
final String name;
@override
Widget build(BuildContext context) {
}
}
생성자를 통해 문자열을 지정할 수 있으며, 그 모든 속성이 최종 속성이므로 이를 상수 생성자로 마크할 수 있습니다.
class DogName extends StatelessWidget {
final String name;
const DogName(this.name);
@override
Widget build(BuildContext context) {
}
}
아제는 같은 위젯을 사용하여 build 메서드를 정의하기만 하면 되며, 오직 지금 Text 위젯이 위젯의 이름 속성에서 가져온 문자열을 표시할 뿐입니다.
class DogName extends StatelessWidget {
final String name;
const DogName(this.name);
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(color: Colors.lightBlueAccent),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(name),
),
);
}
}
이 위젯을 사용하여 원래 코드를 단순화하겠습니다.
import 'package:flutter/material.dart';
void main() {
runApp(new DogApp());
}
class DogApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My Dog App',
home: Scaffold(
appBar: AppBar(
title: Text('Yellow Lab'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DogName('Rocky'),
SizedBox(height: 8.0),
DogName('Spot'),
SizedBox(height: 8.0),
DogName('Fido'),
],
),
),
),
);
}
}
보시다시피, 이렇게 하면 UI는 동일하지만 StatelessWidget과 Flutter의 합성 기능을 사용한 덕분에 코드는 더 간결해집니다.
파트 2 — 위젯 트리와 요소 트리
StatelessWidget을 사용한 UI 작성이 어떤 식으로 이루어지는지 간단한 예를 보셨습니다. 그런데 이때 '이런 build 메서드의 작동 방식은 알겠지만, 이 메서드는 언제 호출되는 걸까?'라는 의문이 드실지 모르겠습니다. 자, 그렇다면 DogName 위젯 단 하나로 시작해봅시다.
우리는 Flutter로 빌드한 앱을 위젯으로 구성된 트리라고 생각하는 경향이 있는데, 그게 나쁜 건 아닙니다. 하지만 서두에서 언급한 바와 같이, 위젯은 앱 UI의 각 부분을 위한 구성일 뿐입니다. 위젯이 바로 UI의 청사진인 셈입니다.
그렇다면 이러한 구성은 무엇을 위한 것일까요? 그건 바로 요소를 위한 것입니다. 요소는 화면상에서 실제로 만들어지고 마운트되는 위젯입니다. 요소 트리는 어떤 특정 순간에 기기에 실제로 표시되는 내용을 나타냅니다.
각각의 위젯 클래스에는 대응하는 요소 클래스와 인스턴스 생성을 위한 메서드가 둘 다 있습니다.
예를 들어 StatelessWidget은 StatelessElement를 생성합니다.
위젯이 트리에 마운트되면 Flutter가 createElement() 메서드를 호출합니다. Flutter는 위젯에 요소를 요구하고, 요소를 생성한 위젯으로 다시 돌아가는 참조 표시와 함께 요소 트리 위에 그 요소를 팝업으로 나타냅니다.
그때 StatefulElement는 '하위 요소가 있는가?'라고 묻고 Widget의 build() 메서드를 호출합니다.
이 앱에는 하위 요소가 여러 개 있습니다. 그러면 이런 위젯이 자체의 대응 요소를 생성합니다.
이렇게 생성된 요소 역시 요소 트리에 마운트됩니다.
그래서 이제는 앱에 두 개의 트리가 있습니다. 하나는 화면에 실제로 표시되는 내용(요소)을 나타내고, 다른 하나는 이러한 요소가 생성된 청사진(위젯)을 보유합니다.
이때 예컨대 요소를 빌드하고 생성하는 프로세스를 시작하는 것은 무엇이고 이 전체 작업을 시작하는 것은 무엇인지 등의 사항이 궁금할 수도 있겠습니다. 그래서 아마 처음에는 미처 알아차리지 못했을 사항을 보여드리겠습니다.
전체 앱을 나타내는 DogApp 클래스는 그 자체가 바로 StatelessWidget입니다.
import 'package:flutter/material.dart';
void main() {
runApp(new DogApp());
}
class DogApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My Dog App',
home: Scaffold(
appBar: AppBar(
title: Text('Yellow Lab'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DogName('Rocky'),
SizedBox(height: 8.0),
DogName('Spot'),
SizedBox(height: 8.0),
DogName('Fido'),
],
),
),
),
);
}
}
Widget은 거의 모든 일을 할 수 있다고 말했던 것을 기억하십니까? 앱의 진입점인 main()을 살펴보면 main()이 runApp() 메서드를 호출하며 그것이 바로 시작점이라는 사실을 알 수 있습니다. runApp() 메서드는 위젯을 택해 화면 크기와 일치하는 높이와 너비 제약 조건을 가진 앱의 루트 요소로 마운트합니다.
import 'package:flutter/material.dart';
void main() {
runApp(new DogApp());
}
class DogApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My Dog App',
home: Scaffold(
appBar: AppBar(
title: Text('Yellow Lab'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DogName('Rocky'),
SizedBox(height: 8.0),
DogName('Spot'),
SizedBox(height: 8.0),
DogName('Fido'),
],
),
),
),
);
}
}
그런 다음, Flutter가 위젯 트리에 있는 모든 build() 메서드를 하나씩 진행하면서 모든 것이 빌드되고 화면상에 마운트되고 배치되어 렌더링 준비가 완료될 때까지 위젯을 생성하고 이를 사용해 요소를 만듭니다.
그게 바로 Flutter가 사랑스러운 누렁이 래브라도의 이름이 표시된 작은 텍스트 상자 세 개를 표시하는 방법입니다.
StatelessWidget으로 작성하여 인터페이스를 빌드하는 방법을 소개해 드렸습니다. 필자가 언급하지 않은 점 한 가지는 데이터 변경 시 인터페이스를 업데이트하거나 다시 빌드하는 방법입니다. 그건 StatelessWidget이 데이터를 변경하지 않기 때문입니다. StatelessWidget은 Stateless 위젯이므로 시간의 경과에 따라 데이터를 추적하거나 스스로 다시 빌드하는 작업을 트리거할 수 없습니다.
다행히도, Flutter에는 StatefulWidget도 있으므로 이 시리즈 게시물의 다음 편에서 그에 관한 내용을 다루도록 하겠습니다.
Flutter와 Flutter의 수많은 위젯에 관해 더 자세히 알아보시려면 flutter.io를 살펴보세요.