FEInterview Prep

Velog

`useOptimistic`으로 즉각 반응하는 앱 만들기

React 19 의 useOptimistic"잘 될 거라고 가정하고 UI 를 먼저 갱신" 하는 패턴을 안전하게 구현하게 해준다. 단, 트랜지션 안에서만 동작하며 실패 롤백은 자동이다.

2025-07-21·6분 읽기
React성능
원문 보기 ↗

핵심 요약

API 시그니처는 단순하다.

const [optimisticState, addOptimistic] = useOptimistic(
  realState,
  (current, action) => nextState
);

핵심 규칙:

  • addOptimistic(action)startTransition 안에서만 호출 가능. 그렇지 않으면 React 가 경고하거나 무시.
  • 비동기 작업이 끝나기 전까지 렌더 결과optimisticState 를, 끝난 뒤엔 다시 realState 를 따른다.
  • 비동기 작업이 에러로 끝나거나 트랜지션이 끝나면 자동 롤백 — 별도 try/catch 로 되돌릴 필요 없음.
function LikeButton({ initialCount, sendLike }) {
  const [count, setCount] = useState(initialCount);
  const [optimisticCount, addOptimistic] = useOptimistic(
    count,
    (cur, delta) => cur + delta
  );
  return (
    <button onClick={() => {
      addOptimistic(1);
      startTransition(async () => {
        await sendLike();
        setCount(c => c + 1);
      });
    }}>👍 {optimisticCount}</button>
  );
}

리스트에 항목 추가 같은 케이스는 임시 id (crypto.randomUUID())pending 플래그를 함께 둬서 시각적으로 구분(반투명/스피너)하는 게 정석.

낙관적 UI 의 핵심은 "서버 응답을 기다리지 말고 결과를 가정해 즉시 보여주자" 다. 직접 구현하면 임시 ID 부여 / 롤백 / 동시성 셋이 어렵다. useOptimistic 은 이 셋을 React 가 떠맡고, 우리는 "진짜 상태""가정한 상태" 두 변수만 분리해 다루면 된다. 그래서 머릿속 모델은 "진짜 상태에 임시 변형을 한 겹 덮어쓴 뷰" 다.

현대 앱에서 입력 → 응답 시간이 100ms 를 넘으면 사용자는 "클릭이 먹혔나?" 의심한다. SPA 가 무거워질수록 이 격차가 커진다. 낙관적 UI 는 체감 지연 0 을 만드는 가장 저렴한 방법이고, 좋아요/별점/댓글/장바구니 추가 같은 가벼운 mutation 에 폭넓게 쓰인다.

면접에서는 다음 세 가지를 분리해 답할 수 있어야 한다.

  • 언제 써야 하는가: 실패율이 낮고 보정 가능한 mutation
  • 언제 쓰면 안 되는가: 결제·중요한 도메인 트랜잭션
  • 어떻게 안전하게 만드는가: 트랜지션·고유 임시 키·실패 시 토스트

학습 포인트

면접 답변으로 연결할 학습 포인트입니다.

"가정한 상태" 와 "진짜 상태" 를 분리한 두 변수 모델

useOptimistic 은 진짜 상태를 건드리지 않고, 렌더용 가짜 상태를 한 겹 덧씌운다.

  • count (진짜): 서버가 확정해 준 값
  • optimisticCount (가정): 사용자가 클릭한 직후 즉시 반영된 값

둘을 동시에 만지면 안 된다. "진짜" 갱신은 setCount 로, "가정" 갱신은 addOptimistic 으로 — 책임이 분리돼 있어서 롤백 = 가정만 버리면 끝.

optimistic statetransitionreducer signature
자주 하는 오해

addOptimistic 안에서 setCount 를 같이 호출. "가정" 과 "진짜" 가 뒤섞여 롤백 시 일관성이 깨진다.

트랜지션 안에서만 동작한다

addOptimistic 은 React 트랜지션 진행 중 에만 유효하다. startTransition(async () => { ... }) 내부에서 비동기 작업을 하면, 트랜지션이 끝나는 시점에 자동으로 가정 상태가 비워진다.

startTransition(async () => {
  await sendLike();          // 이 동안 optimisticCount 가 살아있음
  setCount(c => c + 1);      // 진짜 상태 확정
}); // 트랜지션 종료 → optimistic 자동 해제

그래서 별도의 "롤백 코드" 가 필요 없다. 실패하면 진짜 상태를 그대로 두고, 가정만 사라진다.

startTransitionuseTransitionServer Action
자주 하는 오해

트랜지션 밖에서 addOptimistic 호출 → 무시되거나 경고. 또는 startTransition 콜백을 sync 로 만들어 await 결과가 트랜지션 밖 으로 새는 것.

낙관적 UI 가 위험한 영역과 안전 패턴

낙관적 UI 의 트레이드오프:

적합부적합
좋아요/별점 토글결제, 송금
댓글/태그 추가재고 차감, 좌석 예약
칸반 카드 위치 변경권한 변경, 회원 탈퇴

안전하게 만드는 4가지 가드:

  • 임시 id(crypto.randomUUID())로 서버 id 와 분리
  • pending 플래그로 "미확정 항목" 을 시각적 구분(반투명 + 스피너)
  • 실패 시 토스트로 명시적 알림 + 자동 롤백 신뢰
  • 동일 항목에 대한 연속 클릭 디바운스 또는 큐잉
pending statetemp ididempotencyrollback UX
자주 하는 오해

결제 같은 도메인에 낙관적 UI 를 적용. 사용자에게 "성공" 으로 보였다가 롤백되면 신뢰가 무너진다.

읽는 순서

  1. 1이론

    useOptimistic 시그니처와 트랜지션과의 관계를 정리한다. 진짜 상태/가정 상태 두 변수의 책임이 어떻게 다른지 한 문장으로 요약.

  2. 2구현

    좋아요 버튼과 댓글 추가 두 가지 예제를 직접 구현한다. 댓글 추가에서는 임시 id + pending 플래그를 쓰고, 일부 요청은 강제로 실패시켜 자동 롤백을 눈으로 확인.

  3. 3실무

    프로젝트의 mutation 들 중 낙관 적용 가능 한 것과 불가 한 것을 분류한다. 가능한 1개를 실제 적용해보고 인터랙션 지연(클릭 → 반응)을 측정·비교.

  4. 4설명

    동료에게 "낙관적 UI 가 결제에 부적합한 이유" 를 5분 안에 설명한다. 단순 "실패하니까" 가 아니라 롤백 비용 > 즉시성 이득 의 트레이드오프 언어로.

면접 연결 질문

medium`useOptimistic` 와 `useState` + 수동 롤백 구현의 차이를 설명해보세요.
힌트

[감점 답변] "useOptimistic 가 더 짧다". [좋은 답변] 핵심은 두 가지 책임이 분리 된다는 점. (1) useOptimistic가정 상태 만 관리하고, 실패/완료 시 자동 롤백. (2) useState 수동 구현은 try/catch 로 이전 값 복원 + 동시 요청 충돌 + 임시 id 관리 까지 직접 짜야 한다. 또한 useOptimistic트랜지션과 결합 돼 React 가 낮은 우선순위로 처리해주므로 인터랙션 응답성이 보장된다.

medium낙관적 UI 가 적합하지 않은 도메인을 두 가지 이상 들고 그 이유를 설명해보세요.
힌트

[감점 답변] "실패할 수 있는 건 다 안 됨". [좋은 답변] (1) 결제/송금 — 사용자에게 "성공" 으로 보였다가 롤백되면 회복 불가능한 신뢰 손실. (2) 재고/좌석 예약 — 다른 사용자와 경합하므로 낙관 자체가 거짓 정보를 보여줄 확률이 높음. (3) 권한/탈퇴 — 보안 영향 작업. 공통 기준은 롤백 비용 > 즉시성 이득 인 경우.

hard리스트에 항목을 낙관적으로 추가할 때 임시 id 와 `pending` 플래그가 왜 필요한가요?
힌트

[감점 답변] "안 써도 됨". [좋은 답변] (1) 임시 id: 서버가 부여한 진짜 id 와 충돌하지 않도록 crypto.randomUUID() 같은 클라이언트 id 부여. 응답이 오면 임시 → 진짜 id 로 매핑 교체. (2) pending 플래그: 사용자에게 "아직 확정 아님" 을 시각적으로 알려야 한다(반투명/스피너). 안 그러면 새로고침 시 사라지는 항목이 "버그" 처럼 느껴진다. 이 둘이 없으면 리스트 정렬·삭제 도중 실패 시 충돌이 난다.

자기 점검

`useOptimistic` 가 `startTransition` 밖에서 호출되면 어떤 일이 일어나는지 설명해보세요.
transitionwarning무효롤백
자주 하는 오해

트랜지션 없이도 낙관적 업데이트가 "그냥 쓸 수 있다" 는 인식. 실제로는 React 가 거부하거나 즉시 폐기한다.

`useOptimistic` 의 reducer 가 순수 함수여야 하는 이유를 말해보세요.
순수 함수재실행동시성
자주 하는 오해

reducer 안에서 fetch 같은 부수 효과를 호출해도 된다는 오해. 재렌더 시 다중 실행돼 상태가 어긋난다.