npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

apollo-flavor

v3.0.0

Published

Apollo Client JSX components that eliminate Suspense 2-depth problem

Readme

apollo-flavor

Apollo Client와 GraphQL을 쓰면서 겪었던 불편함들을 선언적인 컴포넌트로 풀어본 라이브러리예요. Toss/Suspensive에서 모티브를 받았고, 거기에 GraphQL과 Apollo에만 있는 문제들을 더 다뤄봤어요.

크게 세 가지를 담았어요.

  • Apollo의 훅을 JSX 컴포넌트로 감싸서 Suspense 2-depth 문제를 없앴어요
  • React가 주는 Suspense / ErrorBoundary의 빈틈을 메우는 프리미티브를 따로 두었어요
  • Apollo에만 있는 문제(재시도가 동작하지 않는 에러 바운더리, 에러 코드 분기)를 GraphQL 전용 컴포넌트로 해결했어요

설치

npm install apollo-flavor
# or
pnpm add apollo-flavor
# or
yarn add apollo-flavor

요구사항

| | 버전 | | --- | --- | | @apollo/client | ^4.2.0 | | react / react-dom | ^18 또는 ^19 |

Apollo Client v3을 쓰신다면 apollo-flavor@2를 사용해주세요. v3에서 v4로 넘어가면서 React 훅의 import 경로(@apollo/client/react)와 에러 모델이 통째로 바뀌었어요. 두 버전을 한 패키지에서 동시에 지원하면 타입이 계속 갈라져서, apollo-flavor는 v3부터 v4 전용으로 가기로 했어요.

어떤 문제를 풀고 있나요

useSuspenseQuery는 훅이라서, 쓰려면 컴포넌트를 하나 더 만들어야 해요.

function UserPage({ userId }) {
  return (
    <Suspense fallback={<Loading />}>
      {/* 이름만 봐서는 안에서 suspend가 일어나는지 알 수 없어요 */}
      <UserProfile userId={userId} />
      <UserPosts userId={userId} />
    </Suspense>
  );
}

// 데이터를 가져오려고 만든, 그것 말고는 하는 일이 없는 컴포넌트들
function UserProfile({ userId }) {
  const { data } = useSuspenseQuery(GET_USER, { variables: { userId } });
  return <div>{data.user.name}</div>;
}

function UserPosts({ userId }) {
  const { data } = useSuspenseQuery(GET_POSTS, { variables: { userId } });
  return data.posts.map((post) => <PostItem key={post.id} post={post} />);
}

이 방식의 문제는 두 가지라고 생각해요. 첫째로 UserProfile이라는 이름만 봐서는 그 안에서 suspend가 일어나는지 알 수 없어요. 둘째로 Suspense 경계를 조정하려면 컴포넌트 구조까지 같이 손봐야 해요.

<SuspenseQuery>를 쓰면 데이터를 가져오는 자리와 Suspense 경계가 같은 depth에 있어요.

function UserPage({ userId }) {
  return (
    <Suspense fallback={<Loading />}>
      <SuspenseQuery query={GET_USER} variables={{ userId }}>
        {({ data }) => <div>{data.user.name}</div>}
      </SuspenseQuery>

      <SuspenseQuery query={GET_POSTS} variables={{ userId }}>
        {({ data }) => data.posts.map((post) => <PostItem key={post.id} post={post} />)}
      </SuspenseQuery>
    </Suspense>
  );
}

껍데기 컴포넌트가 사라지고, 어디서 요청이 나가는지 한눈에 보여요. 형제로 나란히 두면 두 요청이 동시에 시작되기 때문에, 병렬 페칭을 위해 따로 할 일도 없어요.

두 개의 진입점

// Apollo 바인딩 (아래 프리미티브도 전부 여기서 다시 내보내요)
import { SuspenseQuery, GraphQLErrorBoundary } from "apollo-flavor";

// 프리미티브만 — Apollo 없이도 쓸 수 있어요
import { ErrorBoundary, Delay } from "apollo-flavor/core";

프리미티브는 Apollo에 전혀 의존하지 않아서 따로 가져다 쓸 수 있게 서브패스로 나눴어요. 다만 대부분은 두 계층을 같이 쓰기 때문에, 메인 진입점에서도 그대로 내보내고 있어요. import 경로를 두 줄로 나누고 싶지 않다면 apollo-flavor 하나만 써도 괜찮아요.

API 한눈에 보기

데이터 페칭

| 컴포넌트 | 감싼 훅 | 언제 쓰나요 | | --- | --- | --- | | <SuspenseQuery> | useSuspenseQuery | 기본. 데이터가 올 때까지 suspend해요 | | <Query> | useQuery | loading / error를 직접 다루고 싶을 때요 | | <Mutation> | useMutation | 폼 제출 같은 변경 작업이요 | | <Subscription> | useSubscription | 실시간 구독이요 | | <BackgroundQuery> + <ReadQuery> | useBackgroundQuery / useReadQuery | 요청은 먼저 띄우고 suspend는 나중에 하고 싶을 때요 | | <LoadableQuery> | useLoadableQuery | 클릭 같은 상호작용 시점에 요청하고 싶을 때요 | | <SuspenseFragment> | useSuspenseFragment | 프래그먼트를 suspend하며 읽을 때요 | | <WatchFragment> | useFragment | 프래그먼트를 suspend 없이 구독할 때요 |

에러 처리

| | 설명 | | --- | --- | | <GraphQLErrorBoundary> | 재시도할 때 refetch까지 해주는 에러 바운더리예요 | | hasErrorCode / isServerError / isNetworkError / isGraphQLError | shouldCatch에 그대로 넣는 에러 판별 헬퍼예요 | | getErrorCodes / getGraphQLErrors | 에러에서 코드와 목록을 꺼내요 |

프리미티브 (apollo-flavor/core)

| | 어떤 불편함을 없앴나요 | | --- | --- | | <Suspense clientOnly> | 이 경계만 SSR에서 빼요. dynamic()이나 useEffect 가드가 필요 없어요 | | <ErrorBoundary shouldCatch> | 잡을 에러를 골라서 처리하고, 나머지는 위로 흘려보내요 | | <ErrorBoundaryGroup> | 여러 바운더리를 버튼 하나로 한꺼번에 되돌려요 | | <Delay ms> | 금방 끝나는 로딩의 스피너 깜빡임을 없애요 | | <ClientOnly> / useIsClient | 하이드레이션 불일치 없이 클라이언트에서만 렌더해요 | | <DefaultPropsProvider> | ms나 fallback 기본값을 최상단에서 한 번만 정해요 |

데이터 페칭

SuspenseQuery

데이터가 준비될 때까지 suspend하고, 준비되면 children에 결과를 넘겨줘요.

<Suspense fallback={<Spinner />}>
  <SuspenseQuery query={GET_USER} variables={{ id }}>
    {({ data, refetch }) => <Profile user={data.user} onRefresh={refetch} />}
  </SuspenseQuery>
</Suspense>

data는 기본적으로 undefined가 될 수 없어요. suspend가 끝났다는 건 데이터가 왔다는 뜻이니까요. 그래서 data?.user?.name 같은 옵셔널 체이닝 없이 바로 쓸 수 있어요.

Query

Suspense를 쓰지 않고 loading과 error를 직접 다루고 싶을 때 써요. 기존 코드를 조금씩 옮기는 중이거나, 로딩 상태를 화면 안에서 세밀하게 표현해야 할 때 잘 맞는다고 생각해요.

<Query query={GET_USER} variables={{ id }}>
  {({ data, loading, error }) => {
    if (loading) return <Spinner />;
    if (error) return <ErrorView error={error} />;
    return <Profile user={data.user} />;
  }}
</Query>

Mutation

mutation을 쓰려고 컴포넌트를 따로 만들 필요가 없어요. 필요한 자리에서 바로 선언하면 돼요.

<Mutation mutation={CREATE_POST} variables={{ input }}>
  {({ mutate, loading, error, reset }) => (
    <form
      onSubmit={async (e) => {
        e.preventDefault();
        const { data } = await mutate();
        onCreated(data);
      }}
    >
      <button type="submit" disabled={loading}>
        {loading ? "저장 중..." : "저장"}
      </button>
    </form>
  )}
</Mutation>

variables를 prop으로 미리 넘겨두면 mutate()를 인자 없이 부를 수 있어요. 반대로 넘기지 않았다면 mutate({ variables })를 요구하도록 타입을 잡아뒀어요. prop으로 준 것과 호출할 때 주는 것이 타입에서 어긋나지 않게 하고 싶었어요.

에러를 상위 바운더리로 보내고 싶다면 throwOnError를 켜면 돼요.

<Mutation mutation={CREATE_POST} variables={{ input }} throwOnError>

Apollo Client v4에서 useMutation의 onCompleted / onError 콜백이 없어졌어요. 완료와 실패는 await mutate()의 반환값으로 다루거나, throwOnError로 바운더리에 맡겨주세요.

Subscription

<Subscription subscription={ON_COMMENT_ADDED} variables={{ postId }}>
  {({ data, loading }) => (loading ? <Spinner /> : <Comment data={data} />)}
</Subscription>

워터폴 없애기

BackgroundQuery + ReadQuery

<SuspenseQuery>는 자기 자신이 suspend해요. 그래서 그 안에 또 쿼리가 있으면 바깥이 끝나야 안쪽이 시작돼요. 이걸 워터폴이라고 하는데, <BackgroundQuery>는 요청만 먼저 띄우고 suspend는 하지 않아요. 실제로 데이터를 읽는 자리에서 <ReadQuery>로 suspend하면 돼요.

<BackgroundQuery query={GET_USER} variables={{ id }}>
  {({ queryRef }) => (
    <>
      {/* 데이터를 기다리지 않고 먼저 보여줄 수 있어요 */}
      <Header />

      <Suspense fallback={<Spinner />}>
        <ReadQuery queryRef={queryRef}>
          {({ data }) => <Profile user={data.user} />}
        </ReadQuery>
      </Suspense>
    </>
  )}
</BackgroundQuery>

refetch, fetchMore, subscribeToMore도 같이 넘겨줘요. <ReadQuery>가 받는 data의 타입은 <BackgroundQuery>에서 정한 옵션을 그대로 이어받아요.

LoadableQuery

렌더 시점이 아니라 사용자가 뭔가를 했을 때 요청을 시작해요. loadQuery를 부르기 전까지 queryRef는 null이라서, 그걸로 "아직 안 불러온 상태"를 표현할 수 있어요.

<LoadableQuery query={GET_USER}>
  {({ loadQuery, queryRef, reset }) => (
    <>
      <button onClick={() => loadQuery({ id: "1" })}>불러오기</button>

      {queryRef && (
        <Suspense fallback={<Spinner />}>
          <ReadQuery queryRef={queryRef}>
            {({ data }) => <Profile user={data.user} />}
          </ReadQuery>
        </Suspense>
      )}
    </>
  )}
</LoadableQuery>

variables는 컴포넌트가 아니라 loadQuery()를 부를 때 넘겨요.

프래그먼트

SuspenseFragment

프래그먼트 데이터가 캐시에 다 찰 때까지 suspend해요. <SuspenseQuery>와 같은 depth에 둘 수 있어서, 프래그먼트를 쓰려고 컴포넌트를 나눌 필요가 없어요.

<SuspenseQuery query={GET_POST} variables={{ id }}>
  {({ data }) => (
    <article>
      <h1>{data.post.title}</h1>

      <SuspenseFragment fragment={USER_CARD} from={data.post.author}>
        {({ data: author }) => <UserCard user={author} />}
      </SuspenseFragment>
    </article>
  )}
</SuspenseQuery>

from에는 캐시상의 대상을 넘겨요. 쿼리 결과 객체, { __typename, id } 형태의 리터럴, "User:1" 같은 캐시 ID 문자열을 받아요.

from에 넘기는 값의 타입을 interface로 선언했다면 타입 에러가 날 수 있어요. Apollo가 요구하는 StoreObject는 인덱스 시그니처를 필요로 하는데, TypeScript는 interface에만 암묵적 인덱스 시그니처를 주지 않아요. GraphQL codegen은 보통 type 별칭으로 뽑아주기 때문에 대부분은 그냥 동작해요. 직접 interface로 선언한 타입이라면 type으로 바꾸거나 캐시 ID 문자열을 넘겨주세요.

WatchFragment

같은 일을 하지만 suspend하지 않아요. 대신 complete로 데이터가 다 찼는지 확인해야 해요. 이미 화면에 뭔가 그려져 있는 상태에서 일부만 갱신하고 싶을 때 잘 맞아요.

<WatchFragment fragment={USER_CARD} from={{ __typename: "User", id }}>
  {({ data, complete }) => (complete ? <UserCard user={data} /> : <Skeleton />)}
</WatchFragment>

이름을 Fragment가 아니라 WatchFragment로 한 건 일부러예요. React의 Fragment와 이름이 겹치면 import { Fragment }가 어느 쪽인지 헷갈려서 실제 버그로 이어질 수 있다고 생각했어요. 내부적으로 캐시를 구독(watch)하는 동작을 그대로 이름에 담았어요.

에러 처리

GraphQLErrorBoundary

Apollo를 쓰면서 가장 당황스러웠던 부분이에요. 일반 ErrorBoundary의 reset은 React 트리만 되돌려요. 그런데 Apollo는 실패한 쿼리 결과를 들고 있어서, children이 다시 마운트되면 캐시된 같은 에러를 그대로 다시 던져요. 재시도 버튼을 눌러도 아무 일이 없어 보이는 이유예요.

// 재시도 버튼을 눌러도 계속 같은 에러가 나요
<ErrorBoundary fallback={({ reset }) => <button onClick={reset}>다시 시도</button>}>
  <SuspenseQuery query={GET_USER} variables={{ id }}>{...}</SuspenseQuery>
</ErrorBoundary>

<GraphQLErrorBoundary>는 reset할 때 client.refetchQueries를 먼저 끝내고 나서 바운더리를 되돌려요. 순서를 반대로 하면 여전히 캐시된 에러를 다시 던지기 때문에, 이 순서를 컴포넌트 안에 넣어뒀어요.

<GraphQLErrorBoundary
  fallback={({ error, reset, isRetrying }) => (
    <>
      <p>{error.message}</p>
      <button onClick={reset} disabled={isRetrying}>
        {isRetrying ? "재시도 중..." : "다시 시도"}
      </button>
    </>
  )}
>
  <Suspense fallback={<Spinner />}>
    <SuspenseQuery query={GET_USER} variables={{ id }}>
      {({ data }) => <Profile user={data.user} />}
    </SuspenseQuery>
  </Suspense>
</GraphQLErrorBoundary>

refetch가 도는 동안을 isRetrying으로 알려줘요. 버튼을 비활성화하거나 문구를 바꾸는 데 쓸 수 있어요.

| prop | 설명 | | --- | --- | | refetchQueries | 재시도할 때 refetch할 대상이에요. 기본값은 "active"예요 | | onRefetchError | refetch 자체가 실패했을 때 알려줘요 |

fallback을 별도 컴포넌트로 뺐다면 useGraphQLErrorBoundaryFallbackProps()로 error / reset / isRetrying을 가져올 수 있어요. shouldCatch, resetKeys, onError, onReset 같은 <ErrorBoundary>의 prop도 그대로 쓸 수 있어요.

에러 코드로 분기하기

GraphQL 서버는 인증이나 권한 실패를 HTTP 상태가 아니라 extensions.code로 알려주는 경우가 많아요. 그 값으로 바운더리를 나눌 수 있게 헬퍼를 만들었어요. 전부 shouldCatch에 그대로 들어가요.

import { hasErrorCode } from "apollo-flavor";

<GraphQLErrorBoundary fallback={<GeneralError />}>
  {/* 인증 에러만 여기서 잡고, 나머지는 바깥으로 보내요 */}
  <GraphQLErrorBoundary
    shouldCatch={hasErrorCode("UNAUTHENTICATED", "FORBIDDEN")}
    fallback={<Login />}
  >
    <Page />
  </GraphQLErrorBoundary>
</GraphQLErrorBoundary>

| 헬퍼 | 설명 | | --- | --- | | hasErrorCode(...codes) | extensions.code가 하나라도 일치하는지 봐요 | | isServerError(...statusCodes) | HTTP 상태 코드로 판별해요. 인자가 없으면 모든 ServerError를 잡아요 | | isNetworkError() | 네트워크, 파싱, 프로토콜 계층 에러예요. 재시도하면 될 수도 있는 에러를 고를 때 써요 | | isGraphQLError() | 서버가 GraphQL errors를 돌려준 경우예요 | | getErrorCodes(error) | 에러 코드 목록을 꺼내요 | | getGraphQLErrors(error) | GraphQL 에러 목록을 꺼내요 |

ErrorBoundary

React의 에러 바운더리와 다른 점은 shouldCatch로 잡을 에러를 고를 수 있다는 거예요. 걸러진 에러는 상위 바운더리로 그대로 흘러가요.

<ErrorBoundary fallback={<GeneralError />}>
  <ErrorBoundary shouldCatch={UnauthorizedError} fallback={<Login />}>
    <Page />
  </ErrorBoundary>
</ErrorBoundary>

shouldCatch는 boolean, 에러 클래스, 판별 콜백, 그리고 그 배열(하나라도 만족하면 잡아요)을 받아요.

fallback 안에서 prop drilling 없이 error와 reset을 쓰고 싶다면 useErrorBoundaryFallbackProps()를 쓰면 돼요. 이벤트 핸들러나 Promise 콜백에서 난 에러처럼 렌더 중이 아니라 바운더리가 못 잡는 경우에는 useErrorBoundary().setError()로 보낼 수 있어요.

const errorBoundary = useErrorBoundary();

<button onClick={() => submit().catch(errorBoundary.setError)}>보내기</button>

ErrorBoundaryGroup

화면에 바운더리가 여러 개일 때, 각각에 resetKeys를 손으로 엮지 않고 한꺼번에 되돌릴 수 있어요.

<ErrorBoundaryGroup>
  <ErrorBoundaryGroup.Consumer>
    {(group) => <button onClick={group.reset}>전부 다시 시도</button>}
  </ErrorBoundaryGroup.Consumer>

  <GraphQLErrorBoundary fallback={<Failed />}>
    <SuspenseQuery query={GET_PROFILE}>{({ data }) => <Profile {...data} />}</SuspenseQuery>
  </GraphQLErrorBoundary>
  <GraphQLErrorBoundary fallback={<Failed />}>
    <SuspenseQuery query={GET_POSTS}>{({ data }) => <Posts {...data} />}</SuspenseQuery>
  </GraphQLErrorBoundary>
</ErrorBoundaryGroup>

중첩하면 바깥 그룹을 되돌릴 때 안쪽 그룹도 같이 되돌아가요. 안쪽만 독립적으로 두고 싶다면 blockOutside를 켜면 돼요.

프리미티브

Suspense

React의 Suspense를 그대로 대체하면서 clientOnly를 더했어요. 서버에서는 fallback만 그리고, 클라이언트에서만 children을 그려요.

<Suspense clientOnly fallback={<Spinner />}>
  <UsesBrowserOnlyApi />
</Suspense>

useEffect로 마운트 여부를 확인하는 방식 대신 useSyncExternalStore의 getServerSnapshot을 썼어요. React가 서버와 클라이언트 스냅샷이 다르다는 걸 알고 있어서 하이드레이션 경고 없이 넘어가요.

Delay

로딩이 100ms 만에 끝나는데 스피너가 깜빡이고 사라지면 오히려 더 느리게 느껴진다고 생각해요. fallback을 <Delay>로 감싸면 그 시간 안에 끝났을 때 스피너가 아예 보이지 않아요.

<Suspense fallback={<Delay ms={200}><Spinner /></Delay>}>
  <SuspenseQuery query={GET_USER} variables={{ id }}>
    {({ data }) => <Profile user={data.user} />}
  </SuspenseQuery>
</Suspense>

사라졌다 나타나는 대신 서서히 나타나게 하고 싶다면 children에 함수를 넘기면 돼요.

<Delay ms={200}>
  {({ isDelayed }) => (
    <div style={{ opacity: isDelayed ? 1 : 0, transition: "opacity 200ms" }}>
      <Spinner />
    </div>
  )}
</Delay>

ClientOnly

window나 localStorage처럼 브라우저에만 있는 값에 의존해서 서버와 결과가 달라지는 부분을 감싸요.

<ClientOnly fallback={<Skeleton />}>
  <ThemeToggle />
</ClientOnly>

Suspense 경계까지 함께 필요하다면 <Suspense clientOnly> 쪽이 더 간단해요.

DefaultPropsProvider

<Delay ms={200}>을 화면마다 반복해서 쓰는 대신 최상단에서 한 번만 정할 수 있어요. 개별 컴포넌트에 직접 넘긴 prop이 항상 우선해요.

<DefaultPropsProvider defaultProps={{ Delay: { ms: 200 } }}>
  <App />
</DefaultPropsProvider>

with

프리미티브는 모두 Xxx.with(props, Component) 형태의 HOC를 가지고 있어요. JSX를 다시 짜지 않고 기존 컴포넌트에 경계만 씌우고 싶을 때 편해요.

const Page = Suspense.with({ fallback: <Spinner /> }, PageContent);
const Safe = ErrorBoundary.with({ fallback: <Failed /> }, Page);

Props 규약

query와 variables는 top-level prop으로, 그 밖의 모든 옵션은 options prop으로 넘겨주세요.

<SuspenseQuery
  query={GET_USER}
  variables={{ id }}
  options={{ errorPolicy: "all", fetchPolicy: "network-only" }}
>
  {({ data }) => ...}
</SuspenseQuery>

이렇게 나눈 이유가 있어요. 옵션을 top-level로 펼치면 TypeScript가 어떤 옵션이 왔는지 추론하지 못하고 제네릭 제약으로 넘어가버려요. 그러면 넘긴 옵션에 맞춰 data 타입을 좁힐 수 없게 돼요. 그래서 top-level 옵션은 아예 컴파일 에러가 나도록 막아뒀어요.

options로 넘기면 이렇게 좁혀져요.

| options | data 타입 | | --- | --- | | (없음) | TData | | returnPartialData: true | TData \| DeepPartialObject<TData> | | errorPolicy: "all" | TData \| undefined |

이 좁힘이 실제로 훅과 같은지는 타입 테스트로 확인하고 있어요.

자주 쓰는 패턴

조건부 페칭

skipToken 대신 컴포넌트를 조건부로 그리면 돼요. "쿼리를 실행하지 않는 상태"를 따로 다룰 필요가 없어져서 이쪽이 더 낫다고 생각했어요.

{userId && (
  <SuspenseQuery query={GET_USER} variables={{ id: userId }}>
    {({ data }) => <Profile user={data.user} />}
  </SuspenseQuery>
)}

variables가 바뀔 때 깜빡임 없애기

Apollo에는 keepPreviousData 같은 게 없어서 variables가 바뀌면 매번 다시 suspend해요. React의 useDeferredValue를 쓰는 쪽에서 감싸주면 이전 결과가 유지돼요.

이 동작을 컴포넌트 prop으로 넣을지도 고려해봤는데, variables를 비교하려면 결국 직렬화가 필요하고 그 정확성을 보장하기 어려워서 호출하는 쪽에 맡기기로 했어요.

const [keyword, setKeyword] = useState("");
const deferredKeyword = useDeferredValue(keyword);

<Suspense fallback={<Spinner />}>
  {/* 입력하는 동안에도 이전 검색 결과가 그대로 보여요 */}
  <SuspenseQuery query={SEARCH} variables={{ keyword: deferredKeyword }}>
    {({ data }) => <Results items={data.search} />}
  </SuspenseQuery>
</Suspense>

지원 중단

useQueries와 관련 유틸리티는 더 이상 권장하지 않아요. 반복문 안에서 useQuery를 호출하는 구조라 Rules of Hooks를 어기고 있어요. 여러 쿼리를 병렬로 다루고 싶다면 <SuspenseQuery>를 형제로 나란히 두는 쪽이 안전하고, 어차피 동시에 요청돼요.

라이선스

MIT