useTransition
은 UI를 차단하지 않고 상태를 업데이트할 수 있는 React Hook입니다.
const [isPending, startTransition] = useTransition()
레퍼런스
useTransition()
컴포넌트의 최상위 수준에서 useTransition
을 호출하여 일부 state 업데이트를 Transition 으로 표시합니다.
import { useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}
매개변수
useTransition
은 어떤 매개변수도 받지 않습니다.
반환값
useTransition
은 정확히 두 개의 항목이 있는 배열을 반환합니다.
isPending
플래그는 대기 중인 Transition 이 있는지 알려줍니다.startTransition
함수는 상태 업데이트를 Transition 으로 표시할 수 있게 해주는 함수입니다.
startTransition
함수
useTransition
이 반환하는 startTransition
함수를 사용하면 state 업데이트를 Transition 으로 표시할 수 있습니다.
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}
매개변수
scope
: 하나 이상의set
함수를 호출하여 일부 state를 업데이트하는 함수입니다. React는 매개변수 없이scope
를 즉시 호출하고scope
함수를 호출하는 동안 동기적으로 예약된 모든 state 업데이트를 Transition 으로 표시합니다. 이는 non-blocking이며 원치 않는 로딩을 표시하지 않습니다.
반환값
startTransition
은 아무것도 반환하지 않습니다.
주의 사항
-
useTransition
은 Hook이므로 컴포넌트나 커스텀 Hook 내부에서만 호출할 수 있습니다. 다른 곳(예시: 데이터 라이브러리)에서 Transition 을 시작해야 하는 경우, 독립형startTransition
을 호출하세요. -
해당 state의
set
함수에 액세스할 수 있는 경우에만 업데이트를 Transition 으로 래핑할 수 있습니다. 일부 prop이나 커스텀 Hook 값에 대한 응답으로 Transition 을 시작하려면useDeferredValue
를 사용해 보세요. -
startTransition
에 전달하는 함수는 동기식이어야 합니다. React는 이 함수를 즉시 실행하여 실행하는 동안 발생하는 모든 state 업데이트를 Transition 으로 표시합니다. 나중에 더 많은 state 업데이트를 수행하려고 하면(예시: timeout), Transition 으로 표시되지 않습니다. -
The
startTransition
function has a stable identity, so you will often see it omitted from effect dependencies, but including it will not cause the effect to fire. If the linter lets you omit a dependency without errors, it is safe to do. Learn more about removing Effect dependencies. -
Transition으로 표시된 state 업데이트는 다른 state 업데이트에 의해 중단됩니다. 예를 들어, Transition 내에서 차트 컴포넌트를 업데이트한 다음 차트가 다시 렌더링 되는 도중에 입력을 시작하면 React는 입력 업데이트를 처리한 후 차트 컴포넌트에서 렌더링 작업을 다시 시작합니다.
-
Transition 업데이트는 텍스트 입력을 제어하는 데 사용할 수 없습니다.
-
진행 중인 Transition 이 여러 개 있는 경우, React는 현재 Transition 을 함께 일괄 처리합니다. 이는 향후 릴리즈에서 제거될 가능성이 높은 제한 사항입니다.
사용법
state 업데이트를 non-blocking Transition 으로 표시
컴포넌트의 최상위 레벨에서 useTransition
을 호출하여 state 업데이트를 non-blocking Transition으로 표시하세요.
import { useState, useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}
useTransition
은 정확히 두 개의 항목이 있는 배열을 반환합니다.
- 보류 중인 Transition 이 있는지를 알려주는
isPending
플래그입니다. - state 업데이트를 Transition 으로 표시할 수 있는
startTransition
함수입니다.
그 후 다음과 같이 state 업데이트를 Transition 으로 표시할 수 있습니다.
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}
Transition을 사용하면 느린 디바이스에서도 사용자 인터페이스 업데이트의 반응성을 유지할 수 있습니다.
Transition을 사용하면 리렌더링 도중에도 UI가 반응성을 유지합니다. 예를 들어 사용자가 탭을 클릭했다가 마음이 바뀌어 다른 탭을 클릭하면 첫 번째 리렌더링이 완료될 때까지 기다릴 필요 없이 다른 탭을 클릭할 수 있습니다.
예시 1 of 2: Transition에서 현재 탭 업데이트
이 예시에서는 “Posts” 탭이 인위적으로 느려지도록 하여 렌더링하는 데 최소 1초가 걸리도록 했습니다.
”posts”을 클릭한 다음 바로 “Contact”를 클릭합니다. 이렇게 하면 “Posts”의 느린 렌더링이 중단됩니다. “Contact” 탭이 즉시 표시됩니다. 이 state 업데이트는 Transition 으로 표시되므로 느리게 다시 렌더링해도 사용자 인터페이스가 멈추지 않습니다.
import { useState, useTransition } from 'react'; import TabButton from './TabButton.js'; import AboutTab from './AboutTab.js'; import PostsTab from './PostsTab.js'; import ContactTab from './ContactTab.js'; export default function TabContainer() { const [isPending, startTransition] = useTransition(); const [tab, setTab] = useState('about'); function selectTab(nextTab) { startTransition(() => { setTab(nextTab); }); } return ( <> <TabButton isActive={tab === 'about'} onClick={() => selectTab('about')} > About </TabButton> <TabButton isActive={tab === 'posts'} onClick={() => selectTab('posts')} > Posts (slow) </TabButton> <TabButton isActive={tab === 'contact'} onClick={() => selectTab('contact')} > Contact </TabButton> <hr /> {tab === 'about' && <AboutTab />} {tab === 'posts' && <PostsTab />} {tab === 'contact' && <ContactTab />} </> ); }
Transition에서 상위 컴포넌트 업데이트
useTransition
호출에서도 부모 컴포넌트의 state를 업데이트할 수 있습니다. 예를 들어, 아래의 TabButton
컴포넌트는 onClick
로직을 Transition 으로 래핑합니다.
export default function TabButton({ children, isActive, onClick }) {
const [isPending, startTransition] = useTransition();
if (isActive) {
return <b>{children}</b>
}
return (
<button onClick={() => {
startTransition(() => {
onClick();
});
}}>
{children}
</button>
);
}
부모 컴포넌트가 onClick
이벤트 핸들러 내에서 state를 업데이트하기 때문에 해당 state 업데이트는 Transition 으로 표시됩니다. 그렇기 때문에 앞의 예시에서처럼 “posts”을 클릭한 다음 바로 “Contact”를 클릭할 수 있습니다. 선택한 탭을 업데이트하는 것은 Transition 으로 표시되므로 사용자 상호작용을 차단하지 않습니다.
import { useTransition } from 'react'; export default function TabButton({ children, isActive, onClick }) { const [isPending, startTransition] = useTransition(); if (isActive) { return <b>{children}</b> } return ( <button onClick={() => { startTransition(() => { onClick(); }); }}> {children} </button> ); }
Transition 중에 보류 중인 시각적 state 표시
useTransition
이 반환하는 isPending
boolean 값을 사용하여 transition이 진행 중임을 사용자에게 표시할 수 있습니다. 예를 들어 탭 버튼은 특별한 “pending” 시각적 상태를 가질 수 있습니다.
function TabButton({ children, isActive, onClick }) {
const [isPending, startTransition] = useTransition();
// ...
if (isPending) {
return <b className="pending">{children}</b>;
}
// ...
이제 탭 버튼 자체가 바로 업데이트되므로 “Posts”을 클릭하는 반응이 더 빨라진 것을 확인할 수 있습니다.
import { useTransition } from 'react'; export default function TabButton({ children, isActive, onClick }) { const [isPending, startTransition] = useTransition(); if (isActive) { return <b>{children}</b> } if (isPending) { return <b className="pending">{children}</b>; } return ( <button onClick={() => { startTransition(() => { onClick(); }); }}> {children} </button> ); }
원치 않는 로딩 표시기 방지
이 예시에서 PostsTab
컴포넌트는 Suspense-enabled 데이터 소스를 사용하여 일부 데이터를 가져옵니다. “Posts” 탭을 클릭하면 PostsTab
컴포넌트가 suspends 되어 가장 가까운 로딩 폴백이 나타납니다.
import { Suspense, useState } from 'react'; import TabButton from './TabButton.js'; import AboutTab from './AboutTab.js'; import PostsTab from './PostsTab.js'; import ContactTab from './ContactTab.js'; export default function TabContainer() { const [tab, setTab] = useState('about'); return ( <Suspense fallback={<h1>🌀 Loading...</h1>}> <TabButton isActive={tab === 'about'} onClick={() => setTab('about')} > About </TabButton> <TabButton isActive={tab === 'posts'} onClick={() => setTab('posts')} > Posts </TabButton> <TabButton isActive={tab === 'contact'} onClick={() => setTab('contact')} > Contact </TabButton> <hr /> {tab === 'about' && <AboutTab />} {tab === 'posts' && <PostsTab />} {tab === 'contact' && <ContactTab />} </Suspense> ); }
로딩 표시기를 표시하기 위해 전체 탭 컨테이너를 숨기면 사용자 경험이 어색해집니다. TabButton
에 useTransition
을 추가하면 탭 버튼에 보류 중인 상태를 표시할 수 있습니다.
”Posts”을 클릭하면 더 이상 전체 탭 컨테이너가 스피너로 바뀌지 않습니다.
import { useTransition } from 'react'; export default function TabButton({ children, isActive, onClick }) { const [isPending, startTransition] = useTransition(); if (isActive) { return <b>{children}</b> } if (isPending) { return <b className="pending">{children}</b>; } return ( <button onClick={() => { startTransition(() => { onClick(); }); }}> {children} </button> ); }
Suspense에서 Transition 을 사용하는 방법에 대해 자세히 알아보세요.
Suspense-enabled 라우터 구축
React 프레임워크나 라우터를 구축하는 경우 페이지 탐색을 Transition 으로 표시하는 것이 좋습니다.
function Router() {
const [page, setPage] = useState('/');
const [isPending, startTransition] = useTransition();
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...
두 가지 이유로 이 방법을 권장합니다.
- Transition은 중단할 수 있으므로 사용자는 리렌더링이 완료될 때까지 기다릴 필요 없이 바로 클릭할 수 있습니다.
- Transition은 원치 않는 로딩 표시기를 방지하므로 사용자가 탐색 시 갑작스러운 이동을 방지할 수 있습니다.
다음은 탐색을 위해 Transition 을 사용하는 아주 간단한 라우터 예시입니다.
import { Suspense, useState, useTransition } from 'react'; import IndexPage from './IndexPage.js'; import ArtistPage from './ArtistPage.js'; import Layout from './Layout.js'; export default function App() { return ( <Suspense fallback={<BigSpinner />}> <Router /> </Suspense> ); } function Router() { const [page, setPage] = useState('/'); const [isPending, startTransition] = useTransition(); function navigate(url) { startTransition(() => { setPage(url); }); } let content; if (page === '/') { content = ( <IndexPage navigate={navigate} /> ); } else if (page === '/the-beatles') { content = ( <ArtistPage artist={{ id: 'the-beatles', name: 'The Beatles', }} /> ); } return ( <Layout isPending={isPending}> {content} </Layout> ); } function BigSpinner() { return <h2>🌀 Loading...</h2>; }
Error boundary로 사용자에게 오류 표시하기
startTransition에 전달된 함수가 오류를 발생시키면 error boundary를 사용하여 사용자에게 오류를 표시할 수 있습니다. error boundary를 사용하려면 useTransition을 호출하는 컴포넌트를 error boundary로 래핑하세요. startTransition에 전달된 함수에서 오류가 발생하면, error boundary의 fallback이 표시됩니다.
import { useTransition } from "react"; import { ErrorBoundary } from "react-error-boundary"; export function AddCommentContainer() { return ( <ErrorBoundary fallback={<p>⚠️Something went wrong</p>}> <AddCommentButton /> </ErrorBoundary> ); } function addComment(comment) { // For demonstration purposes to show Error Boundary if (comment == null) { throw new Error("Example Error: An error thrown to trigger error boundary"); } } function AddCommentButton() { const [pending, startTransition] = useTransition(); return ( <button disabled={pending} onClick={() => { startTransition(() => { // Intentionally not passing a comment // so error gets thrown addComment(); }); }} > Add comment </button> ); }
Troubleshooting
Transition에서 입력 업데이트가 작동하지 않습니다
입력을 제어하는 state 변수에는 Transition 을 사용할 수 없습니다.
const [text, setText] = useState('');
// ...
function handleChange(e) {
// ❌ 제어된 입력 state에 Transition 을 사용할 수 없습니다.
startTransition(() => {
setText(e.target.value);
});
}
// ...
return <input value={text} onChange={handleChange} />;
이는 Transition 이 non-blocking이지만, 변경 이벤트에 대한 응답으로 입력을 업데이트하는 것은 동기적으로 이루어져야 하기 때문입니다. 입력에 대한 응답으로 Transition 을 실행하려면 두 가지 옵션이 있습니다.
- 두 개의 개별 state 변수를 선언할 수 있습니다. 하나는 입력 state(항상 동기적으로 업데이트됨) 용이고 다른 하나는 Transition 시 업데이트할 state입니다. 이를 통해 동기 state를 사용하여 입력을 제어하고 (입력보다 “지연”되는) Transition state 변수를 나머지 렌더링 로직에 전달할 수 있습니다.
- 또는 state 변수가 하나 있고 실제 값보다 “지연”되는
useDeferredValue
를 추가할 수 있습니다. 그러면 non-blocking 리렌더링이 새로운 값을 자동으로 “따라잡기” 위해 트리거됩니다.
React가 state 업데이트를 transition으로 처리하지 않습니다
state 업데이트를 transition으로 래핑할 때는 startTransition
호출 도중에 발생해야 합니다.
startTransition(() => {
// ✅ startTransition 호출 *도중* state 설정
setPage('/about');
});
startTransition
에 전달하는 함수는 동기식이어야 합니다.
아래와 같은 업데이트는 Transition 으로 표시할 수 없습니다.
startTransition(() => {
// ❌ startTransition 호출 *후에* state 설정
setTimeout(() => {
setPage('/about');
}, 1000);
});
대신 다음과 같이 할 수 있습니다.
setTimeout(() => {
startTransition(() => {
// ✅ startTransition 호출 *도중* state 설정
setPage('/about');
});
}, 1000);
마찬가지로 업데이트를 이와 같은 Transition 으로 표시할 수 없습니다.
startTransition(async () => {
await someAsyncFunction();
// ❌ startTransition 호출 *후에* state 설정
setPage('/about');
});
하지만 이 방법이 대신 동작합니다.
await someAsyncFunction();
startTransition(() => {
// ✅ startTransition 호출 *도중* state 설정
setPage('/about');
});
컴포넌트 외부에서 useTransition
을 호출하고 싶습니다
Hook이기 때문에 컴포넌트 외부에서 useTransition
을 호출할 수 없습니다. 이 경우 대신 독립형 startTransition
메서드를 사용하세요. 동일한 방식으로 작동하지만 isPending
표시기를 제공하지 않습니다.
startTransition
에 전달한 함수는 즉시 실행됩니다
이 코드를 실행하면 1, 2, 3이 출력됩니다.
console.log(1);
startTransition(() => {
console.log(2);
setPage('/about');
});
console.log(3);
1, 2, 3을 출력할 것으로 예상됩니다. startTransition
에 전달한 함수는 지연되지 않습니다. 브라우저 setTimeout
과 달리 나중에 콜백을 실행하지 않습니다. React는 함수를 즉시 실행하지만, 함수가 실행되는 동안 예약된 모든 상태 업데이트는 Transition 으로 표시됩니다. 아래와 같이 작동한다고 상상하면 됩니다.
// React 작동 방식의 간소화된 버전
let isInsideTransition = false;
function startTransition(scope) {
isInsideTransition = true;
scope();
isInsideTransition = false;
}
function setState() {
if (isInsideTransition) {
// ... Transition state 업데이트 예약 ...
} else {
// ... 긴급 state 업데이트 예약 ...
}
}