넓은 의미로 플러터는 웹의 리액트처럼 리액티브, 선언형declarative, 조합할 수 있는 뷰 계층 라이브러리다(하지만 플러터는 렌더링 엔진도 포함하므로 실제로는 리액트와 브라우저를 합한 것과 더 비슷하다). 즉 위젯이라는 작은 컴포넌트component를 조합해 모바일 UI를 만든다. 플러터의 모든 것은 위젯이며 위젯은 뷰를 묘사하는 다트 클래스다. 구조, 스타일, 애니메이션 그리고 그 밖에 UI를 구성하는 모든 것이 위젯이다.1
1 옮긴이_ 이 책을 번역하는 2020년 9월 현재, 플러터는 일부 운영체제의 데스크톱 알파 버전을 지원하며(https://flutter.dev/ desktop),웹은 베타 버전을 지원한다(https://flutter.dev/web).
· · ·
WARNING_ ‘모든 것은 위젯이다Everything is a widget’라는 말을 인터넷이나 공식 문서에서 볼 수 있는데, 가끔 이를 잘못 이해할 수 있다. 플러터에 다른 객체가 없다는 걸 의미하진 않는다. 정확하게는 앱의 모든 조각이 위젯이라는 의미다. 스타일, 애니메이션, 리스트, 텍스트, 버튼 심지어 페이지도 위젯이다. 예를 들어 플러터에는 앱의 루트를 정의하는 ‘App’ 객체가 따로 없다. 기술적으로 어떤 위젯이라도 앱의 루트가 될 수 있다. 참고로 플러터 SDK에는 다른 객체(예를 들면 요소)도 있지만 나중에 자세히 살펴본다.
장바구니 앱을 만든다고 가정하자. 앱은 모든 제품을 나열하고 [추가], [제거] 버튼으로 제품을 추가하거나 삭제한다. 여기서 리스트, 제품, 버튼, 이미지 모든 것이 위젯이다. 다음 그림은 이 위젯의 코드를 보여준다. 위젯 이외에는 자신만의 로직을 구현하는 클래스가 필요한데 이는 플러터와 직접적인 연관이 없다.
그림_ 모든 것은 위젯이다
위젯이 포함하는 모든 것은 위젯이다. 위젯은 상태를 가진다. 예를 들어 카트에 몇 개의 제품을 담았는지 추적하는 수량 위젯이 있다고 가정하자. 위젯의 상태가 바뀌면 프레임워크가 이를 인지하고 새 트리를 기존 트리와 비교한 다음 갱신해야 하는 위젯을 처리한다. 장바구니 예제에서 사용자가 수량 위젯의 [+] 버튼을 누르면 내부 상태가 바뀌면서 플러터는 이 상태에 의존하는 모든 위젯(여기서는 텍스트 위젯)을 갱신한다. 다음 그림은 [+] IconButton 클릭 전과 후의 위젯 모습이다.
그림_ setState로 수량(qty) 갱신
위젯과 상태 갱신 두 가지는 플러터 개발자가 신경써야 하는 두 가지 핵심 개념이다. 이제부터 플러터 내부에서 어떤 일이 일어나는지 자세히 살펴보자.
모든 것이 위젯
플러터의 핵심 개념은 모든 것이 위젯이라는 점이다. 다시 한번 말하지만 플러터에 다른 객체가 없다는 의미가 아니다. 이 책의 뒷부분에서 다른 객체를 자세히 살펴본다. 하지만 앱 개발자 입장에서 위젯 외의 객체는 크게 신경 쓸 필요가 없다. 플러터에는 models, view models 등 다른 특정 클래스 형식이 따로 없다. 위젯은 앱 뷰의 모든 정보를 정의한다. Row와 같은 위젯은 레이아웃 정보를 정의한다. Button, TextField 같은 위젯은 더 구체적이며 구조적인 요소를 정의한다. 심지어 앱의 루트도 위젯이다.
장바구니 예제로 다시 돌아가면, 다음 그림은 레이아웃 위젯 일부를 어떻게 코딩하는지 보여 주며, 그 다음 그림은 구조적 위젯을 보여준다. 참고로 앱을 만들려면 레이아웃, 스타일, 애니메이션 등 다양한 기능이 필요하므로 보이는 것보다 위젯이 아주 많다.
다음은 가장 흔히 볼 수 있는 위젯이다.
그림_ 흔히 사용하는 레이아웃 위젯 예
그림_ 흔히 사용하는 레이아웃 위젯 예
위젯으로 UI 만들기
플러터는 상속inheritance보다 조합composition을 우선시하며 이를 이용해 고유한 위젯을 만든다. 대부분의 위젯은 작은 위젯을 합쳐 만든다. 즉 플러터는 다른 위젯을 상속받아 커스텀 위젯을 만들지 않는다는 의미이므로 다음은 올바른 코드가 아니다.
다음처럼 Button을 다른 위젯으로 감싸서, 즉 위젯을 조합해 커스텀 위젯을 만든다.
웹 개발자라면 작고, 재사용할 수 있는 컴포넌트를 조합하는 리액트의 방식과 비슷하다는 걸 알 수 있다.
위젯은 다양한 생명주기life cycle 메서드와 객체 멤버를 포함한다. 가장 중요한 메서드 중 하나는 build()다. 모든 플러터 위젯은 build() 메서드를 반드시 정의해야 한다. 이 메서드는 반환하는 위젯을 통해 뷰를 실질적으로 묘사한다.
위젯 형식
대부분의 위젯은 상태가 있는 stateful 위젯(StatefulWidget), 상태가 없는 stateless 위젯(Stateless Widget) 둘 중 하나에 속한다. 앱 개발자 입장에서 StatelessWidget은 언제 파괴되어도 괜찮은 위젯이다. 즉 이 위젯은 어떠한 정보를 저장하지 않으므로 위젯이 사라져도 별일이 없다. 상태가 없는 위젯의 모든 정보나 설정을 위젯으로 전달하면 위젯은 필요한 정보와 UI를 표시한다. 이 위젯의 생명은 외부의 힘으로 결정된다. 즉 상태가 없는 위젯은 언제 위젯을 트리에서 제거해야 할지, 언제 리빌드해야 할지 프레임워크에 알리지 않는다. 반대로 프레임워크가 위젯을 언제 리빌드해야 하는지 알려준다. 아직 이 동작이 정확히 어떤 의미인지 이해하기 힘들 수 있다. StatelessWidget과 다른 방식으로 동작하는 StatefulWidget을 배울 때 조금 더 잘 이해할 수 있다.
장바구니 예제에서 AddToCartButton은 상태가 없는 위젯이다. 따라서 이 위젯은 상태를 관리할 필요가 없으며 트리의 어떤 부분도 알 필요가 없다. 사용자가 버튼을 클릭하면 지정된 함수를 실행하는 것이 이 위젯의 임무다. [Add to Cart] 버튼이 결코 변하지 않는다는 의미는 아니다. 예를 들어 다른 위젯이 [Add to Cart] 대신 [Remove from Cart]를 버튼의 텍스트로 표시하도록 정보를 전달하면 AddToCartButton이 다시 그려진다. 즉, 상태가 없는 위젯은 새로운 정보에 반응react한다.
반면 장바구니 앱에서 QuantityCounter는 StatefulWidget이다. 사용자가 장바구니에 추가한 제품의 수량(상태)을 추적해야 하기 때문이다. StatefulWidget은 항상 State 객체를 갖는다. State 객체는 setState라는 특별한 메서드를 제공하는데, 이는 위젯을 다시 그려야 함을 플러터에 알린다.
State 객체는 오래 지속된다. State 객체는 위젯을 다시 그려야 한다고 플러터에 알리기도 하며 상태를 갖는 위젯이 외부 영향으로 다시 그려질 수 있는 것도 알린다.
다시 장바구니 앱을 살펴보자. 지금까지 화면에 나타난 몇 가지 컴포넌트는 상태가 있거나 상태가 없는 위젯으로 구성된다.
특히 QuantityCounter는 StatefulWidget으로 여러 내장 위젯을 조합해 만든 커스텀 위젯이다. 다음 그림은 이 커스텀 위젯의 뼈대를 보여준다.
그림_ 버튼, 텍스트 필드, 레이아웃 위젯을 조합해 QuantityCounter를 만들었다.
build 메서드는 다음처럼 구현한다. 그림에서 가리킨 컴포넌트를 자세히 확인하자.
그림_ 버튼, 텍스트 필드, 레이아웃 위젯을 조합해 QuantityCounter를 만들었다.
레이아웃이나 스타일에 필요한 코드는 생략했지만 필요한 코드는 모두 포함되어 있다. 이 코드의 핵심은 IconButton과 Text 위젯이다.
이 위젯은 기본 State 객체 클래스에서 상속받은 메서드를 사용한다. 그중에서도 가장 중요한 메서드는 setState다. [+], [-] 버튼을 누르면 앱은 setState 메서드를 호출한다. 이 메서드는 위젯의 상태를 갱신하며 이 상태에 의존하는 모든 위젯을 다시 그리도록 플러터에 지시한다. 다음 그림은 [+] 버튼을 눌렀을 때 수량 위젯이 어떻게 갱신되는지 보여준다.
그림_ 사용자의 조작으로 setState가 호출되면 프레임워크가 위젯을 다시 그린다.
위젯을 빌드하고 갱신하는 과정을 생명주기라 부른다. 생명주기는 뒤에서 자세히 설명한다. 다음 그림은 StatefulWidget의 전체 생명주기를 보여준다.
그림_ 상태가 있는 위젯은 위젯과 State 객체, 이 두 가지로 구성된다.
다음은 QuantityWidget의 생명주기다.
1 페이지로 이동하면 플러터가 객체를 만들고 이 객체는 위젯과 관련된 State 객체를 만든다.
2 위젯이 마운트되면 플러터가 initState를 호출한다.
3 상태를 초기화하면 플러터가 위젯을 빌드한다. 그 결과 화면에 위젯을 그린다. 다음 과정을 참고하자.
4 수량 위젯은 다음 세 가지 이벤트 중 하나를 기다린다.
- 사용자가 앱의 다른 화면으로 이동하면서 폐기dispose 상태일 때
- 트리의 다른 위젯이 갱신되면서 수량 위젯이 의존하는 설정이 바뀜. 위젯의 상태는 didUpdateWidget을 호출하며 필요하다면 위젯을 다시 그림. 예를 들어 제품이 품절되어 트리의 상위 위젯에서 해당 제품을 장바구니에 추가할 수 없도록 상태 위젯을 비활성화하는 상황인 경우
- 사용자가 버튼을 눌러 setState를 호출해 위젯의 내부 상태가 갱신되어 플러터가 위젯을 다시 빌드하고 그리는 상황인 경우
플러터 렌더링: 내부 동작 원리
플러터의 진정한 능력은 앱을 셀 수 없이 여러 번 리빌드할 때 발휘된다. 플러터는 눈 깜짝할 새에 거대한 위젯 트리widget tree를 빌드한다. 다음 그림은 한 페이지의 장바구니 위젯 트리가 어떻게 생겼는지를 가상으로 보여준다(현실의 트리는 이보다 더 거대하다).
CartItem 위젯을 살펴보자. 이 위젯은 상태를 가지며 위젯의 자식은 위젯 상태에 의존한다. CartItem 위젯의 상태가 바뀌면 이 위젯을 포함한 모든 하위 위젯이 다시 그려진다.
그림_ 위젯 트리 모습이다. 현실의 위젯 트리는 훨씬 많은 위젯을 포함한다
플러터 위젯은 리액티브다. 즉 외부(또는 setState)에서 새 정보를 얻으면 이에 반응하고, 필요하면 플러터가 위젯을 다시 그린다. 이를 간략히 설명하면 다음과 같다.
1 사용자가 버튼을 누름
2 Button.onPressed 콜백에서 setState를 호출함
3 Button의 상태가 dirty로 바뀌었으므로 플러터는 이 위젯을 리빌드함
4 트리에서 기존 위젯을 새 위젯으로 바꿈
5 플러터가 새 트리를 그림
위젯을 새로 바꿨으므로 이제 위젯을 새로 그릴 수 있다. 렌더링도 여러 과정을 거쳐 진행된다. 다음 그림은 전체적인 플러터의 렌더링 과정이며 세 번째 단계를 눈여겨보자.
전체적인 렌더링 과정
애니메이션 티커animation ticker가 동작하면서 그리기 작업을 시작한다. 예를 들어 리스트를 아래로 스크롤하는 등 위젯을 다시 그려야 하는 상황에서는 화면의 처음 요소 위치에서부터 최종 위치까지 조금씩 이동하면서 애니메이션이 부드럽게 일어난다. 이 과정은 요소가 움직여야 하는 시간을 결정하는 애니메이션 티커가 제어한다. 이렇게 애니메이션이 일어나는 동안 플러터는 프레임마다 위젯을 리빌드하고 그린다. 이 책의 뒷부분에서 플러터의 애니메이션 기능을 자세히 알아본다.
위젯 트리와 레이아웃 조립
플러터는 모든 위젯을 빌드하고 위젯 트리를 만든다. 여기서 위젯이란 화면에 나타날 요소를 결정하는 데이터와 설정을 말한다. 트리에서 버튼을 빌드할 때 실제로 파란색 사각형과 그 안의 텍스트를 빌드하는 것이 아니다. 오히려 이는 마지막 과정에서 일어난다. 위젯은 화면에 나타 날 요소의 설정을 처리할 뿐이다.
위젯 트리가 완성되면 플러터는 레이아웃을 처리한다. 플러터는 필요할 때 트리를 한 차례 탐색한다(선형 시간linear time 소요). 빅오 표기법Big O notation에 익숙하지 않은 독자라면 선형 시간은 빨리 수행된다는 의미라고 생각하면 된다. 트리를 탐색하면서 위젯의 위치 정보를 수집한다. 플러터에서 레이아웃과 크기 제약constraint은 부모에서 자식 위젯 순으로 작성된다.
트리를 거슬러 올라오면서 모든 위젯은 자신의 제약을 알고 있는 상태이므로 실제 크기와 위치를 부모 위젯에 알린다. 위젯은 위젯끼리 서로의 관계를 정리하면서 최종 레이아웃을 결정한다.
장바구니 예제에서 QuantityWidget의 [+] 버튼을 누르면 새로운 수량으로 상태가 바뀌고, 플러터는 위젯 트리를 탐색하면서 내려간다. 이때 QuantityWidget은 버튼과 텍스트 필드에 제약(실제 크기가 아님) 정보를 알려준다. 그러면 버튼은 [+], [-] 아이콘에 이들의 제약을 알려주는 등의 순서로 트리를 타고 내려가며 정보를 전파한다. 단말 노드의 위젯에 도달하면 모든 위젯은 크기 제약 정보를 획득한 상태다. 이제 트리를 거슬러 올라오면서 각 위젯의 크기와 위치를 안전하게 계산할 수 있다. 다음 그림에 이 과정을 묘사했다(예제의 트리는 현실의 플러터 앱 트리보다 훨씬 작다).
그림_ 플러터는 트리를 한 번 내려갔다 올라오면서 모든 자식의 크기 제약 정보를 수집해 모든 위젯의 레이아웃을 완성한다.
이렇게 한 번의 위젯 트리 탐색은 강력하다. 반면 브라우저에서는 DOM, CSS 규칙으로 레이 아웃을 결정한다. CSS의 중첩 특성상 요소의 크기와 위치는 본인이나 부모가 결정하며, 모든 요소의 위치를 결정하려면 DOM 트리를 여러 번 탐색해야 한다. 모던 웹의 가장 큰 성능병목 현상이 여기서 발생한다.
조립 과정
각 위젯의 레이아웃을 결정했고 다른 위젯과 충돌하지 않음을 확인했으니 플러터는 위젯을 그린다. 하지만 이 과정에서도 위젯을 래스터라이징rasterizing하거나 물리적으로 화면에 픽셀을 칠하는 건 아니다.
여기서 조립 과정을 진행한다. 이 과정에서 플러터는 위젯에 실제 화면상의 좌표를 제공하며 위젯은 자신이 차지할 실제 픽셀의 수를 알게 된다.
이 과정은 의도적으로 그리기 과정과 별도로 진행된다. 덕분에 기존에 조립된 위젯을 다시 재사용할 수 있다. 예를 들어 다음 그림처럼 긴 리스트를 스크롤했다고 가정하자. 리스트의 모든 항목을 다시 빌드할 필요 없이 플러터는 기존에 빌드하고 그렸던 위젯을 필요한 곳에 재사용한다.
그림_ 조립 과정을 그리기 과정과 분리해 성능을 높였다.
화면에 그리기
이제 위젯이 준비됐다. 엔진은 전체 트리를 그릴 수 있는 뷰로 모은 다음, 운영체제를 통해 화면에 그리도록 요청한다. 이를 래스터라이징이라 부르며 이 과정을 끝으로 위젯이 화면에 그려진다.
고작 몇 개의 문단으로 전체 프레임워크를 살펴봤다. 정말 많은 내용이다. 플러터가 정확히 어떻게 동작하는지 이해가 가지 않아도 걱정할 필요는 없다. 책 전체에서 계속 설명하기 때문이다. 무엇보다 개발자로 반드시 알아야 할 플러터의 네 가지 주요 개념을 기억하자.
● 플러터는리액티브다.
● 모든 것은 위젯이다.
● State 객체는 오래 살아남으며 종종 재사용된다.
● 위젯의 제약은 부모가 서술한다.
마치며
플러터는 사용하기 쉽고 강력한 도구이지만 잘 사용하기 위해서는 노력이 필요하다. 여러분이 리액트 웹 개발자가 아니라면 UI의 새로운 패러다임을 적용해야 한다. 책으로 플러터를 배우다가 좌절하는 순간이 올 수 있지만 여러분의 잘못이 아니다. 사실 이런 현상이 발생하는 이유가 있다. 우선 여러분은 다른 사람들과 같다. 즉, 프로그래밍은 어렵고 배우는 데 시간이 걸린다. 자료를 여러 번 읽어보고 낮잠을 잔 다음 다시 복습하자. 어느 순간 여러분은 플러터를 깨우칠 것이다. 또 다른 이유로는 필자가 이해하기 쉽게 책을 집필하지 못했기 때문일 수 있다. 플러터를 선택한 것에 문제를 겪었다면 언제든 비난을 들을 준비가 되어 있다. 어느 곳에서든 @ericwindmill로 필자를 찾으면 된다.
● 플러터는 모든 사람이 아름답고, 좋은 성능의 모바일 앱을 만들 수 있는 모바일 SDK이며 다트로 구현되었다.
● 다트는 구글이 만든 언어로 자바스크립트로 컴파일할 수 있다. 다트는 빠르며, 엄격한 형식을 지원하고 배우기 쉽다.
● 플러터는 네이티브 디바이스 코드로 컴파일되므로 다른 크로스 플랫폼 기술보다 성능이 뛰어나다. 또한 다트의 JIT, 플러터의 핫 리로드 덕분에 최상의 개발자 경험을 제공한다.
● 플러터는 훌륭한 성능의 크로스 플랫폼 앱을 빨리 만들어야 하는 사람에게 적합하다. 하지만 두 개의 네이티브 팀을 이미 보유한 큰 회사에게는 플러터가 좋은 선택이 아닐 수 있다.
● 플러터의 모든 것은 위젯이다. 위젯은 뷰를 묘사하는 단순한 다트 클래스다. 여러 작은 위젯을 조립해 위젯 트리를 완성하며 UI를 만든다.
● 위젯은 크게 상태가 없는 위젯과 상태가 있는 위젯으로 분류된다.
● 플러터는 위젯 생명주기 메서드, 특별한 State 객체 등 상태 관리도구를 제공한다.
· · ·