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

크리스가 복잡한 대시보드 컴포넌트를 만들고 있다.
이 컴포넌트는 사용자의 설정에 따라 '일반 모드'와 '고급 모드'로 나뉘어 렌더링 된다. 고급 모드일 때만 특정 Context(AdvancedContext)에 접근해서 데이터를 가져오고 싶다.
그래서 크리스는 직관적으로 코드를 짰다.
// ❌ 크리스의 실수: 훅은 조건문 안에서 쓸 수 없다.
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를 "읽으면" 된다.
// ✅ React 19: use API 사용
import { use } from 'react';
function Dashboard({ isAdvanced }) {
if (isAdvanced) {
// 조건문 안에서 Context 읽기 가능!
const advancedData = use(AdvancedContext);
return <AdvancedView data={advancedData} />;
}
return <SimpleView />;
}use는 useContext를 완전히 대체할 수 있다. 이제 컴포넌트 로직의 흐름을 끊지 않고, 자연스럽게 데이터 흐름에 따라 Context 값을 가져올 수 있게 되었다. 이것은 코드의 응집도(Cohesion)를 높여준다.
2. Promise 풀어헤치기: 비동기를 동기처럼
use API의 진가는 Promise(비동기 작업)를 처리할 때 드러난다.
지금까지 우리는 비동기 데이터를 가져오기 위해 useEffect와 useState를 조합하거나, TanStack Query 같은 라이브러리에 의존해야 했다. 데이터를 가져오는 동안 isLoading 상태를 관리하고, 다 오면 렌더링 하는 복잡한 과정이 필요했다.
하지만 use API는 Promise 객체 자체를 인자로 받을 수 있다. 그리고 그 Promise가 해결(Resolve)될 때까지 리액트 렌더링을 일시 정지(Suspend)시킨다.
클라이언트 컴포넌트에서 Promise 사용하기
서버 컴포넌트(RSC)에서 데이터를 가져오고, 그 Promise를 클라이언트 컴포넌트(Message)에게 넘겨준다고 상상해 보자.
// 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)" 함수다.
이 패턴은 Part 2에서 배웠던 useSuspenseQuery와 유사하다. 하지만 이제는 외부 라이브러리 없이 리액트 순정 기능만으로도 "로딩 상태 없는 선언적인 코드"를 짤 수 있게 된 것이다.
4. 주의사항과 한계
use API가 만능은 아니다. 몇 가지 지켜야 할 규칙이 있다.
1. 렌더링 중에만 사용 가능
use는 컴포넌트나 훅 내부의 렌더링 단계에서만 호출해야 한다. 이벤트 핸들러(onClick)나 useEffect 내부에서는 사용할 수 없다. (거기서는 그냥 await를 쓰면 된다.)
2. 서버 컴포넌트 vs 클라이언트 컴포넌트
3. 캐싱은 별개다
use는 데이터를 읽는 도구일 뿐, 데이터를 저장(Caching)하거나 중복 요청을 막아주지는 않는다. API 요청을 캐싱하려면 여전히 TanStack Query를 쓰거나, 프레임워크(Next.js)의 캐싱 기능을 함께 써야 한다.
핵심 정리
Context와 비동기 처리가 이렇게나 우아해졌다. 그렇다면 가장 골치 아픈 "폼(Form) 처리"는 어떻게 변했을까?
useState로 isSubmitting, error, result 상태를 일일이 만들던 노가다에서 해방될 시간이다.
"useActionState & useFormStatus: 폼(Form) 관리의 혁명" 편에서 계속된다.