Flutter는 앱이 아니라 게임을 렌더링합니다: Unity 개발자가 본 아키텍처 심층 분석

혹시 앱을 만들다가 "이거, 어딘가 게임 만드는 것과 비슷한데?"라고 느껴본 적 있으신가요? 특히 Unity나 Unreal 같은 게임 엔진을 다뤄본 개발자라면 Flutter를 처음 접했을 때 묘한 기시감을 느꼈을지도 모릅니다. 위젯을 조립해 UI를 만드는 과정은 마치 게임 오브젝트를 씬(Scene)에 배치하는 것과 흡사하고, 상태(State)를 변경해 화면을 갱신하는 모습은 게임 루프 속에서 변수를 조작해 캐릭터의 움직임을 만들어내는 원리와 닮아있습니다. 이것은 우연이 아닙니다. Flutter는 태생부터 앱을 '만드는' 방식이 아니라, '렌더링하는' 방식에 있어 게임 엔진의 철학을 깊숙이 공유하고 있기 때문입니다.

이 글에서는 Flutter의 아키텍처를 게임 엔진, 특히 Unity의 씬 그래프(Scene Graph)와 게임 루프(Game Loop) 개념을 통해 심층적으로 해부하고자 합니다. 왜 Unity 개발자가 다른 모바일 앱 개발자보다 Flutter에 더 빠르게 적응하는지, 그 근본적인 이유를 기술적인 관점에서 파헤쳐 봅니다. Widget, Element, RenderObject로 이어지는 Flutter의 3-트리(Three-Tree) 구조가 어떻게 게임 엔진의 렌더링 파이프라인과 맞닿아 있는지, 그리고 최신 렌더링 엔진 '임펠러(Impeller)'가 어떻게 Metal, Vulkan과 같은 로우레벨 그래픽 API를 직접 제어하며 '버벅임(Jank)' 없는 60/120fps 애니메이션에 집착하는지를 따라가다 보면, 여러분은 Flutter가 단순한 UI 툴킷이 아닌, UI를 위한 고성능 실시간 렌더링 엔진이라는 사실을 깨닫게 될 것입니다.

1. 픽셀을 장악하다: OEM 위젯 vs 캔버스 렌더링

대부분의 크로스 플랫폼 프레임워크는 OS가 제공하는 네이티브 위젯(OEM Widgets)을 다리(Bridge)를 통해 제어하는 방식을 취합니다. React Native가 대표적입니다. 하지만 Flutter는 이 방식을 거부했습니다. 대신 게임 엔진처럼 OS로부터 빈 캔버스(Surface) 하나만을 할당받습니다. 그리고 그 위에 Skia(혹은 Impeller)라는 자체 그래픽 엔진을 사용하여 모든 픽셀을 직접 그립니다.

이것이 의미하는 바는 큽니다. 게임 개발자가 Unity에서 버튼을 만들 때 Android의 android.widget.Button을 호출하지 않고, 텍스처(Texture)를 렌더링하여 버튼처럼 보이게 만드는 것과 정확히 동일한 원리입니다.

기술적 사실: Flutter 앱은 기술적으로 안드로이드의 GLSurfaceView (또는 Vulkan Surface) 위에서 돌아가는 단 하나의 Activity일 뿐입니다. 내부의 모든 UI는 Flutter 엔진이 GPU 셰이더를 통해 그려낸 그래픽일 뿐, 네이티브 뷰가 아닙니다.

2. Scene Graph의 재림: Widget, Element, RenderObject

Unity 개발자라면 'Prefab', 'GameObject', 'Component'의 관계를 잘 알 것입니다. Flutter의 트리 구조는 이와 소름 돋을 정도로 유사합니다. 많은 개발자가 Widget만 신경 쓰지만, 성능 최적화를 위해서는 보이지 않는 RenderObject를 이해해야 합니다.

  • Widget (Blueprint): Unity의 Prefab입니다. 불변(Immutable)이며, UI가 어떻게 그려져야 할지에 대한 설정값(Configuration)만 담고 있습니다. 가볍고 빠르게 생성/파괴됩니다.
  • Element (Instance): Unity의 Hierarchy에 실제 존재하는 GameObject입니다. Widget의 설정을 바탕으로 트리에 마운트된 실체입니다.
  • RenderObject (Renderer): Unity의 MeshRenderer + Transform입니다. 실제 레이아웃(Layout) 계산과 페인팅(Painting)을 담당합니다. 가장 무겁고, 가능한 한 재사용됩니다.

다음은 커스텀 렌더링을 위해 RenderObject를 직접 조작하는 코드 예시입니다. 일반적인 앱 개발보다는 게임 개발의 렌더링 로직과 훨씬 유사함을 알 수 있습니다.

class GameLikeRenderObject extends RenderBox {
  // Unity의 Update() 루프처럼 매 프레임 호출될 수 있는 페인팅 로직
  @override
  void paint(PaintingContext context, Offset offset) {
    // 캔버스를 직접 가져옴 (OpenGL/Vulkan 컨텍스트와 유사)
    final Canvas canvas = context.canvas;
    
    final Paint paint = Paint()
      ..color = const Color(0xFF00FF00)
      ..style = PaintingStyle.fill;

    // GPU를 이용해 직접 드로잉 명령 수행
    canvas.drawCircle(offset + const Offset(50, 50), 40.0, paint);
  }

  // 레이아웃 계산 (Unity의 RectTransform 계산과 유사)
  @override
  void performLayout() {
    // 부모 제약 조건(Constraints)에 따라 자신의 크기 결정
    size = constraints.constrain(const Size(100, 100));
  }
}
성능 경고: setState()를 남발하면 Widget 트리가 재생성됩니다. 하지만 Flutter는 Diff 알고리즘을 통해 ElementRenderObject를 최대한 보존(Reuse)합니다. 만약 key를 잘못 사용하여 Element 트리까지 불필요하게 파괴한다면, 게임에서 매 프레임 모델을 Destroy하고 Instantiate하는 것과 같은 성능 저하가 발생합니다.

3. Impeller: 셰이더 컴파일과 Jank(버벅임)와의 전쟁

초기 Flutter(Skia 엔진 사용)는 iOS에서 악명 높은 'Shader Compilation Jank' 문제를 겪었습니다. 애니메이션이 처음 실행될 때 셰이더를 실시간으로 컴파일하면서 프레임이 드랍되는 현상입니다. 이는 PC 게임을 처음 켤 때 발생하는 스터터링(Stuttering)과 정확히 같은 현상입니다.

이에 대한 Flutter 팀의 해결책은 Impeller라는 새로운 렌더링 엔진을 도입하는 것이었습니다. Impeller의 핵심 철학은 AAA 게임 엔진과 같습니다.

  • AOT(Ahead Of Time) Shader Compilation: 앱 빌드 시점에 모든 셰이더를 미리 컴파일합니다. 런타임 컴파일이 없으므로 첫 실행 시에도 60fps가 보장됩니다.
  • Direct API Access: OpenGL을 거치지 않고 Metal(iOS)과 Vulkan(Android)을 직접 제어하여 오버헤드를 줄입니다.
구분 React Native / Web Hybrid Flutter (Impeller/Skia) Unity / Unreal
UI 구성 OS 네이티브 위젯 래핑 자체 렌더링 (픽셀 직접 제어) 자체 렌더링 (폴리곤/텍스처)
통신 방식 JS Bridge (비동기, 병목 발생) FDI (C++ 엔진과 직접 통신) C++/C# 스크립팅
그래픽 API OS 상위 레벨 API Metal, Vulkan, OpenGL Metal, Vulkan, DirectX
일관성 OS 버전에 따라 다름 모든 OS에서 픽셀 단위 동일 모든 기기에서 동일

Conclusion

Flutter를 단순한 "앱 개발 프레임워크"로 바라보는 것은 이 도구의 잠재력을 절반도 이해하지 못한 것입니다. Flutter는 본질적으로 2D 그래픽에 최적화된 게임 엔진입니다. 씬 그래프(Tree)를 관리하고, 게임 루프(vsync)에 맞춰 캔버스에 픽셀을 쏘아주는 이 구조를 이해한다면, 왜 Flutter가 그토록 부드러운 애니메이션을 보여줄 수 있는지, 그리고 왜 Unity 개발자들이 Flutter에 열광하는지 명확해집니다.

복잡한 인터랙션이나 고성능 애니메이션이 필요한 앱을 개발해야 한다면, 이제 앱 개발자가 아닌 '게임 엔진 엔지니어'의 시선으로 Flutter 코드를 작성해 보십시오. RenderObjectCustomPainter의 세계로 내려가는 순간, 여러분이 제어할 수 없는 픽셀은 사라질 것입니다.

Post a Comment