Menu

React use 훅: 렌더링 중에 프로미스와 컨텍스트 읽기

use는 렌더링 중에 프로미스나 컨텍스트의 값을 읽습니다. 프로미스라면 데이터가 도착할 때까지 일시 중단하고 가장 가까운 Suspense의 대체 UI를 보여 줍니다. 다른 훅과 달리 if 문과 반복문 안에서도 실행할 수 있습니다.

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

use는 컴포넌트가 렌더링되는 동안 프로미스나 컨텍스트에서 값을 읽는 React 19 API입니다. use(promise)는 프로미스가 완료될 때까지 컴포넌트를 멈추고 그동안 가장 가까운 <Suspense>의 대체 UI를 보여 줍니다. use(context)는 useContext처럼 동작하지만 if나 반복문 안에 둘 수 있습니다.

아래의 fetchUser 함수가 실제 요청을 대신합니다.

첫 렌더링은 1초 동안 "Loading..."을 보여 준 뒤 프로필을 보여 줍니다. User 2를 클릭하면 새 프로미스가 대기 중인 동안 대체 UI가 돌아옵니다. Profile에는 자기만의 로딩 state가 없습니다. 데이터가 이미 있는 것처럼 작성되어 있습니다.

use(promise)의 동작 방식

Profile이 use(userPromise)를 호출하면 다음과 같습니다.

  • 프로미스가 완료되었다면 use가 그 값을 반환하고 렌더링이 계속됩니다.
  • 아직 대기 중이라면 React는 Profile의 렌더링을 멈추고 그 위에 있는 가장 가까운 <Suspense>의 대체 UI를 보여 줍니다. 프로미스가 정해지면 React가 Profile을 다시 렌더링하고, 이번에는 use가 값을 반환합니다.
  • 거부되었다면 React는 대신 가장 가까운 에러 바운더리를 보여 줍니다.

React는 결과를 프로미스 객체 자체에 기억합니다. 그래서 프로미스는 다음 렌더링에서도 같은 객체여야 합니다.

다음 프로필을 불러오는 동안 이전 프로필을 화면에 두려면 트랜지션 안에서 프로미스를 설정하세요. 그러면 React가 대체 UI를 다시 보여 주는 대신 기다립니다.

<button onClick={() => startTransition(() => setUserPromise(fetchUser(2)))}>User 2</button>

프로미스를 어디서 만들까

사람들이 걸려 넘어지는 부분입니다. 값을 읽는 컴포넌트 안에 fetch 호출을 쓰는 것은 자연스러워 보이지만 동작하지 않습니다.

// Do not do this
function Profile({ id }) {
    const user = use(fetchUser(id)); // a new promise on every render
    return <p>{user.name}</p>;
}

렌더링할 때마다 fetchUser가 호출되어 새로운 대기 중인 프로미스를 받습니다. Profile이 일시 중단됩니다. 그 프로미스가 완료되면 React가 Profile을 다시 렌더링하고, 그러면 또 대기 중인 프로미스가 만들어져 다시 일시 중단됩니다. 컴포넌트는 데이터를 절대 보여 주지 못하고, 네트워크 탭은 요청으로 가득 찹니다.

프로미스는 읽는 쪽이 렌더링될 때마다 다시 실행되지 않는 곳에서 만드세요.

  • 부모에서: state로 보관하거나(첫 번째 예제처럼) 이벤트 핸들러에서 만듭니다.
  • 모듈 최상위에서: 데이터가 props에 의존하지 않을 때입니다. 컴포넌트 위에 const configPromise = fetchConfig();를 둡니다.
  • 캐시에서: 인자를 키로 해서 같은 id면 같은 프로미스를 반환하게 합니다.
  • 프레임워크에서: 서버 컴포넌트는 클라이언트 컴포넌트에 프로미스를 prop으로 넘길 수 있고, 라우트 로더도 같은 일을 합니다. use는 바로 이 경우를 위해 설계되었습니다.

다음은 캐시 버전으로, 컴포넌트가 id로 데이터를 요청하면서도 안정적인 프로미스를 받을 수 있습니다.

세 도시를 차례로 클릭한 다음, 이미 연 도시로 돌아가 보세요. 콘솔에는 도시마다 요청이 하나씩 기록되고, 전에 본 도시는 프로미스가 이미 완료되었으므로 대체 UI 없이 나타납니다. if (!cache.has(city)) 검사를 지우면 컴포넌트가 다시 끝없이 일시 중단됩니다.

실제 캐시에는 항목을 만료시키는 방법도 필요합니다. 데이터 라이브러리와 프레임워크가 그것을 처리하므로, 대부분의 앱은 그런 곳에서 프로미스를 받습니다.

if 안에서도 쓰는 use(context)

use(SomeContext)는 useContext(SomeContext)와 같은 값을 반환합니다. 차이는 어디서 호출할 수 있는지입니다. 다른 모든 훅은 최상위에서, 렌더링마다 같은 순서로 실행되어야 합니다(훅의 규칙). use는 조기 반환 뒤, 조건문 안, 반복문 안에서 호출할 수 있습니다.

버튼을 클릭해서 두 분기를 오가 보세요. use(UserContext)를 useContext(UserContext)로 바꾸면 훅이 어떤 렌더링에서는 실행되고 어떤 렌더링에서는 실행되지 않으므로 훅의 규칙을 어기게 됩니다.

use에도 한계는 있습니다. 컴포넌트를 렌더링하는 동안이나 커스텀 훅 안에서만 호출하세요. 이벤트 핸들러나 이펙트에서는 렌더링 중에 이미 얻은 값으로 컨텍스트를 읽으세요.

오류: try/catch가 아니라 에러 바운더리

프로미스가 거부되면 오류는 렌더링 오류와 마찬가지로 가장 가까운 에러 바운더리로 갑니다.

Post 2를 열면 바운더리가 오류 메시지를 보여 줍니다. Post 1을 다시 열면 동작합니다. key={id}가 게시물마다 바운더리에 새 state를 주므로 이전 오류를 계속 보여 주지 않습니다.

대기 중이거나 거부된 use는 try/catch로 잡을 수 없습니다. 프로미스가 대기 중인 동안 use는 특별한 값을 던져서 렌더링을 중단하는데, 그 주위의 catch가 그것을 삼켜 버립니다. 잡힌 값은 메시지가 "Suspense Exception: This is not a real error!"로 시작하는 오류이며, 개발 환경에서는 React가 "use was called from inside a try/catch block"도 기록합니다. 오류 화면 대신 대체 값을 보여 주려면 프로미스를 아래로 넘기기 전에 거부를 처리하세요.

const postPromise = fetchPost(id).catch(() => 'This post could not be loaded.');

useEffect로 데이터 가져오기와 비교한 use

전통적인 클라이언트 패턴은 이펙트에서 데이터를 가져오고 state 세 개를 유지합니다.

function Profile({ id }) {
    const [user, setUser] = useState(null);
    const [error, setError] = useState(null);

    useEffect(() => {
        let ignore = false;
        fetchUser(id).then(
            (u) => !ignore && setUser(u),
            (e) => !ignore && setError(e),
        );
        return () => {
            ignore = true;
        };
    }, [id]);

    if (error) return <p>Error</p>;
    if (!user) return <p>Loading...</p>;
    return <p>{user.name}</p>;
}
useEffectuse
요청이 시작되는 시점첫 렌더링이 화면에 나온 뒤프로미스가 만들어질 때, 컴포넌트가 렌더링되기 전일 수도 있음
로딩 상태직접 쓴 if (!user)가장 가까운 <Suspense>
오류직접 만든 error state가장 가까운 에러 바운더리
경쟁 상태직접 막음(ignore)컴포넌트는 항상 받은 프로미스를 읽음
프로미스를 만드는 쪽컴포넌트부모, 캐시, 프레임워크

이펙트 버전은 자기 완결적이라서 프레임워크 없는 앱에서는 여전히 가장 흔한 방식입니다. 데이터 가져오기 페이지에서 자세히 다룹니다. use는 컴포넌트 위의 무언가가 요청을 일찍 시작해서 프로미스를 내려줄 때 빛을 발합니다.

자주 묻는 질문

React에서 use 훅은 무엇을 하나요?

use(resource)는 프로미스나 컨텍스트의 값을 반환합니다. 프로미스라면 완료될 때까지 컴포넌트가 일시 중단되고 가장 가까운 <Suspense>가 대체 UI를 보여 주며, 거부되면 가장 가까운 에러 바운더리가 대신 나타납니다.

use는 조건부로 호출할 수 있나요?

네. use는 if 문, 반복문 안과 조기 return 뒤에서 호출할 수 있는 유일한 훅입니다. 그래도 이벤트 핸들러나 이펙트가 아니라 컴포넌트나 훅에서 호출해야 합니다.

컴포넌트 안의 use(fetch())가 끝없이 반복되는 이유는 무엇인가요?

렌더링마다 새 프로미스가 만들어지기 때문입니다. 컴포넌트가 그것 때문에 일시 중단되고, 완료되면 React가 다시 렌더링하는데, 그 렌더링이 또 대기 중인 프로미스를 만듭니다. 프로미스는 렌더링 바깥에서 만드세요. 부모의 state, 모듈 최상위, 캐시, 프레임워크 로더가 그런 곳입니다.

use를 try/catch로 감싸도 되나요?

안 됩니다. 프로미스가 대기 중이면 use는 오류를 던져서 렌더링을 중단하는데, 그것을 잡으면 동작이 깨집니다. 거부된 프로미스는 에러 바운더리로 처리하거나, 프로미스에 .catch()를 붙여서 대체 값으로 완료되게 하세요.

useEffect로 데이터를 가져오던 코드를 use로 바꿔야 하나요?

use에는 한 번 만들어 재사용하는 프로미스가 필요하며, 프레임워크와 데이터 라이브러리가 그것을 제공합니다. 일반 클라이언트 앱에서는 이펙트로(또는 TanStack Query 같은 라이브러리로) 데이터를 가져오는 방식이 여전히 흔하고 괜찮습니다.

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

Coddy로 코딩 배우기

시작하기