퇴근 후 나만의 AI 팀
PART 13버그디버깅

고쳤더니 또 터졌다 — 출시 직전의 밤들

결제의 뿌리를 건드리자 구독이 사라지고, 키보드가 입력창을 삼키고, 동기화가 멈췄다. 승인과 출시 사이의 이틀.

2026-08-117분 분량

심사 승인이 났다고 바로 출시 버튼을 누를 수 있는 건 아니다. 정확히는 누를 수 있었지만 누르면 안 되는 상태였다.

세 번째 반려를 고치면서 결제 구조를 익명 인증으로 다시 짰다. 그건 화면 문구 몇 개 바꾸는 일이 아니라 "이 사용자가 누구인가"를 판단하는 뿌리를 갈아엎는 수술이었다. 그리고 뿌리를 건드리면 잎이 흔들린다. 승인과 출시 사이의 이틀 동안 흔들린 잎들의 기록이다.

첫째 — 구독이 사라졌다

수정본을 내 폰에 내려받자마자 유료 상태가 무료로 떨어졌다. 결제한 적 없는 앱도 아니고, 내가 직접 결제해서 쓰던 계정이었다.

원인을 찾는 데 세 번의 시도가 필요했다. 그리고 첫 번째 진단은 틀렸다. 이건 그날 기록에 그대로 남겼는데, 남긴 이유가 있다. 틀린 진단으로 고친 코드는 대개 "고쳐진 것처럼 보이다가" 다른 데서 다시 터지기 때문이다. 무엇을 잘못 짚었는지 적어두지 않으면 다음 사람(대개 며칠 뒤의 나)이 같은 길로 되돌아간다.

진짜 원인을 한 문장으로 줄이면 이렇다.

결제 시스템에서 사용자 ID는 '조회 키'가 아니라 '정체'다. 영수증을 제출하는 건 확인이 아니라 소유권을 옮기는 행위다.

익명 식별자와 계정 식별자가 오가는 과정에서, 앱이 "이 구독은 이 사람 것"이라고 판단하는 근거가 흔들렸다. 그래서 판단 로직을 화면 코드에서 떼어내 순수한 계산 함수로 분리하고, "이런 상황에서는 이렇게 판단해야 한다"는 규칙표를 자동 검증에 넣었다. 이제 이 부분을 잘못 고치면 커밋 전에 검사기가 잡는다.

프로젝트 초기에 자동 검증이 91건이라고 썼던 걸 기억하실지 모르겠다. 이때쯤엔 303건이 됐다. 늘어난 항목 대부분이 이렇게 사고를 겪고 추가된 것들이다.

둘째 — 키보드가 입력창을 삼켰다

안드로이드 테스터가 영상을 하나 보내왔다. 홈 화면에서 글을 쓰려고 입력창을 누르면 키보드가 올라오면서 입력창을 통째로 가려버린다. 자기가 뭘 치고 있는지 안 보이는 상태.

이게 뼈아팠던 이유는, 최근 수정 때문에 생긴 게 아니라 처음부터 있던 결함이었다는 것이다. 안드로이드 최신 버전부터 화면 처리 방식이 바뀌었는데, 우리 코드는 옛 방식을 전제로 안드로이드 쪽 회피 동작을 아예 꺼두고 있었다. 안드로이드는 그동안 무선 업데이트를 한 번도 받은 적이 없어서, 아무도 이 문제를 못 보고 있었던 것이다.

고치는 데 여섯 번이 걸렸다. 하이라이트만 옮기면 이렇다.

  • 1차 : 안드로이드 회피 동작을 켰다. 안드로이드는 해결
  • 2차 : 그러자 iOS에서 다른 게 보였다 — 입력창이 키보드 뒤에 잠깐 숨었다가 애니메이션이 끝난 뒤에야 나타난다. 회피가 늦게 적용되는 것이라 iOS는 전용 방식으로 교체
  • 5차 : 키보드를 올린 채 앱을 나갔다 돌아오면 입력창이 사라진다. iOS가 키보드를 알림 없이 복원해서, 우리 계산값이 0으로 굳어 있었다
  • 6차 : 그 5차 수정이 새 버그를 만들었다. 키보드가 다 올라온 순간 입력창이 툭 떨어졌다

6차의 원인이 이 구간에서 제일 배울 만했다. 5차에서 "복귀했을 때 다시 재보자"는 안전장치를 넣었는데, 하필 애니메이션으로 움직이는 중인 요소를 자로 재고 있었다. 움직이는 걸 재면 그 순간의 값이 나오고, 그 값을 정답으로 확정하니 화면이 엉뚱한 데 멈춘다.

레이아웃을 잴 때는 움직이지 않는 것에만 자를 댄다. "이 값이 애니메이션 도중에도 참인가"를 물어야 한다.

셋째 — 동기화가 멈췄다, 그리고 괄호 하나가 구했다

같은 밤에 또 하나가 터졌다. 안드로이드에서 "동기화하지 못했어요"라는 배너만 뜨고 기록이 안 넘어갔다. 유료 기능인 클라우드 정리는 잘 되는데 동기화만 안 됐다.

이 상황에서 제일 곤란한 건 "동기화하지 못했어요"라는 문구가 아무것도 알려주지 않는다는 것이었다. 네트워크 문제인지, 권한 문제인지, 계정 문제인지 알 길이 없다. 그래서 고치기 전에 먼저 이걸 했다. 배너 문구 끝에 오류 코드를 괄호로 붙였다.

이 한 줄이 다음 라운드를 통째로 줄였다.

  • 처음엔 통신 방식 문제로 보고 연결 방식을 바꿨다 — 그러자 배너에 permission-denied가 찍혔다
  • 이건 코드 문제가 아니라 계정 데이터 문제라는 뜻이다. 실제로 서버에 기록된 내 계정 등급이 유료가 아니었다. 앱 화면의 유료 배지는 통신이 막혀 있던 동안의 낡은 값이었다
  • 서버에서 등급을 바로잡는 명령 한 줄로 끝

즉 화면상으로는 똑같은 "동기화 실패"였는데, 실체는 연결 문제와 권한 문제 두 개가 겹쳐 있었다. 괄호 안의 코드가 없었다면 둘을 갈라내느라 밤을 더 썼을 것이다.

사용자에게 보이는 에러 문구에 판별 가능한 단서를 하나 심어두면, 다음 진단이 스크린샷 한 장으로 끝난다.

이 이틀에서 배운 것

정리하면 이렇다.

  • 회귀는 대개 수정에서 온다. 이 구간의 버그 중 절반은 새 기능이 아니라 직전에 고친 것 때문에 생겼다. 그래서 고칠 때마다 "이 수정이 뒤집을 수 있는 게 뭐지"를 한 번 더 묻는 습관이 붙었다
  • 틀린 진단을 기록에 남긴다. 맞은 답만 적어두면 다음에 같은 오답을 다시 지난다
  • 관측 가능성이 곧 속도다. 오류 코드 한 줄, 화면 하단의 버전 표시 한 줄. 이런 게 없으면 원격 진단은 추측이 되고, 추측은 밤을 먹는다
  • 출시 직전의 일은 만드는 게 아니라 지키는 일이다. 이 이틀 동안 새 기능은 하나도 안 만들었다. 그런데 아마 프로젝트 전체에서 가장 중요한 이틀이었을 것이다

그리고 하나 더. 이 모든 버그를 발견한 건 AI가 아니라 사람이었다. 안드로이드 키보드는 테스터가 보낸 영상이었고, iOS 입력창 문제는 내가 직접 폰을 만지다 찍은 영상이었다. AI 팀은 원인을 파고 고치는 데 압도적으로 빨랐지만, "어, 이거 이상한데?"는 사람의 눈에서 나왔다.

작업 문서에 진작 적어둔 규칙이 이 밤들에 여러 번 소환됐다. "사용자가 '되는데?'라고 하면 기기에서 본 게 사실이다. 내 추론이 틀린 쪽을 먼저 의심한다." 이번엔 그 사용자가 대체로 나였다.

이제 정말 누를 일만 남았다.