여기까지 왔다면 지금 할 일 앱은 꽤 많은 걸 할 수 있는 상태입니다. 추가, 수정, 삭제, 검색, 정렬, 우선순위, 마감일까지 들어갔으니 작은 프로젝트치고는 제법 손에 익는 도구가 되었을 겁니다. 그런데 이쯤에서 또 한 번 흐름이 바뀌는 지점이 옵니다. 바로 localStorage 중심 앱에서, API를 통한 서버 저장 감각으로 넘어가기 시작하는 구간입니다.

초보자 입장에서는 이 단계가 꽤 낯섭니다. 지금까지는 "저장" 버튼을 누르면 거의 바로 끝난다는 감각이 강했는데, 서버 저장이 들어오는 순간부터는 결과가 바로 확정되지 않을 수 있기 때문입니다. 잠깐 기다려야 하고, 실패할 수도 있고, 화면을 언제 바꿔야 하는지도 다시 생각해야 합니다.

이번 글에서는 왜 localStorage 저장과 서버 저장이 체감상 완전히 다르게 느껴지는지, 그 차이를 실습처럼 느껴볼 수 있게 정리해보겠습니다. 아직 실제 서버를 바로 붙이지는 않겠습니다. 대신 지금 앱 기준으로 같은 저장 버튼인데 왜 구조가 달라져야 하는지, 그리고 초보자가 처음에는 어떤 방식으로 붙여야 덜 흔들리는지를 차근차근 보겠습니다.

이번 글에서 먼저 잡아둘 핵심
겉으로 보이는 문제 실제로 일어나는 일 가장 먼저 봐야 할 방향
저장 버튼을 눌렀는데 결과가 바로 확정되지 않는다 서버 응답을 기다리는 비동기 흐름이 생겼다 저장 단계를 요청 전, 기다리는 중, 성공, 실패로 나눠서 본다
화면은 바뀌었는데 실제 저장은 실패할 수 있다 화면 반영과 실제 저장 시점이 분리된다 언제 기다릴지, 언제 먼저 보여줄지 규칙을 정한다
같은 저장 버튼인데 localStorage와 서버 저장 코드가 다르게 느껴진다 localStorage는 즉시 끝나는 흐름에 가깝고, 네트워크 저장은 기다림과 실패 가능성이 있다 저장 대상이 바뀌면 화면 상태도 같이 바뀌어야 한다고 본다

이번 글의 핵심 한 줄
서버 저장이 낯선 이유는 문법이 어려워서라기보다, 같은 저장 버튼인데 결과가 나중에 확정되는 구조를 처음 만나기 때문입니다.


왜 지금 서버 저장 얘기를 꺼내는가

지금까지의 할 일 앱은 브라우저 안에서 꽤 잘 돌아갑니다. 새로고침해도 남고, 수정도 되고, 정렬도 되고, 마감일도 붙일 수 있으니까요. 그런데 이 구조는 어디까지나 현재 브라우저 안에서 끝나는 앱에 가깝습니다. 즉, 다른 기기에서 이어서 보거나, 다른 사람과 같은 데이터를 같이 쓰는 흐름으로 가려면 결국 서버 쪽 저장 감각을 피할 수 없게 됩니다.

여기서 중요한 건 당장 서버를 크게 배우는 게 아닙니다. 오히려 초보자에게 더 중요한 건 왜 localStorage와 서버 저장이 체감상 다르게 느껴지는지를 먼저 이해하는 것입니다. 이 차이를 모르고 바로 fetchasync 문법으로만 들어가면, 겉으로는 따라 했는데 구조는 잘 안 남는 경우가 많기 때문입니다.

  • 브라우저 안에서 끝나는 저장과 네트워크를 거치는 저장은 감각이 다릅니다.
  • 같은 저장 버튼이라도 기다림과 실패 처리가 생기기 시작합니다.
  • 입력창, 저장 중 상태, 에러 문구 같은 UI가 더 중요해집니다.
  • 실제 서버를 붙이기 전, 왜 구조가 달라져야 하는지 먼저 이해하면 훨씬 덜 흔들립니다.

핵심 포인트
지금 단계에서는 서버 기술보다도, 저장 결과가 바로 확정되지 않는 구조를 받아들이는 것이 더 중요합니다.


localStorage 저장과 서버 저장은 왜 느낌이 다를까

먼저 가장 단순한 비교부터 해보는 편이 좋습니다. 지금까지는 저장이 거의 브라우저 안에서 끝났습니다. 값을 바꾸고 저장하고 다시 그리면, 그 흐름이 거의 즉시 끝난다고 느끼기 쉽습니다. 그래서 초보자는 자연스럽게 "저장 = 지금 바로 끝나는 일"처럼 이해하게 됩니다.

그런데 서버 저장은 다릅니다. 브라우저 안에서 끝나는 게 아니라, 네트워크를 통해 바깥으로 요청을 보내고 결과를 기다려야 합니다. 그러니 같은 버튼이라도 내부 감각은 완전히 달라집니다. 이제부터는 "요청했다"와 "진짜로 저장이 끝났다"를 분리해서 봐야 합니다.

같은 저장이라도 체감 구조는 다릅니다
항목 localStorage 중심 저장 서버 저장
저장되는 곳 현재 브라우저 안 네트워크 너머의 서버
체감 속도 거의 바로 끝난다고 느끼기 쉽다 잠깐 기다리는 시간이 생길 수 있다
실패 가능성 상대적으로 단순하다 응답 실패, 네트워크 끊김, 서버 오류가 생길 수 있다
추가로 필요한 UI 상대적으로 적다 저장 중, 실패, 다시 시도 같은 상태가 더 중요해진다

즉, 저장 대상이 바뀌면서 코드가 갑자기 어렵게 변한 게 아닙니다. 이전에는 거의 안 보이던 단계가, 서버를 의식하는 순간부터 눈앞에 드러나기 시작한 것에 더 가깝습니다.

핵심 포인트
서버 저장이 낯선 이유는 문법보다도, 요청과 확정이 같은 순간이 아니게 된다는 점에 있습니다.


10분 실습: 같은 저장 버튼인데 왜 감각이 달라지는지 직접 보기

이 차이는 말로만 들으면 조금 추상적으로 느껴질 수 있습니다. 그래서 실제 서버를 붙이기 전에, 서버처럼 느껴지는 가짜 저장 함수를 한 번 만들어보는 편이 좋습니다. 이렇게 하면 왜 기다림이 생기고, 왜 실패 처리가 필요해지는지가 훨씬 손에 잡힙니다.

먼저 지금까지의 localStorage 저장 흐름은 대체로 이렇게 생겼습니다.

function saveTodosToStorage(nextTodos) {
  localStorage.setItem(
    "vibe-todos",
    JSON.stringify(nextTodos)
  );

  return nextTodos;
}

여기서는 값을 넣고 바로 끝납니다. 그래서 그다음 줄에서 이미 저장이 끝났다고 느끼기 쉽습니다.

반대로 서버 감각을 흉내 내려면 이렇게 Promise 기반 함수로 바꿔볼 수 있습니다.

function saveTodosToFakeServer(nextTodos) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      const shouldFail = Math.random() < 0.3;

      if (shouldFail) {
        reject(new Error("save failed"));
        return;
      }

      resolve(nextTodos);
    }, 800);
  });
}

이 코드가 중요한 이유는 문법보다도 감각 때문입니다.

  • 지금 저장 버튼을 눌러도 결과가 바로 끝나지 않습니다.
  • 기다리는 시간 동안 저장 중 상태가 필요해집니다.
  • 실패할 수도 있으니 에러 문구나 재시도 흐름이 필요해집니다.

즉, 같은 저장 버튼이라도 이제부터는 "눌렀다"와 "정말 끝났다" 사이에 빈 공간이 생깁니다. 서버 저장이 낯설게 느껴지는 이유가 바로 여기 있습니다.

핵심 포인트
서버 저장을 처음 익힐 때는 실제 서버를 바로 붙이기 전에, 잠깐 기다리고 가끔 실패하는 가짜 저장 흐름만 만들어봐도 감각이 훨씬 좋아집니다.


서버 저장이 들어오면 꼭 생기는 상태들

이제부터는 저장 버튼 하나로 끝나지 않습니다. 버튼을 누르기 전, 기다리는 중, 성공, 실패를 나눠서 봐야 합니다. 그래서 서버 저장을 처음 붙일 때는 아래 상태들을 따로 두는 편이 좋습니다.

서버 저장이 들어오면 같이 중요해지는 상태들
상태 의미 왜 필요한가
isSaving 지금 요청을 보내고 기다리는 중인지 중복 클릭과 중복 저장을 막기 위해서입니다
saveError 저장이 실패했다는 안내 문구 사용자가 지금 무슨 상황인지 알 수 있게 합니다
draft 아직 화면에서 편집 중인 입력 초안 실패해도 방금 입력한 내용을 잃지 않기 위해서입니다

예를 들어 수정 저장을 서버 버전으로 옮긴다면, 최소한 아래 정도 상태는 같이 따라오게 됩니다.

let uiState = {
  editingTodoId: null,
  editingText: "",
  isSaving: false,
  saveError: ""
};

이런 값이 왜 중요하냐면, 서버 저장에서는 더 이상 "저장 전"과 "저장 후" 둘만 보지 않기 때문입니다. 그 사이에 "지금 저장 중"이라는 구간이 끼어들고, 실패했을 때는 입력 초안을 유지할지까지 같이 봐야 합니다.

핵심 포인트
서버 저장에서는 "성공했는가"만 보는 걸로는 부족합니다. 지금 저장 중인지, 실패했을 때 무엇을 유지할지까지 같이 봐야 합니다.


처음 붙일 때는 보수적인 저장 흐름이 더 낫다

서버 저장 얘기가 나오면 많은 초보자가 금방 궁금해하는 게 있습니다. "그럼 버튼을 누르자마자 화면을 먼저 바꾸면 안 되나?" 이건 흔히 낙관적 업데이트라고 부르는 쪽과 연결됩니다.

이 방식 자체가 틀린 건 아닙니다. 실제 서비스에서도 자주 씁니다. 다만 지금 단계에서는 처음부터 이쪽으로 가는 걸 그다지 권하지 않습니다. 이유는 단순합니다. 실패했을 때 되돌리는 규칙까지 한꺼번에 설계해야 하기 때문입니다. 초보자 기준에서는 이 구간이 생각보다 훨씬 빨리 복잡해집니다.

그래서 첫 서버 저장 흐름은 아래처럼 보는 편이 좋습니다.

1. 입력값을 확인한다
2. 저장 중 상태를 켠다
3. 요청을 보낸다
4. 성공하면 확정한다
5. 실패하면 입력 초안을 유지한다
6. 마지막에 저장 중 상태를 끈다

예를 들면 수정 저장은 이런 식으로 볼 수 있습니다.

async function saveEditedTodoWithServer(todoId) {
  const nextText = editingText.trim();

  if (!nextText) {
    uiState.saveError = "수정 내용은 비워둘 수 없습니다.";
    renderTodos();
    return;
  }

  if (uiState.isSaving) {
    return;
  }

  uiState.isSaving = true;
  uiState.saveError = "";
  renderTodos();

  const nextTodos = todos.map(function (todo) {
    if (todo.id === todoId) {
      return {
        ...todo,
        text: nextText
      };
    }

    return todo;
  });

  try {
    const savedTodos =
      await saveTodosToFakeServer(nextTodos);

    todos = savedTodos;
    editingTodoId = null;
    editingText = "";
  } catch (error) {
    uiState.saveError =
      "저장에 실패했습니다. 다시 시도해 주세요.";
  } finally {
    uiState.isSaving = false;
    renderTodos();
  }
}

이 흐름의 좋은 점은 단순합니다. 실패해도 입력 초안을 잃지 않고, 저장이 진짜 끝나기 전에는 화면 확정을 너무 빨리 하지 않기 때문입니다. 초보자에게는 이쪽이 훨씬 설명하기 쉽고, 덜 흔들립니다.

핵심 포인트
처음 서버 저장을 붙일 때는 빠르게 보이게 만드는 것보다, 성공 전에는 확정하지 않고 실패 시 입력을 유지하는 구조가 훨씬 안전합니다.


Codex에게는 이렇게 나눠서 요청하면 된다

이 단계도 Codex에게 맡길 수 있습니다. 다만 지금은 기능 하나를 더 넣는 게 아니라 저장 감각 자체가 달라지는 구간이기 때문에, 범위를 훨씬 조심스럽게 잘라야 합니다. "서버 저장 붙여줘"라고만 던지면 AI가 실제 API 연결, 화면 먼저 반영, 에러 처리까지 한꺼번에 흔들 가능성이 큽니다.

그래서 처음에는 설명부터 받는 편이 좋습니다.

지금 할 일 앱은 localStorage 기반으로 잘 돌아가는 상태야.
이번에는 실제 서버를 바로 붙이기 전에,
왜 서버 저장은 localStorage와 감각이 다른지
현재 코드 기준으로 설명해줘.
저장 중 상태, 실패 상태, 입력 초안 유지가 왜 필요한지도 같이 설명해줘.

실습용 가짜 서버 저장 흐름만 만들고 싶다면 이렇게 자를 수 있습니다.

@script.js
현재 localStorage 저장 흐름은 유지하고,
실제 API 대신 setTimeout과 Promise를 써서
서버처럼 느껴지는 가짜 저장 함수 예시를 만들어줘.
가끔 실패하게 해도 좋고,
isSaving, saveError가 왜 필요한지도 같이 보여줘.
기존 기능을 전부 뜯지 말고 설명용 예시만 추가해줘.

수정 저장 흐름만 따로 보고 싶다면 이렇게 더 좁게 요청할 수 있습니다.

@script.js
수정 기능 기준으로,
localStorage 저장과 서버 저장 흐름이 어디서 달라지는지
비교해서 보여줘.
서버 저장 버전에서는 저장 중 상태와 실패 메시지가
어떻게 들어가야 하는지도 같이 설명해줘.
화면을 먼저 바꾸는 방식은 아직 넣지 말아줘.

이런 식으로 나누면 AI가 실제 구조를 너무 넓게 흔들지 않고, 지금 꼭 이해해야 할 부분만 드러내도록 만들 수 있습니다. 결국 AI를 잘 쓰는 방식도 복잡한 프롬프트보다 이번엔 무엇까지 이해할 건지 범위를 같이 주는 습관에 더 가깝습니다.

핵심 포인트
Codex에게 이 단계를 맡길 때는 "서버 저장 붙여줘"보다 가짜 서버 저장 흐름으로 감각부터 설명해줘처럼 범위를 잘라주는 편이 훨씬 좋습니다.


마무리

이번 편에서 중요한 건 실제 서버를 붙였느냐가 아닙니다. 오히려 그 전 단계에서 왜 localStorage 저장과 서버 저장이 체감상 다르게 느껴지는지를 먼저 이해했다는 점이 더 중요합니다. 지금까지는 저장 버튼을 누르면 거의 바로 끝나는 흐름이었다면, 이제부터는 요청과 기다림과 실패 가능성이 같이 들어오기 시작한다는 걸 본 셈입니다.

특히 이번 단계에서는 같은 저장 버튼이라도, 저장 중 상태와 실패 메시지, 입력 초안 유지가 왜 함께 필요해지는지를 잡는 것이 핵심입니다. 이 감각이 생기지 않으면 fetch나 async 문법만 따라 해도 구조는 잘 안 남습니다. 반대로 이 감각이 먼저 잡히면, 실제 서버를 붙이는 다음 단계도 훨씬 덜 낯설게 느껴집니다.

즉, 지금은 "서버를 붙인다"보다 저장 결과가 바로 확정되지 않는 구조를 받아들인다는 쪽이 먼저입니다. 이 차이를 이해하는 순간부터, 할 일 앱은 다시 한 번 다음 단계로 넘어갈 준비가 되기 시작합니다.