새로고침하면 데이터가 사라질 때: localStorage 저장 진단 — 바이브 코딩 클리닉 1

언어: 한국어 · English

새로고침 뒤 데이터가 사라지면 저장 코드를 고치기 전에, 그 데이터가 어디에 있었는지부터 확인해야 해요. 화면에 보였다는 사실은 브라우저 메모리·브라우저 저장소·서버 데이터베이스 중 어디에도 영구 저장됐다는 증거가 아닙니다.

이번 바이브 코딩 클리닉의 질문은 하나예요. “추가한 메모가 새로고침하면 사라질 때, 어디부터 확인하고 가장 작게 고칠까?”

1. 증상: 추가는 되는데 새로고침하면 사라져요

메모를 입력하고 추가를 누르면 목록에 나타납니다. 오류도 없어요. 그런데 새로고침하면 빈 목록으로 돌아옵니다. 이때 먼저 확인할 것은 “버튼이 작동했는가?”가 아니라 “새 값이 어느 계층까지 갔는가?”예요.

  1. 화면 상태: 현재 탭의 JavaScript 메모리에만 값이 있음
  2. 브라우저 저장소: 같은 origin의 localStorage·IndexedDB 등에 값이 있음
  3. 서버 저장: 네트워크 요청을 거쳐 백엔드와 데이터베이스에 값이 있음

새로고침은 첫 번째 계층을 다시 시작합니다. 두 번째나 세 번째 계층에 쓰고 다시 읽는 코드가 없다면 사라지는 것이 정상이에요.

2. 먼저 구분할 범위: 화면인가, 브라우저인가, 서버인가

모바일에서도 따라가는 진단 흐름

  1. 메모를 추가한 뒤 화면에는 보이는가?
  2. 새로고침 뒤 사라지는가?
  3. DevTools의 Application → Local Storage에 저장 key가 있는가?
  4. Network에 POST·PUT·PATCH 같은 저장 요청이 있는가?
  5. Console이나 화면 상태 영역에 저장 실패가 표시되는가?

3도 없고 4도 없다면 데이터베이스 장애를 의심할 단계가 아닙니다. 아직 화면 메모리 밖으로 쓰는 경로가 없는 거예요.

3. 최소 재현: 화면 상태만 있는 메모

아래 코드는 메모를 배열에 넣고 화면을 다시 그립니다. 화면에서는 정상처럼 보이지만, 새로고침하면 스크립트가 다시 실행되면서 notes가 빈 배열로 돌아가요.

let notes = [];

form.addEventListener('submit', event => {
  event.preventDefault();
  const text = input.value.trim();
  if (!text) return;

  notes = [...notes, text];
  render();
});

재현 순서: 메모 추가 → 목록에 나타남 → 새로고침 → 목록이 비어 있음. 이 결과는 버튼 실패가 아니라 메모리 상태만 사용한 결과예요.

4. 진단 순서: 증거가 생기는 지점을 따라가세요

① 저장 위치를 코드에서 찾기

useState, 일반 배열, 전역 변수만 있고 localStorage.setItem, IndexedDB, fetch, 서버 SDK 호출이 없다면 지속 저장은 구현되지 않은 상태예요.

② 브라우저 저장소를 직접 보기

DevTools → Application → Local Storage에서 현재 페이지의 origin을 선택하세요. key가 없다면 쓰기가 실행되지 않았거나 실패했습니다. key가 있는데도 화면이 비어 있다면 읽기·JSON 변환·초기 렌더 순서를 확인해야 해요.

③ Network에서 저장 요청 확인하기

서버 저장을 기대했다면 Network에서 해당 클릭 뒤 요청이 실제로 생겼는지 봅니다. 요청이 없다면 backend나 database보다 frontend 연결이 먼저예요. 요청은 있지만 4xx·5xx라면 그때 인증·검증·서버·데이터베이스 범위로 이동합니다.

④ origin이 같은지 확인하기

localStorage는 origin별로 분리됩니다. http://localhost:8000과 http://localhost:3000, HTTP와 HTTPS는 같은 저장 공간이 아니에요. 파일을 더블클릭한 file: URL의 localStorage 동작은 브라우저마다 달라질 수 있으므로 이 실습의 검증 기준으로 삼지 않습니다.

5. 가장 작은 수정: 한 브라우저 안에서만 남기기

로그인·기기 간 동기화가 필요 없는 개인 메모라면 localStorage가 가장 작은 수정일 수 있어요. 아래는 외부 라이브러리 없이 실행되는 전체 최소 예제 v1.0입니다. 실제 비밀키나 업무 데이터는 사용하지 않습니다.

<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>새로고침 저장 진단</title>
  <style>
    body { max-width: 42rem; margin: 3rem auto; padding: 0 1rem;
      font-family: system-ui, sans-serif; }
    form { display: flex; gap: .5rem; flex-wrap: wrap; }
    input { flex: 1; min-width: 12rem; padding: .75rem; }
    button { padding: .75rem 1rem; }
    #status[data-error="true"] { color: #c62828; }
    li { margin: .5rem 0; }
  </style>
</head>
<body>
  <h1>메모</h1>
  <form id="note-form">
    <label for="note">새 메모</label>
    <input id="note" autocomplete="off" required>
    <button>추가</button>
  </form>
  <p id="status" role="status" aria-live="polite"></p>
  <ul id="notes"></ul>
  <button id="reset" type="button">저장된 메모 지우기</button>

  <script>
    const STORAGE_KEY = 'vcc-notes-v1';
    const form = document.querySelector('#note-form');
    const input = document.querySelector('#note');
    const list = document.querySelector('#notes');
    const status = document.querySelector('#status');
    const reset = document.querySelector('#reset');

    function setStatus(message, isError = false) {
      status.textContent = message;
      status.dataset.error = String(isError);
    }

    let loadFailed = false;

    function readNotes() {
      try {
        const raw = localStorage.getItem(STORAGE_KEY);
        if (raw === null) return [];

        const parsed = JSON.parse(raw);
        if (!Array.isArray(parsed) ||
            !parsed.every(item => typeof item === 'string')) {
          throw new Error('저장 형식이 예상과 다릅니다.');
        }
        return parsed;
      } catch (error) {
        loadFailed = true;
        setStatus('불러오기 실패: ' + error.message, true);
        return [];
      }
    }

    let notes = readNotes();

    function render() {
      list.replaceChildren(
        ...notes.map(text => {
          const item = document.createElement('li');
          item.textContent = text;
          return item;
        })
      );
    }

    function writeNotes(nextNotes) {
      try {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(nextNotes));
        setStatus('이 브라우저에 저장됐어요.');
        return true;
      } catch (error) {
        setStatus('저장 실패: ' + error.message, true);
        return false;
      }
    }

    form.addEventListener('submit', event => {
      event.preventDefault();
      const text = input.value.trim();
      if (!text) return;

      if (loadFailed) {
        setStatus('불러오기 오류가 남아 있어 저장하지 않았어요. 새로고침해 다시 읽거나, 실습 데이터를 명시적으로 지운 뒤 추가하세요.', true);
        return;
      }

      const nextNotes = [...notes, text];
      if (!writeNotes(nextNotes)) return;

      notes = nextNotes;
      render();
      form.reset();
      input.focus();
    });

    reset.addEventListener('click', () => {
      try {
        localStorage.removeItem(STORAGE_KEY);
      } catch (error) {
        setStatus('초기화 실패: ' + error.message, true);
        return;
      }
      loadFailed = false;
      notes = [];
      render();
      setStatus('저장된 메모를 지웠어요.');
    });

    render();
  </script>
</body>
</html>

실행: 파일명을 index.html로 저장한 뒤 그 폴더에서 python3 -m http.server 8000을 실행하고 http://localhost:8000을 여세요. Windows에서 Python Launcher를 쓴다면 py -m http.server 8000을 사용할 수 있어요.

6. 정상·실패·재시도를 확인하기

  • 정상: 메모를 추가하면 “이 브라우저에 저장됐어요”가 보이고, 같은 origin에서 새로고침해도 메모가 남습니다.
  • 실패: 저장이 차단되면 화면에 “저장 실패”가 표시되고 목록에도 추가하지 않습니다. 깨진 JSON은 “불러오기 실패”로 드러납니다.
  • 재시도: origin과 브라우저 저장 허용 상태를 바로잡은 뒤 새로고침하여 기존 메모가 정상적으로 불러와지는지 확인하고 다시 추가합니다. 깨진 실습 데이터라면 아래 되돌리기를 먼저 실행해요.

Application → Local Storage에서 vcc-notes-v1 key와 JSON 배열을 확인하세요. 화면 메시지만 보고 성공으로 판정하지 말고, 저장된 값과 새로고침 뒤 복원을 함께 확인해야 합니다.

7. 되돌리기

실습 데이터를 지우려면 화면의 저장된 메모 지우기 버튼을 누르거나 Console에서 다음 한 줄을 실행하세요.

localStorage.removeItem('vcc-notes-v1');

저장 기능 자체를 되돌리려면 readNotes() 초기화와 writeNotes() 호출을 제거하고, 변경 전 파일을 복원합니다. 먼저 파일을 복사해 두면 한 단계로 돌아갈 수 있어요.

8. AI에게 줄 좋은 지시문

메모를 추가하면 화면에는 보이지만 새로고침 뒤 사라진다.
프레임워크를 바꾸거나 전체 구조를 다시 쓰지 말고 다음 순서로 처리해줘.

1. 현재 값이 화면 메모리, 브라우저 저장소, 서버 중 어디까지 가는지 코드 근거로 구분한다.
2. Network에 저장 요청이 있는지와 localStorage key가 생기는지 확인할 로그 위치를 제시한다.
3. 이 앱은 로그인·기기 동기화가 없는 개인용 예제이므로, 가장 작은 수정으로 localStorage 저장/복원을 추가한다.
4. JSON 파싱 실패와 저장 차단을 화면에 표시하고, 저장 실패 시 화면 목록만 성공한 것처럼 바꾸지 않는다.
5. 정상, 실패, 새로고침 복원, 초기화 네 경우의 확인 절차를 적는다.
6. 변경한 파일과 줄, 되돌리는 방법을 마지막에 요약한다.

이 지시문이 좋은 이유는 “저장해줘”라는 결과만 요구하지 않고 범위·허용된 해결책·실패 상태·성공 증거·되돌리기를 함께 지정하기 때문이에요. AI가 데이터베이스를 새로 붙이거나 프레임워크를 교체하는 과잉 수정을 줄일 수 있습니다.

9. 흔한 오진

  1. “버튼이 고장 났다”: 클릭 뒤 화면에 나타났다면 이벤트와 렌더는 작동했을 가능성이 큽니다. 지속 저장은 별도 경로예요.
  2. “데이터베이스가 날아갔다”: Network 저장 요청 자체가 없다면 데이터베이스에 도달하지 않았습니다.
  3. “localStorage에 썼으니 모든 기기에서 보인다”: localStorage는 origin과 브라우저 환경에 묶입니다. 계정 동기화가 아니에요.
  4. “key만 있으면 성공이다”: 읽기 시점·JSON 형식·초기 렌더가 틀리면 저장돼 있어도 화면에 복원되지 않습니다.

10. 이 해결법을 쓰면 안 되는 때

localStorage는 비밀번호·access token·민감한 개인정보·결제 정보의 기본 저장소로 쓰지 마세요. 여러 기기에서 같은 데이터를 봐야 하거나, 여러 사용자가 함께 편집하거나, 권한·감사 기록·백업·충돌 처리가 필요하다면 backend와 database가 Source of Truth가 되어야 합니다. 저장량이 크거나 구조화된 검색·트랜잭션이 필요할 때도 localStorage 한 줄로 해결할 문제가 아니에요.

일요일 Fundamentals와 연결해 읽기

이번 Clinic은 「HTML·CSS·JavaScript 차이: 메모 앱으로 배우는 바이브 코딩 기초」의 다음 질문을 다룹니다. 화면을 바꾸는 JavaScript와 원본 파일을 수정하는 일이 다르듯, 화면 상태와 저장된 데이터도 구분해야 해요.

공식 문서와 확인 범위

공식 문서는 2026년 10월 7일에 다시 확인했습니다. 예제 소스는 문법 검사와 Node 기반 DOM·localStorage 모의 테스트에서 쓰기, 새로고침 복원, 깨진 JSON, 차단된 쓰기 상태를 통과했습니다. Playwright용 브라우저 실행 파일이 없어 실제 브라우저 자동 실행은 완료하지 못했습니다. WordPress 초안은 1363×936 미리보기에서 본문 순서, 코드 보존, 페이지 가로 넘침 없음과 코드 블록의 독립 가로 스크롤을 확인했으며, 모바일 viewport 렌더링은 미확인입니다.


다른 글 보기 · 주제 탐색 · 작성자와 편집 기준 · 문의·정정 요청

RUDA DIRECTOR에서 더 알아보기

지금 구독하여 계속 읽고 전체 아카이브에 액세스하세요.

계속 읽기