지난 글에서는 localStorage 저장과 서버 저장이 왜 전혀 다르게 느껴지는지, 그리고 비동기 저장에서는 "저장 중", "성공", "실패"를 따로 봐야 한다는 이야기를 정리했습니다. 여기까지 오면 거의 바로 다음 질문이 따라옵니다. "그럼 서버 응답을 기다리지 말고 화면부터 먼저 바꾸면 안 될까?" 실제로 이 생각은 꽤 자연스럽습니다. 저장이 느리게 느껴지는 순간, 사용자는 일단 화면만이라도 빨리 반응하길 기대하기 때문입니다.

이때 자주 등장하는 방식이 낙관적 업데이트입니다. 이름은 조금 어렵게 들릴 수 있지만 뜻은 단순합니다. "아마 저장이 성공할 거라고 보고, 화면을 먼저 바꾼다"는 접근입니다. 사용자는 더 빠르게 느끼고, 앱은 즉시 반응하는 것처럼 보입니다. 그런데 여기에는 반드시 따라붙는 조건이 하나 있습니다. 실패하면 되돌릴 수 있어야 한다는 점입니다.

이번 글에서는 왜 낙관적 업데이트가 매력적으로 느껴지는지, 어떤 기능은 이 방식을 써도 괜찮고 어떤 기능은 조금 더 조심해야 하는지, 그리고 초보자 기준에서는 "화면을 먼저 바꾸는 것"보다 "실패 시 어디까지 되돌릴지"를 먼저 정하는 편이 왜 훨씬 중요한지를 차근차근 정리해보겠습니다.

이번 글에서 먼저 잡아둘 핵심
겉으로 보이는 문제 실제로 일어나는 일 가장 먼저 봐야 할 방향
화면은 바로 바뀌는데 가끔 저장이 실패한다 서버 응답 전에 UI를 먼저 반영하고 있다 실패 시 되돌릴 이전 상태를 따로 들고 있는지 본다
앱은 빨라졌는데 실패 후 상태가 더 헷갈린다 낙관적 반영은 했지만 롤백 규칙이 없다 "먼저 바꾸기"보다 "실패 시 복구"를 먼저 설계한다
어떤 기능은 먼저 반영해도 괜찮은데 어떤 건 더 위험하다 기능마다 되돌리기 난도와 실패 비용이 다르다 단순 토글부터 시작하고, 삭제나 생성은 더 조심해서 본다
빠르게 보이게 만들려다 중복 요청과 꼬임이 늘었다 isSaving이나 pending 상태 없이 여러 요청이 겹쳤다 낙관적 업데이트여도 저장 중 상태 잠금은 그대로 필요하다

이번 글의 핵심 한 줄
낙관적 업데이트의 핵심은 화면을 먼저 바꾸는 데 있지 않다. 실패했을 때 어디까지, 어떤 순서로 되돌릴지를 먼저 갖추는 데 더 가깝다.


목차

  1. 왜 화면을 먼저 바꾸고 싶어질까
  2. 낙관적 업데이트는 정확히 무엇인가
  3. 어떤 기능부터 낙관적으로 해보는 편이 좋을까
  4. 되돌리기는 무엇을 저장해둬야 가능한가
  5. 가장 안전한 첫 흐름: 백업 -> 화면 반영 -> 요청 -> 실패 시 복구
  6. 초보자가 자주 하는 실수 6가지
  7. 마무리

왜 화면을 먼저 바꾸고 싶어질까

서버 저장을 붙이면 앱이 갑자기 한 박자 느리게 느껴질 때가 있습니다. 버튼을 눌렀는데 바로 결과가 안 보이고, 저장 중이라는 상태가 잠깐 이어지기 때문입니다. 특히 체크리스트 앱처럼 단순한 동작에서는 사용자가 더 예민하게 느낄 수 있습니다. 완료 체크 하나 바꾸는 데도 잠깐 기다려야 한다면 앱이 둔하게 느껴지기 쉽습니다.

그래서 개발자는 자연스럽게 "어차피 성공할 가능성이 높다면 화면은 먼저 바꿔도 되지 않을까?"라는 생각을 하게 됩니다. 이 발상 자체는 이상하지 않습니다. 실제로 많은 앱이 작은 동작에서는 이 방식을 씁니다. 예를 들어 체크 박스를 눌렀을 때 바로 체크 표시가 바뀌고, 뒤에서 서버 저장을 처리하는 식입니다.

왜 낙관적 업데이트가 매력적으로 보일까
사용자 입장 왜 더 좋아 보이나
버튼을 눌렀다 결과가 바로 보여야 반응이 빠르다고 느낀다
완료 체크를 바꿨다 체크 표시가 즉시 바뀌면 앱이 가볍게 느껴진다
수정 저장을 눌렀다 입력창이 바로 닫히고 목록이 갱신되면 완료된 것처럼 느끼기 쉽다

즉, 낙관적 업데이트는 단순히 멋진 기법이 아니라 "앱이 즉시 반응하는 것처럼 보이게 만들고 싶은 욕구"에서 나옵니다. 다만 여기서 꼭 같이 봐야 하는 것이 있습니다. 정말 항상 성공한다고 가정해도 되는가 하는 문제입니다.

핵심 포인트
낙관적 업데이트는 "빨라 보이고 싶은 욕구"에서 출발한다. 그래서 도입 이유는 분명하지만, 실패했을 때 감당할 구조가 없으면 오히려 더 불안해질 수 있다.


낙관적 업데이트는 정확히 무엇인가

입문자 기준으로 가장 쉽게 설명하면 이렇습니다. 응답 후 확정 방식은 서버가 성공했다고 답할 때까지 화면 확정을 조금 미루는 방식이고, 낙관적 업데이트는 서버가 성공할 거라고 보고 화면부터 먼저 바꾸는 방식입니다.

응답 후 확정 방식과 낙관적 업데이트의 차이
방식 흐름 장점 주의할 점
응답 후 확정 검증 -> 요청 -> 성공 시 화면 반영 실패 처리와 설명이 쉽다 체감상 느리게 느껴질 수 있다
낙관적 업데이트 검증 -> 화면 먼저 반영 -> 요청 -> 실패 시 복구 즉시 반응해서 빠르게 느껴진다 되돌리기 설계가 반드시 필요하다

예를 들어 완료 체크를 생각해보겠습니다. 응답 후 확정 방식이라면 사용자가 버튼을 누른 뒤 잠깐 기다리고, 서버가 성공하면 그때 done 값이 바뀝니다. 낙관적 업데이트라면 버튼을 누르는 즉시 done을 먼저 바꾸고 화면도 바로 갱신합니다. 그리고 뒤에서 서버에 저장 요청을 보냅니다.

이 설명만 들으면 낙관적 업데이트가 무조건 더 좋아 보일 수 있습니다. 그런데 여기서 꼭 따라붙는 질문이 있습니다. "만약 실패하면?" 이 질문에 답이 준비되어 있어야 낙관적 업데이트는 의미가 생깁니다. 그렇지 않으면 빨라 보이는 대신 상태가 더 불안해질 수 있습니다.

핵심 정리
낙관적 업데이트는 화면을 먼저 바꾸는 기술이 아니라, "실패 시 복구"까지 포함한 저장 방식이다.


어떤 기능부터 낙관적으로 해보는 편이 좋을까

입문자에게 가장 중요한 질문 중 하나는 이것입니다. "모든 저장에 낙관적 업데이트를 다 써도 될까?" 보통은 그렇지 않습니다. 기능마다 실패했을 때의 부담과 되돌리기 난도가 다르기 때문입니다.

초보자 기준에서는 되돌리기가 단순한 기능부터 낙관적 업데이트를 시도하는 편이 좋습니다. 예를 들면 개인용 체크리스트에서 완료 체크 토글은 비교적 시도해볼 만합니다. 이전 상태가 true였는지 false였는지만 기억하면 다시 되돌리기 쉽기 때문입니다.

처음엔 어떤 기능부터 낙관적으로 보기 쉬울까
기능 처음 시도 난도 이유
완료 체크 토글 낮은 편 이전 done 값만 기억하면 되돌리기 쉽다
간단한 텍스트 수정 중간 이전 text를 기억해야 하고 편집창 처리도 같이 봐야 한다
새 항목 생성 중간 이상 임시 id와 서버가 돌려주는 id를 맞추는 흐름이 생길 수 있다
삭제 조심할 편 실패 시 항목을 다시 정확한 자리로 복구해야 한다
여러 사람이 함께 보는 데이터 변경 더 조심할 편 내 화면만 먼저 바꾸는 것이 곧바로 진실이 아닐 수 있다

즉, 낙관적 업데이트를 처음 배울 때는 "어떤 기능부터 해보는 게 덜 위험한가"를 같이 보는 편이 좋습니다. 모든 걸 한꺼번에 빠르게 보이게 만들려 하기보다, 되돌리기 쉬운 토글부터 익히는 편이 훨씬 현실적입니다.

입문자에게 가장 무난한 출발점
낙관적 업데이트는 완료 체크 같은 단순 토글부터 시도하는 편이 좋다. 생성과 삭제는 복구 규칙이 더 분명해진 뒤에 붙이는 편이 안전하다.


되돌리기는 무엇을 저장해둬야 가능한가

낙관적 업데이트의 핵심은 빠르게 보이는 것이 아니라 되돌릴 수 있는 것입니다. 그래서 먼저 봐야 할 질문은 "어떤 이전 상태를 저장해둬야 rollback이 가능한가"입니다. 여기서 rollback은 실패했을 때 원래 상태로 되돌리는 흐름을 뜻합니다.

완료 체크 토글처럼 단순한 경우에는 이전 item 하나만 저장해도 될 수 있습니다. 하지만 삭제나 재정렬처럼 목록 전체가 흔들리는 동작은 이전 리스트 전체를 들고 있는 편이 더 쉬울 때가 많습니다. 초보자에게는 이 구분이 꽤 중요합니다.

rollback을 위해 무엇을 백업하면 좋을까
동작 되돌릴 때 필요한 것 초보자에게 더 쉬운 방식
완료 체크 토글 이전 done 값 해당 항목만 기억하기
텍스트 수정 이전 text 값 해당 항목의 원래 text를 보관하기
삭제 삭제 전 항목과 위치 정보 처음에는 리스트 전체 snapshot을 두는 편이 더 쉽다
재정렬 이전 order 상태 처음에는 리스트 전체 snapshot이 이해하기 쉽다

입문자에게 가장 무난한 첫 구조는 대개 아래처럼 이전 상태 snapshot을 하나 두는 방식입니다.

let uiState = {

isSaving: false,
saveError: "",
pendingSnapshot: null
};

그리고 낙관적 반영 전에는 이전 todos를 복사해둡니다.

function cloneTodos(source) {

return source.map(todo => ({ ...todo }));
}

여기서 중요한 건 "복사"입니다. 단순히 const prevTodos = todos처럼 같은 참조를 잡아두면, 이후 변경이 같은 객체에 반영돼서 rollback이 제대로 안 될 수 있습니다. 특히 객체 배열에서는 이 부분을 한 번은 꼭 의식해야 합니다.

핵심 포인트
rollback은 "실패하면 알아서 되돌리기"가 아니다. 먼저 무엇을 백업해둘지 정하지 않으면 되돌릴 재료 자체가 없다.


가장 안전한 첫 흐름: 백업 -> 화면 반영 -> 요청 -> 실패 시 복구

이제 초보자가 처음 시도하기 가장 무난한 낙관적 업데이트 흐름을 보겠습니다. 여기서는 완료 체크 토글처럼 비교적 단순한 동작을 기준으로 생각하면 이해가 쉽습니다.

  1. 먼저 이전 상태를 복사해둔다.
  2. 다음 상태를 계산한다.
  3. 화면과 todos를 먼저 next 상태로 반영한다.
  4. 서버에 저장 요청을 보낸다.
  5. 성공하면 snapshot을 지운다.
  6. 실패하면 이전 snapshot으로 되돌린다.
  7. 마지막에 isSaving을 끄고 렌더링한다.
async function toggleTodoOptimistic(todoId) {

if (uiState.isSaving) return;

const prevTodos = cloneTodos(todos);
const nextTodos = todos.map(todo =>
todo.id === todoId
? { ...todo, done: !todo.done }
: todo
);

uiState.isSaving = true;
uiState.saveError = "";
uiState.pendingSnapshot = prevTodos;

todos = nextTodos;
renderTodos();

try {
const savedTodos = await saveTodosToServer(nextTodos);

todos = savedTodos;
uiState.pendingSnapshot = null;

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

이 구조의 핵심은 두 가지입니다. 첫째, 화면을 먼저 바꾸더라도 무작정 바꾸는 게 아니라 이전 상태를 꼭 백업해둔다는 점입니다. 둘째, 성공과 실패가 같은 마무리 단계로 섞이지 않고 서로 다른 결과를 분명히 가진다는 점입니다.

이 흐름이 입문자에게 특히 좋은 이유
장점 왜 도움이 되나
반응이 즉시 보인다 체크 박스 같은 가벼운 동작은 훨씬 빠르게 느껴진다
실패 시 복구가 가능하다 이전 상태를 이미 들고 있기 때문이다
구조가 비교적 단순하다 생성이나 삭제보다 rollback 설명이 훨씬 쉽다

가장 안전한 첫 원칙
낙관적 업데이트를 처음 붙일 때는 화면을 먼저 바꾸는 속도보다, 실패했을 때 이전 상태를 정확히 되돌릴 수 있는지를 먼저 확인하는 편이 훨씬 중요하다.


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

낙관적 업데이트를 붙일 때는 눈에 보이는 속도 향상 때문에 구조적인 위험을 놓치기 쉽습니다. 아래 실수들은 특히 반복해서 자주 보이는 편입니다.

실수 겉으로 보이는 증상 실제로 문제인 부분 어떻게 보는 게 좋은가
이전 상태 백업 없이 화면부터 바꾼다 실패했을 때 되돌릴 방법이 없다 rollback 재료를 안 들고 갔다 낙관적 반영 전에는 꼭 snapshot부터 만든다
"빠르게 보이기"만 보고 모든 기능에 한꺼번에 적용한다 삭제, 생성, 재정렬까지 실패 처리 규칙이 한꺼번에 꼬인다 기능별 되돌리기 난도를 무시했다 처음엔 토글처럼 단순한 동작부터 시도한다
isSaving 없이 연속 클릭을 허용한다 중복 요청과 늦게 온 응답 때문에 상태가 흔들린다 저장 중 잠금이 없다 낙관적 업데이트여도 저장 중 잠금은 그대로 필요하다
snapshot을 같은 참조로만 들고 있다 복구했는데도 이미 바뀐 값이 남아 있다 깊지 않더라도 최소한의 복사가 필요하다는 점을 놓쳤다 배열과 객체를 명시적으로 복사하는 편이 안전하다
실패했는데도 성공한 것처럼 편집창을 닫는다 사용자는 저장됐다고 믿기 쉽다 실패 후 유지해야 할 UI 상태가 없다 실패 시에는 초안 유지와 에러 안내를 같이 본다
응답이 오면 무조건 성공 처리만 한다 실패나 부분 실패를 놓친다 응답 검사를 충분히 안 했다 성공과 실패를 나누는 기준을 먼저 분명히 둔다

특히 세 번째와 여섯 번째 실수는 같이 엮여서 보일 때가 많습니다. 낙관적 업데이트는 화면이 빨리 바뀌는 만큼, 요청이 겹치거나 실패했을 때 흔들림도 더 크게 체감됩니다. 그래서 속도를 얻는 대신 상태 관리를 더 조심해야 한다는 점을 꼭 같이 기억할 필요가 있습니다.

실전에서 가장 많이 흔들리는 부분
낙관적 업데이트가 불안하게 느껴질 때는 "화면을 먼저 바꿔서"가 아니라, "실패 시 어디로 돌아갈지"와 "요청이 겹치면 누가 마지막 결과인지"를 아직 코드가 분명히 모르기 때문인 경우가 많다.


마무리

낙관적 업데이트는 서버 저장을 더 "빠르게 보이게" 만드는 방식이지만, 실제로는 그보다 더 많은 책임을 요구합니다. 화면을 먼저 바꾸는 대신 실패했을 때 되돌릴 수 있어야 하고, 저장 중 상태도 막아야 하고, 어떤 기능부터 적용할지 신중하게 골라야 합니다. 그래서 초보자에게는 단순히 "더 고급 기술"이라기보다 저장 흐름과 상태 복구를 같이 생각하게 만드는 단계에 가깝습니다.

 

이번 글에서 꼭 가져가면 좋은 건 세 가지입니다.
첫째, 낙관적 업데이트는 화면을 먼저 바꾸는 것보다 rollback 구조를 먼저 갖추는 것이 핵심이라는 점.
둘째, 모든 기능에 한꺼번에 적용하기보다 완료 체크 같은 단순 토글부터 시작하는 편이 훨씬 안전하다는 점.
셋째, snapshot, isSaving, saveError 같은 상태를 같이 가져가야 비로소 "빠르게 보이면서도 설명 가능한" 구조가 된다는 점입니다.


여기까지 오면 이제 저장은 단순한 버튼 클릭이 아니라 성공, 실패, 복구까지 포함한 흐름으로 보이기 시작합니다. 다음 편에서는 이 흐름을 이어서, 요청이 겹칠 때 먼저 보낸 응답이 나중에 도착하면 어떤 값이 최종이 되어야 하는지, 즉 race condition과 늦게 도착한 응답 문제를 초보자 기준에서 차근차근 풀어보겠습니다.