선택적 서버 측 렌더링(SSR)
선택적 SSR이란 무엇인가요?
TanStack Start에서는 기본적으로 초기 요청과 일치하는 라우트가 서버에서 렌더링됩니다. 즉, 서버에서 beforeLoad와 loader가 실행된 후 라우트 컴포넌트가 렌더링됩니다. 생성된 HTML은 클라이언트로 전송되며, 클라이언트는 마크업을 완전히 상호작용할 수 있는 애플리케이션으로 하이드레이션합니다.
그러나 다음과 같이 특정 라우트나 모든 라우트에서 SSR을 비활성화해야 하는 경우가 있습니다:
beforeLoad또는loader에 브라우저 전용 API(예:localStorage)가 필요한 경우입니다.- 라우트 컴포넌트가 브라우저 전용 API(예:
canvas)에 의존하는 경우입니다.
TanStack Start의 선택적 SSR 기능을 사용하면 다음을 구성할 수 있습니다:
- 서버에서
beforeLoad또는loader를 실행해야 하는 라우트입니다. - 서버에서 렌더링해야 하는 라우트 컴포넌트입니다.
SPA 모드와 어떻게 다른가요?
TanStack Start의 SPA 모드는 beforeLoad와 loader의 서버 측 실행뿐만 아니라 라우트 컴포넌트의 서버 측 렌더링도 완전히 비활성화합니다. 선택적 SSR을 사용하면 라우트별로 서버 측 처리를 정적 또는 동적으로 구성할 수 있습니다.
구성
ssr 속성을 사용하여 초기 서버 요청 중 라우트가 처리되는 방식을 제어할 수 있습니다. 이 속성을 설정하지 않으면 기본값은 true입니다. createStart의 defaultSsr 옵션을 사용하여 이 기본값을 변경할 수 있습니다:
// src/start.ts
import { createStart } from '@tanstack/solid-start'
export const startInstance = createStart(() => ({
// Disable SSR by default
defaultSsr: false,
}))
ssr: true
별도로 구성하지 않는 한 이것이 기본 동작입니다. 초기 요청 시 다음을 수행합니다:
- 서버에서
beforeLoad를 실행하고 생성된 컨텍스트를 클라이언트로 전송합니다. - 서버에서
loader를 실행하고 로더 데이터를 클라이언트로 전송합니다. - 서버에서 컴포넌트를 렌더링하고 HTML 마크업을 클라이언트로 전송합니다.
// src/routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
ssr: true,
beforeLoad: () => {
console.log('Executes on the server during the initial request')
console.log('Executes on the client for subsequent navigation')
},
loader: () => {
console.log('Executes on the server during the initial request')
console.log('Executes on the client for subsequent navigation')
},
component: () => <div>This component is rendered on the server</div>,
})
ssr: false
다음 서버 측 동작을 비활성화합니다:
- 라우트의
beforeLoad및loader실행입니다. - 라우트 컴포넌트의 렌더링입니다.
// src/routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
ssr: false,
beforeLoad: () => {
console.log('Executes on the client during hydration')
},
loader: () => {
console.log('Executes on the client during hydration')
},
component: () => <div>This component is rendered on the client</div>,
})
ssr: 'data-only'
이 하이브리드 옵션은 다음과 같이 동작합니다:
- 서버에서
beforeLoad을 실행하고 결과 컨텍스트를 클라이언트로 전송합니다. - 서버에서
loader를 실행하고 로더 데이터를 클라이언트로 전송합니다. - 라우트 컴포넌트의 서버 측 렌더링을 비활성화합니다.
// src/routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
ssr: 'data-only',
beforeLoad: () => {
console.log('Executes on the server during the initial request')
console.log('Executes on the client for subsequent navigation')
},
loader: () => {
console.log('Executes on the server during the initial request')
console.log('Executes on the client for subsequent navigation')
},
component: () => <div>This component is rendered on the client</div>,
})
함수형 형식
더 유연하게 제어하려면 ssr 속성의 함수형 형식을 사용하여 런타임에 라우트를 SSR할지 결정할 수 있습니다:
// src/routes/docs/$docType/$docId.tsx
export const Route = createFileRoute('/docs/$docType/$docId')({
validateSearch: z.object({ details: z.boolean().optional() }),
ssr: ({ params, search }) => {
if (params.status === 'success' && params.value.docType === 'sheet') {
return false
}
if (search.status === 'success' && search.value.details) {
return 'data-only'
}
},
beforeLoad: () => {
console.log('Executes on the server depending on the result of ssr()')
},
loader: () => {
console.log('Executes on the server depending on the result of ssr()')
},
component: () => <div>This component is rendered on the client</div>,
})
ssr 함수는 초기 요청 중 서버에서만 실행되며 클라이언트 번들에서는 제거됩니다.
search과 params는 검증 후 판별 유니온으로 전달됩니다:
params:
| { status: 'success'; value: Expand<ResolveAllParamsFromParent<TParentRoute, TParams>> }
| { status: 'error'; error: unknown }
search:
| { status: 'success'; value: Expand<ResolveFullSearchSchema<TParentRoute, TSearchValidator>> }
| { status: 'error'; error: unknown }
검증에 실패하면 status은 error이 되고 error에는 실패 세부 정보가 포함됩니다. 그렇지 않으면 status은 success이 되고 value에는 검증된 데이터가 포함됩니다.
상속
런타임에 자식 라우트는 부모의 Selective SSR 구성을 상속합니다. 그러나 상속된 값은 더 제한적인 값으로만 변경할 수 있습니다(즉, true에서 data-only 또는 false으로, 그리고 data-only에서 false으로 변경할 수 있습니다). 예를 들면 다음과 같습니다:
root { ssr: undefined }
posts { ssr: false }
$postId { ssr: true }
root의 기본값은ssr: true입니다.posts은ssr: false을 명시적으로 설정하므로beforeLoad과loader모두 서버에서 실행되지 않으며, 라우트 컴포넌트도 서버에서 렌더링되지 않습니다.$postId은ssr: true을 설정하지만 부모로부터ssr: false을 상속합니다. 상속된 값은 더 제한적인 값으로만 변경할 수 있으므로ssr: true은 효과가 없으며 상속된ssr: false이 유지됩니다.
또 다른 예는 다음과 같습니다:
root { ssr: undefined }
posts { ssr: 'data-only' }
$postId { ssr: true }
details { ssr: false }
root의 기본값은ssr: true입니다.posts은ssr: 'data-only'을 설정하므로beforeLoad과loader은 서버에서 실행되지만 라우트 컴포넌트는 서버에서 렌더링되지 않습니다.$postId은ssr: true을 설정하지만 부모로부터ssr: 'data-only'을 상속합니다.details은ssr: false을 설정하므로beforeLoad과loader모두 서버에서 실행되지 않으며, 라우트 컴포넌트도 서버에서 렌더링되지 않습니다. 여기서는 상속된 값이 더 제한적인 값으로 변경되므로ssr: false이 상속된 값을 재정의합니다.
폴백 렌더링
ssr: false 또는 ssr: 'data-only'이 설정된 첫 번째 라우트의 경우, 서버는 라우트의 pendingComponent을 폴백으로 렌더링합니다. pendingComponent이 구성되지 않았다면 defaultPendingComponent이 렌더링됩니다. 둘 다 구성되지 않았다면 폴백이 렌더링되지 않습니다.
클라이언트에서 하이드레이션하는 동안 라우트에 beforeLoad 또는 loader이 정의되어 있지 않더라도 이 폴백은 최소 minPendingMs 동안(구성되지 않았다면 defaultPendingMinMs 동안) 표시됩니다.
루트 라우트의 SSR을 비활성화하는 방법
루트 라우트 컴포넌트의 서버 측 렌더링을 비활성화할 수 있지만, <html> 셸은 여전히 서버에서 렌더링해야 합니다. 이 셸은 shellComponent 속성을 통해 구성하며 단일 속성 children을 받습니다. shellComponent은 항상 SSR되며 각각 루트 component, 루트 errorComponent 또는 루트 notFound 컴포넌트를 감쌉니다.
라우트 컴포넌트의 SSR이 비활성화된 루트 라우트의 최소 설정은 다음과 같습니다:
import * as Solid from 'solid-js'
import {
HeadContent,
Outlet,
Scripts,
createRootRoute,
} from '@tanstack/solid-router'
export const Route = createRootRoute({
shellComponent: RootShell,
component: RootComponent,
errorComponent: () => <div>Error</div>,
notFoundComponent: () => <div>Not found</div>,
ssr: false, // or `defaultSsr: false` on the router
})
function RootShell(props) {
return (
<HeadContent />
{props.children}
<Scripts />
)
}
function RootComponent() {
return (
<div>
<h1>This component will be rendered on the client</h1>
<Outlet />
</div>
)
}