useTransition을 쓰면 state 업데이트를 긴급하지 않은 것으로 표시할 수 있습니다. React는 그 업데이트를 백그라운드에서 렌더링하고, 클릭이나 키 입력처럼 더 긴급한 일이 생기면 버리거나 다시 시작하므로, 화면의 느린 부분이 따라잡는 동안에도 페이지가 반응성을 유지합니다.
posts를 클릭한 다음, 목록이 나타나기 전에 contact를 클릭해 보세요. contact가 바로 보입니다. React가 반쯤 끝난 posts 렌더링을 버린 것입니다. 이제 startTransition(() => { setTab(next); })를 일반 setTab(next);로 바꾸고 같은 일을 해 보세요. posts를 클릭한 뒤 페이지가 약 300ms 동안 멈추고, contact 클릭은 posts가 끝날 때까지 기다립니다.
문법
const [isPending, startTransition] = useTransition();
startTransition(() => {
setTab(next);
});
startTransition은 함수를 받아서 바로 실행합니다. 그 안의 모든 state 업데이트는 트랜지션으로 표시됩니다.isPending은 트랜지션을 시작한 순간부터 React가 렌더링을 끝내고 결과를 커밋할 때까지true입니다.
useTransition은 인자를 받지 않습니다. 넘긴 함수는 동기적으로 실행됩니다. 트랜지션은 함수가 언제 실행되는지가 아니라 React가 그 결과로 생긴 업데이트를 어떻게 렌더링하는지에 관한 것입니다.
"긴급하지 않다"는 뜻
React는 업데이트를 두 종류로 나눕니다. 입력, 클릭, 누르기 같은 긴급한 업데이트는 즉시 반영되어야 하며, 그렇지 않으면 페이지가 고장 난 것처럼 느껴집니다. 새 화면으로 전환하거나 필터링된 결과를 보여 주는 것 같은 트랜지션 업데이트는 잠시 걸려도 됩니다.
React는 트랜지션을 작은 조각으로 나눠 렌더링하고, 컴포넌트 사이에서 긴급한 것이 들어왔는지 확인합니다. 들어왔다면 트랜지션을 멈추거나 포기하고, 긴급한 업데이트를 처리한 다음, 최신 state로 트랜지션을 다시 시작합니다. 트랜지션이 렌더링되는 동안에는 이전 화면이 그대로 남아 있으며, React는 반쯤 만들어진 결과를 보여 주지 않습니다. 그래서 예제에서는 posts가 준비될 때까지 about 텍스트가 계속 보이고, 버튼 옆에 Loading 힌트가 나타납니다.
중단은 컴포넌트 사이에서 일어납니다. 300ms 동안 막히는 컴포넌트 하나는 반으로 자를 수 없으며, 그래서 위의 느린 작업은 작은 SlowPost 컴포넌트 300개에 나뉘어 있습니다.
대기 힌트 보여 주기
isPending은 현재 내용을 대체하지 않는 로딩 표시를 위한 것입니다. 버튼 옆의 스피너, 흐려진 텍스트, "Saving..." 라벨 같은 것들입니다. 첫 번째 예제에서는 탭 옆에 Loading...을 보여 줍니다. 내용에 스타일을 주는 데도 쓸 수 있습니다.
<div style={{ opacity: isPending ? 0.6 : 1 }}>{content}</div>
입력하면서 큰 목록 필터링하기
input 자체의 값을 트랜지션에 넣을 수는 없습니다. 제어 <input>은 같은 긴급한 업데이트에서 새 값을 받아야 하며, 그렇지 않으면 글자가 빠집니다. 대신 state를 두 개 두세요. input의 텍스트(긴급)와 목록이 쓰는 query(트랜지션)입니다.
123을 빠르게 입력해 보세요. input은 절대 밀리지 않습니다. React가 Results 렌더링을 시작하자마자 로그를 남기므로 Console에는 여전히 키 입력마다 rendering results 줄이 하나씩 기록되지만, 1과 12에 대한 목록은 화면에 도달하지 않습니다. 더 새로운 키 입력이 진행 중인 렌더링을 중단했고, React는 최신 query로 다시 시작했습니다. Results는 일부러 memo로 감쌌습니다. 그렇지 않으면 키 입력마다의 긴급한 렌더링이 이전 query로 느린 목록까지 렌더링해서 이점을 잃게 됩니다.
훅 없이 startTransition 쓰기
react는 startTransition을 일반 함수로도 export합니다. 훅처럼 업데이트를 트랜지션으로 표시하지만 isPending은 없습니다. 데이터 라이브러리나 컴포넌트 바깥의 라우터처럼 훅을 호출할 수 없는 곳에서 사용하세요.
import { startTransition } from 'react';
function navigate(url) {
startTransition(() => {
setCurrentUrl(url);
});
}
React 19의 비동기 트랜지션
React 19부터는 넘기는 함수가 async일 수 있습니다. 그러면 isPending이 기다리는 시간을 포함해 함수가 실행되는 내내 true로 유지되므로, 요청 중에 대기 상태를 보여 주는 간단한 방법이 됩니다. 아래의 가짜 saveName이 실제 요청을 대신합니다.
setSaved를 감싼 두 번째 startTransition을 보세요. React는 await를 지나면 트랜지션을 추적하지 못하므로, 그 뒤의 state 업데이트를 트랜지션의 일부로 만들려면 다시 감싸야 합니다. React는 이렇게 넘긴 함수를 액션이라고 부르며, useActionState와 폼 액션도 같은 메커니즘 위에 만들어져 있습니다.
트랜지션은 디바운스가 아닙니다
디바운스는 마지막 키 입력 후 정해진 시간을 기다린 뒤에 무언가를 합니다. 트랜지션은 기다리지 않습니다. 바로 렌더링을 시작하고, 더 새로운 업데이트가 오면 이전 작업을 버립니다. 빠른 기기에서는 결과가 전혀 지연 없이 나타나고, 느린 기기에서는 React가 키 입력 사이에 할 수 있는 만큼 합니다. 트랜지션이 할 수 없는 것은 네트워크 요청 수를 줄이는 것입니다. 키를 누를 때마다 fetch가 시작된다면 그 fetch를 디바운스하세요.
트랜지션과 Suspense
트랜지션은 Suspense 경계의 동작도 바꿉니다. 보통 경계 안의 컴포넌트가 일시 중단되면(lazy 컴포넌트 로딩, use로 데이터 읽기) React는 내용을 대체 UI로 바꿉니다. 일시 중단을 일으킨 업데이트가 트랜지션이었다면, React는 대신 이미 화면에 있는 내용을 유지하고, 새 내용이 준비될 때까지 isPending이 true로 남습니다. 라우터가 탐색할 때마다 페이지 크기의 스피너가 번쩍이는 것을 피하는 방법이 이것입니다. 탐색을 트랜지션으로 감싸면 다음 페이지를 렌더링할 수 있을 때까지 이전 페이지가 보입니다. 한 번도 보여 준 적 없는 내용은 여전히 대체 UI를 쓰므로, 새로 나타나는 경계는 평소처럼 동작합니다.
동작하지 않는 것
- timeout 안의 업데이트.
startTransition(() => setTimeout(() => setTab(next), 100))은startTransition이 반환된 뒤에 실행되므로 업데이트를 표시하지 못합니다. 대신startTransition을 timeout 안에 넣으세요. - 느린 컴포넌트 하나. React는 컴포넌트 사이에서만 중단할 수 있으므로, 오랫동안 막히는 컴포넌트 하나는 여전히 막힙니다.
- input 제어하기. 위에서 말했듯 input 자체의 state는 긴급하게 남습니다.
state setter를 가지고 있지 않고 prop처럼 값만 받는 경우에는 useDeferredValue가 같은 동작을 제공합니다.
자주 묻는 질문
useTransition은 무엇을 하나요?
안의 state 업데이트를 긴급하지 않은 것으로 표시하는 startTransition과, 그 업데이트가 아직 렌더링 중인 동안 true인 isPending을 줍니다. React는 클릭과 입력을 먼저 처리하기 위해 긴급하지 않은 렌더링을 중단할 수 있습니다.
useTransition과 startTransition의 차이는 무엇인가요?
둘 다 업데이트를 트랜지션으로 표시합니다. useTransition 훅은 로딩 힌트를 위한 isPending도 줍니다. react에서 import하는 독립 함수 startTransition은 컴포넌트 바깥에서도 동작하지만 대기 플래그가 없습니다.
텍스트 input에 useTransition을 써도 되나요?
input 자체의 값에는 안 됩니다. 제어 input은 동기적으로 업데이트되어야 하며, 그렇지 않으면 입력이 깨집니다. input의 state는 긴급하게 두고, 그것에 의존하는 비용이 큰 업데이트를 트랜지션에 넣거나 useDeferredValue를 쓰세요.
useTransition은 디바운스와 같은가요?
아니요. 디바운스는 무언가를 하기 전에 정해진 시간을 기다립니다. 트랜지션은 바로 렌더링을 시작하고 더 새로운 업데이트가 오면 중단되므로, 빠른 기기에서는 결과가 지연 없이 나타납니다.
startTransition에 async 함수를 넘길 수 있나요?
네, React 19부터 가능합니다. isPending은 async 함수가 끝날 때까지 true로 유지됩니다. await 뒤의 state 업데이트를 트랜지션으로 표시하려면 다른 startTransition 호출로 감싸야 합니다.