본문으로 건너뛰기
Tech Blog

use 는 상태 훅이 아니에요

글 복사 완료!

useContext 가 if 안에서 막힐 때 use 가 풀어주는 자리예요

·10분·

처음 React 19 의 use 를 봤을 때는 "또 새 훅이구나" 정도로 흘려봤어요. 그러다 어떤 분이 "use 는 상태 공유가 아니라 의존성 공유를 위한 도구" 라는 말을 해줬는데, 묘하게 정확한 표현이더라고요. useuseState 의 친척이 아니라 await 의 React 판이에요.

use 는 상태가 아니라 자원을 읽어요

React 공식 19 발표는 use 를 한 줄로 소개해요. 렌더 중에 자원을 읽는 새 API 라고요.

"In React 19 we're introducing a new API to read resources in render: use." - React 19 release blog

"자원을 읽는다" 가 핵심이에요. useState 처럼 상태를 만들거나 보관하는 게 아니라, 이미 어딘가에 존재하는 값을 렌더 안에서 풀어내는 도구거든요.

RFC 를 쓴 acdlite 는 더 짧은 비유를 남겼어요. use 는 React 전용 await 라고요.

await 의 직관을 그대로 가져오면 됩니다. 어떤 비동기 결과를 풀어내야 하는데, 그 결과는 함수 밖에서 만들어져 들어와요. use 도 똑같아요. Promise 든 Context 든 외부에서 만들어져 컴포넌트로 들어온 자원을 그 자리에서 꺼내는 거죠.

그래서 "의존성 공유" 라는 표현이 공식 표현과 의미가 겹쳐요. 컴포넌트 입장에서 외부에서 주입받은 무언가를 풀어 쓴다는 본질이 같거든요. 공식 문서는 "자원을 읽는다" 라는 좀 더 좁은 말을 쓰고요.

use 만 if 안에서 호출해도 되나

기존 Hook 이 조건문 안에서 호출 금지인 이유는 단순해요. React 가 호출 순서로 Hook 상태를 매칭하거든요. useState 가 첫 번째, useEffect 가 두 번째 라는 식으로 자리를 외워두고, 다음 렌더에서도 그 순서로 값을 꺼내요. 한 번이라도 if 로 순서가 흔들리면 React 는 누구 자리를 누구에게 줘야 할지 못 찾아요.

use 는 다른 자리에 서 있어요. RFC 에 적힌 한 문장이 정확해요.

"Unlike most other Hooks, it does not need to 'store' any data that lives beyond a single render of component. Instead, the data for the promise is associated with the promise object itself." - RFC #229

use 는 렌더 사이에 보관할 상태가 없어요. Promise 의 결과는 Promise 객체가 들고 있고, Context 의 값은 Provider 가 들고 있어요. React 가 호출 순서로 외울 자리가 없거든요.

그래서 use 만 조건문, 반복문, early return 뒤에서 호출이 허용돼요. ESLint 의 react-hooks 룰도 이 예외를 명시하고 있어요.

if 가 먼저 와도 괜찮고, 같은 자리에 use 를 여러 번 써도 괜찮아요. 단 컴포넌트 함수나 다른 Hook 안에서 호출하라는 제약은 그대로 유지됩니다. 이 제약 덕분에 React Compiler 가 메모이제이션을 안전하게 추론할 수 있다고 RFC 가 따로 적어두기도 했어요.

use(Context)useContext 를 대체하지 않아요

use(SomeContext)useContext(SomeContext) 는 호출 모양이 똑같아 보여요. 같은 Provider 를 찾고, 같은 값을 돌려줍니다.

차이는 Context 를 여러 개로 쪼개야 하는 이유 에서 다룬 리렌더 모델이 아니라 호출 위치의 자유에 있어요. useContext 는 컴포넌트 최상단에서만 호출할 수 있어요. early return 뒤에서는 못 써요. use(SomeContext) 는 그 자리에서도 됩니다.

React 19 발표문이 보여준 예시가 이걸 그대로 보여줘요.

useContext 였다면 if (!user) 위에서 theme 를 먼저 읽어야 했어요. 안 쓸 수도 있는 값을 미리 꺼내두는 모양이었죠. use 는 진짜 필요한 자리에서만 꺼낼 수 있어요.

대체 관계는 아니에요. 공식 문서도 useContext 를 폐기하라고 말하지 않아요. 컴포넌트 최상단에서 Context 를 한 번 읽고 끝이라면 useContext 로 충분합니다. 조건 뒤에서 읽어야 하거나 Promise 와 Context 를 같은 도구로 다루고 싶을 때 use 가 쓸모가 있는 거예요.

use(Promise) 가 자주 막히는 자리

Promise 쪽으로 넘어와볼게요. use(promise)Suspense 안쪽과 useSuspenseQuery 에서 다룬 Suspense 트리거 중 하나예요. pending 이면 위쪽 <Suspense> 의 fallback 이 뜨고, rejected 면 위쪽 Error Boundary 가 받습니다. Promise 가 해결되면 컴포넌트가 다시 렌더되면서 값을 들고 진행해요.

진짜 어려운 부분은 Promise 를 어디서 만드냐는 거예요.

가장 흔한 안티패턴이 이거예요.

"use client";
import { use } from "react";
 
function CommentsBad() {
  // 매 렌더마다 새 Promise 객체.
  // use 는 객체로 매칭하니까, Suspense 가 풀리지 않고 fallback 만 반복돼요.
  const comments = use(fetch("/api/comments").then(r => r.json()));
  return <List items={comments} />;
}

Client Component 안에서 fetch() 를 매 렌더마다 호출하면 매번 새 Promise 객체가 만들어져요. use 는 Promise 객체로 결과를 매칭하기 때문에, 매번 새 Promise 가 들어오는 건 매번 새 자원을 던지는 것과 같아요. 공식 문서가 이 함정을 따로 경고하고 있어요.

"Promises created in Client Components are recreated on every render. Promises passed from a Server Component to a Client Component are stable across re-renders." - React docs

Client Component 에서 만든 Promise 는 렌더마다 새로 만들어져요. Server Component 에서 만들어서 prop 으로 넘긴 Promise 는 렌더 사이에 안정적으로 유지되고요.

권장 모양은 Promise 를 Server Component 에서 만들고 Client Component 에는 prop 으로 내려주는 형태예요.

// app/comments/page.tsx (Server Component)
import { Suspense } from "react";
import Comments from "./Comments";
 
export default function Page() {
  const commentsPromise = fetchComments();
  return (
    <Suspense fallback={<Skeleton />}>
      <Comments commentsPromise={commentsPromise} />
    </Suspense>
  );
}
// app/comments/Comments.tsx
"use client";
import { use } from "react";
 
export default function Comments({ commentsPromise }) {
  const comments = use(commentsPromise);
  return <ul>{comments.map(c => <li key={c.id}>{c.text}</li>)}</ul>;
}

여기서 use client 한 줄이 어디까지 끌고 오는지가 또 다른 함정이긴 한데, 그건 use client 한 줄이 끌고 오는 것들 에 정리해뒀어요.

Server Component 자체에서는 use(promise) 보다 그냥 async/await 을 쓰는 게 권장이에요. Server Component 는 async 함수로 선언할 수 있고, await 가 직관적으로 동작하거든요. use 는 Server 가 만든 자원을 Client 가 풀어낼 때 의미가 살아나요.

그래서 언제 use 를 고르나

useContext 가 컴포넌트 최상단에서 한 번 읽고 끝나면 그대로 두면 됩니다. use 로 굳이 바꿀 이유가 없어요. early return 뒤에서 읽고 싶거나, 조건에 따라 다른 Context 를 골라 읽어야 하는 경우에 use 가 자리를 만들어줘요.

데이터 페칭이라면 컴포넌트 안에서 useEffect + fetch 로 받아오는 옛 모양 대신, 위쪽 (가능하면 Server Component) 에서 Promise 를 만들어 prop 으로 내리고 아래 Client Component 에서 use 로 풀어내는 모양이 권장이에요. 로딩 상태 분기를 직접 쓸 일이 줄어들고, 그 자리를 Suspense 와 Error Boundary 가 채웁니다.

핵심으로 돌아오면 결국 use 는 새 상태 도구가 아니에요. 외부에서 만들어진 자원을 컴포넌트가 그 자리에서 풀어내는 도구죠. Context 든 Promise 든 같은 자리에서 받아낼 수 있다는 게 use 의 핵심이고, "의존성 공유" 라는 표현이 useStateuse 의 거리를 가장 잘 설명해요.

참고 자료

관련 글