Flutter로 게임 개발? Flame 엔진 렌더링 루프 최적화 및 60FPS 방어 전략

최근 핀테크 앱 내에 유저 리텐션을 위한 '러닝 액션 미니 게임'을 탑재해야 하는 과제를 맡게 되었습니다. "그냥 Flutter의 Positioned 위젯으로 대충 움직이면 되지 않나?"라고 가볍게 접근했던 것이 화근이었습니다. 초기 프로토타입은 갤럭시 S21급 기기에서도 간헐적인 프레임 드랍(Jank)이 발생했고, 특히 오브젝트가 50개 이상 생성되는 시점부터 UI 스레드가 버벅거리는 현상이 목격되었습니다. 일반적인 앱 개발과 게임 개발의 렌더링 파이프라인이 근본적으로 다르다는 것을 간과한 결과였습니다. 오늘은 Flutter로 게임을 개발할 때 반드시 마주치게 되는 '렌더링 루프' 문제와 이를 Flame 엔진으로 해결한 기술적 경험을 공유하려 합니다.

Flutter의 UI 렌더링과 게임 루프의 충돌

보통 Flutter 개발자들은 상태 변화를 위해 setState()Riverpod 같은 상태 관리 라이브러리를 사용합니다. 하지만 게임은 다릅니다. 게임은 초당 60회(60FPS) 이상 화면을 강제로 다시 그려야 합니다.

제가 처음 겪었던 문제는 Flutter의 위젯 트리 재구성(Reconciliation) 비용이었습니다. 캐릭터가 움직일 때마다 setState를 호출하니, Flutter 프레임워크는 Element TreeRenderObject Tree를 계속해서 비교하고 다시 그리는 작업을 수행했습니다. 정적인 UI에서는 이 비용이 미미하지만, 매 프레임마다 수십 개의 오브젝트 좌표가 바뀌는 게임 환경에서는 치명적인 병목이 됩니다. 특히 안드로이드 저사양 기기 테스트 환경(Snapdragon 400번대)에서는 30FPS 방어조차 힘들었습니다.

Critical Error: UI Thread blockage detected. skipped 34 frames! The application may be doing too much work on its main thread.

로그캣(Logcat)에 붉은색으로 찍히는 skipped frames 경고는 단순한 경고가 아니라, 배터리 광탈과 발열의 전조 증상입니다. 우리는 선언형 UI(Declarative UI) 방식이 아닌, 명령형 렌더링(Imperative Rendering) 방식이 필요했습니다.

실패한 접근: Timer와 Positioned 위젯

처음에는 외부 엔진 도입을 꺼려 Timer.periodic을 사용하여 16ms(약 60FPS)마다 Stack 내부의 Positioned 위젯 좌표를 수정하는 방식을 시도했습니다.

이 방식은 두 가지 심각한 문제를 야기했습니다.

  1. Layout Thrashing: 위젯의 위치가 바뀔 때마다 Flutter 레이아웃 패스(Layout Pass)가 전체적으로 다시 계산되었습니다.
  2. 타이머의 부정확성: Dart의 Timer는 OS의 스케줄링에 의존하므로 정확히 16ms를 보장하지 않습니다. 이로 인해 캐릭터가 순간이동하거나 끊겨 보이는 '스터터링(Stuttering)' 현상이 발생했습니다.

해결책: Flame 엔진을 통한 Game Loop 제어

결국 우리는 Flutter 생태계에서 사실상의 표준 게임 엔진인 Flame을 도입하기로 결정했습니다. Flame은 Flutter 위젯 트리 내부에서 동작하지만, 내부적으로는 Game Loop 패턴을 사용하여 RenderObject에 직접 그리기 명령을 내립니다. 이를 통해 불필요한 위젯 빌드 과정을 생략할 수 있습니다.

다음은 최적화된 게임 루프의 핵심 코드입니다. update 메서드에서 dt(Delta Time)를 활용하는 것이 중요합니다.

// MainGame.dart
// FlameGame을 상속받아 게임 루프를 직접 제어합니다.
import 'package:flame/game.dart';
import 'package:flame/components.dart';

class SurvivalGame extends FlameGame {
  late PlayerComponent player;

  @override
  Future<void> onLoad() async {
    await super.onLoad();
    // 이미지 리소스 프리로딩 (중요: 런타임 랙 방지)
    await images.load('player_sprite.png');
    
    player = PlayerComponent();
    add(player); // 위젯 트리가 아닌 Flame Component 시스템에 등록
  }

  @override
  void update(double dt) {
    super.update(dt);
    // 게임 전역 로직 처리
    // dt: 이전 프레임과 현재 프레임 사이의 시간 차 (초 단위)
  }
}

class PlayerComponent extends SpriteComponent with HasGameRef<SurvivalGame> {
  static const double speed = 200.0; // 초당 200 픽셀 이동

  @override
  void update(double dt) {
    super.update(dt);
    
    // ⛔ 나쁜 예: x += 3.0; (프레임 속도에 따라 이동 거리가 달라짐)
    // ✅ 좋은 예: Delta Time을 곱하여 프레임 독립적인 이동 구현
    x += speed * dt;
    
    // 화면 경계 체크 로직
    if (x > gameRef.size.x) {
      x = 0;
    }
  }
}

위 코드에서 가장 주목해야 할 점은 dt(Delta Time)의 활용입니다. 고사양 폰에서 120FPS가 나오든, 저사양 폰에서 30FPS가 나오든 speed * dt를 사용하면 캐릭터는 현실 시간 기준으로 동일한 속도로 이동합니다. 이 로직을 적용하자마자 기기 간 동기화 문제와 움직임의 부자연스러움이 90% 이상 해결되었습니다.

성능 벤치마크 및 결과 분석

단순 위젯 방식과 Flame 엔진 방식을 프로파일링 도구(DevTools)로 비교한 결과입니다. 테스트 기기는 갤럭시 A32(보급형)를 기준으로 했습니다.

지표 (Metric) 순수 Flutter 위젯 (setState) Flame 엔진 (Game Loop)
평균 FPS 32 ~ 45 FPS (불안정) 58 ~ 60 FPS (안정)
CPU 사용률 평균 18% (스파이크 발생) 평균 8% (일정함)
UI Raster Time 14.2ms 4.1ms

결과를 분석해보면, Flame 엔진 사용 시 UI Raster Time이 획기적으로 줄어든 것을 볼 수 있습니다. 이는 Flutter 프레임워크가 레이아웃을 계산(Layout pass)하고 페인팅(Paint pass)하는 과정을 Flame의 컴포넌트 시스템이 효율적으로 대체했기 때문입니다. 특히 setState 호출로 인한 불필요한 객체 생성이 줄어들어 Garbage Collector(GC) 의 개입 빈도가 현저히 낮아졌습니다.

Flutter 공식 성능 최적화 가이드 확인

주의할 점: 언제 Flame을 쓰면 안 될까?

Flame이 강력하지만 만능은 아닙니다. 앱 내에 아주 간단한 애니메이션(예: 좋아요 버튼 터지기, 간단한 슬라이드) 정도를 구현할 때는 오히려 과유불급입니다. Flame은 게임 엔진 특성상 초기 로딩 시 리소스를 메모리에 올리는 과정이 필요하며, 앱 번들 사이즈를 약 2~3MB 정도 증가시킵니다.

Edge Case Warning: 텍스트 입력이 많은 게임(예: 퀴즈 게임, 채팅형 게임)의 경우, Flame 내부의 TextComponent보다 Flutter의 네이티브 Text 위젯을 오버레이(Overlay)로 띄워서 사용하는 것이 한글 입력 처리나 접근성(Accessibility) 면에서 훨씬 유리합니다.

또한, 3D 게임을 본격적으로 개발하려 한다면 Flutter보다는 Unity나 Unreal을 권장합니다. Flutter에도 Impeller 엔진 도입으로 3D 지원이 강화되고 있지만, 아직 물리 엔진이나 쉐이더(Shader) 복잡도 면에서는 전용 3D 엔진을 따라가기 벅찬 부분이 있습니다.

Best Practice: UI(메뉴, 설정)는 Flutter 위젯으로, 인게임(플레이 화면)은 FlameGame 위젯으로 분리하여 개발하는 '하이브리드 패턴'이 가장 생산성이 높습니다.

결론

Flutter는 단순한 앱 개발 프레임워크를 넘어, 2D 캐주얼 게임 개발에도 충분히 강력한 도구임을 확인했습니다. 핵심은 'Flutter가 렌더링하는 방식'을 이해하고, 게임 로직에 맞는 적절한 엔진(Flame)을 선택하여 렌더링 루프를 제어하는 것입니다. 특히 핀테크나 커머스 앱 내에 '게이미피케이션' 요소를 도입하려는 개발자에게, 별도의 Unity 학습 없이 기존 Dart 언어로 고성능 게임을 만들 수 있다는 점은 엄청난 매력입니다. 이번 프로젝트에서 얻은 노하우가 여러분의 앱에 생동감을 불어넣는 데 도움이 되기를 바랍니다.

Post a Comment