localStorage까지 쓰는 체크리스트 앱은 초보자에게 꽤 큰 전환점입니다. 새로고침해도 데이터가 남고, 수정과 삭제도 어느 정도 안정적으로 돌아가니까요. 그런데 여기서 한 단계 더 나아가 서버 API로 저장을 붙이기 시작하면 분위기가 갑자기 달라집니다. 같은 "저장"인데도 느낌이 전혀 다릅니다. 버튼을 누르면 바로 끝나던 흐름이, 이제는 잠깐 기다려야 하고, 실패할 수도 있고, 화면을 언제 바꿔야 하는지도 다시 생각해야 합니다.

이 지점에서 많은 입문자가 처음 당황합니다. localStorage에서는 잘 되던 코드 감각이 왜 서버 저장에서는 바로 안 통하는지 이해가 잘 안 되기 때문입니다. 하지만 조금만 차분히 보면, 문제는 "서버가 어렵다"라기보다 저장 결과가 바로 확정되지 않는 구조를 처음 만나기 시작했다는 쪽에 가깝습니다.

이번 글에서는 왜 localStorage 저장과 서버 저장이 체감상 완전히 다르게 느껴지는지, 동기 저장과 비동기 저장이 초보자 입장에서 무엇이 다른지, 그리고 API 저장을 처음 붙일 때 어떤 순서로 생각하면 훨씬 덜 흔들리는지 차근차근 정리해보겠습니다.

이번 글에서 먼저 잡아둘 핵심
겉으로 보이는 문제 실제로 일어나는 일 가장 먼저 봐야 할 방향
저장 버튼을 눌렀는데 결과가 바로 확정되지 않는다 서버 응답을 기다리는 비동기 흐름이 생겼다 저장 단계를 "요청 전", "기다리는 중", "성공", "실패"로 나눠서 본다
화면은 바뀌었는데 서버 저장은 실패할 수 있다 화면 반영과 실제 저장 시점이 분리된다 언제 화면을 먼저 바꾸고 언제 기다릴지 규칙을 정한다
같은 저장 버튼인데 localStorage와 서버 저장의 코드가 달라 보인다 localStorage는 거의 즉시 끝나고, 서버 요청은 대기와 실패 가능성이 있다 동기 저장과 비동기 저장의 감각 차이를 먼저 이해한다
버튼을 두 번 누르면 중복 요청이 생긴다 저장 중 상태 잠금이 없거나 흐리다 isSaving 같은 저장 중 상태를 분명히 둔다

이번 글의 핵심 한 줄
localStorage 저장은 "지금 바로 끝난다"는 감각에 가깝고, 서버 저장은 "결과가 나중에 돌아온다"는 감각에 가깝다. 이 차이를 받아들이는 순간 저장 구조도 달라진다.


목차

  1. 왜 localStorage 저장과 서버 저장은 느낌이 다를까
  2. 동기 저장과 비동기 저장은 무엇이 다른가
  3. 저장 중, 성공, 실패 상태를 왜 따로 둬야 할까
  4. 가장 안전한 첫 흐름: 검증 -> 요청 -> 성공 반영 -> 실패 유지
  5. 화면을 먼저 바꿀까, 응답을 기다릴까
  6. 초보자가 자주 하는 실수 6가지
  7. 마무리

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

localStorage를 쓸 때는 저장이 거의 같은 브라우저 안에서 끝나는 느낌입니다. 코드를 실행하면 바로 반영되고, 그다음 줄에서 이미 저장된 상태처럼 다룰 수 있습니다. 초보자 입장에서는 "저장 = 지금 바로 끝난 일"처럼 느껴지기 쉽습니다.

반면 서버 저장은 다릅니다. 브라우저 안에서 끝나는 게 아니라, 네트워크를 통해 다른 곳에 있는 서버에 요청을 보내고 응답을 기다려야 합니다. 이 과정에서 시간이 조금 걸릴 수 있고, 실패할 수도 있고, 심지어 응답이 늦어질 수도 있습니다. 즉, 저장을 눌렀다고 해서 바로 "끝났다"고 확정할 수 없습니다.

localStorage 저장과 서버 저장의 체감 차이
항목 localStorage 서버 API 저장
데이터가 가는 곳 현재 브라우저 안 네트워크 너머의 서버
체감 속도 거의 즉시 끝난다 대기 시간이 생길 수 있다
실패 가능성 비교적 단순하다 응답 실패, 네트워크 문제, 서버 오류가 생길 수 있다
다른 기기와 공유 보통 안 된다 설계에 따라 가능하다

이 차이 때문에 localStorage에서는 별로 의식하지 않던 단계가 서버 저장에서는 갑자기 중요해집니다. 저장 중 상태, 실패 처리, 다시 시도, 화면을 언제 확정할지 같은 것들이 대표적입니다. 즉, 저장 대상이 바뀌면서 코드가 복잡해진 것이 아니라, 이전에는 숨겨져 있던 단계가 눈앞에 드러난 것에 더 가깝습니다.

핵심 포인트
localStorage에서는 저장이 거의 "지금 끝난 일"처럼 느껴지지만, 서버 저장은 "지금 요청했고 나중에 결과를 받는 일"에 가깝다.


동기 저장과 비동기 저장은 무엇이 다른가

여기서 한 번은 정리하고 가야 할 단어가 있습니다. 바로 "동기"와 "비동기"입니다. 입문자 입장에서는 이 단어가 꽤 어렵게 느껴질 수 있는데, 지금 단계에서는 아주 간단하게 이렇게 이해해도 충분합니다.

동기 저장은 결과가 거의 바로 확정된다고 보고 다음 줄로 넘어갈 수 있는 흐름입니다. 반면 비동기 저장은 결과가 나중에 돌아오기 때문에, 저장 요청을 보낸 뒤에 "성공", "실패", "대기 중" 같은 상태를 따로 들고 가야 하는 흐름입니다.

입문자 기준에서 보는 동기와 비동기 저장의 차이
구분 동기 저장처럼 느껴지는 경우 비동기 저장처럼 느껴지는 경우
예시 localStorage fetch를 통한 서버 API 요청
저장 후 감각 거의 바로 저장됐다고 느끼기 쉽다 아직 기다리는 중일 수 있다
추가로 필요한 상태 상대적으로 적다 isSaving, saveError 같은 상태가 더 중요해진다

예를 들어 localStorage 저장 코드는 대체로 짧게 끝납니다.

function saveTodosToStorage(nextTodos) {

localStorage.setItem("todos", JSON.stringify(nextTodos));
}

반면 서버 저장은 보통 이런 식의 기다림이 붙습니다.

async function saveTodosToServer(nextTodos) {

const response = await fetch("/api/todos", {
method: "PUT",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(nextTodos)
});

if (!response.ok) {
throw new Error("save failed");
}

return await response.json();
}

여기서 중요한 건 fetch 문법 자체가 아닙니다. 핵심은 await가 붙는 순간, 저장 결과를 기다리는 구조가 된다는 점입니다. 즉, 이제는 저장 버튼을 누른 뒤 잠깐 대기하고, 성공하면 확정하고, 실패하면 다시 보여줄 준비를 해야 합니다.

핵심 정리
비동기 저장은 "코드가 어렵다"보다 "결과가 바로 확정되지 않는다"는 감각이 더 중요하다. 그래서 기다리는 상태와 실패 상태가 함께 필요해진다.


저장 중, 성공, 실패 상태를 왜 따로 둬야 할까

서버 저장이 들어오면 앱은 더 이상 "저장 전"과 "저장 후" 두 가지만 가지지 않습니다. 그 사이에 "지금 저장 중"이라는 상태가 끼어들고, 경우에 따라 "실패함"도 따로 봐야 합니다. 이걸 무시하면 버튼을 두 번 누르는 문제부터, 실패했는데도 화면이 닫혀 버리는 문제까지 한꺼번에 생기기 쉽습니다.

초보자에게는 아래 정도 상태만 있어도 충분한 경우가 많습니다.

let uiState = {

editingId: null,
editDraft: "",
formError: "",
saveError: "",
isSaving: false
};
서버 저장이 들어오면 따로 보기 쉬워지는 상태들
상태 의미 화면에서는 어떻게 보일 수 있나
isSaving 지금 요청을 보내고 기다리는 중인지 저장 버튼 비활성화, "저장 중" 문구 표시
saveError 요청 실패 안내 "저장에 실패했습니다" 같은 메시지 표시
editingId, editDraft 아직 편집 상태를 유지 중인지 실패했을 때 입력창을 닫지 않고 그대로 보여줄 수 있다

특히 실패 시 editDraft를 유지하는 것은 초보자에게 꽤 중요한 감각입니다. 네트워크가 잠깐 끊겼다고 해서 사용자가 방금 입력한 내용을 잃어버리면 체감이 크게 나빠집니다. 그래서 보수적인 첫 버전에서는 실패했을 때 입력창은 그대로 두고, saveError만 보여주는 방식이 꽤 자연스럽습니다.

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


가장 안전한 첫 흐름: 검증 -> 요청 -> 성공 반영 -> 실패 유지

입문자에게 가장 무난한 첫 흐름은 "서버가 성공했다고 답할 때까지 실제 화면 확정을 조금 미루는 방식"입니다. 흔히 더 빠르게 보이게 하려고 화면을 먼저 바꾸는 방식도 있지만, 그건 다음 단계에서 다루는 편이 좋습니다. 처음에는 성공하면 확정하고, 실패하면 입력값을 유지한다는 흐름이 훨씬 설명하기 쉽습니다.

예를 들어 수정 저장은 아래처럼 볼 수 있습니다.

async function saveEditToServer() {

const result = validateTodoText(uiState.editDraft);

if (!result.ok) {
uiState.formError = "내용을 확인해 주세요.";
renderTodos();
return;
}

if (uiState.isSaving) return;

uiState.isSaving = true;
uiState.formError = "";
uiState.saveError = "";

const nextTodos = todos.map(todo =>
todo.id === uiState.editingId
? { ...todo, text: result.value }
: todo
);

try {
const savedTodos = await saveTodosToServer(nextTodos);

todos = savedTodos;
uiState.editingId = null;
uiState.editDraft = "";

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

이 흐름에서 중요한 건 성공과 실패가 다르게 처리된다는 점입니다.

이 흐름이 초보자에게 특히 안전한 이유
상황 무엇이 일어나나
성공 todos를 서버 응답값으로 확정하고 편집 모드를 닫는다
실패 입력값은 유지하고 에러 문구만 보여준다

이 구조가 좋은 이유는 단순합니다. 사용자가 입력한 내용을 잃지 않고, 화면이 실제 서버 상태와 어긋나는 시간도 줄일 수 있기 때문입니다. 특히 입문자에게는 "성공 전에는 진짜 확정이 아니다"라는 감각을 익히기에 좋습니다.

가장 안전한 첫 원칙
서버 저장이 처음이라면, 요청 성공 전까지는 실제 확정을 조금 미루고 실패했을 때는 입력 초안을 유지하는 편이 훨씬 덜 흔들린다.


화면을 먼저 바꿀까, 응답을 기다릴까

여기서 자연스럽게 다음 질문이 나옵니다. "그럼 화면은 꼭 서버 응답을 기다렸다가 바꿔야 하나?" 이 질문은 꽤 중요합니다. 실제 서비스에서는 화면을 먼저 바꾸고, 서버가 실패하면 다시 되돌리는 방식도 자주 씁니다. 이걸 보통 "낙관적 업데이트"라고 부릅니다. 쉽게 말하면 "아마 성공할 거라고 보고 먼저 반영하는 방식"입니다.

다만 입문자에게는 이 방식을 처음부터 기본값으로 두는 것을 그다지 권하지 않습니다. 이유는 실패했을 때 되돌리는 흐름까지 한 번에 설계해야 하기 때문입니다. 즉, 단순히 더 빨라 보이게 만드는 것이 아니라, 되돌리기까지 같이 책임져야 합니다.

화면을 먼저 바꾸는 방식과 기다리는 방식의 차이
방식 장점 주의할 점
응답 후 확정 구조가 이해하기 쉽고 실패 처리도 분명하다 체감상 약간 느리게 느껴질 수 있다
화면 먼저 반영 즉각 반응해서 빠르게 느껴질 수 있다 실패 시 원래 상태로 되돌리는 규칙까지 필요하다

그래서 초보자에게는 보통 이렇게 추천할 수 있습니다. 첫 버전은 응답 후 확정으로 만들고, 나중에 저장 구조가 충분히 익숙해졌을 때 낙관적 업데이트를 시도하는 편이 좋습니다. 이렇게 해야 실패 흐름과 되돌리기까지 함께 설명할 여지가 생깁니다.

용어 한 번만 정리
"낙관적 업데이트"는 서버가 성공할 거라고 보고 화면을 먼저 바꾸는 방식이다. 대신 실패하면 원래 상태로 되돌리는 흐름까지 같이 필요해진다.


초보자가 자주 하는 실수 6가지

서버 저장으로 넘어오면 코드가 갑자기 낯설어지는 이유는 문법이 어렵기 때문만은 아닙니다. 이전에는 거의 보이지 않던 "대기", "실패", "되돌리기"가 한꺼번에 등장하기 때문입니다. 그래서 아래 같은 실수도 꽤 자주 반복됩니다.

실수 겉으로 보이는 증상 실제로 문제인 부분 어떻게 보는 게 좋은가
localStorage 하던 감각대로 todos를 먼저 확정해 버린다 실패 시 화면과 서버 상태가 어긋난다 비동기 저장 구조를 동기 저장처럼 다뤘다 첫 버전은 성공 후 확정 구조로 가는 편이 낫다
isSaving 없이 요청을 바로 보낸다 연속 클릭으로 요청이 중복된다 저장 중 상태 잠금이 없다 비동기 저장에서는 isSaving이 훨씬 중요해진다
요청 보내자마자 편집창을 닫는다 실패했을 때 입력값이 사라진 것처럼 느껴진다 성공 전 화면 확정을 너무 빨리 했다 실패 시 초안을 유지할지 먼저 정하는 편이 좋다
catch는 있는데 finally가 없다 실패 뒤에 버튼이 계속 잠긴다 마무리 상태 정리가 빠져 있다 잠금 해제와 렌더링은 마지막 공통 단계로 두는 편이 낫다
서버 응답을 무시하고 nextTodos를 그대로 확정한다 서버가 정리해 준 값이나 최신 상태를 놓칠 수 있다 응답값보다 클라이언트 계산값만 믿고 있다 API 설계에 따라 저장 후 응답값을 반영하는 편이 더 안전할 수 있다
추가, 수정, 삭제가 서로 다른 실패 처리 방식을 쓴다 어떤 기능은 조용히 실패하고 어떤 기능은 경고를 보여준다 공통 저장 흐름이 없다 기능이 달라도 저장 파이프라인은 최대한 비슷하게 맞추는 편이 낫다

특히 다섯 번째 실수는 서버 저장으로 넘어가면서 더 중요해질 수 있습니다. 어떤 API는 저장된 결과를 그대로 다시 돌려줄 수 있고, 서버 쪽에서 id나 시간 값을 다시 붙여서 줄 수도 있습니다. 그래서 무조건 nextTodos만 믿기보다, 서버 응답을 무엇으로 줄지에 따라 최종 반영 기준을 다시 볼 필요가 있습니다.

실전에서 가장 많이 흔들리는 부분
비동기 저장이 어색할 때는 fetch 문법보다 "지금은 대기 중인가, 성공했는가, 실패했는가"를 코드가 정말 분리해서 보고 있는지부터 확인하는 편이 훨씬 빠르다.


마무리

서버 저장으로 넘어오는 순간부터 체크리스트 앱은 "저장 버튼을 누르면 끝나는 구조"에서 조금씩 벗어나기 시작합니다. 이제는 요청 전 검증, 저장 중 잠금, 성공 후 확정, 실패 시 유지 같은 흐름이 같이 필요해집니다. 처음에는 번거롭게 느껴질 수 있지만, 이 구분이 생겨야 중복 저장도 줄고 실패했을 때도 무엇을 유지하고 무엇을 닫아야 할지 설명하기 쉬워집니다.

 

이번 글에서 꼭 가져가면 좋은 건 세 가지입니다.

첫째, localStorage와 서버 저장은 같은 "저장"이지만 체감 구조는 다르다는 점.

둘째, 비동기 저장에서는 isSaving, saveError 같은 상태가 훨씬 중요해진다는 점.

셋째, 추가, 수정, 삭제도 결국은 "다음 상태 계산 -> 요청 -> 성공 확정 -> 실패 유지 -> 마지막 정리"라는 저장 파이프라인으로 묶어볼 수 있다는 점입니다.

 

여기까지 오면 이제 저장 구조는 단순한 버튼 처리 수준을 넘어서, 성공과 실패까지 설명할 수 있는 단계로 올라옵니다. 다음 편에서는 이 흐름을 이어서, 서버 응답을 기다리지 않고 화면을 먼저 바꾸는 낙관적 업데이트를 언제 써도 되고 언제는 조심해야 하는지, 그리고 실패했을 때 되돌리기를 어떻게 이해하면 좋은지를 본격적으로 다루겠습니다.