지난 글에서는 빈값, 공백, 중복 같은 입력 검증을 어디서 처리해야 하는지 다뤘습니다. 여기까지 오면 바로 다음 단계가 보입니다. 검증이 끝난 뒤, 실제로 "저장"은 어떤 순서로 일어나야 하는가 하는 문제입니다. 초보자 입장에서는 보통 저장 버튼을 누르면 값만 바뀌면 되는 것처럼 느껴집니다. 하지만 실제로는 그 안에 생각보다 많은 단계가 들어 있습니다.

왜 이게 중요하냐면, 저장 흐름이 흐리면 이상한 문제들이 하나씩 나오기 때문입니다. 저장 버튼을 두 번 눌렀더니 같은 항목이 두 번 들어가거나, 목록 화면은 이미 바뀌었는데 새로고침하면 이전 상태로 돌아오거나, 수정 입력창은 닫혔는데 실제 저장은 실패한 상태가 되는 식입니다. 기능 하나하나는 맞아 보여도 저장 순서가 분명하지 않으면 앱 전체가 어색해지기 쉽습니다.

이번 글에서는 왜 저장을 하나의 동작으로만 보면 금방 꼬이는지, 저장을 어떤 단계로 나눠 보면 좋은지, 그리고 localStorage를 쓰는 작은 앱에서도 왜 저장 흐름을 분리해 보는 습관이 중요한지를 차근차근 정리해보겠습니다. 나중에 서버 저장으로 넘어갈 때도 결국 이 감각이 그대로 이어집니다.

이번 글에서 먼저 잡아둘 핵심
겉으로 보이는 문제 실제로 일어나는 일 가장 먼저 봐야 할 방향
저장 버튼을 빠르게 두 번 누르면 중복 저장된다 저장 중 상태를 잠그지 않았다 isSaving 같은 저장 중 상태가 있는지 본다
화면은 저장된 것처럼 보이는데 다시 열면 이전 상태다 화면 변경과 실제 저장 단계를 섞어 봤다 데이터 반영과 저장 단계를 나눠서 본다
입력창은 닫혔는데 저장 실패가 뒤늦게 보인다 실패 처리를 생각하지 않고 화면부터 먼저 닫아 버렸다 실패 시 어떤 상태를 유지할지 먼저 정한다
추가, 수정, 삭제가 각자 제멋대로 저장된다 저장 흐름을 기능마다 따로 짰다 공통 저장 파이프라인을 먼저 세운다

이번 글의 핵심 한 줄
저장은 버튼 하나가 아니라 "검증 -> 잠금 -> 실제 저장 -> 화면 확정 -> 상태 정리" 같은 여러 단계를 가진 흐름으로 보는 편이 훨씬 안전하다.


목차

  1. 왜 저장은 버튼 하나로 끝나지 않을까
  2. 저장은 어떤 순서로 나뉘어야 할까
  3. localStorage 저장도 흐름을 나눠서 보는 편이 좋은 이유
  4. 저장 중 상태와 에러 상태는 어떻게 들고 갈까
  5. 추가, 수정, 삭제는 결국 같은 저장 파이프라인으로 묶을 수 있다
  6. 초보자가 자주 하는 실수 6가지
  7. 마무리

왜 저장은 버튼 하나로 끝나지 않을까

입문자에게 저장은 보통 "값을 바꾸고 끝"처럼 보입니다. 예를 들어 수정 저장이라면 text를 새 값으로 바꾸고, 추가 저장이라면 배열에 새 항목을 넣으면 되는 것처럼 느껴집니다. 그런데 실제 앱에서는 그 전에 해야 할 일과 그 뒤에 해야 할 일이 적지 않습니다.

예를 들어 수정 저장만 생각해도 최소한 이런 질문이 붙습니다. 입력값은 유효한가? 지금 저장 중은 아닌가? 실제 데이터는 어느 시점에 바꿀 것인가? 저장이 성공했을 때 편집창은 언제 닫을 것인가? 실패하면 입력값은 유지할 것인가? 이 질문들에 답이 없으면 저장 버튼은 눌리는데 결과가 매번 들쭉날쭉해지기 쉽습니다.

겉으로는 저장 버튼 하나지만 실제로는 여러 단계가 붙는다
단계 왜 필요한가
검증 빈값, 공백, 너무 긴 값 같은 문제를 실제 데이터에 넣지 않기 위해
잠금 저장 중 중복 클릭이나 다른 충돌 동작을 막기 위해
실제 저장 브라우저 저장소나 서버에 현재 상태를 반영하기 위해
화면 확정 편집 모드 종료, 목록 갱신, 버튼 비활성화 해제 등을 위해
에러 처리 실패 시 무엇을 유지하고 무엇을 되돌릴지 분명히 하기 위해

즉, 저장은 "데이터 한 줄 바꾸기"보다 상태 전환에 더 가깝습니다. 저장 전에는 편집 중 상태였고, 저장 후에는 실제 데이터가 바뀐 일반 상태로 돌아가야 합니다. 이 흐름이 분명해야 버튼이 두 번 눌려도 버티고, 실패해도 어디까지 되돌릴지 설명할 수 있습니다.

핵심 포인트
저장은 "값 변경" 자체보다, 입력값 검증부터 상태 정리까지 이어지는 한 묶음의 흐름이라고 보는 편이 훨씬 정확하다.


저장은 어떤 순서로 나뉘어야 할까

입문자 기준에서 가장 무난한 저장 순서는 아래처럼 잡는 편이 좋습니다. 이 순서만 지켜도 저장 관련 버그가 많이 줄어듭니다.

  1. 입력값을 정리하고 검증한다.
  2. 저장 중 상태를 켠다.
  3. 다음 상태를 계산한다.
  4. 실제 저장소에 반영한다.
  5. 성공했을 때 화면 상태를 닫거나 초기화한다.
  6. 마지막에 저장 중 상태를 끈다.

이 흐름을 코드로 보면 대략 이런 느낌입니다.

function saveEdit() {

const result = validateTodoText(uiState.editDraft);

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

if (uiState.isSaving) return;

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

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

saveTodosToStorage(nextTodos);

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

} catch (error) {
uiState.formError = "저장 중 문제가 생겼습니다.";
} finally {
uiState.isSaving = false;
renderTodos();
}
}

이 코드에서 초보자가 특히 봐야 할 부분은 두 군데입니다. 첫째, nextTodos를 먼저 계산한 뒤 저장을 시도한다는 점입니다. 둘째, 성공하기 전까지는 현재 편집 상태를 너무 빨리 지우지 않는다는 점입니다. 이 순서가 있어야 실패했을 때도 사용자가 입력하던 값을 다시 보여줄 수 있습니다.

저장 순서를 나누면 뭐가 좋아지나
순서가 분명할 때 순서가 흐릴 때
실패해도 어느 단계에서 멈췄는지 보기가 쉽다 화면은 닫혔는데 저장은 안 된 식의 애매한 상태가 생기기 쉽다
중복 저장 방지가 쉽다 버튼 연속 클릭에 취약해진다
성공과 실패 시 UI를 다르게 설명할 수 있다 무조건 닫거나 무조건 열어두는 식이 되기 쉽다

핵심 정리
검증과 저장과 화면 종료를 한 줄에 섞기보다, "검증 -> 잠금 -> 저장 -> 성공 시 확정 -> 마지막 정리" 순서로 나누면 훨씬 설명하기 쉬워진다.


localStorage 저장도 흐름을 나눠서 보는 편이 좋은 이유

입문자에게는 이런 생각이 들 수 있습니다. "지금은 서버도 없고 그냥 localStorage에 넣는 건데, 이렇게까지 단계를 나눠야 하나?" 어느 정도는 이해되는 생각입니다. localStorage는 눈앞에서 바로 저장되는 느낌이 강하고, 네트워크 요청처럼 오래 기다릴 일도 보통 없기 때문입니다.

그런데도 흐름을 나눠서 보는 편이 좋은 이유가 있습니다. localStorage든 나중의 서버 저장이든, 앱 입장에서는 둘 다 외부 저장 단계입니다. 즉, todos를 그냥 메모리에서 바꾸는 것과는 역할이 다릅니다. 이 구분을 지금부터 해두면 나중에 서버로 넘어가도 구조를 통째로 갈아엎지 않아도 됩니다.

localStorage도 저장 단계로 따로 보는 편이 좋은 이유
이유 왜 도움이 되나
화면 변경과 저장 단계를 분리할 수 있다 지금 바뀐 것이 화면인지 실제 저장인지 구분하기 쉬워진다
공통 저장 함수로 묶기 쉽다 추가, 수정, 삭제가 같은 흐름을 공유할 수 있다
나중에 서버 저장으로 바꾸기 쉽다 saveTodosToStorage 부분만 바꿔도 전체 흐름은 유지될 수 있다

예를 들어 지금은 이렇게 둘 수 있습니다.

function saveTodosToStorage(nextTodos) {

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

이 함수가 크지 않아 보여도 역할은 분명합니다. todos를 어디에 반영할지, 어떻게 문자열로 바꿀지, 나중에 다른 저장 방식으로 바꿀 여지가 어디에 있는지를 한 군데로 모아주기 때문입니다. 즉, localStorage가 단순하다고 해서 흐름까지 단순하게 섞어버릴 이유는 없습니다.

쉽게 말하면
지금은 localStorage만 써도 괜찮다. 다만 "데이터 계산"과 "외부 저장"을 다른 단계로 보는 습관은 일찍 들일수록 좋다.


저장 중 상태와 에러 상태는 어떻게 들고 갈까

저장 흐름을 안정적으로 만들려면 값 검증만으로는 부족합니다. 지금 저장 중인지, 직전에 실패했는지, 어떤 메시지를 보여줘야 하는지도 같이 봐야 합니다. 이 값들은 todo 데이터 자체가 아니라 화면 상태에 가깝기 때문에, uiState 쪽으로 분리해서 드는 편이 좋습니다.

let uiState = {

editingId: null,
editDraft: "",
formError: "",
isSaving: false
};

여기서 formError는 입력 검증 실패나 저장 실패 안내 문구를 보여주는 데 쓸 수 있고, isSaving은 연속 클릭을 막고 버튼을 비활성화하는 데 쓸 수 있습니다. 중요한 건 이 두 값이 실제 todo 데이터 안으로 들어가면 안 된다는 점입니다. 사용자가 다시 앱을 열었을 때 "저장 중"이나 "입력 오류" 같은 값까지 영구 데이터처럼 남아 있을 필요는 없기 때문입니다.

저장 흐름에서 자주 쓰는 화면 상태
상태 이름 무슨 뜻인가 화면에서는 어떻게 쓸 수 있나
isSaving 지금 저장 처리 중인지 저장 버튼 비활성화, "저장 중" 표시
formError 지금 보여줄 검증 또는 저장 에러 문구 입력창 아래 안내 문구 표시

이 상태들이 있어야 사용자는 왜 저장이 안 됐는지 이해할 수 있고, 개발자도 "값 문제인지", "중복 클릭 문제인지", "저장 중 잠금 문제인지"를 훨씬 쉽게 구분할 수 있습니다. 결국 저장 흐름은 값만 다루는 게 아니라, 지금 화면이 어떤 단계에 있는지도 함께 다루는 일입니다.

핵심 정리
isSaving과 formError는 todo 데이터가 아니라 저장 흐름을 설명하는 화면 상태다. 그래서 uiState 쪽에 두는 편이 훨씬 자연스럽다.


추가, 수정, 삭제는 결국 같은 저장 파이프라인으로 묶을 수 있다

입문자가 저장 구조를 처음 볼 때 가장 많이 놓치는 점 중 하나는, 추가, 수정, 삭제가 서로 완전히 다른 기능이라고 느끼는 것입니다. 화면에서는 그렇게 보일 수 있습니다. 하지만 저장 흐름 관점에서 보면 셋은 꽤 비슷합니다. 모두 다음 todos를 계산하고, 저장하고, 화면 상태를 정리한다는 점에서 같습니다.

겉보기는 달라도 저장 파이프라인은 비슷하다
동작 달라지는 부분 공통으로 묶을 수 있는 부분
추가 배열 끝에 새 항목 하나를 넣는다 검증, isSaving 잠금, save 호출, render
수정 특정 항목의 text를 바꾼다 검증, isSaving 잠금, save 호출, render
삭제 특정 항목을 배열에서 제거한다 isSaving 잠금, save 호출, render

그래서 기능이 늘기 시작하면 저장 파이프라인을 조금씩 공통으로 빼볼 수 있습니다. 예를 들면 다음 상태를 계산하는 함수만 바꿔 끼우는 식입니다.

function persistNextTodos(createNextTodos) {

if (uiState.isSaving) return false;

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

try {
const nextTodos = createNextTodos(todos);

saveTodosToStorage(nextTodos);
todos = nextTodos;

return true;

} catch (error) {
uiState.formError = "저장 중 문제가 생겼습니다.";
return false;
} finally {
uiState.isSaving = false;
renderTodos();
}
}

그리고 수정은 이렇게 쓸 수 있습니다.

function saveEdit() {

const result = validateTodoText(uiState.editDraft);

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

const ok = persistNextTodos((currentTodos) => {
return currentTodos.map(todo =>
todo.id === uiState.editingId
? { ...todo, text: result.value }
: todo
);
});

if (ok) {
uiState.editingId = null;
uiState.editDraft = "";
renderTodos();
}
}

입문자 입장에서는 이런 구조가 조금 길어 보일 수 있습니다. 하지만 기능이 늘수록 저장 흐름이 한 군데로 모여 있다는 장점이 커집니다. 저장 버튼 잠금, 실패 메시지, 실제 저장, 렌더링이 매번 제각각 흩어지지 않기 때문입니다.

핵심 포인트
추가, 수정, 삭제는 화면상으로는 달라도 "다음 todos 계산 -> 저장 -> 화면 정리"라는 공통 흐름을 공유한다. 그래서 저장 파이프라인을 공통으로 빼두면 나중에 훨씬 덜 흔들린다.


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

저장 구조를 붙이기 시작하면 기능은 더 안정돼 보이지만, 비슷한 실수도 반복됩니다. 대부분은 순서가 섞이거나, 저장과 화면 상태를 한꺼번에 너무 빨리 닫아버리는 쪽에서 나옵니다.

실수 겉으로 보이는 증상 실제로 문제인 부분 어떻게 보는 게 좋은가
검증 전에 저장부터 시도한다 잘못된 값이 실제 데이터에 먼저 들어간다 검증과 저장 순서가 뒤바뀌었다 검증이 먼저, 저장은 그다음이라고 고정해서 본다
isSaving 없이 바로 저장한다 버튼 연속 클릭에 취약하다 행동 잠금 단계가 없다 값 검증과 별도로 저장 중 잠금도 꼭 본다
todos를 먼저 바꾸고 저장 실패를 나중에 처리한다 화면과 저장 결과가 어긋날 수 있다 다음 상태 계산과 실제 반영 순서가 흐리다 nextTodos를 먼저 계산하고, 반영 시점을 분명히 두는 편이 낫다
성공 여부와 상관없이 편집창을 바로 닫는다 실패했는데도 사용자는 저장된 줄 알기 쉽다 실패 시 유지해야 할 화면 상태를 생각하지 않았다 성공 후에만 editingId를 닫는 편이 좋다
추가, 수정, 삭제가 각자 다른 저장 흐름을 쓴다 어떤 곳은 에러가 보이고 어떤 곳은 안 보인다 공통 파이프라인이 없다 저장 공통 흐름부터 먼저 빼는 편이 낫다
catch만 두고 finally를 안 쓴다 저장 실패 뒤 버튼이 계속 비활성화된 채 남을 수 있다 마무리 상태 정리가 한쪽 경우에만 일어난다 잠금 해제와 렌더링은 finally처럼 공통 마무리 단계로 두는 편이 안전하다

특히 여섯 번째 실수는 처음엔 눈에 잘 안 띄지만 나중에 꽤 크게 느껴집니다. 성공 케이스만 계속 테스트하면 문제가 없어 보이는데, 한 번이라도 예외가 생기면 버튼이 풀리지 않거나 입력창이 이상한 상태로 남을 수 있기 때문입니다. 그래서 저장 흐름은 "성공했을 때 뭐 할까"만큼이나 "실패하거나 중간에 끊기면 무엇을 정리할까"도 같이 봐야 합니다.

실전에서 가장 많이 흔들리는 부분
저장이 이상하게 느껴질 때는 저장 함수 한 줄만 볼 게 아니라 "검증 -> 잠금 -> 반영 -> 정리"의 순서가 흐트러졌는지를 같이 보는 편이 훨씬 빠르다.


마무리

저장 흐름을 분리해서 보기 시작하면 체크리스트 앱이 한 단계 더 안정적으로 보입니다. 이전까지는 기능마다 그때그때 다르게 처리하던 작업이, 이제는 "검증하고, 잠그고, 저장하고, 성공 시 확정하고, 마지막에 정리한다"는 공통 흐름 안으로 들어오기 때문입니다. 이 구조가 생기면 중복 저장도 줄고, 실패했을 때도 어디까지 되돌릴지 설명하기 쉬워집니다

.

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

첫째, 저장은 버튼 하나가 아니라 여러 단계가 있는 흐름이라는 점.

둘째, localStorage를 쓰는 작은 앱에서도 데이터 계산과 외부 저장 단계를 나눠 보는 습관이 중요하다는 점.

셋째, 추가, 수정, 삭제도 결국은 같은 저장 파이프라인 안에서 정리할 수 있다는 점입니다.

 

여기까지 오면 이제 체크리스트 앱은 기능이 많은 수준을 넘어, 데이터가 실제로 어떻게 확정되는지 설명할 수 있는 구조를 갖추기 시작합니다. 다음 편에서는 이 흐름을 이어서, localStorage만 쓰던 앱을 서버 API 저장 구조로 바꿔보려 할 때 왜 갑자기 "기다림", "실패", "되돌리기" 문제가 커지는지, 즉 동기 저장에서 비동기 저장으로 넘어갈 때 달라지는 감각을 다루겠습니다.