버그 리포트에 게임 상태 담기 — 무엇을 스냅샷할 것인가
환경·세션·순간·재현 단서 네 갈래로 나눈 게임 상태 분류, 비용 대비 가치 순위, 담으면 안 되는 것, 직렬화할 때 걸리는 함정까지 정리합니다.
- unity
- bug-report
- performance
스크린샷만 있는 리포트가 재현으로 이어지지 않는 이유는 재현이 실패하는 이유에서 다뤘듯 조건이 통째로 유실되기 때문입니다. 화면에 찍힌 건 결과이고, 원인은 그 순간의 게임 상태에 있습니다. 이 글은 그 상태에서 무엇을 어디까지 담을지를 정리합니다.
네 갈래로 나눈다
모든 걸 다 담으려 하면 어디부터 손대야 할지 막막해집니다. 넷으로 쪼개면 각각이 답할 질문이 분명해집니다.
| 분류 | 담는 값 | 답하는 질문 |
|---|---|---|
| 환경 | 기기 모델, OS, GPU, Unity 버전, 빌드 번호 | 특정 기기·버전에서만 나는가 |
| 세션 | 씬 이름, 플레이 시간, 진행도 | 어디서, 얼마나 플레이한 뒤인가 |
| 순간 | FPS, 메모리, 최근 로그 N줄 | 터지기 직전 상태는 어땠나 |
| 재현 단서 | 최근 입력 이력, 랜덤 시드값 | 어떤 조작을 어떤 순서로 했나 |
환경과 세션은 캡처 시점에 한 번 모으면 끝입니다. 순간은 값이 계속 바뀌므로 캡처 직전 최신값을 떠야 하고, 재현 단서는 애초에 게임 로직에서 계속 쌓아 두고 있어야 캡처 순간에 꺼내 쓸 수 있습니다. 이 차이를 놓치면 캡처 코드 자체는 멀쩡한데 정작 값이 비어 있는 상황이 생깁니다 — 예를 들어 입력 이력을 캡처하는 순간에만 수집하기 시작하면, 그 시점엔 이미 쌓여 있어야 할 이전 입력이 하나도 없습니다.
비용 대비 가치 순위
전부 같은 비용이 아닙니다. 무료에 가까운 것부터 붙이는 게 순서입니다.
- 환경 정보 —
SystemInfo,Application.version몇 줄로 끝. 비용 거의 없음, 가치는 즉시 나타남("이 기기에서만 난다"를 바로 걸러줍니다) - 씬 이름·최근 로그 — 이미 최소 구현 가이드에 코드가 있습니다. 비용 낮음
- FPS·메모리 — 계산 몇 줄. 성능 관련 버그가 아니어도 "그 순간 프레임이 떨어지고 있었다" 같은 정황을 덤으로 얻습니다
- 프로젝트별 진행도 — 인벤토리 개수, 퀘스트 상태, 접속 서버. 프로젝트마다 무엇을 뽑을지 설계해야 해서 비용이 조금 올라갑니다
- 최근 입력 이력·시드값 — 재현에 가장 직접적으로 기여하지만, 입력을 계속 쌓아 두는 구조 자체를 새로 설계해야 해서 비용이 가장 높습니다
13번은 하루 안에 끝나는 작업이고 재현율에 즉시 반영됩니다. 45번은 프로젝트 로직에 손을 대야 해서 팀 상황에 따라 우선순위가 갈립니다. 예를 들어 QA 가 5명이고 주 2회 빌드를 받는 팀이라면 13번만 붙여도 "재현 안 됨"으로 닫히던 티켓의 상당수가 줄어드는 걸 바로 체감할 수 있고, 45번은 그 뒤에 프로젝트 로직과 맞춰 가며 설계해도 늦지 않습니다.
담으면 안 되는 것
스냅샷이 커질수록 담고 싶은 유혹도 커지지만, 세 가지는 선을 그어야 합니다.
- 개인정보: 이메일, 실명, 결제 정보. 디바이스 식별자도 플랫폼 정책과 개인정보 규정부터 확인해야 하는 항목입니다.
- 인증 토큰: 세션 토큰이나 API 키가 스냅샷에 섞여 들어가면 리포트 자체가 보안 사고가 됩니다. 직렬화 대상 목록에서 인증 관련 필드는 화이트리스트 방식으로 아예 제외해야 합니다.
- 과도한 덤프: 씬 전체 오브젝트 트리나 전체 세이브 파일을 통째로 넣는 식입니다. 리포트 하나가 수 MB 를 넘어가면 전송 실패율이 오르고, 정작 봐야 할 값이 노이즈에 묻힙니다.
직렬화할 때 걸리는 함정
- 메인 스레드 전용 API:
Time,SystemInfo계열 다수가 메인 스레드 전용입니다. 캡처 트리거가 워커 스레드(예: 예외 콜백)에서 걸리면 값을 못 읽고 예외가 납니다. 캡처 로직은 메인 스레드에서 실행되도록 큐잉하거나, 메인 스레드가 주기적으로 갱신해 둔 캐시값을 대신 읽어야 합니다. - JSON 크기: 리스트·딕셔너리를 그대로 직렬화하면 항목 수 제한 없이 커집니다. 최근 입력 이력처럼 계속 쌓이는 데이터는 캡처 시점에 최근 N개로 잘라서 담아야 합니다.
- 순환 참조:
GameObject나 커스텀 클래스를 직렬화기에 그대로 넘기면 참조가 서로를 가리키다가 무한 루프에 빠지거나 예외가 납니다. 원본 객체가 아니라 값만 뽑아 평평한 DTO 로 옮긴 뒤 직렬화해야 합니다.
// 원본 객체를 직렬화하지 않는다 — 값만 평평하게 옮긴다
struct StateSnapshot
{
public string scene;
public float fps;
public long memoryMB;
public string[] recentInputs; // 원본 입력 큐가 아니라 최근 N개만 복사
}
정리
환경은 싸고 즉시 유용하고, 순간은 조금 더 손이 가고, 재현 단서는 가장 비싸지만 가장 결정적입니다. 순서대로 붙이면 어느 단계에서 멈춰도 손해가 아닙니다. 개인정보·인증 토큰은 애초에 수집 대상에서 빼고, 직렬화는 원본 객체가 아니라 값을 뽑은 DTO 로 합니다. 트리거·전송까지 포함한 전체 그림은 1편, 로그와 함께 붙이는 최소 코드는 최소 구현 가이드에 있습니다.
저희 Rekon은 이 스냅샷을 영상·로그와 같은 시간축에 자동으로 묶어 둡니다. 직접 설계하신다면 위 우선순위 그대로, 환경 정보부터 붙이시길 권합니다.