본문으로 건너뛰기

프리페치 및 Router 통합

import { Switch, Match } from 'solid-js'

function Article(props) {
const articleQuery = useQuery(() => ({
queryKey: ['article', props.id],
queryFn: getArticleById,
}))

return (
<Switch>
<Match when={articleQuery.isPending}>
Loading article...
</Match>
<Match when={articleQuery.isSuccess}>
<ArticleHeader articleData={articleQuery.data} />
<ArticleBody articleData={articleQuery.data} />
<Comments id={props.id} />
</Match>
</Switch>
)
}

function Comments(props) {
const commentsQuery = useQuery(() => ({
queryKey: ['article-comments', props.id],
queryFn: getArticleCommentsById,
}))

...
}
import { Switch, Match } from 'solid-js'

function Article(props) {
const articleQuery = useQuery(() => ({
queryKey: ['article', props.id],
queryFn: getArticleById,
}))

// Prefetch
useQuery(() => ({
queryKey: ['article-comments', props.id],
queryFn: getArticleCommentsById,
// Optional optimization to avoid rerenders when this query changes:
notifyOnChangeProps: [],
}))

return (
<Switch>
<Match when={articleQuery.isPending}>
Loading article...
</Match>
<Match when={articleQuery.isSuccess}>
<ArticleHeader articleData={articleQuery.data} />
<ArticleBody articleData={articleQuery.data} />
<Comments id={props.id} />
</Match>
</Switch>
)
}

function Comments(props) {
const commentsQuery = useQuery(() => ({
queryKey: ['article-comments', props.id],
queryFn: getArticleCommentsById,
}))

...
}

또 다른 방법은 쿼리 함수 내부에서 프리페치하는 것입니다. 문서를 가져올 때마다 댓글도 필요할 가능성이 매우 높다는 것을 알고 있다면 이 방법이 적합합니다. 이를 위해 queryClient.query를 사용합니다:

const queryClient = useQueryClient()
const articleQuery = useQuery(() => ({
queryKey: ['article', id],
queryFn: (...args) => {
void queryClient
.query({
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
})
.catch(noop)

return getArticleById(...args)
},
}))

effect에서 프리페치하는 것도 작동합니다:

import { createEffect } from 'solid-js'

const queryClient = useQueryClient()

createEffect(() => {
void queryClient
.query({
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
})
.catch(noop)
})

요약하면, 컴포넌트 수명 주기 중에 쿼리를 프리페치하려는 경우 몇 가지 방법이 있으며, 상황에 가장 적합한 방법을 선택하면 됩니다:

  • useQuery를 사용하고 결과 무시하기
  • 쿼리 함수 내부에서 프리페치하기
  • effect에서 프리페치

다음으로 조금 더 고급 사례를 살펴보겠습니다.

import { lazy, Switch, Match, For } from 'solid-js'

// This lazy loads the GraphFeedItem component, meaning
// it wont start loading until something renders it
const GraphFeedItem = lazy(() => import('./GraphFeedItem'))

function Feed() {
const feedQuery = useQuery(() => ({
queryKey: ['feed'],
queryFn: getFeed,
}))

return (
<Switch>
<Match when={feedQuery.isPending}>
Loading feed...
</Match>
<Match when={feedQuery.isSuccess}>
<For each={feedQuery.data}>
{(feedItem) => {
if (feedItem.type === 'GRAPH') {
return <GraphFeedItem feedItem={feedItem} />
}
return <StandardFeedItem feedItem={feedItem} />
}}
</For>
</Match>
</Switch>
)
}

// GraphFeedItem.tsx
function GraphFeedItem(props) {
const graphQuery = useQuery(() => ({
queryKey: ['graph', props.feedItem.id],
queryFn: getGraphDataById,
}))

...
}
function Feed() {
const queryClient = useQueryClient()
const feedQuery = useQuery(() => ({
queryKey: ['feed'],
queryFn: async (...args) => {
const feed = await getFeed(...args)

for (const feedItem of feed) {
if (feedItem.type === 'GRAPH') {
void queryClient.query({
queryKey: ['graph', feedItem.id],
queryFn: getGraphDataById,
}).catch(noop)
}
}

return feed
}
}))

...
}

라우터 통합

컴포넌트 트리 자체에서 데이터를 가져오면 요청 폭포 현상이 쉽게 발생할 수 있고 이를 해결하는 여러 방법도 애플리케이션 전체에 누적되면서 번거로워질 수 있으므로, 라우터 수준에서 통합하는 것이 프리페치를 구현하는 매력적인 방법입니다.

이 접근 방식에서는 해당 컴포넌트 트리에 어떤 데이터가 필요한지 각 _route_별로 미리 명시적으로 선언합니다. 전통적으로 Server Rendering은 렌더링을 시작하기 전에 모든 데이터를 불러와야 했기 때문에, 오랫동안 SSR된 앱에서는 이 방식이 지배적이었습니다. 이 방식은 여전히 일반적이며 Server Rendering 및 Hydration 가이드에서 자세히 알아볼 수 있습니다.

지금은 클라이언트 측 사례에 집중하여 TanStack Router로 이를 구현하는 방법의 예시를 살펴보겠습니다. 이 예시에서는 간결함을 위해 많은 설정과 상용구 코드를 생략하므로, TanStack Router 문서에서 전체 Solid Query 예시를 확인할 수 있습니다.

라우터 수준에서 통합할 때는 모든 데이터가 준비될 때까지 해당 경로의 렌더링을 _차단_하거나, 프리페치를 시작하되 결과를 기다리지 않도록 선택할 수 있습니다. 그러면 가능한 한 빨리 경로 렌더링을 시작할 수 있습니다. 이 두 접근 방식을 혼합하여 일부 핵심 데이터는 기다리되, 모든 보조 데이터의 로딩이 완료되기 전에 렌더링을 시작할 수도 있습니다. 이 예제에서는 글 데이터의 로딩이 완료될 때까지 렌더링하지 않도록 /article 경로를 구성하고, 댓글 프리페치는 가능한 한 빨리 시작하되 댓글 로딩이 아직 완료되지 않았더라도 경로 렌더링을 차단하지 않도록 구성합니다.

많은 route loader가 error boundary를 사용하여 오류 fallback을 트리거한다는 점에 유의하세요. 지금까지는 useQuery에서 재시도할 데이터의 오류를 무시하기 위해 .catch(noop)를 사용했지만, 없으면 route가 작동하지 않는 중요한 데이터의 경우에는 noop 없이 Promise를 await하고 try 블록이나 router의 오류 처리(예: TanStack Router의 errorComponent)에서 오류를 처리해야 합니다.

const queryClient = new QueryClient()
const routerContext = new RouterContext()
const rootRoute = routerContext.createRootRoute({
component: () => { ... }
})

const articleRoute = new Route({
getParentRoute: () => rootRoute,
path: 'article',
beforeLoad: () => {
return {
articleQueryOptions: { queryKey: ['article'], queryFn: fetchArticle },
commentsQueryOptions: { queryKey: ['comments'], queryFn: fetchComments },
}
},
loader: async ({
context: { queryClient },
routeContext: { articleQueryOptions, commentsQueryOptions },
}) => {
// Fetch comments asap, but don't block or throw errors
void queryClient.query(commentsQueryOptions).catch(noop)

// Don't render the route at all until article has been fetched
// As this is critical data we want the error component to trigger
// as soon as possible if something goes wrong
await queryClient.query({
...articleQueryOptions,
// If we have the article loaded already, we don't want to block on
// an extra prefetch; fallback on the default useQuery behavior to
// keep the data fresh
staleTime: 'static'
})
},
component: ({ useRouteContext }) => {
const { articleQueryOptions, commentsQueryOptions } = useRouteContext()
const articleQuery = useQuery(() => articleQueryOptions)
const commentsQuery = useQuery(() => commentsQueryOptions)

return (
...
)
},
errorComponent: () => 'Oh crap!',
})

다른 라우터와의 통합도 가능합니다.