본문 바로가기
C.W.K.
Stream
Lesson 07 of 08 · published

낙관적 업데이트로 기다림 감추기

~20 min · useOptimistic, instant UI

Level 0호기심
0 XP0/68 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

서버 확인 전에 예상 결과를 보여 줘

useOptimistic은 서버 왕복이 끝나기 전에 사용자가 기대한 결과를 임시 상태로 표시해. 실제 데이터가 확정되면 그 상태로 합쳐지고, 액션이 실패하면 임시 변경은 사라져 원래 값으로 돌아가.

실제 상태와 임시 reducer를 함께 둬

const [optimistic, addOptimistic] = useOptimistic(realState, reducer)처럼 만들고, 변경을 시작할 때 addOptimistic을 먼저 호출한 뒤 서버 액션을 실행해. 임시 상태는 액션이 끝나고 캐시 재검증으로 실제 값이 돌아올 때까지 화면을 이어 줘.

즉각 반응이 의미 있는 곳에 써

  • 좋아요, 하트, 즐겨찾기처럼 클릭 결과가 명확한 동작
  • 왕복이 100ms를 넘는 토글
  • 목록에 항목을 추가하며 “전송 중” 상태를 함께 보여 주는 경우

실패가 모호한 변경에는 조심해

결제나 권한 변경처럼 실패 비용이 큰 작업을 성공한 것처럼 먼저 확정하면 사용자를 속일 수 있어. 되돌림이 자연스럽고 예상 결과가 하나뿐인 동작에 쓰고, 실패 메시지와 재시도 길도 함께 준비해.

낙관적 변경마다 임시 항목을 실제 응답과 맞출 키, 실패 메시지, 재시도 또는 되돌림 행동을 정해. 목록 추가는 임시 ID와 전송 중 표시를 쓰고, 서버 결과가 오면 실제 ID로 교체해. 같은 작업을 빠르게 여러 번 눌렀을 때 reducer 순서가 결과를 보존하는지도 시험해.

즉시 반응이 항상 낙관적 성공을 뜻하지 않아. 결제처럼 확인 전 성공을 보여 주면 안 되는 작업은 버튼 반응과 진행 표시만 즉시 제공하고 결과는 기다려. 사용자의 체감 속도와 사실의 확정 시점을 분리하면 거짓 성공 없이도 빠르게 느껴질 수 있어.

좋아요를 느린 성공, 실패, 빠른 연속 클릭으로 실행해 optimistic reducer의 순서와 되돌림을 관찰해. 목록 추가라면 임시 ID가 서버 ID로 정확히 교체되고 실패 항목에 재시도 길이 남는지 확인해. 화면 반응만 빠르고 사실이 엇갈리면 통과가 아니야.

낙관적 항목과 실제 항목의 정렬 기준도 정해. 서버 시간이 돌아오며 순서가 갑자기 바뀌면 성공했어도 실패처럼 느껴질 수 있어. 임시 상태가 어디까지 닮아야 하는지 제품 경험으로 결정해. 낙관적 항목마다 임시 ID와 서버 ID를 맞추는 규칙을 두고 실패 시 되돌리기나 재시도를 제공해. 빠른 화면보다 틀린 상태가 남지 않는 일이 먼저야.

Code

낙관적으로 갱신하는 할 일 목록·tsx
'use client';
import { useOptimistic } from 'react';
import { addTodo } from '@/app/actions';

type Todo = { id: string; text: string; sending?: boolean };

export function TodoList({ todos }: { todos: Todo[] }) {
  const [optimistic, addOptimistic] = useOptimistic(
    todos,
    (state, newText: string) => [
      ...state,
      { id: crypto.randomUUID(), text: newText, sending: true },
    ]
  );

  async function handleSubmit(formData: FormData) {
    const text = formData.get('text') as string;
    addOptimistic(text);          // 즉시 화면에 표시
    await addTodo(formData);      // 백그라운드
  }

  return (
    <>
      <ul>
        {optimistic.map(t => (
          <li key={t.id} style={{ opacity: t.sending ? 0.5 : 1 }}>
            {t.text}{t.sending && <span className="ml-2 text-xs">saving&hellip;</span>}
          </li>
        ))}
      </ul>
      <form action={handleSubmit}>
        <input name="text" required />
        <button>Add</button>
      </form>
    </>
  );
}

External links

Exercise

느린 좋아요나 목록 추가에 useOptimistic을 적용해. 성공·실패·빠른 연속 클릭에서 임시 상태가 실제 응답과 정확히 합쳐지거나 되돌아가는지 확인해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.