← 글 목록

notes

use ??

JavaScript nullish coalescing 연산자 ??의 동작·사용 기준과 주의점 메모.

  • Promise 나 Context 를 컴포넌트 안에서 바로 읽는 함수다
  • 로딩 처리를 컴포넌트마다 하지 않고 Suspense 한 곳에 모은다
  • Promise 는 렌더마다 새로 만들면 안 되고 밖에서 만들어 넘겨야 한다

use 는

use 는 React 19 에 들어온 API 다. Promise 결과나 Context 값을 컴포넌트 안에서 바로 읽는다. 이름은 훅처럼 생겼는데 조건문이나 반복문 안에서도 부를 수 있다는 게 일반 훅과 다르다.

Promise 읽기

Promise 를 넘기면 아직 안 끝났을 때는 가까운 Suspense 의 fallback 이 나오고, 끝나면 결과값을 돌려준다. 실패하면 가까운 Error Boundary 가 받는다.

Promise 예제
async function getCamp(seq: number) {
  const res = await fetch(`/api/camp/${seq}`);
  return res.json();
}

function CampDetail({ campPromise }: { campPromise: Promise<Camp> }) {
  const camp = use(campPromise); // resolve 될 때까지 Suspense 가 기다린다
  return <h1>{camp.name}</h1>;
}

// 부모
<Suspense fallback={<Skeleton />}>
  <CampDetail campPromise={getCamp(123)} />
</Suspense>

useEffect 에서 fetch 하고 useState 에 담던 패턴을, 부모나 서버에서 Promise 를 만들어 넘기고 use 로 읽는 방식으로 바꿀 수 있다. 로딩 UI 도 컴포넌트마다 if (!data) 로 두지 않고 Suspense 한 곳에서 처리한다.

// 예전
const [data, setData] = useState(null);
useEffect(() => {
  fetch('/api').then(r => r.json()).then(setData);
}, []);
if (!data) return <Loading />;

// use + Suspense
const data = use(dataPromise); // 로딩은 Suspense fallback 이 맡는다

Context 읽기

use(ThemeContext) 는 useContext(ThemeContext) 와 같은 값을 돌려준다. 차이는 조건문 안에서도 부를 수 있다는 것 하나다.

const ThemeContext = createContext<'light' | 'dark'>('light');

function Panel({ showTheme }: { showTheme: boolean }) {
  if (showTheme) {
    const theme = use(ThemeContext); // useContext 는 여기서 못 쓴다
    return <div>{theme}</div>;
  }
  return null;
}

Next.js 에서 자주 쓰는 흐름

서버 컴포넌트에서 fetch 를 시작만 하고 await 하지 않은 Promise 를 클라이언트 컴포넌트에 넘긴다. 클라이언트는 use 로 이어 받고, 그동안 Suspense 가 로딩 화면을 맡는다. 데이터를 기다리지 않고 페이지 뼈대부터 보여 줄 수 있다.

서버에서 클라이언트로 넘기는 예제
// Server Component
export default function Page() {
  const campPromise = getCampData(seq); // await 하지 않는다
  return (
    <Suspense fallback={<Skeleton />}>
      <CampClient campPromise={campPromise} />
    </Suspense>
  );
}

// Client Component
'use client';
function CampClient({ campPromise }: { campPromise: Promise<Camp> }) {
  const camp = use(campPromise);
  return <div>{camp.name}</div>;
}

조심할 것

제일 많이 걸리는 건 렌더 안에서 Promise 를 만드는 거다. use(fetch(...)) 처럼 쓰면 렌더할 때마다 새 Promise 가 생겨서 Suspense 가 계속 다시 걸린다. Promise 는 부모나 서버 컴포넌트, 캐시에서 만들어 넘긴다.

서버 컴포넌트에서도 use 를 부를 수는 있지만 거기서는 그냥 await 하는 게 더 간단하다. 그래서 주로 클라이언트 컴포넌트에서 쓴다. reject 된 Promise 는 Error Boundary 가 있어야 잡힌다.

React 19 미만에는 없는 API 라 회사 프로젝트에 쓰기 전에 React 버전부터 확인했다.