React|React Hook 톱아보기

use API: useContext의 진화와 Promise(Suspense) 처리

1
use API: useContext의 진화와 Promise(Suspense) 처리

크리스가 복잡한 대시보드 컴포넌트를 만들고 있다.

이 컴포넌트는 사용자의 설정에 따라 '일반 모드'와 '고급 모드'로 나뉘어 렌더링 된다. 고급 모드일 때만 특정 Context(AdvancedContext)에 접근해서 데이터를 가져오고 싶다.

그래서 크리스는 직관적으로 코드를 짰다.

typescript
// ❌ 크리스의 실수: 훅은 조건문 안에서 쓸 수 없다.
function Dashboard({ isAdvanced }) {
  if (isAdvanced) {
    // Error: React Hooks must be called in the exact same order...
    const advancedData = useContext(AdvancedContext);
    return <AdvancedView data={advancedData} />;
  }
  
  return <SimpleView />;
}

저장을 누르자마자 리액트는 "Hook 규칙 위반"이라며 빨간 에러를 뿜어낸다.

리액트의 훅(Hook)은 마법이 아니라 배열 인덱스에 의존하는 순서 기반 시스템이기 때문에, 조건문, 반복문, 중첩 함수 내부에서 호출할 수 없다.

결국 크리스는 useContext를 최상단으로 끌어올려야 했다. isAdvanced가 false라서 데이터가 필요 없는데도 말이다.

하지만 React 19에서는 이 제약이 풀린다. 훅(Hook)이 아니면서 훅처럼 동작하는 새로운 API, use의 등장 덕분이다.

1. Context 유연하게 소비하기

use API는 리액트 훅과 비슷해 보이지만, 결정적인 차이가 있다. 바로 조건문이나 반복문 안에서 호출할 수 있다는 점이다.

이제 크리스는 억지로 훅을 위로 올릴 필요가 없다. 필요한 순간에, 필요한 곳에서 Context를 "읽으면" 된다.

typescript
// ✅ React 19: use API 사용
import { use } from 'react';

function Dashboard({ isAdvanced }) {
  if (isAdvanced) {
    // 조건문 안에서 Context 읽기 가능!
    const advancedData = use(AdvancedContext);
    return <AdvancedView data={advancedData} />;
  }
  
  return <SimpleView />;
}

useuseContext를 완전히 대체할 수 있다. 이제 컴포넌트 로직의 흐름을 끊지 않고, 자연스럽게 데이터 흐름에 따라 Context 값을 가져올 수 있게 되었다. 이것은 코드의 응집도(Cohesion)를 높여준다.

2. Promise 풀어헤치기: 비동기를 동기처럼

use API의 진가는 Promise(비동기 작업)를 처리할 때 드러난다.

지금까지 우리는 비동기 데이터를 가져오기 위해 useEffectuseState를 조합하거나, TanStack Query 같은 라이브러리에 의존해야 했다. 데이터를 가져오는 동안 isLoading 상태를 관리하고, 다 오면 렌더링 하는 복잡한 과정이 필요했다.

하지만 use API는 Promise 객체 자체를 인자로 받을 수 있다. 그리고 그 Promise가 해결(Resolve)될 때까지 리액트 렌더링을 일시 정지(Suspend)시킨다.

클라이언트 컴포넌트에서 Promise 사용하기

서버 컴포넌트(RSC)에서 데이터를 가져오고, 그 Promise를 클라이언트 컴포넌트(Message)에게 넘겨준다고 상상해 보자.

typescript
// ClientComponent.tsx
import { use, Suspense } from 'react';

function Message({ messagePromise }: { messagePromise: Promise<string> }) {
  // 1. Promise가 해결될 때까지 기다린다. (여기서 멈춤)
  // 2. 해결되면 그 결과값(string)을 반환한다.
  const messageContent = use(messagePromise);

  return <p>{messageContent}</p>;
}

export function MessageContainer({ promise }) {
  return (
    // 3. 기다리는 동안 보여줄 UI는 상위 Suspense가 담당한다.
    <Suspense fallback={<p>메시지 다운로드 중...</p>}>
      <Message messagePromise={promise} />
    </Suspense>
  );
}

이 코드를 보면 await 키워드가 보이지 않는다. 그런데도 비동기 데이터가 마치 동기 데이터 변수처럼 messageContent에 담긴다.

이것이 바로 리액트가 추구하는 "Suspense 기반의 데이터 페칭"이다.

3. 원리: 값을 '읽는' 새로운 멘탈 모델

use API는 데이터를 "가져오는(Fetching)" 함수가 아니라, 데이터를 "읽는(Reading)" 함수다.

  • 컴포넌트가 렌더링 되다가 use(promise)를 만난다.
  • Promise가 아직 Pending 상태라면, 리액트는 "아직 준비 안 됐네?" 하고 렌더링을 멈추고(Throw Promise), 가장 가까운 Suspensefallback을 보여준다.
  • Promise가 Resolved 되면, 리액트는 컴포넌트를 다시 실행한다.
  • 이번에는 use(promise)가 결과값을 즉시 반환한다. 렌더링이 완료된다.
  • 만약 Rejected(에러) 되면? 가장 가까운 Error Boundary가 에러를 잡는다.
  • 이 패턴은 Part 2에서 배웠던 useSuspenseQuery와 유사하다. 하지만 이제는 외부 라이브러리 없이 리액트 순정 기능만으로도 "로딩 상태 없는 선언적인 코드"를 짤 수 있게 된 것이다.

    4. 주의사항과 한계

    use API가 만능은 아니다. 몇 가지 지켜야 할 규칙이 있다.

    1. 렌더링 중에만 사용 가능

    use는 컴포넌트나 훅 내부의 렌더링 단계에서만 호출해야 한다. 이벤트 핸들러(onClick)나 useEffect 내부에서는 사용할 수 없다. (거기서는 그냥 await를 쓰면 된다.)

    2. 서버 컴포넌트 vs 클라이언트 컴포넌트

  • 서버 컴포넌트: async/await를 직접 쓸 수 있으므로 use가 굳이 필요 없다.
  • 클라이언트 컴포넌트: async/await를 쓰면 안 된다(훅이 아니게 되므로). 여기서 Promise를 props로 받아 처리할 때 use가 빛을 발한다.
  • 3. 캐싱은 별개다

    use는 데이터를 읽는 도구일 뿐, 데이터를 저장(Caching)하거나 중복 요청을 막아주지는 않는다. API 요청을 캐싱하려면 여전히 TanStack Query를 쓰거나, 프레임워크(Next.js)의 캐싱 기능을 함께 써야 한다.

    핵심 정리

  • use API: React 19의 새로운 기능으로, ContextPromise를 읽을 수 있다.
  • 조건부 호출 가능: 기존 훅(useContext)과 달리 if 문이나 반복문 안에서 자유롭게 사용할 수 있어 유연성이 극대화된다.
  • Promise 처리: use(promise) 형태로 사용하면, 비동기 데이터가 준비될 때까지 컴포넌트 렌더링을 일시 중지(Suspend)시킨다.
  • 패러다임 변화: 데이터를 "요청하고 기다리는" 방식에서, 데이터가 "준비되면 읽는" 방식으로 사고를 전환해야 한다. 로딩 처리는 Suspense에게, 에러 처리는 Error Boundary에게 위임한다.
  • Context와 비동기 처리가 이렇게나 우아해졌다. 그렇다면 가장 골치 아픈 "폼(Form) 처리"는 어떻게 변했을까?

    useState로 isSubmitting, error, result 상태를 일일이 만들던 노가다에서 해방될 시간이다.

    "useActionState & useFormStatus: 폼(Form) 관리의 혁명" 편에서 계속된다.


    🔗 참고 링크

  • React Docs - use API
  • React 19 Hooks Changes
  • RFC: First-class Support for Promises
  • 댓글 (0)

    0/1000
    댓글을 불러오는 중...