데이터 로딩
데이터 로딩은 웹 애플리케이션의 일반적인 관심사이며 라우팅과 관련이 있습니다. 애플리케이션의 페이지를 로드할 때 페이지의 모든 비동기 요구 사항을 가능한 한 일찍 병렬로 가져와 충족하는 것이 이상적입니다. 콘텐츠가 렌더링되기 전에 사용자가 어디로 이동하는지 아는 곳은 대개 라우터뿐이므로, 라우터가 이러한 비동기 종속성을 조정하기에 가장 적합합니다.
Next.js의 getServerSideProps나 Remix/React-Router의 loader에 익숙할 수 있습니다. TanStack Router에도 라우트별로 에셋을 병렬로 프리로드/로드하는 유사한 기능이 있어 suspense를 통해 가져오면서 가능한 한 빠르게 렌더링할 수 있습니다.
이러한 일반적인 라우터의 기대를 넘어 TanStack Router는 라우트 로더를 위한 장기 메모리 내 캐싱 계층인 내장 SWR 캐싱을 제공합니다. 따라서 TanStack Router를 사용해 라우트 데이터를 프리로드하여 즉시 로드하거나, 이전에 방문한 라우트의 데이터를 일시적으로 캐시해 나중에 다시 사용할 수 있습니다.
라우트 로딩 생명 주기
URL/히스토리 업데이트가 감지될 때마다 라우터는 다음 순서를 실행합니다.
- 라우트 매칭(하향식)
route.params.parseroute.validateSearch
- 라우트 프리로딩(순차)
route.beforeLoadroute.onErrorroute.errorComponent/parentRoute.errorComponent/router.defaultErrorComponent
- 라우트 로딩(병렬)
route.component.preload?route.loaderroute.pendingComponent(선택 사항)route.component
route.onErrorroute.errorComponent/parentRoute.errorComponent/router.defaultErrorComponent
라우터 캐시를 사용할까요?
TanStack의 라우터 캐시는 대부분의 소규모에서 중간 규모 애플리케이션에 잘 맞을 가능성이 높지만, TanStack Query와 같은 더 강력한 캐싱 솔루션과 비교해 사용할 때의 장단점을 이해하는 것이 중요합니다.
TanStack Router 캐시의 장점:
- 내장되어 사용하기 쉽고 추가 의존성이 없습니다.
- 라우트별 중복 제거, 프리로드, 로드, stale-while-revalidate, 백그라운드 다시 가져오기를 처리합니다.
- 대략적인 무효화(모든 라우트와 캐시를 한 번에 무효화)입니다.
- 자동 가비지 컬렉션을 지원합니다.
- 라우트 간에 공유하는 데이터가 적은 앱에서 잘 작동합니다.
- SSR에서 "그냥 작동"합니다.
TanStack Router 캐시의 단점:
- 영속성 어댑터/모델이 없습니다.
- 라우트 간 공유 캐싱/중복 제거가 없습니다.
- 내장 뮤테이션 API가 없습니다(많은 예제에서 기본
useMutation훅을 제공하며, 많은 사용 사례에 충분할 수 있습니다). - 내장 캐시 수준 낙관적 업데이트 API가 없습니다(그래도
useMutation훅과 같은 곳의 임시 상태를 사용해 컴포넌트 수준에서 이를 구현할 수 있습니다).
[!TIP] 처음부터 TanStack Query와 같은 더 강력한 기능을 사용하려고 하거나 사용해야 한다면 외부 데이터 로딩 가이드로 바로 이동합니다.
라우터 캐시 사용
라우터 캐시는 내장되어 있으며 어떤 라우트의 loader 함수에서 데이터를 반환하기만 하면 쉽게 사용할 수 있습니다. 방법을 알아보겠습니다.
라우트 loader
라우트 매치가 로드되면 라우트 loader 함수를 호출합니다. 많은 유용한 속성을 포함하는 객체 하나를 매개변수로 받습니다. 이러한 속성은 잠시 후 살펴보고, 먼저 지원되는 두 가지 loader 형식을 확인하겠습니다.
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
})
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: {
handler: () => fetchPosts(),
},
})
staleReloadMode와 같은 로더별 동작을 구성하려면 객체 형식을 사용합니다.
loader 매개변수
loader 함수는 다음 속성을 가진 객체 하나를 받습니다.
abortController- 이 공유 가능한 로더 호출을 위한 컨트롤러입니다. 프리로드와 이동은 동일한 진행 중 로더 작업을 공유할 수 있습니다. 호출이 오래되어 더 이상 필요한 소비자가 없으면 신호가 취소됩니다.cause- 현재 라우트 매치의 원인입니다. 다음 중 하나일 수 있습니다.enter- 이전 위치에서 매칭되지 않았던 라우트가 매칭되고 로드될 때입니다.preload- 라우트를 프리로드할 때입니다.stay- 이전 위치에서 매칭되었던 라우트가 다시 매칭되고 로드될 때입니다.
context- 다음을 병합한 라우트 컨텍스트 객체입니다.- 부모 라우트 컨텍스트
beforeLoad옵션으로 제공한 이 라우트의 컨텍스트
deps-Route.loaderDeps함수가 반환하는 객체 값입니다.Route.loaderDeps가 정의되지 않으면 대신 빈 객체를 제공합니다.location- 현재 위치입니다.params- 라우트의 경로 매개변수입니다.parentMatchPromise-Promise<RouteMatch>(루트 라우트에서는undefined)preload- 라우트를 로드하는 대신 프리로드할 때true인 불리언입니다.route- 라우트 자체입니다.
이러한 매개변수를 사용하면 많은 작업을 수행할 수 있지만, 먼저 이를 제어하는 방법과 loader 함수가 호출되는 시점을 살펴보겠습니다.
loader에서 데이터 사용
loader의 데이터를 사용하려면 Route 객체에 정의된 useLoaderData 훅을 사용합니다.
const posts = Route.useLoaderData()
라우트 객체에 바로 접근할 수 없다면(즉, 현재 라우트의 컴포넌트 트리 깊은 곳에 있다면) getRouteApi를 사용해 동일한 훅과 Route 객체의 다른 훅에 접근할 수 있습니다. 순환 의존성을 만들 가능성이 있는 Route 객체를 가져오는 것보다 이 방법을 사용하는 것이 좋습니다.
React
import { getRouteApi } from '@tanstack/react-router'
// in your component
const routeApi = getRouteApi('/posts')
const data = routeApi.useLoaderData()
Solid
import { getRouteApi } from '@tanstack/solid-router'
// in your component
const routeApi = getRouteApi('/posts')
const data = routeApi.useLoaderData()
종속성 기반 Stale-While-Revalidate 캐싱
TanStack Router는 라우트의 종속성을 키로 사용하는 라우트 로더용 내장 Stale-While-Revalidate 캐싱 계층을 제공합니다.
- 라우트의 완전히 파싱된 pathname
- 예:
/posts/1과/posts/2
- 예:
loaderDeps옵션으로 제공하는 추가 종속성- 예:
loaderDeps: ({ search: { pageIndex, pageSize } }) => ({ pageIndex, pageSize })
- 예:
이러한 종속성을 키로 사용하면 TanStack Router는 라우트의 loader 함수가 반환한 데이터를 캐시하고 동일한 라우트 매치의 후속 요청을 처리하는 데 사용합니다. 즉, 라우트 데이터가 이미 캐시에 있으면 즉시 반환한 다음 데이터의 "최신 상태"에 따라 필요한 경우 백그라운드에서 다시 가져옵니다.
주요 옵션
라우터 종속성과 "최신 상태"를 제어하기 위해 TanStack Router는 라우트 로더의 키 지정 및 캐싱 동작을 제어하는 다양한 옵션을 제공합니다. 사용할 가능성이 높은 순서대로 살펴보겠습니다.
routeOptions.loaderDeps- 라우터에 검증된 검색 매개변수를 제공하고
loader함수에서 사용할 직렬화 가능한 종속성 객체를 반환하는 결정적이고 부작용 없는 함수입니다. 이동마다 이러한 종속성이 변경되면staleTime과 관계없이 라우트를 다시 로드합니다. 종속성은 깊은 동등성 검사로 비교합니다.
- 라우터에 검증된 검색 매개변수를 제공하고
routeOptions.staleTimerouterOptions.defaultStaleTime- 로드를 시도할 때 라우트 데이터를 최신 상태로 간주할 밀리초 수입니다.
routeOptions.preloadStaleTimerouterOptions.defaultPreloadStaleTime- 프리로드를 시도할 때 라우트 데이터를 최신 상태로 간주할 밀리초 수입니다.
routeOptions.gcTimerouterOptions.defaultGcTime- 가비지 컬렉션되기 전에 라우트 데이터를 캐시에 보관할 밀리초 수입니다.
routeOptions.shouldReload- 동일한
beforeLoad및loaderContext매개변수를 받고 라우트를 다시 로드해야 하는지를 나타내는 불리언을 반환하는 함수입니다.staleTime과loaderDeps보다 한 단계 더 세밀하게 라우트를 다시 로드할 시점을 제어하며 Remix의shouldLoad옵션과 유사한 패턴을 구현하는 데 사용할 수 있습니다.
- 동일한
routeOptions.loader.staleReloadModerouterOptions.defaultStaleReloadMode- 매칭된 라우트에 이미 stale 상태인 성공 데이터가 있을 때의 동작을 제어합니다. stale-while-revalidate에는
'background'를 사용하고, 계속 진행하기 전에 stale 로더 다시 로드가 완료되기를 기다리려면'blocking'을 사용합니다.
- 매칭된 라우트에 이미 stale 상태인 성공 데이터가 있을 때의 동작을 제어합니다. stale-while-revalidate에는
⚠️ 중요한 기본값
- 기본적으로
staleTime은0으로 설정되므로 재사용 가능한 성공 데이터가 즉시 stale 상태로 간주됩니다. 동일한 로더 키에 다시 진입하거나router.load()를 명시적으로 호출하면 기본적으로 stale 데이터를 백그라운드에서 다시 검증합니다. 다른 로더 키는 별도의 캐시 항목을 나타내며 재사용할 데이터가 없으면 로드해야 합니다. - 기본적으로 프리로드로 생성된 로더 데이터는 30초 동안 최신 상태로 간주합니다. 모든 프리로드와 이동은 자체
beforeLoad체인을 실행하지만, 이후 프리로드와 최초 이동은 해당 기간 동안 프리로드의 로더 데이터 또는 진행 중인 로더 작업을 재사용할 수 있습니다. 이동이 해당 로더 세대를 수락한 후에는 후속 최신 상태 확인에 표준staleTime을 사용합니다. - 기본적으로
gcTime과preloadGcTime은 5분의 보존 기간을 정의합니다. 사용되지 않은 데이터가 적용 가능한 기간보다 오래되면 이후 캐시 조정 중 정리 대상이 됩니다. 두 기간은 독립적으로 구성할 수 있습니다. - 기본적으로
staleReloadMode는'background'이므로 stale 상태인 성공 매치는 로더가 백그라운드에서 다시 검증되는 동안 기존loaderData로 계속 렌더링합니다. router.invalidate()는 일치하는 커밋된 로더 세대, 캐시된 로더 세대 및 진행 중인 로더 세대를 무효화 대상으로 선택하고 일치하는 활성 프리로드 레인을 종료합니다. 현재 활성 라우트는 일반 로딩 프로토콜을 통해 다시 로드하고, 캐시된 비활성 데이터는 stale 상태로 유지되었다가 재사용될 때 다시 로드합니다.sync: true를 요청하지 않으면 기본적으로 stale 상태인 성공 로더 데이터를 백그라운드에서 다시 검증합니다.
loaderDeps로 검색 매개변수에 접근
/posts 라우트가 offset과 limit 검색 매개변수로 페이지 매김을 지원한다고 가정해 보겠습니다. 캐시가 이 데이터를 고유하게 저장하려면 loaderDeps 함수를 통해 이러한 검색 매개변수에 접근해야 합니다. 이를 명시적으로 식별하면 offset과 limit이 서로 다른 /posts의 각 라우트 매치가 뒤섞이지 않습니다.
이러한 종속성을 설정하면 종속성이 변경될 때마다 라우트를 다시 로드합니다.
loaderDeps는 라우트 계획 중 캐시 키를 정의합니다. 동일한 검증된
검색 입력에 대해 이동하거나 상태를 변경하지 않고 동일한 값을 반환해야 합니다.
반환값과 toJSON과 같은 사용자 지정 직렬화 메서드도 부작용이 없어야 합니다.
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
loader: ({ deps: { offset, limit } }) =>
fetchPosts({
offset,
limit,
}),
})
[!WARNING] 로더에서 실제로 사용하는 종속성만 포함합니다.
흔히 하는 실수는 전체
search객체를 반환하는 것입니다.// ❌ Don't do this - causes unnecessary cache invalidation
loaderDeps: ({ search }) => search,
loader: ({ deps }) => fetchPosts({ page: deps.page }), // only uses page!이렇게 하면 로더에서 사용하지 않는 매개변수(
viewMode나sortDirection등)를 포함해 ANY 검색 매개변수가 변경될 때마다 라우트를 다시 로드합니다. 대신 필요한 항목만 추출합니다.// ✅ Do this - only reload when used params change
loaderDeps: ({ search }) => ({
page: search.page,
limit: search.limit,
}),
loader: ({ deps }) => fetchPosts(deps),
staleTime으로 데이터의 최신 상태 유지 시간 제어
기본적으로 수락된 이동 데이터의 staleTime은 0ms이고
preloadStaleTime은 30초입니다. 따라서 성공한 프리로드는 해당 기간 동안
첫 이동에 로더 데이터를 제공할 수 있습니다. 이동이 해당 로더 세대를
수락하면 일반 이동 최신 상태가 적용됩니다. 기본 staleTime에서는 동일한
로더 키를 나중에 사용할 때 데이터가 즉시 stale 상태가 되고 캐시된 데이터가
표시된 채 백그라운드에서 다시 검증됩니다. 다른 로더 키는 별도의 캐시 항목을 나타냅니다.
이는 대부분의 사용 사례에 적합한 기본값이지만, 일부 라우트 데이터는 더 정적이거나 로드 비용이 클 수 있습니다. 이 경우 staleTime 옵션으로 이동 시 라우트 데이터를 최신 상태로 간주할 시간을 제어할 수 있습니다. 예제를 살펴보겠습니다.
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
// Consider the route's data fresh for 10 seconds
staleTime: 10_000,
})
staleTime 옵션에 10_000을 전달하면 라우터가 라우트 데이터를 10초 동안 최신 상태로 간주합니다. 즉, 사용자가 마지막 로더 결과 후 10초 이내에 /about에서 /posts로 이동하면 라우트 데이터를 다시 로드하지 않습니다. 이후 10초가 지나 /about에서 /posts로 이동하면 라우트 데이터를 백그라운드에서 다시 로드합니다.
백그라운드 stale 다시 로드와 차단 방식 중 선택
기본적으로 stale 상태인 성공 매치는 stale-while-revalidate 동작을 사용합니다. 즉, 라우터는 기존 loaderData로 즉시 렌더링한 다음 백그라운드에서 데이터를 새로 고칠 수 있습니다.
특정 로더가 계속 진행하기 전에 stale 다시 로드가 완료되기를 기다리게 하려면 객체 형식을 사용하고 staleReloadMode: 'blocking'을 설정합니다.
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: {
handler: () => fetchPosts(),
staleReloadMode: 'blocking',
},
})
전체 라우터의 기본값도 변경할 수 있습니다.
const router = createRouter({
routeTree,
defaultStaleReloadMode: 'blocking',
})
다시 검증하는 동안 stale 데이터를 표시해도 괜찮다면 'background'를 사용합니다. stale 매치가 새로 로드하는 것처럼 동작하며 새 로더 결과를 기다리게 하려면 'blocking'을 사용합니다.
자동 stale 다시 로드 끄기
라우트의 자동 stale 다시 로드를 비활성화하려면 staleTime 옵션을 Infinity로 설정합니다.
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
staleTime: Infinity,
})
라우터에서 defaultStaleTime 옵션을 설정해 모든 라우트에 대해 이 기능을 끌 수도 있습니다.
const router = createRouter({
routeTree,
defaultStaleTime: Infinity,
})
이는 staleReloadMode: 'blocking'과 다릅니다.
staleTime: Infinity는 처음부터 라우트가 stale 상태가 되지 않도록 합니다.staleReloadMode: 'blocking'은 stale 다시 로드를 허용하지만 백그라운드에서 실행하는 대신 완료될 때까지 기다립니다.
shouldReload와 gcTime으로 캐싱 사용하지 않기
Remix의 기본 기능과 마찬가지로 라우트에 진입할 때나 중요한 로더 종속성이 변경될 때만 로드하도록 구성할 수 있습니다. gcTime 옵션을 shouldReload 옵션과 함께 사용하면 됩니다. shouldReload는 boolean 또는 동일한 beforeLoad 및 loaderContext 매개변수를 받고 라우트를 다시 로드해야 하는지를 나타내는 불리언을 반환하는 함수를 받습니다.
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
loader: ({ deps }) => fetchPosts(deps),
// Do not cache this route's data after it's unloaded
gcTime: 0,
// Only reload the route when the user navigates to it or when deps change
shouldReload: false,
})
프리로드는 유지하면서 캐싱 사용하지 않기
일반 라우트 데이터 보존을 사용하지 않도록 선택하더라도 프리로드의 이점은
계속 누릴 수 있습니다. 프리로드된 결과는 보존에 preloadGcTime을 사용하고
최신 상태에 preloadStaleTime을 사용하므로, 기본 설정에서는 최근 프리로드를
메모리에 유지하고 첫 이동에서 다른 로더 호출 없이 이를 재사용합니다.
자동 링크 프리로드를 제어하려면 routerOptions.defaultPreload을 사용합니다. routeOptions.preload을
false로 설정하면 더 제한적으로 동작합니다. 추정 레인은 해당 라우트의
beforeLoad를 계속 실행하지만 loader는 건너뜁니다. 이동에서는 둘 다 정상적으로 실행합니다.
모든 로더 이벤트를 외부 캐시에 전달
이 사용 사례는 외부 데이터 로딩 페이지에서 자세히 다루지만, TanStack Query와 같은 외부 캐시를 사용하려면 모든 로더 이벤트를 외부 캐시에 전달하면 됩니다. 기본값을 사용하는 경우 변경할 사항은 라우터의 defaultPreloadStaleTime 옵션을 0으로 설정하는 것뿐입니다.
const router = createRouter({
routeTree,
defaultPreloadStaleTime: 0,
})
이렇게 하면 Router에서 확정된 프리로드 데이터가 즉시 stale 상태가 되어
외부 캐시가 가져올지 결정할 수 있습니다. 보존은 계속
preloadGcTime을 따르며, 겹치는 프리로드 또는 이동 소비자는 진행 중인
로더 작업을 계속 공유할 수 있습니다. 라우트의 shouldReload 옵션으로
로더 호출을 억제할 수도 있습니다.
Router 컨텍스트 사용
loader 함수에 전달되는 context 인수는 다음을 병합한 객체입니다.
- 부모 라우트 컨텍스트
beforeLoad옵션으로 제공한 이 라우트의 컨텍스트
라우터의 가장 상위에서 context 옵션을 통해 라우터에 초기 컨텍스트를 전달할 수 있습니다. 이 컨텍스트는 라우터의 모든 라우트에서 사용할 수 있으며, 각 라우트가 매칭될 때 복사되고 확장됩니다. 이는 beforeLoad 옵션을 통해 라우트에 컨텍스트를 전달하는 방식으로 동작합니다. 이 컨텍스트는 해당 라우트의 모든 자식 라우트에서 사용할 수 있습니다. 그 결과로 만들어진 컨텍스트는 라우트의 loader 함수에서 사용할 수 있습니다.
이 예제에서는 라우트 컨텍스트에 게시물을 가져오는 함수를 만들고 loader 함수에서 사용합니다.
🧠 컨텍스트는 종속성 주입을 위한 강력한 도구입니다. 이를 사용해 라우터와 라우트에 서비스, 훅 및 기타 객체를 주입할 수 있습니다. 라우트의
beforeLoad옵션을 사용해 각 라우트에서 라우트 트리 아래로 데이터를 추가 전달할 수도 있습니다.
/utils/fetchPosts.tsx
export const fetchPosts = async () => {
const res = await fetch(`/api/posts?page=${pageIndex}`)
if (!res.ok) throw new Error('Failed to fetch posts')
return res.json()
}
/routes/__root.tsx
React
import { createRootRouteWithContext } from '@tanstack/react-router'
// Create a root route using the createRootRouteWithContext<{...}>() function and pass it whatever types you would like to be available in your router context.
export const Route = createRootRouteWithContext<{
fetchPosts: typeof fetchPosts
}>()() // NOTE: the double call is on purpose, since createRootRouteWithContext is a factory ;)
Solid
import { createRootRouteWithContext } from '@tanstack/solid-router'
// Create a root route using the createRootRouteWithContext<{...}>() function and pass it whatever types you would like to be available in your router context.
export const Route = createRootRouteWithContext<{
fetchPosts: typeof fetchPosts
}>()() // NOTE: the double call is on purpose, since createRootRouteWithContext is a factory ;)
/routes/posts.tsx
// Notice how our postsRoute references context to get our fetchPosts function
// This can be a powerful tool for dependency injection across your router
// and routes.
export const Route = createFileRoute('/posts')({
loader: ({ context: { fetchPosts } }) => fetchPosts(),
})
/router.tsx
import { routeTree } from './routeTree.gen'
// Use your routerContext to create a new router
// This will require that you fullfil the type requirements of the routerContext
const router = createRouter({
routeTree,
context: {
// Supply the fetchPosts function to the router context
fetchPosts,
},
})
경로 매개변수 사용
loader 함수에서 경로 매개변수를 사용하려면 함수 매개변수의 params 속성으로 접근합니다. 예제는 다음과 같습니다.
// src/routes/posts.$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
loader: ({ params: { postId } }) => fetchPostById(postId),
})
라우트 컨텍스트 사용
라우터에 전역 컨텍스트를 전달하는 것도 좋지만, 라우트별 컨텍스트를 제공하려면 어떻게 해야 할까요? 이때 beforeLoad 옵션을 사용합니다. beforeLoad 옵션은 라우트 로드를 시도하기 직전에 실행되고 loader와 동일한 매개변수를 받는 함수입니다. 잠재적 매치 리디렉션, 로더 요청 차단 등을 할 수 있을 뿐 아니라 라우트의 컨텍스트에 병합할 객체를 반환할 수도 있습니다. beforeLoad 옵션을 통해 라우트 컨텍스트에 데이터를 주입하는 예제를 살펴보겠습니다.
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
// Pass the fetchPosts function to the route context
beforeLoad: () => ({
fetchPosts: () => console.info('foo'),
}),
loader: ({ context: { fetchPosts } }) => {
fetchPosts() // 'foo'
// ...
},
})
로더에서 검색 매개변수 사용
❓ 잠깐만요, Tanner... 검색 매개변수는 대체 어디에 있나요?
loader 함수의 매개변수에서 search를 직접 사용할 수 없는 이유가 궁금할 수 있습니다. 이는 개발을 돕기 위해 의도적으로 이렇게 설계했습니다. 이유를 살펴보겠습니다.
- 로더 함수에서 검색 매개변수를 사용한다는 것은 해당 검색 매개변수도 로드할 데이터를 고유하게 식별하는 데 사용해야 한다는 매우 좋은 지표입니다. 예를 들어 라우트 매치 내부의 데이터를 고유하게 식별하는
pageIndex와 같은 검색 매개변수를 사용하는 라우트가 있을 수 있습니다. 또는userId검색 매개변수로 애플리케이션의 특정 사용자를 식별하는/users/user라우트를 생각해 보세요. URL을/users/user?userId=123과 같이 모델링할 수 있습니다. 이는user라우트가 특정 사용자를 식별하는 데 추가 도움이 필요하다는 뜻입니다. - 로더 함수에서 검색 매개변수에 직접 접근하면 현재 URL pathname과 검색 매개변수에 로드할 데이터가 고유하지 않아 캐싱과 프리로딩에 버그가 발생할 수 있습니다. 예를 들어
/posts라우트에 2페이지 결과를 프리로드하도록 요청할 수 있지만, 라우트 구성에서 페이지를 구분하지 않으면 백그라운드에서 프리로드하는 대신/posts또는?page=1화면에 2페이지 데이터를 가져와 저장하고 표시하게 됩니다. - 검색 매개변수와 로더 함수 사이에 경계를 두면 라우터가 종속성과 반응성을 이해할 수 있습니다.
// /routes/users.user.tsx
export const Route = createFileRoute('/users/user')({
validateSearch: (search) =>
search as {
userId: string
},
loaderDeps: ({ search: { userId } }) => ({
userId,
}),
loader: async ({ deps: { userId } }) => getUser(userId),
})
routeOptions.loaderDeps로 검색 매개변수에 접근
// /routes/posts.tsx
export const Route = createFileRoute('/posts')({
// Use zod to validate and parse the search params
validateSearch: z.object({
offset: z.number().int().nonnegative().catch(0),
}),
// Pass the offset to your loader deps via the loaderDeps function
loaderDeps: ({ search: { offset } }) => ({ offset }),
// Use the offset from context in the loader function
loader: async ({ deps: { offset } }) =>
fetchPosts({
offset,
}),
})
Abort Signal 사용
loader 함수의 abortController 속성은 해당 로더 호출을 위한 AbortController입니다. 프리로드와 이동은 진행 중인 호출을 공유할 수 있으므로 소비자가 작업을 필요로 하는 동안 신호가 활성 상태로 유지됩니다. 호출이 오래되고 남은 소비자가 없으면 신호를 취소합니다. fetch 호출과 함께 사용하는 예제는 다음과 같습니다.
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: ({ abortController }) =>
fetchPosts({
// Pass this to an underlying fetch call or anything that supports signals
signal: abortController.signal,
}),
})
preload 플래그 사용
loader 함수의 preload 속성은 라우트를 로드하는 대신 프리로드할 때 true인 불리언입니다. 일부 데이터 로딩 라이브러리는 표준 fetch와 다르게 프리로드를 처리할 수 있으므로 데이터 로딩 라이브러리에 preload를 전달하거나 적절한 데이터 로딩 로직을 실행하는 데 사용할 수 있습니다.
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: async ({ preload }) =>
fetchPosts({
maxAge: preload ? 10_000 : 0, // Preloads should hang around a bit longer
}),
})
느린 로더 처리
대부분의 라우트 로더는 짧은 시간 안에 데이터를 해결할 수 있으므로 플레이스홀더 스피너를 렌더링할 필요 없이, 다음 라우트가 완전히 준비되었을 때 렌더링하도록 suspense에 의존하는 것이 이상적입니다. 하지만 라우트 컴포넌트 렌더링에 필요한 중요 데이터가 느리다면 두 가지 옵션이 있습니다.
- 빠른 데이터와 느린 데이터를 별도의 promise로 나누고 빠른 데이터가 로드된 후까지 느린 데이터를
defer합니다(지연된 데이터 로딩 가이드 참고). - 모든 데이터가 준비될 때까지 낙관적 suspense 임계값 이후 대기 컴포넌트를 표시합니다(아래 참고).
대기 컴포넌트 표시
기본적으로 TanStack Router는 해결하는 데 1초 넘게 걸리는 로더에 대기 컴포넌트를 표시합니다. 이는 다음을 통해 구성할 수 있는 낙관적 임계값입니다.
routeOptions.pendingMs또는routerOptions.defaultPendingMs
대기 시간 임계값을 초과하면 라우터는 구성된 경우 라우트의 pendingComponent 옵션을 렌더링합니다.
대기 컴포넌트 깜박임 방지
대기 컴포넌트를 사용할 때 대기 시간 임계값에 도달한 직후 데이터가 해결되어 대기 컴포넌트가 갑자기 깜박이는 상황은 피하고 싶을 것입니다. 이를 방지하기 위해 TanStack Router는 기본적으로 대기 컴포넌트를 최소 500ms 동안 표시합니다. 이는 다음을 통해 구성할 수 있는 낙관적 임계값입니다.
routeOptions.pendingMinMs또는routerOptions.defaultPendingMinMs
오류 처리
TanStack Router는 라우트 로딩 생명 주기 중 발생하는 오류를 처리하는 몇 가지 방법을 제공합니다. 이를 살펴보겠습니다.
routeOptions.onError로 오류 처리
routeOptions.onError 옵션은 라우트 로딩 중 오류가 발생하면 호출하는 함수입니다.
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
onError: ({ error }) => {
// Log the error
console.error(error)
},
})
routeOptions.onCatch로 오류 처리
routeOptions.onCatch 옵션은 라우터의 CatchBoundary가 오류를 포착할 때마다 호출하는 함수입니다.
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
onCatch: ({ error, errorInfo }) => {
// Log the error
console.error(error)
},
})
routeOptions.errorComponent로 오류 처리
routeOptions.errorComponent 옵션은 라우트 로딩 또는 렌더링 생명 주기 중 오류가 발생할 때 렌더링하는 컴포넌트입니다. 다음 속성과 함께 렌더링합니다.
error- 발생한 오류입니다.reset- 내부CatchBoundary를 재설정하는 함수입니다.
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
errorComponent: ({ error }) => {
// Render an error message
return <div>{error.message}</div>
},
})
reset 함수로 사용자가 오류 경계의 일반 자식 요소를 다시 렌더링하도록 재시도할 수 있습니다.
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
errorComponent: ({ error, reset }) => {
return (
<div>
{error.message}
<button
onClick={() => {
// Reset the router error boundary
reset()
}}
>
retry
</button>
</div>
)
},
})
오류가 라우트 로드의 결과라면 대신 router.invalidate()를 호출해야 합니다. 이 함수가 라우터 다시 로드와 오류 경계 재설정을 모두 조정합니다.
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
errorComponent: ({ error, reset }) => {
const router = useRouter()
return (
<div>
{error.message}
<button
onClick={() => {
// Invalidate the route to reload the loader, which will also reset the error boundary
router.invalidate()
}}
>
retry
</button>
</div>
)
},
})
기본 ErrorComponent 사용
TanStack Router는 라우트 로딩 또는 렌더링 생명 주기 중 오류가 발생할 때 렌더링되는 기본 ErrorComponent를 제공합니다. 라우트의 오류 컴포넌트를 재정의하더라도 포착되지 않은 오류는 항상 기본 ErrorComponent로 렌더링하도록 대체하는 것이 좋습니다.
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
errorComponent: ({ error }) => {
if (error instanceof MyCustomError) {
// Render a custom error message
return <div>{error.message}</div>
}
// Fallback to the default ErrorComponent
return <ErrorComponent error={error} />
},
})