재접속 한 번에 버그 5종이 쏟아졌다
burger-stack은 레시피 순서대로 키를 눌러 버거를 쌓는 브라우저 게임이다. Phaser3와 TypeScript로 만든 클라이언트가 화면을 그리고, Socket.io로 붙은 NestJS 서버(별도 리포)가 실시간 대전을 중계한다. 스테이지가 오르면 재료가 해금되고, 레시피대로 재료를 쌓으면 콤보 점수가 붙는 구조다.
이 글은 이 게임에 재접속 기능을 처음 얹으면서 겪은 일을 정리한다. 게임 도중 새로고침하면 클라이언트는 서버에 다시 붙어야 하고, 서버는 그 플레이어의 진행 상태를 통째로 다시 내려줘야 하며, 상대방 화면의 상대 패널도 끊긴 적 없는 것처럼 계속 갱신돼야 한다. 계획은 버그 2개로 시작했지만, 하나를 고칠 때마다 다음 층위에서 다른 증상이 튀어나와 2026-04-12부터 04-13까지 이틀에 걸쳐 다섯 갈래로 번졌다.
타이머가 처음부터 다시 도는 문제
가장 먼저 잡은 건 타이머였다.
- 증상: 재접속하면 남은 시간이 아니라 스테이지 제한 시간 전체가 다시 카운트됨.
- 원인: 서버가 스테이지 시작 시각을 들고 있지 않아, 클라이언트는 재접속 시 항상 0초부터 셀 수밖에 없었음.
- 해법: 서버가 스테이지 시작 시각을 기록해 게임 시작·스테이지 클리어·시간 초과 세 시점마다 갱신하고, 재접속 시 클라이언트에 함께 내려준다. 클라이언트는 그 시각과 현재 시각의 차이를 계산해 타이머 시작점을 앞으로 당기고, 이 보정은 재접속 직후 첫 스테이지에만 한 번 적용한 뒤 버린다.
타이머는 스톱워치와 같다. 재접속은 스톱워치를 새로 사는 일이 아니라, 이미 흐른 시간을 다시 입력해 이어 붙이는 일이어야 한다. 이때 계산해 둔 “재접속 시 이미 흐른 시간” 값은 뒤에서 카운트다운 버그를 판별하는 기준으로 다시 쓰인다.
상대 패널이 사라졌다 두 개가 됐다 멈추는 문제
두 번째로 계획했던 버그는 상대 패널 미표시였는데, 고치는 과정에서 3단계로 진화했다.
1단계 — 패널 자체가 안 생긴다
- 증상: 재접속 후 상대방 패널이 화면에 아예 나타나지 않음.
- 원인: 재접속 응답에 상대 플레이어 목록 자체가 담겨 있지 않았음.
- 해법: 재접속 응답에 상대의 식별 정보(소켓 ID·닉네임)를 포함하도록 서버를 고침.
2단계 — 패널이 두 개가 된다
- 증상: 두 플레이어가 게임을 시작한 뒤 한쪽이 새로고침하면, 상대 화면에 같은 플레이어 패널이 2개 뜸.
- 원인: 상대의 진행 상태(스테이지·점수·재고)까지 즉시 복구하도록 정보를 확장했는데, 로비 화면이 게임 시작 이벤트에서 확장 전 옛 형식의 플레이어 목록을 그대로 넘기고 있었음.
- 해법: 게임 시작 시 넘기는 플레이어 정보를 새 형식으로 변환해, 모든 플레이어를 동일한 초기 상태로 재구성하도록 고침.
3단계 — 패널이 갱신되지 않는다
- 증상: 재접속한 플레이어가 재료를 계속 쌓아도 상대 화면의 패널이 움직이지 않음.
- 원인: 재접속하면 소켓 ID가 바뀌는 Socket.io 특성 때문에, 패널을 소켓 ID로 찾아 관리하던 내부 목록이 새 ID를 인식하지 못했고, 패널을 못 찾은 갱신 처리는 그대로 포기하고 있었음.
- 해법: 패널을 못 찾으면 같은 닉네임을 가진 기존 패널을 찾아 목록의 키만 새 소켓 ID로 옮겨 달았다(패널 객체와 화면 위치는 그대로 유지).
이 소스에서 소켓 ID 변경이 명시적 원인으로 지목된 버그는 이 한 곳뿐이다. 즉, 재접속으로 바뀐 건 신원 확인표일 뿐이니 표만 갈아 끼우면 된다는 판단이다.
카운트다운이 다시 뜨는 문제
재접속인데도 시작 카운트다운(3,2,1)이 또 뜨는 버그는 두 차례 수정했다.
- 1차 수정: 재접속 여부를 나타내는 화면 내부 플래그를 재접속 시 꺼서 카운트다운을 건너뛰게 함.
그런데 다음 날 같은 증상이 재발했다. 화면 진입 처리 순서상 이 플래그를 설정하기 전에 스테이지 시작 처리가 먼저 실행돼 버려 플래그 상태가 혼동됐기 때문이다. 같은 증상이 두 번 나왔다는 건 1차 수정이 표면만 건드렸다는 뜻이다.
- 2차 수정: 플래그를 아예 믿지 않고, 스테이지가 시작되는 시점에 이미 확실한 값 — 앞서 타이머 보정에 썼던 “재접속 시 이미 흐른 시간”의 유무 — 으로 직접 판단하도록 바꿨다.
const isReconnect = this.reconnectElapsedMs !== null;
if (this.isFirstStage && !isReconnect) {
// 카운트다운 실행
} else {
// 즉시 게임 시작
}이 값은 재접속 때만 채워지고 한 번 쓰면 바로 비워지므로, 값의 유무만 보면 플래그 없이도 지금이 재접속 직후인지 판단할 수 있다.
“reconnectElapsedMs는 재접속 시에만 초기값(non-null)을 가짐” · “startStage() 호출 시점에 직접 판단하므로 플래그 중복 설정 문제 회피”
스테이지가 통째로 리셋되는 심각한 버그
새로고침 후 재접속하면 스테이지가 무조건 1부터 다시 시작했다.
- 증상: 진행 중이던 스테이지를 통째로 잃고 처음부터 재시작.
- 원인: 화면이 새로 생성될 때 스테이지 진행 상태를 기본값(1스테이지)으로 초기화하는데, 재접속 시 넘어온 원래 스테이지 번호는 화면 표시용 값에만 반영되고, 정작 난이도·해금 판정에 쓰이는 진행 상태 값에는 반영되지 않았음.
- 해법: 재접속 시 그 진행 상태 값도 함께 맞춰 넣도록 고침(전달받은 스테이지 인덱스를 게임 내부의 1부터 시작하는 스테이지 번호로 변환).
이 값은 난이도 판정과 재료 해금 여부, 화면 상단 정보(HUD) 표시에까지 쓰인다. 즉, 이 하나가 안 맞으면 그 값을 참조하는 곳마다 다른 얼굴의 버그가 튀어나온다 — 실제로 파생 버그가 두 건 더 나왔다.
파생 A — 초기값 깜빡임
- 증상: 재접속 직후 잠깐 스테이지 1·점수 0이 보였다가 뒤늦게 진짜 값으로 바뀜.
- 원인: 화면 상단 정보가 초기값으로 먼저 갱신된 뒤, 진행 상태를 재설정해도 화면 상단 정보는 따라 갱신되지 않았음.
- 해법: 상태 복구 직후 화면 상단 정보를 명시적으로 한 번 더 갱신.
파생 B — 재료 해금 상태 미복구
- 증상: 스테이지 7에서 새로고침하면 토마토 같은 해금 재료가 다시 안 보임(스테이지 1 기준으로 표시).
- 원인: 해금된 재료 목록을 계산하는 시점엔 아직 진행 상태 값이 1로 남아 있었고, 이후 값을 바꿔도 이미 그려진 재료 선택판은 따라 바뀌지 않았음.
- 해법: 재접속 상태 복구 시 현재 스테이지 기준으로 해금 재료 목록을 다시 계산하고 재료 선택판을 다시 그림.
재고에서 BUN_BOTTOM이 사라지는 문제
마지막 버그는 원인이 클라이언트와 서버 양쪽에 하나씩 있었다.
클라이언트 원인
- 증상: 재접속한 플레이어가 재료를 쌓을수록 상대 화면에선
BUN_BOTTOM없이 얹은 재료만 보임. - 원인: 재접속하면 재고가
BUN_BOTTOM하나로 리셋되는데, 그 리셋 사실을 상대에게 알리는 신호를 보내지 않았음. - 해법: 재고 초기화 직후 그 신호를 보내도록 추가.
서버 원인
- 증상: 클라이언트 수정 후에도
BUN_BOTTOM이 계속 사라짐. - 원인: 서버가 플레이어 상태를 방송할 때 재고 배열에서
BUN_BOTTOM을 의도적으로 제외하고 있었음. - 해법: 서버 팀에 전체 배열을 그대로 보내달라고 요청해 반영.
“game:player-state에서 stackIngredients를 브로드캐스트할 때 BUN_BOTTOM을 제외하지 말고 전체 배열을 그대로 보내주세요.”
최종 테스트와 남은 일
2026-04-13, 마지막으로 확인한 결과는 다음과 같다.
- 첫 진입: 상대 패널 정확히 2개 표시
- 카운트다운: 3-2-1 정상 표시
- 타이머: 게임 중 계속 차감(카운트다운 중에도 정상)
- 재접속: 패널이 중복되지 않고 기존 위치 유지
- 실시간 동기화: 재접속한 플레이어가 재료를 쌓으면 상대 패널이 실시간 업데이트
- 재고 상태:
BUN_BOTTOM포함 전체 재고 정상 표시
다만 여기서 끝나지 않는다. 다음 태스크로 이월된 것만 셋이다.
- 플레이어 연결 끊김 처리(3초 대기 후 자동 제거)
- 연결 끊김 상태를 화면에 표시
- 재연결 후 게임 재개 기능
재접속이 가르쳐준 것
이틀 동안 열 차례 넘게 수정을 거치며 남은 결론은 하나다. 재접속은 다시 연결되는 이벤트가 아니라, 서버와 클라이언트에 흩어진 여러 상태를 동시에 다시 맞추는 문제다. 이번에 맞춰야 했던 상태만 해도 여섯 가지였다.
- 타이머 기준 시각
- 상대 플레이어 목록
- 화면 진입 여부를 가리는 내부 플래그
- 진행 중인 스테이지 번호
- 재료 해금 상태
- 재고 배열
즉, 소켓 ID 변경은 이 여러 어긋남 중 상대 패널이 식별자를 잃어버리는 딱 한 사례였을 뿐, 다섯 버그 전체를 관통하는 단일 원인은 아니었다.
같은 이유로, 소켓 재연결로 식별자가 바뀌는 문제는 앞으로도 반복될 수 있다. 소켓 ID를 키로 쓰는 자료구조는 재접속 한 번에 그 키가 통째로 무효화된다는 걸 이번에 확인했다. 해법은 새 키로 새 객체를 만드는 게 아니라, 같은 사람인지 다른 단서(닉네임)로 확인해 기존 객체의 키만 옮기는 쪽이었다. 그리고 카운트다운 버그가 보여줬듯, 같은 증상이 두 번 나온다면 이전 수정이 원인이 아니라 표면만 건드렸다는 신호로 봐야 한다.