AICella는 방 도면을 그리고 가구를 배치하는 웹 도면 편집기다. 어느 날 이런 제보가 들어왔다.

공간 하나 추가하고 저장을 했는데 새로고침 후에 사라져버렸어.

도면 편집기에서 작업물이 사라지는 건 가장 치명적인 종류의 버그다. 그래서 더더욱 "일단 고쳐 보자"는 유혹이 컸다. 이 글은 그 유혹을 누르고 원인부터 찾았더니 전혀 다른 곳에 범인이 있었던 기록이다.

1. 고치기 전에 규칙부터

이 버그를 맡기면서 AI 코딩 도구(Claude Code)에 디버깅 스킬을 먼저 불러오게 했다. 핵심 규칙은 한 줄이다.

 
 
NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST
(근본 원인을 조사하기 전에는 어떤 수정도 하지 않는다)
 
 

증상은 "공간 추가 후 사라짐"이다. 가장 그럴듯한 용의자는 두 군데였다.

  • 공간 추가 로직이 상태를 잘못 만든다.
  • 저장 로직이 새 공간을 빠뜨린다.

둘 다 그럴듯하다. 그리고 둘 다 틀렸다. 코드부터 고쳤다면 멀쩡한 코드를 뜯어고치고도 버그는 그대로였을 것이다.

2. 재현: 브라우저에서 직접 눌러 보기

코드를 읽어 추측하는 대신 개발 서버를 띄우고 그대로 재현했다.

  1. 공간 추가를 누른다.
  2. 브라우저 콘솔에서 localStorage를 확인한다.
  3. 새로고침한다.
  4. localStorage를 다시 확인한다.

새로고침 전에는 이렇게 나왔다.

 
JSON.parse(localStorage.getItem('room-planner:autosave:v1')).rooms?.map(r => r.name)
// ["내 방", "공간 2"]
 

공간 추가와 저장은 정상이었다. 새 공간은 제대로 저장되어 있었다. 이 한 줄로 첫 번째, 두 번째 용의자가 모두 풀려났다.

새로고침 후에는 이렇게 나왔다.

 
// → undefined (rooms 필드가 아예 없음)
 

저장된 값이 기본 레이아웃(방 1개짜리 단일 방 형식)으로 바뀌어 있었다. 누군가 새로고침 도중에 저장소를 기본값으로 덮어쓴 것이다. 문제는 "저장이 안 된다"가 아니라 "저장된 걸 누군가 지운다"였다.

3. 범인: 마운트 순서

덮어쓴 범인은 FileMenu 컴포넌트의 자동 로컬저장 useEffect였다.

 
 
스토어 생성 → 기본 레이아웃(방 1개)
↓
자식 FileMenu의 effect 실행 → 기본 레이아웃을 localStorage에 저장 (이전 내용 덮어씀!)
↓
부모 App의 effect 실행 → localStorage에서 복원 (이미 망가진 값을 읽음)
 
 

React는 자식 컴포넌트의 effect를 부모보다 먼저 실행한다. 복원은 부모(App.tsx)가, 자동저장은 자식(FileMenu)이 맡고 있었으니 순서가 거꾸로였다. 복원하기도 전에 자동저장이 먼저 돌아 기본값으로 이전 작업물을 지워 버린 것이다.

더 불편한 사실이 있다. 이건 "공간 추가 후"에만 생기는 버그가 아니었다. 새로고침할 때마다 이전 세션 내용이 날아갈 수 있는 구조적 결함이었다. 제보자는 공간을 추가했기 때문에 눈치챈 것뿐이다.

4. 수정: effect가 아니라 생성 시점에서

해법은 단순하다. 복원을 컴포넌트 effect에 맡기지 않고, 스토어가 만들어지는 순간 동기적으로 하는 것이다.

 
 
// 컴포넌트 effect(마운트 순서에 좌우됨)가 아니라 스토어 생성 시점에 동기적으로 복원한다
const initialLayout = loadLocalLayout() ?? createDefaultLayout();
 
 

어떤 컴포넌트의 effect도 이보다 먼저 실행될 수 없으므로 순서 문제 자체가 사라진다. 그다음 App.tsx의 사후 복원 effect를 지웠다. 두 곳에서 같은 일을 하니 경쟁이 생겼던 것이다.

검증도 같은 방식으로 했다. 새 브라우저 상태에서 공간을 추가하고, 새로고침하고, "공간 2"가 살아 있는 것을 눈으로 확인했다. 타입체크, 테스트 217개, 빌드도 통과시켰다.

5. 한 걸음 더: 자동저장이 근본 원인의 일부였다

고치고 나서 더 근본적인 질문이 남았다. 왜 이 버그가 이렇게까지 위험했을까?

편집할 때마다 브라우저와 서버에 조용히 쓰는 자동저장이 있었기 때문이다. 사용자가 누르지 않았는데 일어나는 쓰기는, 이번처럼 한 번 잘못 일어나면 되돌릴 수 없는 덮어쓰기가 된다. 그래서 자동저장을 통째로 걷어내고 "저장" 버튼을 눌렀을 때만 저장하도록 단순화했다. 이것도 브라우저에서 확인했다. 편집해도 localStorage는 변하지 않고, 저장 버튼을 누를 때만 기록된다.

이 커밋은 일부러 별도로 나눴다. 버그 수정(경쟁 상태 제거)과 동작 변경(자동저장 제거)은 성격이 다르기 때문이다. 나중에 둘 중 하나만 되돌려야 할 때 뒤섞여 있으면 곤란하다.

6. 덤으로 나온 UX 문제

같은 세션에서 작은 문제가 하나 더 나왔다. 저장본의 제목이 "지금 편집 중인 공간"의 이름을 따라가고 있었다. 공간 2를 추가하고 선택하면 저장본 제목이 "공간 2"로 바뀐다. 레이아웃 전체를 저장하는데 방 하나만 저장하는 것처럼 보이는 것이다.

제목의 기준을 "편집 중인 공간"이 아니라 "첫 번째 공간"으로 고정해서 해결했다. 이것도 수정 후 브라우저에서 입력창이 "내 방"으로 남아 있는지 확인했다.

7. 디버깅 중 발견한 것을 어떻게 다룰까

작업을 마무리하며 브랜치를 정리하던 중, 이 세션과 무관한 자동 보안 스캔이 백엔드 인증 코드에서 항목들을 보고했다.

  • 데모 계정이 항상 시딩된다 (CRITICAL)
  • 인증에 하드코드된 fallback secret이 있는 것으로 보인다 (CRITICAL)
  • 로그인에 rate limit이 없다 (MEDIUM)

여기서 흔한 실수는 "어차피 열려 있으니 같이 고치자"다. 하지만 이 파일들은 이번 작업과 무관했고, 이번 PR의 범위가 아니었다. 섞어 넣으면 리뷰가 어려워지고, "새로고침 버그 수정" PR에 인증 변경이 끼어드는 이상한 이력이 남는다.

그래서 고치지도, 무시하지도 않고 별도 작업 카드로 넘겼다. 클릭 한 번으로 새 세션에서 처음부터 다룰 수 있게 내용과 대상 파일을 정리해 남겼다. 같은 이유로 이번 세션 변경분이 아닌 CSS 경고도 "범위 밖이라 건드리지 않았다"고 명시하고 넘어갔다. 발견한 것을 기록해 두되 지금 하던 일에 섞지 않는다는 원칙이다.

정리

처음 의심한 곳 실제 원인
공간 추가 로직 정상
저장 로직 정상
자식 effect가 부모 effect보다 먼저 실행되는 순서 범인

배운 점을 세 가지로 줄이면 이렇다.

  1. 증상이 가리키는 곳이 원인이 아닐 수 있다. "공간 추가 후 사라짐"의 원인은 공간 추가가 아니라 앱이 뜨는 순서였다.
  2. 콘솔에서 값을 한 줄 찍어 보는 것이 추측 열 번보다 낫다. "새로고침 전에는 있고, 후에는 기본값"이라는 두 줄이 용의자를 모두 가렸다.
  3. 조용한 자동 쓰기는 부채다. 사용자가 누르지 않았는데 데이터를 덮어쓰는 코드는, 단 한 번의 순서 실수가 곧 데이터 손실이 된다.