본문으로 건너뛰기

선택적 서버 측 렌더링(SSR)

선택적 SSR이란 무엇인가요?

TanStack Start에서는 기본적으로 초기 요청과 일치하는 라우트가 서버에서 렌더링됩니다. 즉, beforeLoadloader이 서버에서 실행된 후 라우트 컴포넌트가 렌더링됩니다. 생성된 HTML은 클라이언트로 전송되며, 클라이언트는 마크업을 완전한 대화형 애플리케이션으로 하이드레이션합니다.

하지만 다음과 같이 특정 라우트 또는 모든 라우트에서 SSR을 비활성화하려는 경우가 있습니다:

  • beforeLoad 또는 loader에 브라우저 전용 API(예: localStorage)가 필요한 경우입니다.
  • 라우트 컴포넌트가 브라우저 전용 API(예: canvas)에 의존하는 경우입니다.

TanStack Start의 선택적 SSR 기능을 사용하면 다음을 구성할 수 있습니다:

  • 서버에서 beforeLoad 또는 loader을 실행할 라우트.
  • 서버에서 렌더링할 라우트 컴포넌트.

SPA 모드와 어떻게 비교되나요?

TanStack Start의 SPA 모드beforeLoadloader의 서버 측 실행과 라우트 컴포넌트의 서버 측 렌더링을 완전히 비활성화합니다. 선택적 SSR을 사용하면 라우트별로 서버 측 처리를 정적 또는 동적으로 구성할 수 있습니다.

구성

ssr 속성을 사용하여 초기 서버 요청 중 라우트가 처리되는 방식을 제어할 수 있습니다. 이 속성을 설정하지 않으면 기본값은 true입니다. createStartdefaultSsr 옵션을 사용하여 이 기본값을 변경할 수 있습니다:

// src/start.ts
import { createStart } from '@tanstack/react-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

다음 서버 측 동작을 비활성화합니다:

  • 라우트의 beforeLoadloader 실행.
  • 라우트 컴포넌트 렌더링.
// 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 함수는 초기 요청 중 서버에서만 실행되며 클라이언트 번들에서 제거됩니다.

searchparams은 검증 후 판별 유니온으로 전달됩니다:

params:
| { status: 'success'; value: Expand<ResolveAllParamsFromParent<TParentRoute, TParams>> }
| { status: 'error'; error: unknown }
search:
| { status: 'success'; value: Expand<ResolveFullSearchSchema<TParentRoute, TSearchValidator>> }
| { status: 'error'; error: unknown }

검증에 실패하면 statuserror이 되고 error에는 실패 세부 정보가 포함됩니다. 그렇지 않으면 statussuccess이 되고 value에는 검증된 데이터가 포함됩니다.

상속

런타임에 하위 라우트는 상위 라우트의 선택적 SSR 구성을 상속합니다. 그러나 상속된 값은 더 제한적인 값으로만 변경할 수 있습니다(즉, true에서 data-only 또는 false으로, 그리고 data-only에서 false으로 변경할 수 있습니다). 예를 들면 다음과 같습니다:

root { ssr: undefined }
posts { ssr: false }
$postId { ssr: true }
  • root의 기본값은 ssr: true입니다.
  • postsssr: false를 명시적으로 설정하므로, beforeLoadloader 모두 서버에서 실행되지 않으며 라우트 컴포넌트도 서버에서 렌더링되지 않습니다.
  • $postIdssr: true를 설정하지만, 부모로부터 ssr: false를 상속합니다. 상속된 값은 더 제한적인 방향으로만 변경할 수 있으므로, ssr: true는 효과가 없으며 상속된 ssr: false가 유지됩니다.

또 다른 예는 다음과 같습니다:

root { ssr: undefined }
posts { ssr: 'data-only' }
$postId { ssr: true }
details { ssr: false }
  • root의 기본값은 ssr: true입니다.
  • postsssr: 'data-only'를 설정하므로, beforeLoadloader는 서버에서 실행되지만 라우트 컴포넌트는 서버에서 렌더링되지 않습니다.
  • $postIdssr: true를 설정하지만, 부모로부터 ssr: 'data-only'를 상속합니다.
  • detailsssr: false를 설정하므로, beforeLoadloader 모두 서버에서 실행되지 않으며 라우트 컴포넌트도 서버에서 렌더링되지 않습니다. 여기서는 상속된 값이 더 제한적인 방향으로 변경되므로, 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 React from 'react'

import {
HeadContent,
Outlet,
Scripts,
createRootRoute,
} from '@tanstack/react-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({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
)
}

function RootComponent() {
return (
<div>
<h1>This component will be rendered on the client</h1>
<Outlet />
</div>
)
}