목차
Chapter Point
•
플러터란?
•
다트란?
•
플러터의 동작원리
•
플러터가 다트를 사용하는 이유는 무엇인가
•
플러터를 언제 사용해야할까?
1.0 플러터란
구글에서 만들어 오픈소스로 공개한 모바일 SDK
앱을 만들기위한 기존의 방식은 안드로이드와 IOS를 별도로 구현해 배포해야하기에 요구되는 기술스펙과 비용이 컸지만, 플러터를 사용하면 한 번의 구현으로 양쪽 진영에 모두 배포가 가능하다.
플러터는 렌더링 엔진, UI 컴포넌트, 테스트 프레임워크, 도구, 라우터 등 앱 제작에 필요한 기능을 모두 제공한다. 그 덕에 개발자는 앱 구현에 집중하여 생산성을 높힐 수 있다.
1.1 플러터에서 다트를 사용하는 이유
스프링이나 안드로이드에서는 자바 혹은 코틀린을 사용한다. IOS에서는 Object-C 혹은 Swift를 사용한다. 그리고 플러터에서는 다트(Dart)를 사용해 구현한다. 다트는 구글 소유로 구글에서 유지 보수하는 언어인데, 어째서 우리에게 익숙한 자바, 코틀린, 자바스크립트 등등이 아닌 다트라는 생소한 언어를 사용하는 것일까? 그 이유는 아래와 같다.
•
다트는 JIT(just-in-time) 컴파일과 AOT(ahead-of-time)컴파일을 모두 지원한다.
◦
AOT 컴파일러는 다트 코드를 그에 맞는 네이티브 코드로 바꿔줘서 플러터가 모두에게 빠르게 동작하며 플러터 전체 프레임워크의 대부분을 다트로 구현 가능하게 한다.
◦
다트의 선택형 JIT 컴파일러는 핫 리로드(hot reload)를 지원해 빠른 개발속도와 반복(iteration)을 가능하게 해 생산성을 높혀준다.
•
다트는 객체지향이기에 마크업 언어를 사용하지 않고 다트 언어만으로 시각적 사용자 경험을 쉽게 구현한다.
•
기존 언어(Java, Javascript 등) 과 유사해 생산성이 좋고 예측이 가능하다. 기존 다른 언어에 익숙할수록 쉽게 접근할 수 있다.
1.2 다트
다트는 프로그래밍 언어다. 하지만 베이스가 다른 고급 프로그래밍 언어와 유사하기에 자바스크립트나 자바 혹은 C나 이에서 파생된 언어를 사용한 경험이 있다면 다트의 문법에서 비슷한부분을 많이 발견할 수 있다.
다트의 최초 목적은 웹 개발언어였다. 그렇기에 자바스크립트를 대체하려고 했다가 방향을 틀어 컴파일러를 구현하기로 결정했는데, 이는 다트의 모든 기능이 자바스크립트로 표현이 가능하다는 의미가 된다.
다트는 자바와 비슷하지만 코드가 더 간결하다. 가령 예를들어 다트에서는 논리값으로 true/false만 가진다. 자바스크립트나 자바에서 if (1) { } 문법을 사용한다면 true로 판단해 블록 내 코드를 수행하지만 다트에서는 허용하지 않는다.
그리고 다트는 모듈을 지원하지 않고 오직 객체지향을 기반으로 한다.
1.3 모바일 개발의 종류
1.3.1 네이티브 개발(IOS, Android)
네이티브 앱을 사용하는 방법으로 앱을 가장 완벽하게 제어하며 디버깅 도구, 성능을 100% 활용가능하다. 하지만, 이를 위해 플랫폼별로 앱을 두 번 구현해야 하기에 두 개의 팀을 구성해야하며 두 팀간의 협업도 해야 한다.
1.3.2 자바스크립트 기반 크로스 플랫폼
웹뷰(WebView), 리액트 네이티브(React Native)와 같은 자바스크립트 기반의 크로스 플랫폼을 사용하는 방법이 있다. 네이티브 개발 경험을 하지는 못하지만 최신 자바스크립트 기술을 어느정도 파악한다면 웹개발자도 모바일 앱 개발에 참여할 수 있다.
단점으로는 자바스크립트 다리(Javascript bridge) 를 꼽을 수 있다.
우선, 웹뷰(WebView)는 웹킷(WebKit)(브라우저 렌더링 엔진)으로 구동되는데, 웹페이지를 그대로 보여준다. 이 경우 DOM을 처리하는 일에 대한 비용이 너무 높기에 성능이 좋지 않다.
그래서, 자바스크립트가 네이티브 코드와 직접 통신할 수 있도록 자바스크립트 다리(bridge)를 만들어 이런 문제를 해결하기도 했는데, 이는 DOM을 사용하지 않아 속도도 빨라졌다.
하지만, 앱이 렌더링 엔진과 소통할 때마다 다리 건너 있는 자바스크립트 코드를 네이티브 코드로 컴파일해야 한다.
자바스크립트 다리(Javascript bridge)에서 병목현상이 발생한다.
1.3.3 플러터
플러트는 빌드를 할 때 ARM(최신 IOT기기등에 사용하는 프로세서) 코드로 컴파일한다. 그리고 자체 렌더링 엔진을 탑재하고 있기에 앱이 네이티브로 구동되며 자바스크립트 다리와 같은 다리도 필요없다.
자바스크립트 다리 관점에서 비교한 플러터 플랫폼
이런 다양한 크로스 플랫폼의 비용문제들을 플러터는 위 그림과 같이 자체 렌더링 엔진을 통해 해결했다.
1.4 플러터의 장점
1.4.1 자바스크립트 다리가 없다.
크로스 플랫폼 기술로 앱을 개발할 때 자바스크립트 다리를 사용하는데 이에 필요한 비용이 상당히 높기에 앱의 최적화및 디버깅이 힘들다.
반면, 플러터는 자체 렌더링 엔진을 이용하기에 실제 네이티브 코드로 컴파일하여 크롬이 사용하는 렌더링 엔진(Skia)을 사용하기에 실행 시 다트를 변환하지 않는다. 그렇기에 앱의 최적화가 더 유리하다.
1.4.2 컴파일 시간
플러터의 전체 컴파일 시간은 30초에서 최대 1분을 넘기지 않으며 핫 리로드를 제공해서 개발주기를 높혀 높은 생산성을 보장한다.
1.4.3 한 번의 구현으로 모든 플랫폼 배포
IOS와 안드로이드를 플러터와 다트를 사용해 한 번만 구현하면 동시에 배포가 가능하다.
다트 유닛 테스트역시 제공하는데 사용하기도 쉬우며 플러터는 테스팅 위젯 라이브러리를 제공한다.
1.4.4 코드 공유
플러터 기술은 자바스크립트로도 가능하기 때문에 (네이티브 개발로는 불가능하다.) 웹과 모바일 앱은 클라이언트 뷰를 제외하고 모든 코드 공유가 가능하다. DI를 사용하면 앵귤러 다트앱과 플러터 앱에 같은 모델과 컨트롤러를 사용할수도 있다.
그리고, IOS와 안드로이드는 필연적으로 모든 코드를 공유한다.
경우에따라 다르지만 각각의 플랫폼이 네이티브로 커스터마이징을 해야하는 경우도 있다.
(ex: VOIP, Push Notification)
1.5 플러터 동작 원리 간단 소개
넓게 보면 플러터는 웹의 리액트와 같이 리액티브 선언형 조합할 수 있는 뷰 계층 라이브러리다.
(플러트는 렌더링 엔진도 포함하기에 실제로는 리액트 + 브라우저와 더 비슷하다.)
즉, 위젯이라는 작은 컴포넌트를 조합해 모바일 UI를 만든다. 플러터의 모든 것은 위젯이며 위젯은 뷰를 묘사하는 다트 클래스다. 구조, 스타일 애니메이션 그리고 그 밖에 UI를 구성하는 모든 것이 위젯이다.
다른 객체가 없다는 건 아니고, 앱의 모든 조각이 위젯이라는 의미이다.
이미지, 버튼 모두 위젯이다.
위젯은 상태를 가진다. 위 그림에서 BETTER SHOES버튼은 qty라는 상태를 가지며 위젯의 플러스(+) 혹은 마이너스(-) 버튼을 누르면 내부 상태가 바뀌며 이 상태에 의존하는 모든 위젯을 갱신한다.
위젯과 상태 갱신 두 가지는 플러터에서 신경써야하는 핵심 개념이다.
이제 플러터 내부에서 어떤일이 일어나는지 좀 더 살펴보자.
1.5.1 모든 것이 위젯
위에서 플러터는 모든 것이 위젯이라 했다. 이 위젯은 앱 뷰의 모든 정보를 정의하는데 Row와 같은 위젯은 레이아웃 정보를 정의하고, Button, TextField같은 위젯은 더 구체적이며 구조적인 요소를 정의한다. 심지어 앱의 루트도 위젯이다.
다음은 자주쓰이는 위젯들이다.
•
레이아웃: Row, Column, Scaffold, Stack
•
스타일: TextStyle, Color
•
위치와 정렬: Center, Padding
•
구조: Button, Toast, MenuDrawer
•
애니메이션: FadeInPhoto, Transform
1.5.2 위젯으로 UI만들기
플러트는 상속(inheritance)보다 조합(composition)을 우선시하며 이를 이용해 고유한 위젯을 만든다. 대부분의 위젯은 작은 위젯을 합쳐 만든다. 즉 플러터는 다른 위젯을 상속받아 커스텀 위젯을 만들지 않는다는 의미이다.
//class AddToCartButton extends Button{} // 잘못 된 코드
class AddCartButton extends StatelessWidget{
//...클래스 멤버
// Button을 다른 위젯으로 감싸서, 즉 위젯을 조합해 커스텀 위젯을 만든다.
build() {
return Center( // AddToCartButton을 중앙으로 정렬하는 위젯
child: Button( // 텍스트를 전달하는 새 커스텀 컴포넌트를 만든다.
child: Text('Add to Cart'),
)
);
}
}
Dart
복사
작고, 재사용이 가능한 컴포넌트를 조합하는 방식은 리액트와 유사하다.
위젯은 다양한 생명주기(life-cycle)메서드와 객체 멤버를 포함한다. 가장 중요한 메서드는 build()인데 모든 플러터 위젯은 build()메서드를 반드시 정의해야 한다. 이 메서드는 반환하는 위젯을 통해 뷰를 실질적으로 묘사한다.
1.5.3 위젯 형식
대부분의 위젯은 상태가 있는 위젯이거나(StatefulWidget) 없는 위젯(StatelessWidget)이다.
•
상태가 없는 위젯(StatelessWidget)
: 어떠한 정보를 저장하지 않기에 사라져도 영향이 없으며 언제 파괴되어도 괜찮은 위젯.
생명주기가 외부에 의해 결정되며, 그렇기에 언제 위젯을 트리에서 제거 혹은 리빌드 해야할지 프레임워크에 알리지 않으며 프레임워크에서 언제 리빌드 할 지를 알려준다.
class AddCartButton extends StatelessWidget{
build() {
return Center(
child: Button(
child: Text('Add to Cart'),
)
);
}
}
Dart
복사
AddCartButton은 상태가 없는 위젯이다. 그렇기에 따로 상태를 관리하거나 트리의 어떤 부분도 알 필요가 없다. 그저 사용자가 해당 버튼을 눌렀을때 지정된 함수를 실행하는게 이 위젯의 임무다. 그럼 이 버튼의 텍스트를 Add to Cart 에서 Remove from Cart 로 다른위젯에서 정보를 전달하면 AddCartButrton은 다시 그려진다. 즉, 상태가 없는 위젯은 새로운 정보에 반응(react)한다.
•
상태가 있는 위젯(StatefulWidget)
: 항상 State 객체를 갖는다. State 객체는 setState라는 메소드를 제공하는데, 이는 위젯을 다시 그려야 함을 플러터에 알린다.
버튼, 텍스트필드, 레이아웃 위젯을 조합한 QuantityCounter
위 그림은 장바구니앱의 수량을 나타내는 QuantityCount (StatefulWidget)으로 여러 내장 위젯을 조합해 만든 커스텀 위젯이다.
Widget build(BuildContext context){
return Container( // build 메서드는 항상 위젯을 반환한다.
child: Row(
children: List<Widget> [
IconButton(
icon: Icons.subtract,
onPressed: (){
setState((){ // 사용자의 동작을 감지하는 버튼 위젯의 내장 프로퍼티
this.quantity--; // 상태에 저장된 수량을 감소시킨다
});
}
),
new Text("Qty:" ${this.quantity}), //상태 객체의 수량이 바뀌면 트리의 위젯을 다시 그린다.
new IconButton(
icon: Icons.add,
onPressed: (){ // 이 콜백이 상태의 수량을 증가시키는 setState를 호출한다
setState((){
this.quantity++;
});
}
),
],
)
);
}
Dart
복사
이 위젯은 기본적으로 State 객체 클래스에서 상속받은 메서드를 사용하는데, 이중 setState 메서드가 핵심이다. [+], [-] 버튼을 누르면 앱은 setState 메서드를 호출하는데 이 메서드는 위젯의 상태를 갱신하며 이 상태에 의존하는 모든 위젯을 다시 그리도록 플러터에 지시한다.
이렇게 위젯을 빌드하고 갱신하는 과정을 생명주기(life-cycle)이라 한다.
상태가 있는 위젯의 전체 생명 주기
상태가 있는 위젯은 위젯과 State객체 두 가지로 구성된다.
QuantityWidget의 생명주기
1.
페이지로 이동하면 플러터가 객체를 만들고 이 객체는 위젯과 관련된 State객체 생성
2.
위젯이 마운트되면 플러터가 initState 호출
3.
상태를 초기화하면 플러터가 위젯을 빌드하고 화면에 위젯을 그린다.
4.
수량 위젯은 다음 세 가지 이벤트 중 하나를 기다린다.
•
사용자가 앱의 다른 화면으로 이동하며 폐기(dispose) 상태일 때
•
트리의 다른 위젯이 갱신되며 수량 위젯이 의존하는 설정이 바뀜. 위젯의 상태는 didUpdateWidget을 호출하며 필요하다면 위젯을 다시 그린다. 예를 들어 제품이 트리의 상위 위젯에서 해당 제품을 장바구니에 추가할 수 없도록 상태 위젯을 비활성화 하는 상황
•
사용자가 버튼을 눌러 setState를 호출해 위젯의 내부 상태가 갱신되어 플러터가 위젯을 다시 빌드하고 그리는 상황
1.6 플러터 렌더링: 내부 동작 원리
위젯 트리
장바구니 위젯 트리. 실제로는 훨씬 많은 위젯을 포함한다.
플러트는 커다란 위젯 트리를 매우 빠르게 빌드한다. 위의 위젯 트리중 CartItem 위젯을 보면 이 위젯은 상태를 가지고 위젯의 자식은 위젯 상태에 의존한다.
그렇기에 CartItem의 상태가 바뀌면 이 위젯을 포함한 모든 하위 위젯이 다시 그려진다.
플러터 위젯은 리액티브이기에 외부나 setState를 통해 새 정보를 얻으면 이에 반응해 필요할 경우 플러터가 위젯을 다시 그린다. 이 과정은 아래와 같다.
1.
사용자가 버튼을 누른다.
2.
Button.onPressed 콜백에서 setState를 호출한다.
3.
Button의 상태가 dirty로 바뀌었기에 플러터는 이 위젯을 리빌드한다.
4.
트리에서 기존 위젯을 새 위젯으로 바꾼다.
5.
플러터가 새 트리를 그린다.
렌더링 과정
전체적인 렌더링 과정
애니메이션 티커(Animation ticker)가 동작하며 그리기 작업을 시작한다.
(ex: 리스트를 스크롤링하거나 위젯을 다시 그려야 하는 상황에서 화면의 처음 요소 위치부터 최종 위치까지 조금씩 이동하며 애니메이션이 부드럽게 일어난다. 이 과정은 요소가 움직여야 하는 시간을 애니메이션 티커가 제어한다.)
1.6.1 위젯 트리와 레이아웃 조립
•
위젯 : 화면에 나타낼 요소를 결정하는 데이터와 설정
트리에서 버튼을 빌드할 때 실제로 특정 색깔(파란색) 사각형과 그 안의 텍스트를 빌드하는게 아닌 화면에 나타낼 요소의 설정을 처리할 뿐이다.
•
위젯 트리가 완성되면 플러터는 레이아웃을 처리한다.
•
플러터는 필요할 때 트리를 한 차례 탐색한다(선형 시간 소요)
•
트리를 탐색하며 위젯의 위치정보를 수집한다.
•
레이아웃과 크기 제약(constraint)은 부모에서 자식 위젯 순으로 작성된다.
◦
트리를 거슬러 올라오며 모든 위젯은 자신의 제약을 알고있는 상태이기에 싱제 크기와 위치를 부모 위젯에 알린다. 위젯은 서로의 관계를 정리하며 최종 레이아웃을 결정한다.
Example. 장바구니 예제
1.
QuantityWidget의 [+] 버튼을 누른다.
2.
새로운 수량으로 상태가 변경된다.
3.
플러터는 위젯 트리를 탐색하며 내려가는데, 이 때 QuantityWidget은 버튼과 텍스트 필드에 제약정보를 알려준다.
4.
버튼은 [+], [-]아이콘에 이들의 제약을 알려주는 등의 순서로 트리를 타고 내려오며 정보를 전파한다.
5.
단말 노드의 위젯에 도달하면 모든 위젯은 크기 제약 정보를 획득한 상태이다.
6.
트리를 거슬러 올라가며 각 위젯의 크기와 위치를 안전하게 계산한다.
1.6.2 조립 과정
여기서 조립 과정을 진행하며 플러터는 위젯에 실제 화면상의 좌표를 제공하며 위젯은 자신이 차지할 실제 픽셀의 수를 알게 된다.
이 과정은 의도적으로 분리해 진행되는데, 그 덕에 조립된 위젯을 재사용할 수 있다.
조립 과정을 그리기 과정과 분리해 성능을 높혔다.
1.6.3 화면에 그리기
엔진은 전체 트리를 그릴 수 있는 뷰로 모은 다음, 운영체제를 통해 화면에 그리도록 요청한다.
이를 래스터라이징이라 부르며 이 과정을 끝으로 위젯이 화면에 그려진다.
여기까지 플러터 프레임워크가 위젯이 화면에 그려지기까지의 과정을 살펴봤는데, 네 가지 개념을 기억하도록 한다.
•
플러트는 리액티브다.
•
모든 것은 위젯이다.
•
State 객체는 오래 살아남으며 종종 재사용된다.
•
위젯의 제약은 부모가 서술한다.
1. 선이나 면 따위의 도형 요소로 구성되는 벡터 그래픽스를 작은 점의 집합으로 표현하는 비트맵 그래픽스로 변환하는 일.