🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Flutter Flame 게임 테스팅과 디버깅 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2026. 2. 2. · 3 Views

Flutter Flame 게임 테스팅과 디버깅 완벽 가이드

Flutter와 Flame 엔진으로 개발한 게임의 품질을 보장하는 테스팅 기법과 디버깅 도구를 다룹니다. 단위 테스트부터 골든 테스트, 크래시 리포팅까지 실무에서 바로 적용할 수 있는 내용을 담았습니다.


목차

  1. 단위_테스트_작성
  2. 통합_테스트
  3. 골든_테스트
  4. 디버그_오버레이
  5. 치트_시스템_구축
  6. 크래시_리포팅

1. 단위 테스트 작성

김개발 씨는 첫 번째 Flame 게임을 완성한 후 뿌듯한 마음으로 선배에게 코드 리뷰를 요청했습니다. 박시니어 씨가 코드를 훑어보더니 조심스럽게 물었습니다.

"게임 로직 테스트는 어디 있어요?" 김개발 씨는 당황했습니다. 게임에도 테스트가 필요한 걸까요?

단위 테스트는 게임의 가장 작은 단위인 개별 컴포넌트나 함수가 의도대로 동작하는지 검증하는 것입니다. 마치 자동차 공장에서 엔진, 브레이크, 핸들을 각각 따로 검사하는 것과 같습니다.

플레이어의 체력 계산, 점수 시스템, 충돌 판정 로직 등을 개별적으로 테스트하면 버그를 조기에 발견할 수 있습니다.

다음 코드를 살펴봅시다.

// player_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_game/components/player.dart';

void main() {
  group('Player 테스트', () {
    late Player player;

    setUp(() {
      // 각 테스트 전에 새 플레이어 생성
      player = Player(maxHealth: 100);
    });

    test('플레이어가 데미지를 받으면 체력이 감소한다', () {
      player.takeDamage(30);
      expect(player.health, equals(70));
    });

    test('체력이 0 이하가 되면 사망 상태가 된다', () {
      player.takeDamage(150);
      expect(player.isDead, isTrue);
      expect(player.health, equals(0));
    });
  });
}

김개발 씨는 입사 3개월 차 주니어 개발자입니다. 회사에서 첫 번째 모바일 게임 프로젝트를 맡게 되었고, Flutter와 Flame 엔진을 사용해 열심히 개발했습니다.

캐릭터가 움직이고, 적이 나타나고, 점수가 올라가는 것을 보며 뿌듯했습니다. 그런데 QA 팀에서 버그 리포트가 쏟아지기 시작했습니다.

"플레이어가 데미지를 두 번 연속으로 받으면 체력이 이상하게 계산됩니다." "보스를 처치해도 가끔 점수가 안 올라가요." 김개발 씨는 밤새 버그를 수정했지만, 하나를 고치면 다른 곳에서 문제가 터졌습니다. 박시니어 씨가 다가와 조언했습니다.

"단위 테스트를 작성했어야 해요. 게임 로직을 하나씩 검증했다면 이런 문제를 미리 발견했을 거예요." 그렇다면 단위 테스트란 정확히 무엇일까요?

쉽게 비유하자면, 단위 테스트는 마치 요리사가 음식을 완성하기 전에 각 재료의 신선도를 확인하는 것과 같습니다. 양파가 상했는지, 고기가 신선한지, 양념의 맛이 적절한지 하나씩 확인합니다.

모든 재료가 완벽해야 최종 요리도 맛있는 법입니다. 게임 개발에서도 마찬가지입니다.

플레이어의 체력 시스템, 적의 AI 로직, 점수 계산 공식 등 각각의 구성 요소가 올바르게 동작해야 전체 게임이 제대로 돌아갑니다. 단위 테스트가 없던 시절에는 어땠을까요?

개발자들은 게임을 직접 플레이하면서 버그를 찾아야 했습니다. "체력이 제대로 깎이나?" 확인하려면 적에게 맞아봐야 했고, "점수가 올바르게 계산되나?" 확인하려면 아이템을 일일이 먹어봐야 했습니다.

이런 방식은 시간이 오래 걸릴 뿐만 아니라, 사람이 놓치는 경우가 많았습니다. 바로 이런 문제를 해결하기 위해 단위 테스트가 등장했습니다.

flutter_test 패키지를 사용하면 게임 로직을 자동으로 검증할 수 있습니다. 한 번 작성해두면 코드를 수정할 때마다 자동으로 실행되어 기존 기능이 깨지지 않았는지 확인해줍니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 group 함수로 관련된 테스트들을 묶어줍니다.

이렇게 하면 테스트 결과를 보기 쉽고 관리하기도 편합니다. setUp 함수는 각 테스트가 실행되기 전에 호출되어 깨끗한 상태의 Player 객체를 만들어줍니다.

test 함수 안에서 실제 검증이 이루어집니다. 플레이어에게 30의 데미지를 주고, 체력이 70이 되었는지 확인합니다.

expect 함수가 바로 이 검증을 담당합니다. 예상 값과 실제 값이 다르면 테스트가 실패합니다.

실제 현업에서는 어떻게 활용할까요? 대부분의 게임 회사에서는 CI/CD 파이프라인에 테스트를 연동합니다.

개발자가 코드를 푸시하면 자동으로 모든 테스트가 실행되고, 하나라도 실패하면 배포가 중단됩니다. 이렇게 하면 버그가 있는 코드가 사용자에게 전달되는 것을 막을 수 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 테스트를 너무 세세하게 작성하는 것입니다.

구현 세부 사항까지 테스트하면 코드를 조금만 리팩토링해도 테스트가 깨집니다. 테스트는 "무엇을" 하는지에 집중해야 하며, "어떻게" 하는지는 검증하지 않는 것이 좋습니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언을 들은 김개발 씨는 주말 동안 핵심 게임 로직에 대한 단위 테스트를 작성했습니다.

그러자 숨어 있던 버그들이 하나둘 드러나기 시작했습니다. "아, 여기서 음수 체력을 처리 안 하고 있었네!" 테스트 덕분에 버그를 빠르게 찾아 수정할 수 있었습니다.

실전 팁

💡 - 테스트 파일은 test/ 폴더에 _test.dart 접미사로 저장하세요

  • flutter test 명령어로 모든 테스트를 한 번에 실행할 수 있습니다
  • 핵심 게임 로직(체력, 점수, 충돌)부터 테스트를 작성하세요

2. 통합 테스트

김개발 씨의 단위 테스트는 모두 통과했습니다. 그런데 이상합니다.

테스트는 다 성공인데, 실제 게임을 실행하면 플레이어가 적과 충돌해도 아무 일이 일어나지 않습니다. 박시니어 씨가 웃으며 말했습니다.

"단위 테스트만으로는 부족해요. 컴포넌트들이 함께 잘 동작하는지도 확인해야 해요."

통합 테스트는 여러 컴포넌트가 함께 상호작용할 때 올바르게 동작하는지 검증합니다. 마치 오케스트라에서 각 악기가 개별적으로는 완벽해도, 함께 연주할 때 조화를 이뤄야 아름다운 음악이 되는 것과 같습니다.

Flame 게임에서는 FlameGame 전체를 테스트하여 컴포넌트 간의 상호작용을 확인합니다.

다음 코드를 살펴봅시다.

// game_integration_test.dart
import 'package:flame/game.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_game/my_game.dart';

void main() {
  // Flame 게임 테스트를 위한 설정
  final gameTest = FlameTester(() => MyGame());

  group('게임 통합 테스트', () {
    gameTest.test('플레이어와 적이 충돌하면 데미지를 받는다',
      (game) async {
        // 게임 컴포넌트들이 로드될 때까지 대기
        await game.ready();

        final player = game.player;
        final enemy = game.spawnEnemy(position: player.position);

        // 게임 업데이트를 실행하여 충돌 처리
        game.update(0.1);

        expect(player.health, lessThan(100));
      },
    );
  });
}

김개발 씨는 곤혹스러웠습니다. 분명히 Player 클래스의 takeDamage 메서드는 완벽하게 동작합니다.

Enemy 클래스의 attack 메서드도 문제없습니다. 그런데 왜 게임에서는 충돌해도 데미지가 안 들어가는 걸까요?

박시니어 씨가 코드를 살펴보더니 문제를 찾았습니다. "CollisionDetection 콜백에서 takeDamage를 호출하는 부분이 빠져있네요.

각 컴포넌트는 잘 만들었는데, 연결하는 부분을 놓쳤어요." 이것이 바로 통합 테스트가 필요한 이유입니다. 쉽게 비유하자면, 통합 테스트는 마치 자동차 조립 후 시운전을 하는 것과 같습니다.

엔진, 브레이크, 핸들 각각은 완벽하게 작동합니다. 하지만 조립한 후에 핸들을 돌렸을 때 바퀴가 제대로 움직이는지, 브레이크를 밟았을 때 엔진과 연동되어 차가 멈추는지 확인해야 합니다.

Flame 게임에서 통합 테스트를 작성하려면 flame_test 패키지가 필요합니다. 이 패키지는 게임 루프를 시뮬레이션하고, 컴포넌트들이 실제로 상호작용하는 환경을 만들어줍니다.

위의 코드를 살펴보겠습니다. FlameTester는 게임 인스턴스를 생성하고 관리하는 헬퍼 클래스입니다.

매 테스트마다 깨끗한 게임 인스턴스를 제공하여 테스트 간의 간섭을 방지합니다. game.ready() 호출은 매우 중요합니다.

Flame에서 컴포넌트들은 비동기로 로드되기 때문에, 모든 컴포넌트가 준비될 때까지 기다려야 합니다. 이 과정을 건너뛰면 컴포넌트가 null이거나 초기화되지 않은 상태에서 테스트가 실행될 수 있습니다.

game.update(0.1) 호출은 게임 루프를 한 프레임 진행시킵니다. 인자로 전달되는 0.1은 델타 타임으로, 이전 프레임으로부터 경과한 시간을 초 단위로 나타냅니다.

이 호출을 통해 충돌 감지, 물리 연산, 상태 업데이트 등이 실행됩니다. 실제 현업에서 통합 테스트는 주로 핵심 게임플레이 시나리오를 검증하는 데 사용됩니다.

"플레이어가 아이템을 먹으면 인벤토리에 추가되고 화면에 표시되는가?", "보스를 처치하면 스테이지가 클리어되고 다음 레벨로 넘어가는가?" 같은 시나리오를 테스트합니다. 주의할 점이 있습니다.

통합 테스트는 단위 테스트보다 실행 속도가 느립니다. 게임 전체를 초기화하고 여러 컴포넌트를 로드해야 하기 때문입니다.

따라서 모든 것을 통합 테스트로 검증하려 하지 말고, 단위 테스트로 검증할 수 있는 것은 단위 테스트로 처리하세요. 김개발 씨는 통합 테스트를 작성하면서 깨달았습니다.

"아, 컴포넌트를 연결하는 부분에서 실수가 많이 생기는구나." 통합 테스트 덕분에 컴포넌트 간의 연결 버그를 사전에 발견할 수 있게 되었습니다.

실전 팁

💡 - pubspec.yamlflame_test 패키지를 dev_dependencies에 추가하세요

  • 통합 테스트는 핵심 게임플레이 시나리오 위주로 작성하세요
  • **game.update()**의 델타 타임을 조절하여 다양한 상황을 시뮬레이션할 수 있습니다

3. 골든 테스트

김개발 씨가 UI 작업을 마치고 PR을 올렸습니다. 다음 날 출근해보니 디자이너에게서 메시지가 와 있었습니다.

"어제 HUD 레이아웃이 조금 바뀐 것 같은데, 의도한 건가요?" 김개발 씨는 당황했습니다. 분명히 다른 부분만 수정했는데, 어떻게 HUD까지 영향을 받은 걸까요?

골든 테스트는 위젯이나 컴포넌트의 렌더링 결과를 이미지로 저장해두고, 이후 변경사항이 생겼을 때 비교하는 시각적 회귀 테스트입니다. 마치 건축 도면을 보관해두었다가 리모델링 후 원래 설계와 비교하는 것과 같습니다.

의도치 않은 시각적 변경을 자동으로 감지할 수 있습니다.

다음 코드를 살펴봅시다.

// hud_golden_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_game/widgets/game_hud.dart';

void main() {
  group('GameHUD 골든 테스트', () {
    testWidgets('HUD가 올바르게 렌더링된다', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          home: GameHUD(
            health: 80,
            score: 12500,
            level: 3,
          ),
        ),
      );

      // 골든 이미지와 비교
      await expectLater(
        find.byType(GameHUD),
        matchesGoldenFile('goldens/game_hud.png'),
      );
    });
  });
}

김개발 씨는 점수 표시 로직을 수정했을 뿐인데, 어떻게 체력바 위치가 바뀐 걸까요? 코드를 살펴보니 실수로 Padding 값을 건드렸던 것이었습니다.

이런 사소한 실수가 사용자 경험에 큰 영향을 미칠 수 있습니다. 박시니어 씨가 말했습니다.

"골든 테스트를 사용했다면 PR 단계에서 바로 알아챘을 거예요." 골든 테스트란 무엇일까요? 쉽게 비유하자면, 골든 테스트는 마치 사진을 찍어두는 것과 같습니다.

인테리어를 완료한 후 사진을 찍어둡니다. 나중에 누군가 가구를 옮기거나 벽지를 바꾸면, 예전 사진과 비교해서 무엇이 달라졌는지 한눈에 알 수 있습니다.

Flutter에서 골든 테스트는 위젯이 렌더링된 결과를 PNG 이미지로 저장합니다. 이 이미지를 "골든 파일"이라고 부릅니다.

이후 테스트를 실행하면 현재 렌더링 결과와 골든 파일을 픽셀 단위로 비교합니다. 골든 테스트가 없던 시절에는 어땠을까요?

개발자나 QA 담당자가 화면을 직접 보면서 "어, 여기 뭔가 달라진 것 같은데?" 하고 느낌으로 찾아야 했습니다. 사람의 눈은 미세한 변화를 놓치기 쉽고, 모든 화면을 매번 확인하는 것은 현실적으로 불가능합니다.

위의 코드를 살펴보겠습니다. tester.pumpWidget으로 테스트할 위젯을 렌더링합니다.

MaterialApp으로 감싸는 이유는 테마, 방향성 등 Flutter의 기본 설정을 제공하기 위해서입니다. matchesGoldenFile이 핵심입니다.

이 matcher는 지정된 경로의 골든 이미지와 현재 렌더링 결과를 비교합니다. 파일이 없으면 새로 생성하고, 있으면 비교합니다.

골든 파일을 처음 생성하거나 업데이트하려면 flutter test --update-goldens 명령어를 사용합니다. 이 명령어를 실행하면 현재 렌더링 결과가 새로운 골든 파일로 저장됩니다.

실제 현업에서 골든 테스트는 주로 HUD, 메뉴, 인벤토리 UI 등 시각적으로 중요한 요소에 적용합니다. 게임의 인게임 그래픽은 프레임마다 달라질 수 있어 골든 테스트에 적합하지 않지만, 고정된 UI 요소는 완벽한 테스트 대상입니다.

주의할 점이 있습니다. 골든 테스트는 플랫폼과 환경에 민감합니다.

macOS에서 생성한 골든 파일이 Linux CI 서버에서는 다르게 렌더링될 수 있습니다. 따라서 골든 파일은 CI 환경과 동일한 환경에서 생성하는 것이 좋습니다.

김개발 씨는 주요 UI 컴포넌트에 골든 테스트를 추가했습니다. 이제 코드를 수정한 후 테스트를 돌리면 의도치 않은 시각적 변경이 있는지 바로 알 수 있습니다.

"이제 디자이너에게 혼나는 일은 없겠네요!"

실전 팁

💡 - 골든 파일은 버전 관리 시스템에 함께 커밋하세요

  • CI 환경에서 일관된 렌더링을 위해 동일한 Flutter 버전을 사용하세요
  • 의도적인 UI 변경 시에는 --update-goldens로 골든 파일을 업데이트하세요

4. 디버그 오버레이

김개발 씨가 게임을 테스트하다가 이상한 현상을 발견했습니다. 플레이어가 특정 위치에서 벽을 뚫고 지나갑니다.

충돌 박스가 제대로 설정된 것 같은데, 눈으로 확인할 방법이 없었습니다. 박시니어 씨가 조언했습니다.

"디버그 오버레이를 켜보세요. 충돌 박스가 화면에 보일 거예요."

디버그 오버레이는 게임 화면 위에 개발용 정보를 표시하는 기능입니다. 마치 자동차 계기판처럼 현재 상태를 실시간으로 보여줍니다.

Flame에서는 충돌 박스, FPS, 컴포넌트 수 등을 시각적으로 확인할 수 있어 버그 원인을 빠르게 파악할 수 있습니다.

다음 코드를 살펴봅시다.

// my_game.dart
import 'package:flame/game.dart';
import 'package:flame/components.dart';

class MyGame extends FlameGame with HasCollisionDetection {
  // 디버그 모드 플래그
  bool showDebugInfo = false;
  late TextComponent fpsText;
  late TextComponent componentCountText;

  @override
  Future<void> onLoad() async {
    // 디버그 정보 텍스트 컴포넌트
    fpsText = TextComponent(position: Vector2(10, 10));
    componentCountText = TextComponent(position: Vector2(10, 30));

    add(fpsText);
    add(componentCountText);
  }

  @override
  void update(double dt) {
    super.update(dt);

    if (showDebugInfo) {
      fpsText.text = 'FPS: ${(1 / dt).toStringAsFixed(1)}';
      componentCountText.text = 'Components: ${children.length}';
    }

    // 충돌 박스 디버그 렌더링 활성화
    debugMode = showDebugInfo;
  }
}

김개발 씨는 플레이어가 벽을 뚫고 지나가는 버그를 해결하려고 몇 시간째 코드를 들여다보고 있었습니다. 충돌 감지 코드는 문제가 없어 보입니다.

히트박스 크기도 정확하게 설정했습니다. 도대체 뭐가 문제일까요?

박시니어 씨가 말했습니다. "코드만 봐서는 한계가 있어요.

실제로 히트박스가 어떻게 그려지는지 눈으로 확인해야 해요." 디버그 오버레이란 무엇일까요? 쉽게 비유하자면, 디버그 오버레이는 마치 X-ray 안경과 같습니다.

평소에는 보이지 않는 게임의 내부 구조를 들여다볼 수 있게 해줍니다. 충돌 박스의 실제 위치와 크기, 현재 프레임 레이트, 메모리에 로드된 컴포넌트 수 등을 실시간으로 확인할 수 있습니다.

Flame 엔진에는 강력한 디버그 기능이 내장되어 있습니다. debugMode 속성을 true로 설정하면 모든 히트박스가 반투명한 색상으로 화면에 표시됩니다.

이를 통해 충돌 박스가 스프라이트와 정확히 일치하는지 확인할 수 있습니다. 위의 코드를 살펴보겠습니다.

showDebugInfo 플래그로 디버그 모드를 켜고 끌 수 있습니다. 개발 중에는 켜두고, 배포할 때는 끄면 됩니다.

더 좋은 방법은 환경 변수나 빌드 설정으로 자동 제어하는 것입니다. fpsText는 현재 프레임 레이트를 표시합니다.

1 / dt로 계산하는 이유는 dt가 프레임 간 경과 시간이기 때문입니다. 0.016초가 걸렸다면 1/0.016 = 62.5 FPS입니다.

debugMode = showDebugInfo 라인이 핵심입니다. FlameGame의 debugMode를 활성화하면 모든 자식 컴포넌트의 디버그 렌더링이 켜집니다.

히트박스, 컴포넌트 경계, 앵커 포인트 등이 시각적으로 표시됩니다. 실제 현업에서는 디버그 오버레이에 더 많은 정보를 추가합니다.

플레이어의 현재 좌표, 속도 벡터, 상태(idle, running, jumping), 현재 애니메이션 프레임 등을 표시합니다. 이런 정보들이 있으면 버그 재현과 분석이 훨씬 쉬워집니다.

김개발 씨가 디버그 모드를 켜자 문제가 바로 보였습니다. 벽의 히트박스가 스프라이트보다 작았던 것입니다.

타일맵 에디터에서 충돌 박스를 잘못 설정한 것이 원인이었습니다. "눈으로 보니까 바로 알겠네요!" 주의할 점이 있습니다.

디버그 오버레이는 성능에 영향을 미칩니다. 특히 많은 텍스트를 매 프레임 업데이트하면 프레임 드롭이 발생할 수 있습니다.

또한 배포 버전에는 반드시 비활성화해야 합니다. kDebugMode 상수를 활용하면 디버그 빌드에서만 자동으로 활성화되게 할 수 있습니다.

실전 팁

💡 - kDebugMode를 사용하여 디버그 빌드에서만 오버레이가 표시되게 하세요

  • 키보드 단축키(F3 등)로 디버그 모드를 토글할 수 있게 만들면 편리합니다
  • 디버그 정보는 별도의 레이어에 렌더링하여 게임 플레이에 방해되지 않게 하세요

5. 치트 시스템 구축

QA 팀에서 요청이 들어왔습니다. "보스전을 테스트하려면 매번 30분씩 플레이해야 해요.

바로 보스 스테이지로 갈 수 있는 방법 없나요?" 김개발 씨는 고민에 빠졌습니다. 테스트를 위해 게임을 수정하면 나중에 되돌리는 것도 일이고, 실수로 치트 코드가 배포 버전에 포함되면 큰 문제가 됩니다.

치트 시스템은 개발과 테스트를 위한 특수 명령어 체계입니다. 마치 건물 관리자가 마스터키를 가지고 있는 것처럼, 개발자가 게임의 모든 부분에 빠르게 접근할 수 있게 해줍니다.

무적 모드, 스테이지 스킵, 아이템 지급 등을 통해 테스트 효율을 크게 높일 수 있습니다.

다음 코드를 살펴봅시다.

// cheat_system.dart
import 'package:flutter/foundation.dart';

class CheatSystem {
  static final CheatSystem _instance = CheatSystem._internal();
  factory CheatSystem() => _instance;
  CheatSystem._internal();

  final Map<String, Function> _cheats = {};

  void registerCheat(String code, Function action) {
    // 디버그 모드에서만 치트 등록
    if (kDebugMode) {
      _cheats[code.toUpperCase()] = action;
    }
  }

  bool executeCheat(String code) {
    final cheat = _cheats[code.toUpperCase()];
    if (cheat != null) {
      cheat();
      return true;
    }
    return false;
  }
}

// 사용 예시
void setupCheats(MyGame game) {
  final cheats = CheatSystem();
  cheats.registerCheat('GODMODE', () => game.player.invincible = true);
  cheats.registerCheat('SKIPBOSS', () => game.loadLevel(bossLevel));
  cheats.registerCheat('GOLD999', () => game.player.gold = 999999);
}

김개발 씨는 QA 팀의 요청을 듣고 고민에 빠졌습니다. 테스트 효율을 높이고 싶지만, 치트 코드가 실수로 배포 버전에 포함되면 게임의 밸런스가 무너지고 사용자 경험이 망가질 수 있습니다.

박시니어 씨가 조언했습니다. "체계적인 치트 시스템을 만들고, 디버그 빌드에서만 동작하게 하면 돼요.

Flutter의 kDebugMode를 활용하면 배포 버전에서는 아예 코드가 제거됩니다." 치트 시스템이란 무엇일까요? 쉽게 비유하자면, 치트 시스템은 마치 놀이공원의 VIP 패스와 같습니다.

일반 손님은 줄을 서서 차례를 기다려야 하지만, VIP는 모든 놀이기구에 바로 탑승할 수 있습니다. 개발자와 QA 팀은 이 VIP 패스를 통해 게임의 어떤 부분이든 즉시 접근하여 테스트할 수 있습니다.

위의 코드를 살펴보겠습니다. 싱글톤 패턴을 사용하여 게임 전체에서 하나의 치트 시스템 인스턴스만 존재하도록 합니다.

이렇게 하면 어디서든 쉽게 치트를 실행할 수 있습니다. kDebugMode 체크가 핵심입니다.

이 상수는 디버그 빌드에서는 true, 릴리즈 빌드에서는 false입니다. Dart 컴파일러는 if (kDebugMode) 블록이 false인 경우 해당 코드를 완전히 제거합니다.

따라서 배포 버전에는 치트 코드가 물리적으로 존재하지 않습니다. registerCheat 메서드로 치트를 등록합니다.

첫 번째 인자는 치트 코드 문자열이고, 두 번째 인자는 실행할 함수입니다. toUpperCase()로 변환하여 대소문자 구분 없이 치트를 실행할 수 있게 합니다.

실제 현업에서는 더 다양한 치트를 구현합니다. 무적 모드, 원하는 스테이지로 이동, 골드/경험치 지급, 모든 아이템 해금, 시간 배속 조절 등이 일반적입니다.

QA 팀과 협의하여 자주 필요한 기능을 치트로 만들어두면 테스트 효율이 크게 올라갑니다. 치트 입력 방식도 다양합니다.

개발용 콘솔 UI를 만들어 명령어를 입력하게 할 수도 있고, 특정 버튼 조합(예: 볼륨 업 5번 연타)으로 활성화할 수도 있습니다. 중요한 것은 일반 사용자가 우연히 발견하기 어렵게 만드는 것입니다.

주의할 점이 있습니다. 치트 시스템을 너무 남용하면 실제 플레이 경험을 놓칠 수 있습니다.

QA 팀이 "스테이지 스킵"만 사용하면 스테이지 전환 시 발생하는 버그를 발견하지 못할 수 있습니다. 치트는 특정 기능을 집중 테스트할 때 사용하고, 전체 플로우 테스트는 정상 플레이로 진행해야 합니다.

김개발 씨는 치트 시스템을 구축한 후 QA 팀에 전달했습니다. "이제 보스전 테스트하기 훨씬 편해졌어요!" QA 팀의 테스트 효율이 올라가면서 더 많은 버그를 더 빨리 발견할 수 있게 되었습니다.

실전 팁

💡 - 치트 명령어 목록을 문서화하여 QA 팀과 공유하세요

  • 치트 사용 로그를 남기면 어떤 상태에서 버그가 발생했는지 추적하기 쉽습니다
  • 온라인 게임이라면 치트가 서버 검증을 우회하지 않도록 주의하세요

6. 크래시 리포팅

김개발 씨의 게임이 드디어 출시되었습니다. 스토어 리뷰를 살펴보던 중 가슴이 철렁했습니다.

"게임 시작하자마자 튕겨요." "보스전에서 앱이 강제 종료됩니다." 문제는 어떤 상황에서 크래시가 발생하는지 전혀 알 수 없다는 것이었습니다. 사용자의 기기, OS 버전, 게임 상태 등 아무 정보도 없었습니다.

크래시 리포팅은 앱에서 발생한 오류와 충돌을 자동으로 수집하고 분석하는 시스템입니다. 마치 비행기의 블랙박스처럼 문제 발생 시점의 모든 정보를 기록합니다.

Firebase Crashlytics를 사용하면 크래시 원인, 발생 빈도, 영향받은 사용자 수 등을 실시간으로 파악할 수 있습니다.

다음 코드를 살펴봅시다.

// main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  // Flutter 프레임워크 에러 캡처
  FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;

  // 비동기 에러 캡처
  PlatformDispatcher.instance.onError = (error, stack) {
    FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
    return true;
  };

  // 사용자 정보 설정 (선택)
  FirebaseCrashlytics.instance.setUserIdentifier('user_12345');

  // 커스텀 로그 추가
  FirebaseCrashlytics.instance.log('Game started');

  runApp(const MyGameApp());
}

김개발 씨는 막막했습니다. 스토어 리뷰에는 "앱이 튕겨요"라는 내용만 있을 뿐, 어떤 상황에서 왜 튕겼는지 알 수 없었습니다.

버그를 고치려면 먼저 재현해야 하는데, 재현 조건조차 모르니 손을 쓸 수가 없었습니다. 박시니어 씨가 물었습니다.

"크래시 리포팅 설정 안 했어요?" 김개발 씨는 고개를 저었습니다. 박시니어 씨가 한숨을 쉬며 말했습니다.

"출시 전에 꼭 설정했어야 하는 건데... 지금이라도 빨리 추가해요." 크래시 리포팅이란 무엇일까요?

쉽게 비유하자면, 크래시 리포팅은 마치 병원의 전자 의무 기록 시스템과 같습니다. 환자가 응급실에 왔을 때 의사는 기록을 보고 어떤 약을 먹었는지, 언제부터 아팠는지, 과거 병력은 무엇인지 파악합니다.

마찬가지로 크래시 리포팅은 앱이 충돌했을 때 어떤 코드에서 문제가 발생했는지, 사용자의 기기 정보는 무엇인지, 충돌 직전에 어떤 동작을 했는지 알려줍니다. Firebase Crashlytics는 가장 널리 사용되는 크래시 리포팅 서비스입니다.

무료이면서도 강력한 기능을 제공합니다. 크래시 발생 시 스택 트레이스, 기기 정보, OS 버전 등을 자동으로 수집하고, 웹 대시보드에서 분석할 수 있게 해줍니다.

위의 코드를 살펴보겠습니다. FlutterError.onError는 Flutter 프레임워크 내에서 발생하는 에러를 캡처합니다.

위젯 빌드 중 발생하는 예외, setState 관련 오류 등이 여기서 잡힙니다. PlatformDispatcher.instance.onError는 비동기 코드에서 발생하는 에러를 캡처합니다.

Future나 async/await에서 발생한 예외가 여기서 잡힙니다. fatal: true로 설정하면 이 에러를 심각한 크래시로 분류합니다.

setUserIdentifier는 크래시에 사용자 정보를 연결합니다. 개인정보 보호에 유의하면서, 문제 발생 시 특정 사용자에게 연락하거나 해당 사용자의 다른 크래시를 함께 분석할 수 있게 해줍니다.

log 메서드로 커스텀 로그를 남길 수 있습니다. "보스 스테이지 진입", "아이템 구매 시도" 등의 로그를 남겨두면 크래시 발생 직전에 사용자가 무엇을 하고 있었는지 파악하기 쉽습니다.

실제 현업에서는 크래시 리포팅과 함께 사용자 행동 분석도 설정합니다. Firebase Analytics와 연동하면 "이 크래시가 발생한 사용자의 50%가 특정 아이템을 장착하고 있었다"와 같은 인사이트를 얻을 수 있습니다.

주의할 점이 있습니다. 너무 많은 정보를 로깅하면 개인정보 문제가 발생할 수 있습니다.

이메일, 비밀번호, 결제 정보 등 민감한 데이터는 절대 로깅하지 마세요. 또한 크래시가 발생해도 사용자 데이터가 손실되지 않도록 자동 저장 기능을 구현해두는 것이 좋습니다.

김개발 씨는 Firebase Crashlytics를 설정하고 업데이트를 배포했습니다. 다음 날 대시보드를 확인하니 크래시의 원인이 명확하게 보였습니다.

"아, 특정 기기에서 메모리 부족으로 발생하는 문제였구나!" 이제 원인을 알았으니 해결책을 찾을 수 있게 되었습니다.

실전 팁

💡 - 출시 전에 반드시 크래시 리포팅을 설정하세요. 나중에 추가하면 이미 발생한 크래시 정보를 놓치게 됩니다

  • 중요한 게임 이벤트(스테이지 시작, 보스 처치 등)에 커스텀 로그를 추가하세요
  • 크래시 알림을 설정하여 심각한 문제 발생 시 즉시 대응할 수 있게 하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Flutter#Flame#Testing#Debugging#GameDevelopment#Flutter,Flame,Game

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.