태그
목차

useTransition

useTransition Hook 문서

생성일: 2024-02-12

수정일: 2024-02-12

useTransition 은 UI를 차단(block)하지 않고 상태를 업데이트할 수 있는 React Hook 이다.

const [isPending, startTransition] = useTransition();

레퍼런스

useTransition()

컴포넌트의 최상위 수준에서 useTransition 을 호출하여 일부 상태 업데이트를 트랜지션으로 표시한다.

import { useTransition } from 'react';

function TabContainer() {
  const [isPending, startTransition] = useTransition();
  // ...
}

파라미터

useTransition 은 파라미터를 받지 않는다.

리턴

useTransition 은 다음의 두 개의 아이템이 있는 배열을 리턴한다:

  1. 보류 중인 트랜지션이 있는지 여부를 알려주는 isPending 플래그.
  2. 상태 업데이트를 트랜지션으로 표시할 수 있는 startTransition 함수.

startTransition 함수

useTransition 이 반환하는 startTransition 함수를 사용하면 상태 업데이트를 트랜지션으로 표시할 수 있다.

function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('about');

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }
  // ...
}

파라미터

파라미터 설명
scope 하나 이상의 set 함수를 호출하여 일부 상태를 업데이트하는 함수. React는 매개변수 없이 즉시 scope 를 호출하고 scope 함수 호출 중에 동기적으로 예약된 모든 상태 업데이트를 트랜지션으로 표시한다. 이 함수는 non-blocking 이며 원치 않는 로딩 표시기를 표시하지 않는다.

리턴

startTransition 함수는 아무것도 리턴하지 않는다.

주의사항

사용법

상태 업데이트를 논 블로킹 트랜지션으로 표시하기

컴포넌트의 최상위 수준에서 useTransition 을 호출하여 상태 업데이트를 논 블로킹 트랜지션으로 표시한다.

import { useState, useTransition } from 'react';

function TabContainer() {
  const [isPending, startTransition] = useTransition();
  // ...
}

useTransition 은 정확히 두 개의 아이템이 있는 배열을 리턴한다:

  1. 보류 중인 트랜지션이 있는지 여부를 알려주는 isPending 플래그.
  2. 상태 업데이트를 트랜지션으로 표시할 수 있는 startTransition 함수.

그런 다음 상태 업데이트를 다음과 같이 트랜지션으로 표시할 수 있다:

function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('about');

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }
  // ...
}

트랜지션을 사용하면 느린 기기에서도 사용자 인터페이스 업데이트의 반응성을 유지할 수 있다.

트랜지션을 사용하면 리렌더링 중에도 UI가 반응성을 유지한다. 예를 들어 사용자가 탭을 클릭했다가 마음이 바뀌어 다른 탭을 클릭하면 첫 번째 리렌더링이 완료될 때까지 기다릴 필요 없이 다른 탭을 클릭할 수 있다.

트랜지션으로 부모 컴포넌트 업데이트하기

useTransition 으로 부모 컴포넌트의 상태를 업데이트할 수도 있다. 예를 들어, 다음의 TabButton 컴포넌트는 onClick 로직을 트랜지션으로 래핑한다:

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 이벤트 핸들러 내에서 상태를 업데이트하므로 해당 상태 업데이트는 트랜지션으로 표시된다. 그렇기 때문에 앞의 예에서처럼 'Posts' 를 클릭한 다음 바로 'Contact' 를 클릭할 수 있다. 선택한 탭을 업데이트하는 것은 트랜지션으로 표시되므로 사용자 상호 작용을 차단하지 않는다.

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>
  );
}

트랜지션 중 펜딩 시각적 상태 표시하기

useTransition 이 리턴 하는 isPending 부울 값을 사용하여 트랜지션이 진행 중임을 사용자에게 표시할 수 있다. 예를 들어 탭 버튼은 'pending' 에 대한 특별한 시각적 상태를 가질 수 있다:

function TabButton({ children, isActive, onClick }) {
  const [isPending, startTransition] = useTransition();
  // ...
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  // ...

이제 'Posts' 를 클릭하면 탭 버튼 자체가 바로 업데이트되므로 반응성이 향상된다:

// TabButton.js

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 컴포넌트는 서스펜스 가능한 데이터 소스를 사용하여 일부 데이터를 페치 한다. 'Posts' 탭을 클릭하면 PostsTab 컴포넌트가 일시 중단되어 가장 가까운 로딩 폴백이 표시된다:

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>
  );
}
export default function TabButton({ children, isActive, onClick }) {
  if (isActive) {
    return <b>{children}</b>;
  }
  return (
    <button
      onClick={() => {
        onClick();
      }}
    >
      {children}
    </button>
  );
}

로딩 표시기를 표시하기 위해 전체 탭 컨테이너를 숨기면 사용자 환경이 불안정해진다. TabButtonuseTransition 을 추가하면 대신 TabButton 에 펜딩 상태를 표시할 수 있다.

'Posts' 를 클릭하면 더 이상 전체 탭 컨테이너가 스피너로 대체되지 않는다:

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>
  );
}
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>
  );
}

서스펜스와 트랜지션 함께 사용하기 문서를 참조한다.

Note

트랜지션은 이미 표시된 콘텐츠(예: 탭 컨테이너)를 숨기지 않을 만큼만 '대기'한다. Posts 탭에 중첩된 <Suspense> 경계가 있는 경우 트랜지션은 이를 기다리지 않는다.

서스펜스 지원 라우터 빌드하기

React 프레임워크나 라우터를 구축하는 경우 페이지 네비게이션을 트랜지션으로 표시하는 것이 좋다.

function Router() {
  const [page, setPage] = useState('/');
  const [isPending, startTransition] = useTransition();

  function navigate(url) {
    startTransition(() => {
      setPage(url);
    });
  }
  // ...

두 가지 이유로 이 방법을 권장한다:

다음은 네비게이션에 트랜지션을 사용하는 간단한 라우터 예시다.

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>;
}
export default function Layout({ children, isPending }) {
  return (
    <div className="layout">
      <section
        className="header"
        style={{
          opacity: isPending ? 0.7 : 1,
        }}
      >
        Music Browser
      </section>
      <main>{children}</main>
    </div>
  );
}
export default function IndexPage({ navigate }) {
  return (
    <button onClick={() => navigate('/the-beatles')}>
      Open The Beatles artist page
    </button>
  );
}
import { Suspense } from 'react';
import Albums from './Albums.js';
import Biography from './Biography.js';
import Panel from './Panel.js';

export default function ArtistPage({ artist }) {
  return (
    <>
      <h1>{artist.name}</h1>
      <Biography artistId={artist.id} />
      <Suspense fallback={<AlbumsGlimmer />}>
        <Panel>
          <Albums artistId={artist.id} />
        </Panel>
      </Suspense>
    </>
  );
}

function AlbumsGlimmer() {
  return (
    <div className="glimmer-panel">
      <div className="glimmer-line" />
      <div className="glimmer-line" />
      <div className="glimmer-line" />
    </div>
  );
}

Note

서스펜스 지원 라우터는 기본적으로 네비게이션 업데이트를 트랜지션으로 래핑할 것으로 예상된다.

에러 바운더리로 사용자에게 오류 표시하기

Canary

useTransition 에러 바운더리는 현재 React의 카나리아 및 실험 채널에서만 사용할 수 있다. React의 릴리스 채널에 대한 자세한 내용은 여기를 참조한다.

startTransition 에 전달된 함수가 오류를 발생시키면 에러 바운더리를 사용하여 사용자에게 오류를 표시할 수 있다. 에러 바운더리를 사용하려면 useTransition 을 호출하는 컴포넌트를 에러 바운더리로 감싸면 된다. startTransition 에 전달된 함수가 에러를 발생시키면 에러 경계에 대한 폴백이 표시된다.

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>
  );
}

트러블슈팅

트랜지션에서 입력 업데이트가 작동하지 않는 경우

입력을 제어하는 상태 변수에는 트랜지션을 사용할 수 없다:

const [text, setText] = useState('');
// ...
function handleChange(e) {
  // ❌ 입력 상태에 트랜지션을 사용할 수 없다.
  startTransition(() => {
    setText(e.target.value);
  });
}
// ...
return <input value={text} onChange={handleChange} />;

이는 트랜지션이 논 블로킹 이지만 체인지 이벤트에 대한 응답으로 입력을 업데이트하는 것은 동기적으로 이루어져야 하기 때문이다. 입력에 대한 응답으로 트랜지션을 실행하려는 경우 두 가지 옵션이 있다:

  1. 두 개의 상태 변수를 선언할 수 있다. 하나는 입력 상태(항상 동기적으로 업데이트됨)용이고 다른 하나는 트랜지션 시 업데이트할 상태 변수다. 이렇게 하면 동기 상태를 사용하여 입력을 제어하고, 나머지 렌더링 로직에 (입력보다 "지연"되는) 트랜지션 상태 변수를 전달할 수 있다.
  2. 또는 상태 변수가 하나만 있고 실제 값보다 "지연"되는 useDeferredValue 를 추가할 수 있다. 그러면 논 블로킹 리렌더링이 새 값을 자동으로 '따라잡기' 위해 트리거 된다.

React가 상태 업데이트를 트랜지션으로 취급하지 않는 경우

상태 업데이트를 트랜지션으로 래핑할 때는 startTransition 호출 중에 상태를 업데이트 해야 한다:

startTransition(() => {
  // ✅ startTranstion 호출 "중에" 상태를 설정해야 한다.
  setPage('/about');
});

startTransition 에 전달하는 함수는 동기식이어야 한다.

다음과 같은 업데이트는 트랜지션으로 표시할 수 없다:

startTransition(() => {
  // ❌ startTransition 호출 "후에" 성태를 설정하면 안된다.
  setTimeout(() => {
    setPage('/about');
  }, 1000);
});

대신 이렇게 할 수 있다:

setTimeout(() => {
  startTransition(() => {
    // ✅ startTranstion 호출 "중에" 상태를 설정해야 한다.
    setPage('/about');
  });
}, 1000);

마찬가지의 이유로 다음과 같은 식으로 트랜지션을 사용할 수 없다:

startTransition(async () => {
  await someAsyncFunction();
  // ❌ startTransition 호출 "후에" 성태를 설정하면 안된다.
  setPage('/about');
});

하지만 대신 아래의 방법으로 해결할 수 있다:

await someAsyncFunction();
startTransition(() => {
  // ✅ startTranstion 호출 "중에" 상태를 설정해야 한다.
  setPage('/about');
});

컴포넌트 외부에서 useTransition을 호출하고 싶은 경우

Hook이기 때문에 컴포넌트 외부에서는 useTransition 을 호출할 수 없다. 이 경우 대신 독립형 startTransition 메서드를 사용한다. 같은 방식으로 작동하지만 isPending 표시기를 제공하지 않는다.

startTransition에 전달한 함수가 즉시 실행되는 경우

아래의 코드를 실행하면 1, 2, 3이 프린트 된다:

console.log(1);
startTransition(() => {
  console.log(2);
  setPage('/about');
});
console.log(3);

startTransition 에 전달한 함수는 지연되지 않는다. 브라우저 setTimeout 과 달리 나중에 콜백을 실행하지 않는다. React는 함수를 즉시 실행하지만 함수가 실행되는 동안 예약된 상태 업데이트는 트랜지션으로 표시된다. 이렇게 작동한다고 상상할 수 있다:

// 단순화된 버전

let isInsideTransition = false;

function startTransition(scope) {
  isInsideTransition = true;
  scope();
  isInsideTransition = false;
}

function setState() {
  if (isInsideTransition) {
    // ... 트랜지션 상태 업데이트 예약 ...
  } else {
    // ... 긴급 상태 업데이트 예약 ...
  }
}