BUG · 버그 도감 #13
적은 기록이 다음 날 사라졌다: '2026-8-29'와 '2026-08-29'
사주 플로우의 하루 기록장에 적은 내용이 저장은 되는데 다음에 열면 없었습니다. 쓰는 쪽은 날짜를 '2026-8-29'로, 읽는 쪽은 '2026-08-29' 꼴만 받았기 때문입니다. 1년 중 300일이 걸리는 0 하나의 차이와, 이미 저장된 기록을 버리지 않고 살려 낸 해결을 정리했습니다.
버그 도감은 KKflow 프로젝트에서 실제로 만난 버그를 한 편에 하나씩 기록하는 시리즈입니다. 모든 내용은 해당 저장소의 커밋 기록과 코드를 근거로 합니다.
프로젝트 사주 플로우 · 시기 2026년 8월 24~29일 · 관련 기술 날짜 문자열, localStorage, 저장 형식 바꾸기
기록장이 생기다
사주 플로우는 8월 24일 오후(한국 시각)에 하루 기록장을 붙였습니다. 앱은 아침에 "오늘 72점" 같은 그날의 운세 점수를 보여 주고, 사용자는 저녁에 실제 하루가 어땠는지 1~5로 적습니다. 기록은 서버가 아니라 사용자의 브라우저 안(localStorage)에 날짜를 열쇠로 해서 저장됩니다.
닷새 뒤인 8월 29일 저녁, 기록장에 새 기능을 붙이던 커밋이 설명 맨 앞에 이런 제목을 달았습니다.
"만들다가 찾은 것: 기록이 조용히 사라지고 있었다"
이 커밋은 Claude Code가 작성했고, 원래 목적은 앱의 예상이 실제로 맞았는지 세는 "적중률"과, 꾸준히 적으면 자라는 캐릭터를 붙이는 것이었습니다. 버그는 그 작업 도중에 발견됐습니다.
증상: 저장은 되는데 다음에 열면 없다
커밋 설명이 증상을 이렇게 적었습니다. "적은 것이 저장은 되는데 다음에 열면 없다." 기록을 적은 그날은 화면에 멀쩡히 보입니다. 하지만 다음 날 앱을 다시 열면 그 기록이 사라져 있습니다. 오류 메시지도, 경고도 없었습니다. 코드 주석의 표현으로 "화면에는 아무 표시도 안 났다 — 그날은 멀쩡히 보이고, 다음 날 열면 없다."
원인: 쓰는 쪽과 읽는 쪽의 다른 약속
기록장은 날짜마다 한 칸씩 기록을 담고, 그 칸을 찾는 열쇠로 날짜 문자열을 씁니다. 문제는 그 문자열을 만드는 쪽과 읽는 쪽이 서로 다른 모양을 기대했다는 것입니다.
// 쓰는 쪽 (lib/params.ts, 고치기 전)
today: `${t.year}-${t.month}-${t.day}`, // → "2026-8-29"
// 읽는 쪽 (lib/journal/store.ts)
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/; // 달과 날은 반드시 두 자리
if (!DATE_RE.test(date) || …) continue; // 꼴이 안 맞으면 건너뜀
화면은 숫자를 그대로 이어 붙여 2026-8-29를 만들었습니다. 읽는 쪽은 저장된 기록을 불러올 때 열쇠가 YYYY-MM-DD, 즉 달과 날이 두 자리인지 확인하고, 아니면 잘못된 데이터로 보고 건너뛰었습니다. 그래서 저장된 기록이 다음에 읽을 때 조용히 버려졌습니다.
읽는 쪽의 검사 자체는 나쁜 생각이 아니었습니다. 브라우저 저장소에는 무엇이든 들어 있을 수 있으니, 날짜처럼 생기지 않은 열쇠를 거르는 것은 합리적인 방어입니다. 다만 그 "날짜처럼 생긴 모양"의 정의가 쓰는 쪽과 달랐습니다.
1년 중 300일
이 차이가 얼마나 넓었는지가 이 버그의 핵심입니다. 0이 빠지는 것은 달이 한 자리(1~9월)이거나 날이 한 자리(1~9일)일 때입니다. 커밋은 이렇게 적었습니다. "한 자리 달(1~9월)과 한 자리 날(1~9일)이 다 그랬으니 8월은 통째로, 다른 달도 초순이 전부 그랬다."
2026-8-29는 달이나 날이 한 자리일 때마다 읽는 쪽의 꼴(YYYY-MM-DD)과 어긋납니다. 1년에 맞는 날은 10~12월 가운데 10일 이후의 날짜뿐입니다. 기록 기능은 8월 24일에 생겼으니, 고칠 때까지 적은 날은 모두 빨간 칸이었습니다.2026년으로 세어 보면 365일 중 300일이 0 빠진 열쇠가 됩니다. 우연히 두 모양이 같아지는 날은 10~12월 가운데 10일 이후의 날짜, 65일뿐입니다. 기록장은 8월 24일에 생겼으니, 고쳐진 8월 29일까지 적은 날은 모두 8월, 모두 빨간 칸이었습니다. 커밋의 표현대로 "기록 기능이 생긴 뒤로 거의 내내 그랬다."
반대로 10월 중순에 이 기능을 처음 만들어 시험했다면 한동안 아무 문제도 보이지 않았을 것입니다. 날짜 형식 버그가 무서운 이유입니다. 어떤 날에 테스트하느냐에 따라 버그가 보이기도 하고 숨기도 합니다.
테스트는 버그를 못 박고 있었다
사주 플로우에는 테스트가 많습니다. 그런데 이 버그는 테스트를 통과했습니다. 오히려 테스트가 버그를 지키고 있었습니다. 커밋은 이렇게 적었습니다. "옛 검사 셋이 '0 없는 꼴'을 기대하고 있었다. 버그를 못 박고 있던 셈이다."
// tests/streak.test.ts (고치기 전 → 고친 뒤)
- expect(dayKeys({ year: 2026, month: 3, day: 1 }).prev).toBe('2026-2-28');
+ expect(dayKeys({ year: 2026, month: 3, day: 1 }).prev).toBe('2026-02-28');
- expect(dayKeys({ year: 2026, month: 8, day: 8 }).today).toBe('2026-8-8');
+ expect(dayKeys({ year: 2026, month: 8, day: 8 }).today).toBe('2026-08-08');
날짜 열쇠를 만드는 함수의 테스트는 "그 함수가 지금 내놓는 값"을 정답으로 적어 두었습니다. 함수 하나만 놓고 보면 테스트는 통과합니다. 하지만 그 값을 받아서 읽는 쪽과 맞는지는 아무도 확인하지 않았습니다. 그래서 고친 뒤에는 쓰는 쪽이 만든 열쇠를 읽는 쪽이 그대로 읽는지를 한 번에 보는 테스트가 들어갔습니다("화면이 만드는 열쇠를 기록장이 그대로 읽는다").
해결: 고치는 것보다 살리는 것
쓰는 쪽을 고치는 것은 간단했습니다. 열쇠를 만드는 함수 하나(dayKey)를 두고 항상 0을 채우게 했습니다.
const pad = (n) => String(n).padStart(2, '0');
export function dayKey(t) {
return `${t.year}-${pad(t.month)}-${pad(t.day)}`; // → "2026-08-29"
}
어려운 쪽은 이미 저장된 기록이었습니다. 사용자의 브라우저에는 2026-8-29 같은 옛 열쇠가 남아 있을 수 있습니다. 읽는 쪽이 계속 그것을 버리면, 버그를 고친 날에도 그동안의 기록은 돌아오지 않습니다. 그래서 커밋은 읽는 쪽을 "버리지 않고 고쳐서 받도록" 바꿨습니다. 이유도 적어 두었습니다. "지금 브라우저에 남아 있는 기록을 살릴 수 있는 자리가 여기뿐이다." 서버에 저장하지 않는 앱이라, 사용자 기기에 있는 데이터를 고칠 기회는 앱이 그것을 읽는 순간밖에 없습니다.
// lib/journal/store.ts (요약)
const LOOSE_DATE_RE = /^(\d{4})-(\d{1,2})-(\d{1,2})$/; // 한 자리도 받는다
export function normalizeDateKey(date) {
// "2026-8-29" → "2026-08-29"로 채우고,
// 실제로 그런 날이 있는지 Date로 만들어 다시 확인한다
}
여기에는 한 가지 조심이 더 있습니다. 모양만 맞다고 날짜는 아닙니다. 2026-13-01이나 2026-02-30도 숫자 모양은 맞습니다. 그래서 고친 열쇠로 실제 날짜를 만들어 보고, 같은 날이 나오지 않으면 그 기록은 여전히 버립니다. 커밋은 그 이유를 "없는 날이 들어가면 그날 일진을 못 내 통계가 어긋난다"고 적었습니다. 버그 도감 #7의 2월 31일이 3월로 굴러가던 바로 그 성질을 거꾸로 이용한 검사입니다.
연속 기록(며칠째 이어서 적었는지)에도 같은 처리가 들어갔습니다. 연속 기록은 그동안 쓰는 쪽과 읽는 쪽이 같은 모양을 써서 멀쩡히 돌아가고 있었습니다. 그런데 열쇠 모양을 바꾸는 순간 "어제"와 "오늘"이 다른 모양이 되어, 이어 보던 사람들의 기록이 전부 1로 돌아갈 수 있었습니다. 커밋은 이 부분도 읽을 때 모양을 맞추도록 했습니다. 버그를 고치는 변경이 다른 곳에서 새 버그가 되지 않게 막은 것입니다.
이 커밋 기준으로 자동 검사는 678건에서 719건으로 늘었습니다.
교훈
- 날짜 문자열은 한 곳에서 만든다. 같은 날짜를 여러 곳에서 각자 이어 붙이면 언젠가 모양이 어긋납니다. 0을 채운
YYYY-MM-DD는 문자열로 정렬해도 날짜 순서가 맞는다는 장점도 있습니다(2026-8-29는2026-10-01보다 뒤로 정렬됩니다). - "오늘" 테스트는 날짜를 가린다. 이 버그는 1년 중 65일 동안은 보이지 않습니다. 날짜가 들어가는 코드는 한 자리 달·한 자리 날·월말·윤년을 일부러 넣어 시험해야 합니다.
- 함수 혼자가 아니라 주고받는 짝을 테스트한다. 쓰는 함수의 테스트는 지금 값을 정답으로 적어 버그를 지켰습니다. 쓰고 읽는 왕복을 한 번에 보는 테스트가 있었다면 첫날 걸렸을 것입니다.
- 저장 형식을 바꿀 때는 옛 데이터를 읽는 길을 남긴다. 특히 데이터가 사용자 기기에만 있는 앱이라면, 읽는 순간이 고칠 수 있는 유일한 기회입니다.
사주 플로우의 다른 버그는 #11 손으로 붙인 조사, #7 2월 31일생의 사주에, 전체 과정은 사주 플로우 제작기에 있습니다.