저는 이제 3년차가 된 주니어 앱 개발자로, 안드로이드로 개발자로서의 커리어를 시작했으나 현재는 플러터를 주력으로 사용하고 있습니다. 플러터 개발을 한 지는 이제 1년 반 정도 된 것 같은데요, 개발 도메인임에도 불구하고 그동안 플러터라는 프레임워크에 대해 제대로 이해하는 시간을 가지지 못한 채 일에만 집중했던 것 같습니다. 안드로이드 네이티브 개발을 할 때도 비슷한 경험을 해서, 안드로이드에 대해 이해하고자 Android Quick Overview 라는 글을 작성한 적이 있는데요, 플러터 및 다트의 특징/플러터가 화면을 렌더링하는 과정 등 대략적인 것만 이해하고 있어도 개발을 하는데 좀 더 도움이 될 것이라고 생각해서 이번에도 이렇게 글을 작성하게 되었습니다.
목차는 다음과 같습니다.
아직 많이 부족하지만 공식 문서와 유튜브들, 기타 테크 블로그들을 참고하여 글을 작성했습니다. 이 글이 플러터 개발을 처음 시작하시려는 분들께도 도움이 되기를 바랍니다.
플러터나 리액트 네이티브가 등장하기 전에는 각 플랫폼 별로 앱을 개발해야 했습니다. 안드로이드는 안드로이드 스튜디오를 기반으로 자바 또는 코틀린을 사용해서, iOS의 경우 XCode를 기반으로 Object C 또는 스위프트를 사용하여 각각의 플랫폼을 개발해야 했습니다. 하지만 모바일 앱을 하나의 플랫폼을 타겟으로만 만드는 경우는 거의 없기에, 앱을 개발할 경우 안드로이드 네이티브 개발 인력과 iOS 네이티브 개발 인력이 모두 필요했죠. 또는 iOS만 타겟해서 개발했다가 추후에 안드로이드를 타겟으로 개발해야 하는 상황도 있었습니다. 이는 비용면에서도, 시간면에서도 효율적이지 못한 상황이었습니다.
이같은 비효율적인 상황을 해결하기 위해 페이스북(현 메타)에서는 리액트 네이티브, 구글은 플러터라는 크로스플랫폼 앱 개발이 가능한 프레임워크를 출시했습니다.
플러터는 앞서 말씀드렸듯 크로스플랫폼 앱 개발 프레임워크입니다. 플러터를 사용하여 우리는 안드로이드, iOS 앱 뿐만 아니라 데스크탑 앱까지 단일 코드베이스, 즉 Dart라는 언어 하나로 다 개발할 수 있습니다. 그뿐 아니라 Flame이라는 게임 엔진을 사용해서 게임까지 개발할 수 있죠.
플러터는 왜 공식 언어로 다트를 택했을까요? 이는 공식 유튜브와 문서에도 명시되어 있는데요, 다트는 모든 플랫폼에서 빠른 앱을 개발하기 위해 클라이언트에 최적화된 언어이기 때문입니다. UI 개발에 최적화되어 있을 뿐만 아니라, hot reload를 통한 생산적인 개발이 가능하며, 모든 플랫폼에서 빠르게 컴파일 될 수 있습니다. 또한 자바스크립트와 유사하여 웹을 개발하던 사람들은 플러터에 빠르게 적응할 수 있다는 장점도 있습니다. 플러터는 오픈소스이고, 오픈소스 생태계에서 살아남기 위해서는 아무래도 개발자들이 적응하기 쉬운 언어가 접근성을 높이는데 좋기도 하고요, 안드로이드가 최초 공식 언어로 자바를 택한 이유처럼요.
플러터가 다트를 선택한 이유에 대한 공식 영상을 보고싶다면 아래 링크를 클릭해주세요.
Flutter Dev - Why Flutter uses Dart
사실 플러터는 UI Toolkit이라고도 불립니다. 우리가 컴퓨터에 설치된 Android Studio 또는 VS Code를 기반으로 플러터를 사용해 원하는 화면을 코드로 작성하면, 고성능의 플러터 엔진은 그에 맞게 화면을 앱에 렌더링해줍니다. 플러터가 화면을 렌더링하는 과정에 대해서는 Flutter Architectural Overview에서 설명하도록 하겠습니다.
다트에 대해서 잠시 소개하자면, 다트는 구글이 개발한 순수 객체지향 언어입니다. 앞서 언급했듯 자바스크립트와 유사하지만 훨씬 더 간단하고, 빠르며 사용자 친화적으로 크로스 플랫폼 앱 개발에 최적화되어 있습니다. 하지만 아직은 플러터 외에는 거의 쓰이지 않는 게 단점이기도 하죠. 이러한 이유 때문에 네이티브보다는 확장성 면에서 경쟁력이 떨어진다는 게 개인적으로 좀 아쉽기도 합니다. 하지만 네이티브 앱 개발과 비교한다면 좀 더 단순하고 쉽게 앱 개발할 수 있는 것엔 동의합니다. 다트가 자바, 코틀린과 같은 멀티 스레드 언어가 아닌 자바스크립트와 같은 싱글 스레드 언어이기 때문입니다.
다트는 싱글 스레드이자 동시성 프로그래밍 컨셉의 언어입니다. 스레드는 기본적으로 자원 공유가 가능하다는 특징이 있고, 따라서 멀티 스레드 환경에서 작업들을 수행할 시 서로 자원을 기다리다가 예기치 못한 오류로 자원을 무한히 기다리게 되는 Deadlock 현상이 발생할 수 있습니다. 또한 멀티 스레드 환경에서의 처리과정은 싱글 스레드 환경보다 다소 복잡하고 오류가 발생하기 쉬었기에, 다트는 싱글 스레드 방식을 내세우며 이러한 단점들을 극복하였습니다.
추가적으로 설명하자면 싱글 스레드 언어이긴 하지만 마치 멀티 스레드가 가능한 것처럼 작업할 수 있도록 isolate이라는 것을 지원하고 있습니다. 보통 사진, 오디오 및 비디오를 처리하고 압축할 때, 오디오 및 미디오 파일을 변환할 때, 대량의 네트워크 요청을 처리할 때 등과 같이 무거운 연산을 해야할 때 사용합니다(저도 아직 isolate을 써본 적은 없습니다). isolate은 메인 스레드와 분리되어 동작할 수 있으며, 독립적인 메모리 공간을 가집니다. 다만 서로 다른 isolate 간에는 직접적인 데이터 공유가 불가능하기 때문에 스레드라고 할 수는 없습니다.
플러터는 여러 플랫폼을 단일 언어로 개발할 수 있되, 굉장히 빠른 빌드 속도를 자랑합니다. 또한 부드럽고 자연스러운 화면을 위해 1초에 최소 60번 이상 화면을 렌더링합니다. 이같은 작업은 굉장히 많은 연산을 요구하며, 일일이 처리하다간 오버헤드가 늘어날 수 있습니다.
하지만 플러터는 빠르고 효율적인 렌더링 성능을 보장하기 위해 상태가 변경된 UI만 화면에 보여줄 수 있도록 필요한 부분만 렌더링합니다. 어떻게 이런 작업이 가능한 것일까요?
플러터는 빠르고 효율적인 렌더링 성능을 보장하기 위해 트리 구조를 사용합니다. 저도 렌더링 과정을 깊게 파보지는 않았고 원리만 이해하고 있는데요, 이것을 이해하는데 유튜버 [코딩셰프] 님의 강의가 굉장히 도움이 되었습니다. 강의를 보고싶으신 분들은 아래 링크를 참고해주세요.
코딩셰프 : 플러터(Flutter) 조금 매운맛🌶️ 강좌 1| Stateful widget part 1Flutter’s 3 tree architecture
플러터는 위젯 트리, 엘리먼트 트리, 렌더 트리 이 3가지의 트리를 기반으로 화면을 효율적으로 렌더링하고 있습니다.
플러터 개발자는 명시된 디자인대로 앱을 개발하기 위해 코드를 작성합니다. 이 코드를 기반으로 생성되는 것이 바로 위젯 트리입니다. 플러터 앱은 '위젯'이라는 UI 요소로 구성되며, 위젯은 화면 전체가 될 수도 있고 화면을 구성하는 하위 구성요소가 될 수도 있습니다.
class HomeScreen extends StatlessWidget {
Widget build(BuildContext context) {
return MaterialApp(
...
);
}
}
위젯 트리는 불변이기 때문에 한번 생성될 때 변경되지 않으며, 변경되려면 아예 새롭게 생성되어야 합니다. 따라서 우리가 만약 버튼의 색상이나 텍스트를 변경하면, 위젯 내의 build()
라는 함수가 호출되고, 이 때 기존 위젯 트리는 폐기되고 새 위젯트리가 생성됩니다.
위젯 트리처럼 엘리먼트 트리와 렌더 트리까지 리빌드가 된다면 앱 성능에 좋지 않을 것입니다. 하지만 나머지 두 트리는 매번 리빌드되지 않습니다.
엘리먼트 트리와 렌더 트리는 위젯 트리를 기반으로 생성되며, 위젯 트리와 다르게 플러터가 내부적으로 컨트롤하는 트리입니다. 위젯 트리가 생성됨과 동시에 엘리먼트 트리도 생성되는데, 이 때 엘리먼트 트리는 위젯 트리와 일대일로 대응되어 위젯 트리의 정보를 갖고 있습니다.
이 정보를 기반으로, 엘리먼트 트리는 위젯 트리가 리빌드 될 때 기존 위젯 트리와 비교해서 변경된 부분(위젯의 속성, 예를 들어 버튼의 배경색이나 텍스트 사이즈같은 것들)이 있다면 변경된 부분을 렌더 트리에 전달합니다.
렌더 트리는 렌더 객체(Render Object)로 구성되어 있으며, 렌더 객체는 레이아웃 및 페인팅을 담당합니다(실제 화면에 보이는 것이 바로 렌더 객체입니다). 따라서 변경된 부분에 대한 정보를 전달받은 렌더 트리는 해당 정보를 기반으로 기존 렌더 객체에서 변경된 부분만 업데이트 할 수 있습니다.
결론적으로, 플러터는 위 같은 방식으로 3가지 트리를 적절히 활용하여 변경된 부분만 화면을 렌더링하고 있기 때문에 빠르고 효율적인 것입니다.
주니어 플러터 개발자로서, 크로스플랫폼 앱개발 프레임워크는 아직 네이티브에 비해 퍼포먼스 면에서 부족하다곤 하지만, 이상하게 안드로이드 네이티브 개발을 할 때보다 더 재밌습니다. 아무래도 네이티브 개발할 때 처럼 라이프사이클 관리같은 것들을 덜 신경써도 되고, 언어 syntax 면에서 코틀린이나 자바 보다는 다트가 쉬우며, 빌드가 빨라서일까요?
이번 글을 통해 플러터라는 프레임워크에 대해서 이해하는 시간을 가질 수 있었습니다. 매번 느끼지만 하나의 목적을 위해 여러명의 뛰어난 실력을 가진 개발자들이 모여 툴을 만드는 것이 저에게는 너무 대단하게 느껴짐과 동시에, 스스로가 아직 많이 부족하고 많이 공부해야 함을 깨닫습니다. 그래도 네이티브 개발할 때보단 재밌어서 하나하나 알아가는 것이 재밌게 느껴지는 요즘입니다 :)
Flutter Dev - Why Flutter uses Dart
Flutter Dev - Flutter architectural overview
코딩셰프 : 플러터(Flutter) 조금 매운맛🌶️ 강좌 1| Stateful widget part 1Flutter’s 3 tree architecture