Menu

React useOptimistic 훅: 즉각적인 UI와 롤백

useOptimistic은 async 액션이 아직 실행 중인 동안 예상되는 결과를 보여 주고, 끝나면 실제 state로 돌아가므로 실패한 요청은 스스로 되돌려집니다. 문법, 리듀서 형태, 그리고 액션 안에서 실행해야 하는 이유를 배웁니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

useOptimistic은 액션이 끝나기 전에 화면이 액션의 결과를 보여 주게 하는 React 19 훅입니다. 액션 안에서 낙관적 값을 설정하면, React는 액션이 대기 중인 동안 그 값을 보여 주고 그다음 실제 state로 돌아갑니다. 요청이 실패했다면 실제 state는 한 번도 바뀌지 않았으므로 화면이 스스로 롤백됩니다.

이 예제의 가짜 API는 실제 요청을 대신하며, 세 번째 호출마다 실패하므로 두 가지 결과를 모두 볼 수 있습니다.

클릭 사이에 1초씩 기다리면서 버튼을 세 번 클릭해 보세요. 처음 두 번은 바로 바뀌고, 저장된 줄은 1초 뒤에 따라잡습니다. 세 번째도 바로 바뀌지만, 요청이 실패하면 다시 돌아갑니다. calls % 3을 calls % 2로 바꾸면 두 번째 클릭마다 실패합니다.

문법

const [optimisticState, setOptimistic] = useOptimistic(state, updateFn?);
  • state는 실제 값으로, 보통 useState, props, useActionState에서 옵니다. 대기 중인 액션이 없으면 optimisticState는 정확히 이 값입니다.
  • setOptimistic(value)는 현재 액션이 실행되는 동안 낙관적 값을 설정합니다.
  • updateFn은 선택 사항입니다: (currentState, optimisticValue) => nextState. 이것이 있으면 setOptimistic은 완전히 새로운 값 대신 변경분을 받습니다. 아래의 리듀서 형태를 참고하세요.

클릭 한 번의 생명주기는 항상 같습니다.

  1. 액션 안에서 setOptimistic을 호출합니다. 화면이 바로 업데이트됩니다.
  2. 액션이 요청을 기다립니다.
  3. 성공하면 실제 state를 업데이트합니다(await 뒤라면 startTransition 안에서).
  4. 액션이 끝납니다. optimisticState는 다시 state를 따라갑니다. 성공하면 저장된 값을, 실패하면 이전 값을 보여 줍니다.

4단계 덕분에 예제에는 롤백 코드가 없습니다. catch 블록은 오류 메시지만 설정합니다.

액션 안에서 실행해야 합니다

낙관적 값은 트랜지션이 대기 중인 동안에만 존재하므로, setOptimistic은 트랜지션 안에서 호출해야 합니다. 다음은 모두 해당됩니다.

  • startTransition(또는 useTransition의 startTransition)에 넘기는 함수,
  • <form action={...}>이나 <button formAction={...}>에 넘기는 함수,
  • useActionState에 주는 액션.

일반 onClick에서 호출하면 낙관적 값이 머물 액션이 없습니다. React는 바로 되돌리고, 개발 환경에서는 "An optimistic state update occurred outside a transition or action"을 기록합니다.

await 뒤에 일어나는 실제 state 업데이트는 위의 startTransition(() => setLiked(next))처럼 자기만의 startTransition으로 감싸야 합니다. await 뒤에서는 React가 아직 이전 트랜지션 안에 있다는 것을 알 수 없습니다.

리듀서 형태로 만드는 메시지 목록

낙관적 state가 목록이라면 두 번째 인자로 업데이트 함수를 넘기세요. 그러면 setter는 새 항목을 받고, React가 그 순간의 실제 목록에 덧붙입니다.

텍스트를 다르게 해서 메시지 세 개를 하나씩 보내 보세요. 각각 흐리게 "(sending...)"과 함께 나타났다가 전달되면 선명해지고, 세 번째는 채팅에서 사라지고 "Not sent"로 표시됩니다. 대신 빠르게 보내면 세 개 모두 마지막 요청이 끝날 때까지 흐리게 남습니다. React는 대기 중인 모든 액션이 끝날 때까지 낙관적 값을 유지하기 때문입니다. 폼 액션은 이미 트랜지션이므로, 여기서는 addOptimistic에 별도의 startTransition이 필요 없습니다.

각 낙관적 항목에는 실제 항목과 겹치지 않는 key를 주세요. 예제는 pending-에 텍스트를 붙여 쓰므로, 둘 다 대기 중일 때 같은 텍스트를 두 번 보내면 겹칩니다. 실제 앱이라면 클라이언트에서 id를 만들 것입니다.

여러 업데이트를 동시에

업데이트 함수가 현재 state를 받으므로 낙관적 업데이트는 쌓입니다. 빠르게 클릭하면 대기 중인 각 클릭이 직전 클릭 위에 적용됩니다.

빠르게 네 번 클릭해 보세요. 장바구니가 바로 4를 보여 줍니다. 요청이 끝나면 세 번째 요청이 실패했으므로 개수는 3에 자리 잡고, 콘솔이 그렇게 알려 줍니다. React는 대기 중인 모든 액션이 끝날 때까지 낙관적 값을 유지한 다음 실제 개수를 보여 줍니다.

언제 쓰고, 언제 쓰지 말까

낙관적 업데이트는 거의 항상 성공하고 되돌리기 쉬운 동작에 적합합니다. 좋아요, 별표, 토글, 이름 바꾸기, 메시지 추가 같은 것들입니다. 흔한 경우에 사용자는 스피너를 보지 않습니다.

잘못된 "완료"가 오해를 낳을 곳에서는 피하세요. 결제, 계정 삭제, 결과를 알기 전에 사용자가 행동할 수 있는 모든 것이 그렇습니다. 그런 경우에는 useActionState나 useTransition의 isPending으로 대기 상태를 보여 주고 실제 응답을 기다리세요.

롤백이 일어나면 항상 사용자에게 알리세요. 조용히 되돌아가는 값은 버그처럼 보입니다. 위의 두 예제 모두 오류 메시지를 일반 state에 두어서 액션보다 오래 남게 합니다.

useActionState와 함께 쓰기

useOptimistic과 useActionState는 잘 맞습니다. useActionState의 액션은 이미 트랜지션이므로 그 시작 부분에서 낙관적 값을 설정할 수 있고, 반환된 state가 낙관적 값이 돌아갈 실제 값이 됩니다.

const [state, formAction] = useActionState(async (previous, formData) => {
    const title = formData.get('title');
    setOptimisticTitle(title);
    const saved = await saveTitle(title);
    return { title: saved.title };
}, { title: 'Untitled' });

const [optimisticTitle, setOptimisticTitle] = useOptimistic(state.title);

저장하는 동안 페이지는 새 제목을 보여 줍니다. 완료되면 state.title에 저장된 값이 들어 있습니다. saveTitle이 오류를 던지면 액션의 오류는 가장 가까운 에러 바운더리로 갑니다. 대신 메시지와 함께 이전 제목을 보여 주려면, 액션 안에서 오류를 잡고 이전 state와 오류를 반환하세요.

자주 하는 실수

액션 바깥에서 setter 호출하기. 낙관적 값이 화면에 남지 않고, React가 개발 환경에서 경고합니다. startTransition으로 감싸거나 폼 액션으로 옮기세요.

실제 state 업데이트를 잊는 것. 성공하면 액션은 useOptimistic이 따라가는 state(setLiked, setMessages)를 바꿔야 합니다. 그렇지 않으면 액션이 끝날 때 낙관적 값이 사라지고, 성공한 요청이 롤백처럼 보입니다.

트랜지션 없이 await 뒤에 실제 state 업데이트하기. 업데이트는 여전히 반영되지만, React가 액션이 끝나는 시점과 다른 순간에 보여 줄 수 있습니다. 세 예제 모두 그렇게 하듯이 startTransition으로 감싸세요.

낙관적 값을 진실의 원천으로 쓰기. 요청을 보내고 합계를 계산할 때는 실제 state를 쓰세요. 낙관적 값은 화면 표시용일 뿐이며 언제든 버려질 수 있습니다.

자주 묻는 질문

useOptimistic은 무엇을 하나요?

async 액션이 실행되는 동안 즉시 바꿀 수 있는 state의 사본을 줍니다. 액션이 끝나면 사본은 다시 실제 state를 따라가며, 그때 실제 state에는 저장된 결과나 이전 값이 들어 있습니다.

useOptimistic은 오류가 나면 어떻게 롤백하나요?

특별한 오류 경로가 필요 없습니다. 낙관적 값은 액션이 대기 중인 동안에만 존재합니다. 요청이 실패하고 실제 state를 업데이트하지 않으면, 훅이 다시 실제 state, 즉 클릭 전의 값을 보여 줍니다.

"An optimistic state update occurred outside a transition or action" 경고는 왜 나나요?

setter를 일반 이벤트 핸들러에서 호출했기 때문입니다. startTransition에 넘긴 함수 안, <form action> 안, 또는 useActionState의 액션 안에서 호출하세요.

useOptimistic의 두 번째 인자는 무엇인가요?

선택적인 업데이트 함수 (currentState, optimisticValue) => newState로, 리듀서와 비슷합니다. 이것이 있으면 setter는 변경분(새 메시지, +1)만 받고, 여러 업데이트가 동시에 대기 중이어도 React가 낙관적 state를 계산합니다.

useOptimistic은 폼에서만 쓰나요?

아니요. 어떤 트랜지션에서든 동작하므로, 폼 액션뿐 아니라 startTransition(async () => { ... })을 호출하는 버튼에서도 쓸 수 있습니다.

Coddy 프로그래밍 언어 일러스트

Coddy로 코딩 배우기

시작하기