Search
Duplicate

CH.3 플러터의 세계로

목차

CHAPTER POINT

카운터 앱으로 플러터 기초 다지기
플러터 위젯 클래스
BuildContext, 위젯 트리, 요소 트리
플러터 개발 환경과 팁

1.1 카운터 앱 소개

이제 플러터 프로젝트를 시작해보자.
Android Studio나 기타 IDE가 있다면 해당 툴을 이용해도 좋다. 여기서는 terminal 을 이용해 프로젝트를 생성한 뒤 VSCode에서 작업을 하도록 한다.
만약 아직 플러터 설치가 안되었다면 아래 링크를 참고해 자신의 os에 맞는 flutter를 설치하자.

1. 프로젝트 생성

terminal창을 키고 적절한 위치에 플러터를 만든다.
$ flutter create counter_app $ cd counter_app && fluter pub get
Bash
복사
생성이 완료되었다면 IDE에서 생성한 플러터 프로젝트를 연 다음 자신의 기기 혹은 시뮬레이터를 이용해 프로젝트를 구동해본다.
첫 필러터 프로젝트 구동 화면
시뮬레이터의 + 버튼을 누르면 동작하는것도 확인할 수 있다. 플러터의 counter_app은 언어로 치면 hello world와 같다고 보면 된다. 이제 첫 단추를 꿰었으니 프로젝트의 구조부터 알아보자.

1.1.1 플러터 프로젝트 구조

위 이미지에서 프로젝트의 디렉토리 구조를 보면 무엇인가 많이 생성되어 있다. 바로 모든 디렉토리에 대해 알 필요는 없지만 그래도 대략적으로나마 알아보자.
C:\workspace\flutterSpace\counter_app ├── android → 컴파일된 안드로이드 앱 ├── ios → IOS 앱 ├── lib → 플러터의 실질 코드가 들어가는 곳 | └── main.dart → 프로젝트의 진입점. 반드시 존재해야하며 maiin() 메서드를 포함해야 한다. ├── pubspec.lock → 편집하면 안 되는 잠금(lock)파일을 생성한다. pubspec.yaml 업데이트시 갱신 ├── pubspec.yaml → 모든 다트 프로젝트에 필요하며 의존성과 메타데이터를 관리한다. ├── README.md └── test → 테스트 공간이지만 보통 0.1%도 사용하지 않는다.
Bash
복사

1.1.2 플러터 앱 해부

기본 생성된 카운터 앱의 main.dart 코드를 분석해보자.
import 'package:flutter/material.dart';
Dart
복사
→ material 라이브러리를 임포트 했는데 이는 구글의 material 디자인 시스템 기본 위젯을 제공한다.
flutter/cupertino.dart 라는 IOS 스타일 컴포넌트도 존재한다.

앱 진입점

void main() => runApp(MyApp());
Dart
복사
플러터 앱도 main 함수가 진입점이다.
플러터에서는 runApp이라는 메서드로 최상위 위젯을 감싼다.

1.1.3 모든 구조는 위젯이다

위젯은 뷰를 묘사하는 다트 클래스로 실제 앱화면에 나오는 버튼, 텍스트, 레이아웃들은 모두 위젯이다. 이러한 위젯은 웹이나 다른 프레임워크에서는 컴포넌트 라 부르는데 이와 유사하다. 위젯또한 특정 UI를 정의하는 클래스로 여러 위젯을 사용해 이를 다양한 방법으로 조합해 더 큰 위젯으로 만들며 앱을 만든다.
모든것은 위젯이라 했는데, 어느정도를 말하는걸까? 플러터에서는 Row 위젯으로 레이아웃 정보를 정의하고 Button이나 TextField같은 위젯으로 구조적 요소를 정의하고 theme 위젯으로 앱의 색이나 폰트를 정의한다. 심지어 한 위젯의 스타일조차 다른 위잣으로 설정한다.
앱의 경로도 위젯인데 App이라는 객체가 따로 있는게 아닌 build 메서드로 다른 위젯을 반환하는 MyApp이라는 커스텀 위젯을 정의한다.
그렇기에 플러터 라이브러리에서는 다양한 위젯들을 제공한다.
레이아웃: Row, Column, Scaffold, Stack
스타일: TextStyle, Color, Padding
위치와 정렬: Center, Padding
구조: Button, Toast, MenuDrawer
애니메이션: FadeInPhoto, Transform

1.1.4 build 메서드

모든 위젯은 다른 위젯을 반환하는 build 메서드를 반드시 포함해야 한다.
class MyApp extends StatelessWidget { Widget build(BuildContext context) { // ---(1) return MaterialApp(//---(2) title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: MyHomePage(title: 'Flutter Demo Home Page'), // ---(3) ); } }
Dart
복사
1.
모든 위젯은 다른 위젯을 반환하는 build 메서드를 포함한다.
2.
앱의 모든 위젯에서 Material 디자인 기능을 이용할 수 있도록 MaterialApp(내장위젯)으로 앱을 감싼다.
3.
위젯은 인수를 받는 생성자를 포함하는 클래스다. MaterialApp은 title, theme, home을 선택형 파라미터로 , String, ThemeData, Widget을 지정 파라미터로 받는다.

1.1.5 new, const 생성자

플러터는 한 위젯의 여러 인스턴스를 만드는데, 여기서 많은 내장 위젯은 일반 생성자와 const 생성자를 모두 제공한다. 변경할 수 없는 위젯 인스턴스는 성능이 좋기에 가능하면 const를 사용하는게 좋다. 이전 챕터에서 이미 다뤘지만 new나 const 키워드를 둘 다 사용하지 않으면 프레임워크에서 타입추론을 통해 가능한 const로 위젯을 추론하기에 크게 신경쓰지 않아도 된다.
Widget build(BuildContext context) { return Button( // new 생략 child: Text("Submit"), ); }
Dart
복사
Widget build(BuildContext context) { return new Button( //new 사용 child: Text("Submit"), ); }
Dart
복사
플러터가 알아서 처리하기 때문에 이처럼 new 키워드를 붙히고 안붙히고 구분 할 필요는 떨어진다.

1.1.6 핫 리로드

다트는 AOT 컴파일러이면서 JIT 컴파일러인데, 컴퓨터로 앱을 개발 할 때는 JIT을 사용한다.
JIT(just in time): 실시간으로 코드를 컴파일하고 실행한다는 의미
앱을 상용배포할 때는 AOT컴파일러를 사용하지만, 개발시점에서는 코드를 다시 컴파일하는 경우가 잦기 때문에, JIT의 효용성은 높아진다.
만들어 둔 카운터 앱을 통해 핫 리로드를 바로 확인해본다.
... //home: MyHomePage(title: 'Flutter Demo Home Page'), home: MyHomePage(title: 'Hot Reload Demo'), // ...
Dart
복사
main.dart 29라인의 title을 Hot Reload Demo 로 변경 후 저장을 해보았다.
IDE마다 핫 리로드 단축키는 다르다.
IntelliJ, VSCode, Android Studio
window : Ctrl + S
mac : Cmd + S
터미널
플러터 실행 후 r 을 입력해 핫 리로드를 실행한다.
시뮬레이터 원본 화면
시뮬레이터 핫 리로드 이후 화면
그밖에도 색이나 모양등을 바꿔보며 테스트해보자.

1.2 위젯 트리와 형식, State 객체

플러터 UI를 개발한다는 것은 여러 위젯들을 조합해 위젯 트리 를 만들겠다는 의미이다.
트리의 각 노드는 위젯이고 이 노드가 모여 트리가 된다.
즉, build 메서드에서 위젯을 추가할 때마다 트리에 새 노드를 추가하는 것이고 각 노드는 부모 자식관계로 연결된다.
카운터 앱의 위젯 트리
위젯은 자신의 자식이 또 다른 위젯(들)을 포함한다고 설명하는 방식으로 트리를 만든다.
return Container( child: Padding( // Container 위젯은 다른 위젯을 받는 child 프로퍼티를 받는다. padding: EdgeInsets.all(8.0), child: Text("Padded Text")//padding 위젯도 다른 위젯을 받는 child 프로퍼티를 갖는다. ), );
Dart
복사
위젯 트리에서 Container는 Padding의 부모이고 Padding은 Text 위젯의 부모다.
모든 위젯이 child 프로퍼티를 갖는 것은 아니고 child 프로퍼티 외에 children, builder 와 같은 프로퍼티를 갖는 위젯도 있다.

1.2.1 상태를 갖지 않는 위젯(StatelessWidget)

위젯 생명주기 동안 내부 상태를 갖지 않는다.
설정이나 자신이 표시하는 데이터를 신경쓰지 않는다.
상태를 바꿀 수 없는 위젯이다.
커스텀 버튼 위젯 코드를 보며 알아보자.
class SubmitButton extends StatelessWidget { Widget build(context) { return Button( child: Text('Submit'), ); } }
Dart
복사
이 코드는 Submit이라는 텍스트를 가진 버튼 위젯이다. 그렇다면, 이 버튼의 텍스트를 Update로 변경하려면 어떻게 해야할까?
class SubmitButton extends StatelessWidget { final String buttonText; // ---(1) SubmitButton(this.buttonText); // ---(2) Widget build(context) { return Button( child: Text(buttonText),//---(3) ); } }
Dart
복사
1.
위젯으로 전달한 모든 데이터를 설정으로 활용한다.
2.
버튼의 텍스트로 설정할 데이터가 필요하다.
3.
문자열 보다는 변수를 전달하면. 다른 변수를 전달하면 플러터가 이를 감지해 버튼을 다시 그린다.
이 위젯은 자기자신을 직접 갱신하는 로직은 존재하지 않는다.
그저 부모 위젯의 설정에 따라 정해진 문자열(this.buttonText)을 보여줄 뿐이다. 게다가 상태가 있는 위젯과는 다르게 자기 자신을 언제, 어떻게 리빌드 해야하는지에 대해서도 모른다.
상태가 없는 위젯도 메서드를 가질 수 있지만 핵심은 플러터가 위젯 트리에서 상태 없는 위젯을 제거하면 어떠한 흔적 없이 깔끔하게 사라져야 한다는 점이다.

1.2.2 상태를 갖는 위젯(StatefulWidget)

내부 상태를 가지며 이를 관리한다.
상태 객체를 갖는다.
위 위젯트리 이미지를 보면 MyHomePage 트리 노드는 MyHomePageState 트리 노드와 연결되어 있다.
모든 StatefulWidget 인스턴스는 실제로도 클래스 두 개를 갖는다.
class MyHomPage extends StatefulWidget { // --- (1) _MyHomePageState createState() => _MyHomePageState(); // --- (2) } class _MyHomePageState extends State<MyHomePage> { // --- (3) Widget build(BuildContext context) { // --- (4) //.. } }
Dart
복사
1.
StatefulWidget 을 상속받는다.
2.
모든 StatefulWidgetState 객체를 반환하는 createState메서드를 반드시 정의해야 한다.
3.
상태 클래스는 플러터의 State클래스를 상속받는다.
4.
StatefulWidget의 필수 build 메서드이다.
사실 StatefulWidget 클래스는 build 메서드를 포함하지 않는다. 하지만 모든 StatefulWidget은 상태 객체(State)를 포함하는데, 모든 상태 객체는 build 메서드를 포함한다.
언더바(_)로 시작하는 다트의 비공개 값 위 예제에서 클래스 이름이 언더바(_)로 시작했다. 이는 비공개(private) 클래스를 의미한다. 모든 행을 비공개로 설정할 수 있다. 클래스처럼 최상위 수준의 값을 현재 파일에서만 이용하려면 비공개로 설정한다.

1.2.3 setState

이 메서드는 객체의 상태와 관련이 있는데, 내무 파람미터로 전달 된 콜백메서드를 실행 후 설정이 바뀌는 모든 위젯을 다시 그려준다.
기본 앱 화면
우측하단 버튼 클릭 후 앱 화면
위 앱 예제에서 MyHomePageState 위젯의 모습이며 해당 위젯은 상태 객체로 모든 자식은 상태에 의존하며 상호작용한다. 버튼을 클릭하면 지정된 메서드(_incrementCounter())가 호출되는데 이 로직을 보면
void _incrementCounter() { setState(() { ... _counter++; }); }
Java
복사
위와같이 setState 메서드를 호출해 내부 콜백 메서드로 _counter 상태를 후위증가시키는 로직만 있다.
그럼 _MyHomePageState.build 메서드를 실행하며 설정이 바뀐 모든 위젯을 다시 그린다. (아래 그림 참고)
이처럼 setState는 콜백 메서드를 실행 후 설정이 바뀐 모든 위젯을 다시 그리는 역할이 전부이며, 비동기 코드를 실행할 수 없다는 점을 주의하자. 그렇기에 setState가 실행되기 전 비동기 코드를 완료해야 한다.
setState 흐름
1.
버튼을 누를 때 마다 이 함수(increaseCounter)를 실행하라
2.
버튼이 눌렸음을 감지
3.
MyApp은 새로운 카운터 정보로 자식 빌드

1.2.4 initState

플러터가 위젯을 그리기 전 필요한 모든 초기화를 State.initState 메서드에서 수행한다.
예를 들어, 위젯의 build 메서드가 실행되면 화면에 위젯을 그리기 전 String을 원하는 형태로 포맷한다.
class FirstNameTextState extends State<FirstNameText> { String name; FirstNameTextState(this.name); @override initState(){ super.initState(); name = name.toUpperCase(); } Widget build(BuildContext context) { return Text(name); } }
Java
복사

1.3 BuildContext

이전에 ThemeData에서 테마를 바꾸거나 타이틀을 변경해서 앱을 변경및 갱신해봤었다.
전체 위젯 트리를 추적하는 것과 관련이 이는데 특히 위젯의 위치와 관련이 있다. 이를 이해하기 위해서는 BuildContext 개념을 알아야 한다.
위젯의 모든 build 메서드는 위젯트리에서 위젯의 위치를 참조하는 BuildContext 하나를 인수로 받는다. build는 프레임워크가 호출하므로 BuildContext를 개발자가 관리할 필요는 없지만 자주 이를 사용하게 된다.
모든 위젯은 자신만의 빌드 콘텍스트르 ㄹ가지며 한 위젯이 다양한 테마를 반환하게 만들어 한 트리에 여러 테마를 적용할 수 있다. 카운터 앱의 테마나 다른 of 메서드는 트리에서 형식이 같은 가장 가까운 부모를 반환한다.
빌드 콘텍스트는 특정 위젯을 정확하게 어떻게 표현할지 결정한다.
예를 들어 플러터는 빌드콘텍스트를 통해 모달(modal)과 라우트(route)를 표시한다. 모달을 만드는 메서드에 BuildContext를 전달한다. 빌드컨텍스트는 위젯 자체의 정보가 아니라 위젯 트리에서 위젯의 위치 정보를 포함한다.

1.4 카운터 앱 개선하기

기본적으로 자동생성된 플러터 카운터 앱에 몇가지 위젯을 알아보고 적용시켜 개선해보도록 하자.
카운터 앱에서 현재 사용하는 위젯을 살펴 보면 Column, Text, Icon, Scaffold, AppBar 위젯을 사용한다.
여기에 다른 위젯을 카운터 앱에 추가해보자.

1.4.1 RaisedButton

카운터를 감소시키는 버튼을 추가해보자. 그러기 위해 아래와 같은 기능이 필요하다.
클릭할 버튼
_counter를 1 감소시키는 함수
Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ ... RaisedButton( child: Text("Decrement Counter"), onPressed: _decrementCounter, ), ], ), ), ... ); } void _decrementCounter() { setState(() => _counter--); }
Dart
복사
이처럼 RaisedButton을 추가한 뒤 onPressed 이벤트시 _decrementCounter 메서드를 호출해 _counter 상태값이 감소하도록 했다.
버튼 클릭전 앱 화면
버튼 클릭 후 앱 화면
감소버튼이 정상적으로 생성된 것을 확인할 수 있고 동작이 제대로 되는것도 확인했다.
이 외에도 onTapped, onHorizontalDrag 등 다양한 상호작용에 대한 콜백이 존재한다.

1.5 상속보단 조합

상속이 is a 관계라면 조합은 has a 관계라 부를 수 있다.
(ex: 갤럭시는 핸드폰이며, 갤럭시는 삼성페이를 갖는다.)
보통 객체의 실체를 설계할 때 상속을 선택하고, 객체의 기능을 설계할 때 조합을 선택한다.

1.5.1 조합이란

is a 관계에서 부모의 기능들을 재사용하는것은 큰 문제가 되지 않는다. 하지만, is not 관계임에도 해당 기능을 재사용하고 싶으면 어떻게 해야할까? 상속으로는 이런 이슈에 대한 명확한 해결책을 줄 수 없다. 이럴 경우 조합을 사용해 이슈를 해결할 수 있다.
class Phone { void call(); } class Galaxy extends Phone{ ... } class IPhone extends Phone { ... }
Dart
복사
갤럭시와 아이폰이 Phone이라는 부모클래스를 상속해서 전화기능을 재사용할 수 있기에 둘 모두 전화를 할 수 있다. 그런데 만약 갤럭시워치가 있다고 할 때 이 시계가 전화기능을 사용하기위해서 Phone을 상속할 수는 없다.
그렇다고 해당 기능을 따로 정의하는것도 좋지 않다. 그렇다면 조합을 통해 이를 해결해본다.
class Phone { void call(); } class Galaxy { PhoneInstance.call(); } class IPhone { PhoneInstance.call(); } class GalaxyWatch { PhoneInstance.call(); ... }
Dart
복사
위와같이 상속이 아닌 조합을 사용한다면 갤럭시워치는 폰클래스를 상속받지 않으면서도 전화기능을 사용할 수 있다.

1.5.2 플러터 조합 예제

카운터앱의 추가했던 RasiedButton 은 조합을 이용한 코드다.
... RaisedButton( child: Text("Decrement Counter"), onPressed: _decrementCounter, ), ...
Dart
복사
이 버튼은 텍스트를 설저하는 다른 위젯(Text)에 버튼을 전달해 텍스트를 표시한다.
이처럼 플러터는 항상 상속보다는 조합으로 재사용할 수 있고 결합되지 않은 위젯을 만든다.
그렇다면 조합을 이용해 버튼 위젯을 좀 더 응용해보도록 하자.
class PanicButton extends StatelessWidget { final Widget display; final VoidCallback onPressed; PanicButton({this.display, this.onPressed}); Widget build(BuildContext context) { return RaisedButton( color: Colors.red, child: display, onPressed: onPressed, ); } }
Dart
복사
상태가 없는(Stateless) 패닉 버튼 위젯을 만들었다. 이 버튼은 조합을 이용했기에 child로 받은 display를 포함하기에 만약 텍스트가 아닌 아이콘을 보여주고싶다면 Text대신 Icon을 전달하면 된다.

1.6 플러터 레이아웃

플러터 렌더링 엔진은 한 가지 정해진 레이아웃 시스템을 사용하지 않는다.
플러터는 개발자가 플렉스 레이아웃(flex layout), 데카르트 그래프, 너비 인, 높이 아웃(width in, height out)(WIHO) 같은 특정 시스템을 사용하도록 강요하지 않는다. 상황에 따라 여러 시스템을 조합하여 필요한 레이아웃을 만들 수 있다.
이번 장에서는 플러터에서 자주 사용 레이아웃 위젯과 제약조건에 대해 살펴보도록 하자.

1.6.1 Row와 Column

플러터에서는 웹의 FlexBox와 비슷한 플렉시블 레이아웃이 가장 흔하다. Column, Row 위젯으로 플렉시블 레이아웃을 사용한다. 카운터앱에서도 body부분을 잘 살펴보면 Column 위젯을 사용하는 것을 알 수 있다.
Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( // ---- (1) mainAxisAlignment: MainAxisAlignment.center, // ---- (2) children: <Widget>[ // ---- (3) Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headline4, ), PanicButton( display: Text("decrement"), onPressed: _decrementCounter, ) ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: Icon(Icons.add), ), ); } }
Dart
복사
카운터앱의 _MyHomPageState 부분 코드
1.
Column 위젯은 자식들을 열로 배치한다.
2.
CSS의 FlexBox와 비슷한 정렬 프로퍼티를 제공한다. 정렬 프로퍼티는 Column의 자식들을 서로 어떻게 배치할지 결정한다('서로'가 핵심이다.)
3.
어떤 위젯(특히 레이아웃 위젯)은 한 개의 자식이 아니라 여러 위젯 자식을 갖는다.
Row위젯은 Column 위젯과 비슷하지만 수평축으로 자식을 배치하는 점이 다르다. 즉, Row 위젯은 모든 자식을 왼쪽에서 오른쪽으로 나란히 배치한다. Row위젯을 이용해 감소버튼을 수평으로 위치시켜보자.
Widget build(BuildContext context) { return Scaffold( ... body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ ... Row( //Row WIdget 추가 children: <Widget>[ RaisedButton( color: Colors.red, child: Text( "Decrement", style: TextStyle(color: Colors.white), ), onPressed: _decrementCounter, ) ], ) ], ), ), ... ); }
Dart
복사
RaisedButton을 감싸는 Row 위젯을 Column안에 넣어줬다. 이제 핫 리로드로 실행결과를 확인하면 Decrement 버튼이 왼쪽으로 정렬된 것을 확인할 수 있다. 플렉시블 위젯은 메인축(main axis)의 남은 공간을 모두 차지하는 특징이 있어서 Row 위젯은 수평으로 남은 공간 즉, 부모(Column 위젯)가 허용한 전체 화면을 차지한다.

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

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

1.6.3 RenderObject

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

1.6.4 RenderObject와 제약조건

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

1.6.5 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("..."), ) ] ) ] )
Dart
복사
위 코드에서 Column은 자식이 원하는 크기만큼 갖도록 하며, 자식에서는 Expanded로 부모가 허락하는 만큼 최대한이라는 제약조건이 두개가 겹치며 오류를 발생시킨다.
위젯은 위처럼 제약 조건을 트리 아래로 전달하기에 중복된 플렉스 상자를 어느정도 분리해야 한다. 개발을 하다보면 Column안에 Row를 갖거나 반대의 경우와 같은 플렉시블 위젯을 중첩해서 사용하는 경우가 잦다.
우리는 플렉시블 위젯 동작을 최대한 이해해 이 문제를 쉽게 해결할 수 있도록 해야 한다.

1.6.6 여러 자식을 갖는 위젯

학습한 플렉시블 위젯의 제약 조건과 사용방법을 실제 코드에 적용해보며 위젯내에 여러 자식 위젯들을 추가해보자.
Row( children: <Widget>[ RaisedButton( color: Colors.red, child: Text( "Decrement", style: TextStyle(color: Colors.white), ), onPressed: _decrementCounter, ), RaisedButton( color: Colors.green, child: Text( "Increment", style: TextStyle(color: Colors.white), ), onPressed: _incrementCounter, ) ], )
Dart
복사
_MyHomePageState.build의 Row부분
기존의 빨간색 감소버튼(Decrement)에 추가로 숫자를 증가시키는 두 번째 버튼을 추가한다
각각 왼쪽, 오른쪽으로 정렬되어있는데, 너무 우측으로 치우쳐져 있기에 Row에 정렬 프로퍼티를 추가해준다
Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: <Widget>[ ... ], ... )
Dart
복사
플렉시블 위젯의 mainAxisAlignment프로퍼티로 자식들의 공간을 다양하게 정렬할 수 있는데, 위에서는 spaceAround 정렬을 적용해주었다.
여기까지 여러 자식을 포함하는 Row, Column 위젯을 살펴봤는데, 이 위젯들은 실제로 실무에서 자주 쓰이는 위젯이기에 이해하실 필요가 있다.
플러터의 Alignment 스타일

1.6.7 아이콘과 FloatingActionButton

플러터는 기본적으로 머티리얼 디자인 아이콘을 내장하여 상수로 이를 제공한다. 그래서 잘 활용만 하면 외부 라이브러리를 따로 사용하거나 이미지를 업로드할 필요가 없다. 머티리얼 디자인에서 제공하는 아이콘은 북마크를 통해 확인하자.
자주 사용되는 머티리얼 디자인 아이콘
카운터 앱에서도 이 아이콘들을 숫자 증가하는 버튼에서 사용한다.
floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: Icon(Icons.add), ),
Dart
복사

카운터앱의 FAB (FloatingActionButton)변경해보기

기존의 FAB 버튼을 누르면 카운터를 증가시키지 않고 재설정하도록 동작을 바꿔보자.
1.
resetCounter 메서드를 만들어서 onPressed 인수로 전달
2.
FAB 아이콘 변경
Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( ... ), floatingActionButton: FloatingActionButton( onPressed: _resetCounter,// onPressed 인수 변경 tooltip: 'Reset Counter', child: Icon(Icons.refresh), //아이콘 변경 ), ); } //resetCounter 메소드 구현 void _resetCounter() { setState(() => _counter = 0); }
Dart
복사
위처럼 FAB 버튼의 모양을 바꾸고 onPressed 인수를 기존의 카운터 증가가 아닌 카운터 초기화 메서드로 변경해줬다. 이제 버튼을 누를 경우 카운터 값이 초기화되는 것을 확인할 수 있다.

1.6.8 Image

Image위젯은 이미지 소스(로컬, 인터넷 등)에 따라 다양한 생성자를 제공한다.
인터넷: Image.network("URL")
로컬의 경우 Image.asset생성자를 이용하지만 우선 pubspec.yaml 파일에 이미지 위치를 선언해야 한다.
1.
pubspec에 이미지경로 설정
... flutter: uses-material-design: true assets: - images/
YAML
복사
앱에서 사용하는 모든 애셋(asset)을 스펙 파일의 assets 헤더에 추가한다.
default는 lib 경로를 기준으로 설정되나 따로 폴더를 만들어서 설정하고자 한다면(ex:images) 해당 폴더 경로를 같이 써주면 된다. 나같은 경우 images 폴더를 만들고 그 하위 모든 이미지를 사용하고자 했기 때문에 images/ 으로 작성해줬다.
스펙 파일을 바꾸었을때는 핫 리로드로는 적용디지 않고 핫 리스타트를 이용해 확인한다. 그럼 아예 껐다 키는 것보다 빠르게 확인할 수 있다.

1.6.9 Container 위젯

Container위젯은 html의 <div>태그와 비슷한데, 내부에 자식위젯을 1개 가질 수 있으며 각종 프로퍼티를 통해 여백, 색상, 크기등을 조절해서 자식 위젯을 꾸밀 수 있다. 이제 Container 위젯으로 이미지 위젯을 꾸며보자.
Container( margin: EdgeInsets.only(bottom: 100.0), padding: EdgeInsets.all(8.0), decoration: BoxDecoration( color: Colors.blue.withOpacity(0.25), borderRadius: BorderRadius.circular(4.0), ), child: Image.asset( 'images/flutter_logo.png', width: 100.0, ), ),
Dart
복사
기존의 Image.asset(...)을 Container 위젯으로 감싼 뒤 margin, padding, decoration 프로퍼티를 통해 크기와 색상을 변경해줬다.

1.7 요소 트리

플러터 SDK의 계층 추상화를 단순화한 그림
이제 플러터 내부에서 일어나는 일에 대해 알아보자.
개발자는 보통 위젯레이어 계층에서 대부분의 작업을 수행하는데, 그 아래 계층으로는 렌더링 계층과 dart:ui 라이브러리가 있다. dart:ui는 다트로 구현한 프레임워크 중 가장 낮은 수준의 기능으로 디바이스의 렌더링 엔진과 직접 소통할 수 있도록 API를 공개한다.
dart:ui는 canvas API로 화면에 직접 그리는 기능을 제공ㅎ며 hit testing을 이용해 사용자의 상호작용을 인지한다.
요소 트리가 개발자에게 중요한 이유 일반적으로 요소를 개발자가 직접 사용하고 다룰일은 없지만, 플러터 내부 동작을 이해해 특정 상황에서의 문제를 '쉽게' 해결할 수 있고 디버깅을 할 때 역시 도움이 된다.
고수준으로 추상화된 위젯으로 개발자가 작업을 하면 위젯과 dart:ui 저수준 디바이스 힛 테스팅이나 픽셀 수준의 계산을 처리한다.
요소 트리는 위젯 트리처럼 앱의 구조를 표현하며 위젯 트리에 모든 이젯은 요소를 포함하는 요소 트리를 갖는다.
실제로 트리에 존재하고 마운트되는 위젯이 바로 요소다.
플러터 앱을 실행할 때 디바이스에 실제로 표시되는 모든 것은 요소다.
렌더객체(RenderObject)는 고수준 코드와 저수준(dart:ui)을 연결하는 인터페이스다.
플러터 프레임워크는 세 가지 트리를 관리하며 모든 트리는 요소 트리를 통해 상호작용한다.

1.7.1 요소와 위젯

위젯이 요소를 만든다.
새 위젯을 만들면 프레임워크가 Widget.createElement(this)를 호출하는데 이 때 요소를 설정하며 요소는 자신을 만든 위젯을 참조(reference)하기 시작한다.
요소는 자체적으로 트리를 갖는데, 이 트리는 앱의 골격과 같다. 그 이유는 앱의 구조는 가지고 있지만 위젯이 제공하는 세부 사항(색상, 크기, 이벤트 등)은 가지고 있지 않기때문인데, 그렇기에 앱은 관련 위젯을 참좋여 세부 설정들을 파악한다.
요소는 다시 빌드되지 않고 오직 갱신된다.
위젯을 다시 빌드하거나 트리의 부모가 다른 위젯을 삽입하면 요소의 위젯 참조를 다시 만들지 않고 갱신한다.
애니메이션의 경우 매 프레임마다 build를 호출하는데, 매 프레임마다 위젯은 그대로지만 설정들이 조금씩 바뀐다. 이런상황에서 트리 자체의 구조는 바뀌지 않기에 요소는 자신을 다시 빌드할 필요가 없고 위젯 참조만 수정하기에 빌드가 계속 일어나도 성능이 유지된다.
위젯이 아니라 요소가 상태 객체를 관리한다.

1.7.2 요소 트리 살펴보기

앱에 기능을 추가하면서 요소 트리가 어떻게 동작하는지 살펴보자.
우선 Reset 버튼을 누르면 증가및 감소 버튼이 서로 바뀌도록 만들어보자.
1.
필요 변수 선언
class _MyHomePageState extends State<MyHomePage> { int _counter = 0; bool _reserved = false; //버튼 변경 기준 논리 값 List<UniqueKey> _buttonKeys = [UniqueKey(), UniqueKey()]; //키 //... }
Dart
복사
2.
FancyButton 위젯 만들기 : 자신의 배경색을 관리하며 버튼을 누르면 전달된 콜백을 호출한다.
class FancyButton extends StatefulWidget { final VoidCallback onPressed; final Widget child; const FancyButton({Key key, this.onPressed, this.child}) : super(key: key); _FancyButtonState createState() => _FancyButtonState(); } class _FancyButtonState extends State<FancyButton> { Widget build(BuildContext context) { return Container( child: RaisedButton( color: _getColors(), // 자신의 색을 관리한다 child: widget.child, onPressed: widget.onPressed, ), ); } // 색 관리 메서드 Color _getColors() { return _buttonColors.putIfAbsent(this, () => colors[next(0, 5)]); } } Map<_FancyButtonState, Color> _buttonColors = {}; final _random = Random(); int next(int min, int max) => min + _random.nextInt(max - min); List<Color> colors = [ Colors.blue, Colors.green, Colors.orange, Colors.purple, Colors.amber, Colors.lightBlue ];
Dart
복사
putIfAbset() 메서드는 이 버튼이 맵에 포함되어 있을 경우 색을 가져오고 아니면 새로운 색을 맵에 추가한 다음 색을 반환하라는 의미다.
3.
_MyHomePageState.build에서 FancyBugtton 사용
class _MyHomePageState extends State<MyHomePage> { ... Widget build(BuildContext context) { final incrementButton = FancyButton( child: Text( "Increment", style: TextStyle(color: Colors.white), ), onPressed: _incrementCounter, ); // --- (1) final decrementButton = FancyButton( child: Text( "Decrement", style: TextStyle(color: Colors.white), ), onPressed: _decrementCounter, ); // --- (2) // --- (3) List<Widget> _buttons = <Widget>[incrementButton, decrementButton]; if (_reversed) { // --- (4) _buttons = _buttons.reversed.toList(); } return Scaffold( ... body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ ... Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: _buttons, // --- (5) ) ], ), ), ... ); } ... }
Dart
복사
a.
IncrementButton 을 대표하는 FancyButton
b.
DecrementButton을 대표하는 FancyButton
c.
선언한 FancyButton들을 _buttons 변수에 담아서 Row로 전달해 화면에 표시한다.
d.
_reversed 논리 값의 참/거짓에 따라 참일 경우 버튼을 바꾼다.
e.
3에서 선언한 _buttons를 children에 인수로 넘겨준다.
4.
Reset버튼을 누르면 두 버튼을 바꾸도록 로직 추가하기
void _resetCounter() { setState(() => _counter = 0); _swap(); } void _swap() { setState(() => _reversed = !_reversed); }
Dart
복사
_resetCounter 메서드 호출시 _swap메서드도 호출해 _reversed값을 바꿔준다.
여기까지 따라왔다면 핫 리로드를 통해 실행결과를 확인해보자.
리셋버튼을 누르면 두 개의 버튼이 서로 변경되는걸 볼 수 있다. 하지만, 아직 버튼의 배경색이 그대로라는 점을 알 수 있다.

1.7.3 요소 트리와 상태 객체

상태 객체는 요소 트리가 관리한다.
상태 객체는 오래 산다(long-lived) 위젯과 달리 위젯을 그릴때마다 파괴하고 다시 빌드하지 않는다.
상태 객체는 다시 사용할 수 있다.
요소는 위젯을 참조한다.
요소와 위젯의 관계
각 요소는 다른 위젯을 기리키며 각각의 형식을 알고 있다.
플러터가 위젯을 다시 빌드해도 요소는 기존 참조가 가리키는 위젯의 위치를 그대로 유지한다.
요소는 메타 정보와 위젯 참조를 포함하지만 위젯이 바뀌었을 때 레퍼런스를 어떻게 갱신해야 하는지 모른다.
build를 호출한 이후 플러터가 위젯을 다시 빌드해도 요소는 기존의 위젯 참조를 유지한다.
참조 대상 위젯이 이전과 같은지 비교해 다르거나 다른 위젯으로 바뀌었다면 위젯을 다시 그린다.
버튼의 위치가 스왑되더라도 요소는 같은 위치를 참조하기에 요소의 참조는 새 위젯을 가르키게 된다.
요소는 위젯에서 다음과 같은 일부 프로퍼티를 확인해 바뀐 내용을 판독한다.
런타임의 정확한 형식
(키가 있다면)위젯의 키

그렇다면, 어째서 색이 변하지 않는 것인가

: 위젯의 색은 설정이 아닌 상태 객체가 가지고 있다. 요소는 새로 스왑된 위젯을 가르키며 새 설정을 표시하지만 상태 객체는 바뀌지 않는다. 그렇기에 요소는 새 위젯이 트리에 삽입되었다는 사실은 알 수 있지만 키가 존재하지 않고 형식은 기존의 FancyButton과 동일한 FancyButton 형식이기에 참조를 갱신하지않고 상태 객체를 그대로 적용한다.
요소는 위젯의 형식이 같기에 같은 위젯으로 판단한다.
그러면 어떻게 색을 변경시킬 수 있을까?
플러터는 프레임워크가 위젯을 식별할 수 있도록 키(key)라는 기능을 제공한다.

1.7.4 위젯 키

키를 사용하면 형식은 같지만 다른 위젯을 플러터에 알릴 수 있다.
이런 부분은 여러 자식을 갖는 Row나 Column에서 유용하게 사용할 수 있다. UniqueKey를 이용해 문제를 해결해보자.
List<UniqueKey> _buttonKeys = [UniqueKey(), UniqueKey()]; ... final incrementButton = FancyButton( key: _buttonKeys.first, // --- (1) child: Text( "Increment", style: TextStyle(color: Colors.white), ), onPressed: _incrementCounter, ); final decrementButton = FancyButton( key: _buttonKeys.last, child: Text( "Decrement", style: TextStyle(color: Colors.white), ), onPressed: _decrementCounter, );
Dart
복사
1.
같은 종류의 위젯을 고유 키로 구분한다.

→키의 형식과 사용 방법

ValueKey, ObjectKey, UniqueKey, GlobalKey, PageStorageKey등 여러 종류의 키가 있으며 서로 공통된 부분이 있다.
PageStorageKey는 ValueKey<T>를 상속받는다.
ValueKey<T>는 LocalKey를 상속받는다.
LocalKey는 Key를 상속받는다.
ObjectKey와 UniqueKey는 LocalKey를 구현한다.
GlobalKey는 Key를 상속받는다.
글로벌 키는 사용을 권장하지 않으며 잘 사용하지 않는다. 그 이유는 글로벌 키는 대부분 글로벌 상태관리로 바꿀 수 있기 때문이다.

글로벌 키

위젯 트리에서 상태를 관리하고 위젯을 이동할 때 글로벌 키를 사용한다.
하지만 플러터 팀에서 권고하기를 글로벌 키로 상태를 관리하는 것을 권장하지 않는다. 다른 기법으로 더 안전하게 상태를 관리할 수 있다.
글로벌 키는 성능에 영향을 미치므로 자주 사용하지 않는다.

로컬 키

로컬 키는 키를 생성한 빌드콘텍스트의 영역을 갖는다.
ValueKey<T>: 상수를 갖는 객체에 키를 추가할 때 ValueKey를 사용한다.
(예: 할일 목록 앱에서 할 일을 표시하는 위젯은 고유한 상수 Todo.text를 포함한다.)
ObjectKey: 같은 형식의 객체지만 프로퍼티 값이 다른 여러 객체가 있을 때 ObjectKey를 사용한다. (예: 전자상거래 앱에서 두 제품의 이름이 같을 경우나 한 판매자가 여러 제품을 판다면 제품명과 판매자명을 조합해 특정 제품을 식별할 수 있다. 즉, ObjectKey로 전달하는 리터럴객체가 키다.)
Key key = ObjectKey({ "seller": product.seller, "product": product.title })
Dart
복사
UniqueKey: 컬렉션에 자식이 있고 이들이 만들어지기 전까지 자식의 값을 모르는 상황이라면 UniqueKey를 자식에 추가한다.
PageStorageKey: 스크롤 위치 등 페이지 정보를 저장하는 특수 키다

1.8 정리

이번 장에서는 플러터가 어떻게 돌아가는지에 대해 대략적으로 알아보았다.
더 깊은 이해와 원리는 실습과 함께 점진적으로 익히도록 한다.
플러터의 모든 것은 위젯이며 위젯은 뷰를 묘사하는 다트 클래스다.
위젯은 앱 뷰의 모든 정보를 정의할수 있다. Row 같은 위젯은 레이아웃을 정의한다. 어떤 위젯은 추상화가 덜 되어 있으며 Button, TextField처럼 구조적 요소를 정의한다.
플러터는 상속보다 조합을 중시한다.
든 위젯은 위젯을 반환하는 build메서드를 포함해야 한다.
플러터에서 위젯은 변경할 수 없지만 상태 객체는 바꿀 수 있다.
위젯은 대부분 const 생성자를 갖는다. (위젯을 만들 때 키워드는 생략하는 것이 좋다(타입추론))
StatefulWidget은 상태 객체로 자신의 내부 상태를 관리한다. StatelessWidget은 뇌가 없으며 플러터가 위젯 트리에서 위젯을 제거하면 완전히 파괴된다.
setState는 플러터에 상태를 갱신하고 위젯을 다시 그리도록 지시한다. setState에서는 비동기 작업을 수행해서는 안된다.
initState와 기타 생명주기 메서드는 상태 객체의 강력한 도구.
BuildContext는 위젯 트리에서 위젯의 위치를 기리킨다. 즉 위젯은 트리에서 자신의 위치 정보를 얻을 수 있다.
요소 트리는 영리하다. 요소 트리는 위젯을 관리하며 실제 사용될 요소의 청사진 역할을 한다.
플러터에서 위젯과 관련된 RenderBox객체가 위젯을 그린다. 이들 RenderBox는 위젯의 실제 쿨리적 크기를 결정한다. 이들 객체는 부모로부터 제약 조건을 받아 자신의 실제 크기를 결정한다.
Container 위젯은 개별 위젯에서 얻어야 할 정보를 '편리하게'모아 제공한다.
플러터의 Row, Column 위젯은 CSS의 FlexBox와 비슷한 플렉스 레이아웃 개념을 사용한다.