결론부터 말하자면 Tree Shaking 과 AOT 때문이다.
1. Tree Shaking 이란?
💡 트리 쉐이킹(Tree Shaking)이란
사용되지 않는(dead) 코드를 제거하여 앱 크기를 줄이고 최적화하는 기법이다.
Flutter는 트리 쉐이킹을 통해 앱 크기를 줄이고 성능을 최적화한다. 즉, 쓰지 않는 코드를 "흔들어서 떨어뜨린다(Shaking the Tree)".
이때 플러터의 트리 쉐이킹은 웹프론트 진영의 웹팩 최적화에서 사용되는 트리쉐이킹 원리와 거의 동일하다. 둘다 빌드할 때 참조로 실행되는 코드만 남기고 나머지는 삭제하는 방법을 채택하고 있다. Flutter에서는 dart2js와 dart-compiler가 이 최적화를 수행한다.
만약 위 코드가 빌드된다면 unusedFunction()은 어디에서도 호출되지 않으므로, Flutter 빌드 시 자동으로 제거된다.
2. 트리쉐이킹 적용되면 Reflection이 안된다!
하지만 Swift는 트리쉐이킹을 채택하지않고 있다. 왜 이렇게 좋은 TreeShaking을 어떤언어는 채택하지 않고 어떤언어는 채택하고 있는걸까? 모든 기술선택에는 트레이드 오프가 있듯이 트리쉐이킹을 하게되면 런타임에 대한 개발자의 지배력을 잃게된다. 대표적으로 reflection을 하지 못하게 된다.
💡 Reflection 이란?
구체적인 클래스 타입을 알지 못하더라도 런타임에서 해당 클래스의 메서드, 변수에 접근할 수 있는 기법이다. 주로 컴파일 타임에 타입을 알 수 없는 경우에 사용된다.
protocol FormField {
var label: String { get }
var value: Any { get set }
}
class UserForm {
var name: String = ""
var age: Int = 0
}
func generateForm(for object: Any) {
let mirror = Mirror(reflecting: object)
for (property, _) in mirror.children {
if let property = property {
print("Generate field for: \(property)")
}
}
}
let form = UserForm()
generateForm(for: form)
// 출력:
// Generate field for: name
// Generate field for: age
Swift는 reflection 기능을 Mirror를 통해 제공해주고 있다. 위 코드를 보면 알 수 있듯이 컴파일 타임에서는 할 수 없는걸 런타임에서 객체를 낚아채서 별에별걸 다할 수 있다. 사실상 머든 다할 수 있다. 하지만 당연히! 런타임에서 하는거라 성능도 떨어지고 타입안정성도 보장이 안되기 때문에 크래쉬날 각오하고 써야할 기능이다. 따라서 본래 프로덕션 코드에 적용하기 보다는 로그 처리, 테스트 코드 작성시 프라이빗 프로퍼티/메서드 들여다볼때등 프로덕션 코드에 얹혀서 무언가 하고 싶을때 사용되곤 한다. 문제는 트리쉐이킹을 하게되면 이렇게 무언가를 "얹혀서" 하는게 불가능해진다, 당연히 reflection도 안된다.
https://www.youtube.com/watch?v=awpdM665y-k&t=778s
위 세션은 Objective-C의 NSMutableString 이용해 메모리에서 원본 문자열을 노출시키지않게하는 고오오오오오오급 스킬에 대해서 소개하고 있다. 이렇게 옵젝C가 런타임에 동적으로 코드와 메모리에 접근해서 마술부리는 걸 Flutter는 하지 못한다는 것이다. (트리쉐이킹 뿐만 아니라 AOT(Ahead-of-Time) 컴파일 때문이기도 하다 - AOT: 앱 실행 전에 모든 코드가 이미 컴파일되므로, 런타임에서 코드를 변경할 수 없음)
이렇게 tree shaking 과 AOT를 얻고 reflection 과 런타임에 마법부리기를 포기하는 것이 함의하는바는 고오오오오오급 기능을 포기하고 대신 컴팩트하고 날렵한 앱을 만들겠다는 플러터의 의지이다.
3. 앱의 가벼움과 런타임 조작이 안되는 단점을 보완하기 위한 차선책: Code Gen
아무리 플러터가 리플랙션을 포기했다고는 하지만 이게 없으면 코드 유연성이 줄어든다. 모든 로직을 프로덕션 코드에 전부 박아야하기 때문이다. 그래서 개발자들이 생각해낸 것이 바로 Code Gen이다.
즉 런타임에서 동적으로 코드를 실행할 작업들을 CodeGen을 통해 아예 미리 코드로 만들어버리는 것이다! 이렇게 미리 코드를 정의함으로써 트리 쉐이킹에 의해 삭제되는 일도 방지하고 실제 프로덕션 코드와 build runner로 생성되는 코드를 분리시키는 일도 가능해지는 것이다! 이렇게 Flutter는 코드 젠을 통해 런타임 오버헤드를 없애고, 런타임에 하고 싶은, 무언가 "얹혀서" 하고 싶은 로직을 실제 프로덕션 코드와 분리시켜 두마리 토끼를 모두 잡을 수 있다.
다른 모바일 개발자들은 어떻게 생각할지 모르겠으나 나는 개인적으로 플러터 방식이 더 마음에 든다😅 정말 카카오톡이나 토스처럼 고오오오오급 기능이 필요한 경우에는 메서드 채널링으로 부분적으로 문제를 해결하고 대부분의 코드는 런타임에 코드 접근을 차단해버리는 것이 더 맞다고 생각하기 때문이다 :)
'Flutter' 카테고리의 다른 글
디자인시스템 버튼 비활성화를 구현하면서 마주친 트러블 슈팅 (0) | 2025.01.16 |
---|---|
코디네이터 패턴을 플러터에 적용하며 생긴 _debugLocked 에러 (0) | 2025.01.14 |
플러터 웹은 isolated 를 지원하지 않는다. 만약 웹에서 동시성을 하고 싶다면.. (0) | 2024.11.01 |
Tabbar 정렬 및 여백조정이 안되는 이슈 (0) | 2024.08.31 |
Warning: integration_test plugin was not detected. 이슈 (0) | 2024.08.14 |