본문으로 건너뛰기

SSR

[!WARNING] 이러한 API를 Tanstack Start의 변경 사항과 분리하기 위해 최선을 다했지만 내부적으로 공유되는 구현이 있습니다. 따라서 변경될 수 있으며 Start가 안정화될 때까지 실험적 기능으로 간주해야 합니다.

서버 사이드 렌더링(Server Side Rendering, SSR)은 서버에서 컴포넌트를 렌더링하고 HTML 마크업을 클라이언트로 보내는 과정입니다. 그러면 클라이언트가 마크업을 완전히 상호 작용할 수 있는 컴포넌트로 하이드레이트합니다.

TanStack Router는 Next.js, Remix 또는 React Router DOM이 아닙니다. 라우트나 컴포넌트를 생성할 때 src/pages/, app/layout.tsx, _app/index.tsx, getServerSideProps, getStaticProps, Remix 스타일의 loader/action export 또는 react-router-dom이나 next/의 import를 사용하지 않습니다. TanStack은 createFileRoute와 함께 src/routes/를 사용하고, @tanstack/react-router에서 Link/useNavigate/redirect를 사용하며, (Start의 경우) @tanstack/<framework>-start에서 createServerFn을 사용합니다. 잘못된 프레임워크의 코드는 일반적으로 빌드에 실패하거나 런타임에 충돌하는 / 라우트를 생성합니다.

일반적으로 고려할 SSR 유형은 다음 두 가지입니다.

  • 스트리밍하지 않는 SSR
    • 전체 페이지를 서버에서 렌더링하고, 클라이언트에서 하이드레이트하는 데 필요한 직렬화된 애플리케이션 데이터와 함께 하나의 HTML 요청으로 클라이언트에 보냅니다.
  • 스트리밍 SSR
    • 페이지의 중요한 첫 페인트를 서버에서 렌더링하고, 클라이언트에서 하이드레이트하는 데 필요한 직렬화된 데이터와 함께 하나의 HTML 요청으로 클라이언트에 보냅니다.
    • 그런 다음 페이지의 나머지 부분을 서버에서 렌더링하는 대로 클라이언트에 스트리밍합니다.

이 가이드에서는 TanStack Router로 두 SSR 유형을 구현하는 방법을 설명합니다.

스트리밍하지 않는 SSR

스트리밍하지 않는 서버 사이드 렌더링은 전체 애플리케이션 페이지의 마크업을 서버에서 렌더링하고 완성된 HTML 마크업(및 데이터)을 클라이언트로 보내는 전통적인 방식입니다. 그러면 클라이언트가 마크업을 다시 완전히 상호 작용할 수 있는 애플리케이션으로 하이드레이트합니다.

TanStack Router로 스트리밍하지 않는 SSR을 구현하려면 다음 유틸리티가 필요합니다.

React

  • RouterClient from @tanstack/react-router
    • 예: <RouterClient router={router} />
    • 클라이언트 진입점에서 이 컴포넌트를 렌더링하면 애플리케이션이 렌더링되고 RouterWrap 컴포넌트 옵션도 자동으로 구현됩니다.
  • 그리고 다음 중 하나입니다.
    • defaultRenderHandler from @tanstack/react-router
      • 서버 진입점에서 애플리케이션을 렌더링하고 애플리케이션 수준의 하이드레이션/디하이드레이션을 자동으로 처리하며 RouterServer 컴포넌트도 자동으로 구현합니다. 또는:
    • renderRouterToString from @tanstack/react-router
      • RouterWrap 컴포넌트 옵션을 필요한 다른 provider와 함께 직접 지정할 수 있다는 점이 defaultRenderHandler와 다릅니다.
    • RouterServer from @tanstack/react-router
      • RouterWrap 컴포넌트 옵션을 구현합니다.

Solid

  • RouterClient from @tanstack/solid-router
    • 예: <RouterClient router={router} />
    • 클라이언트 진입점에서 이 컴포넌트를 렌더링하면 애플리케이션이 렌더링되고 RouterWrap 컴포넌트 옵션도 자동으로 구현됩니다.
  • 그리고 다음 중 하나입니다.
    • defaultRenderHandler from @tanstack/solid-router
      • 서버 진입점에서 애플리케이션을 렌더링하고 애플리케이션 수준의 하이드레이션/디하이드레이션을 자동으로 처리하며 RouterServer 컴포넌트도 자동으로 구현합니다. 또는:
    • renderRouterToString from @tanstack/solid-router
      • RouterWrap 컴포넌트 옵션을 필요한 다른 provider와 함께 직접 지정할 수 있다는 점이 defaultRenderHandler와 다릅니다.
    • RouterServer from @tanstack/solid-router
      • RouterWrap 컴포넌트 옵션을 구현합니다.

자동 서버 히스토리

클라이언트에서 Router는 기본적으로 createBrowserHistory 인스턴스를 사용하며, 이는 클라이언트에서 사용하기에 권장되는 히스토리 타입입니다. 그러나 서버에서는 대신 createMemoryHistory 인스턴스를 사용해야 합니다. createBrowserHistory는 서버에 존재하지 않는 window 객체를 사용하기 때문입니다. 이 처리는 RouterServer 컴포넌트에서 자동으로 수행됩니다.

자동 로더 디하이드레이션/하이드레이션

라우트가 가져온 확인된 로더 데이터는 이 가이드에 설명된 표준 SSR 단계를 완료하면 TanStack Router가 자동으로 디하이드레이트하고 다시 하이드레이트합니다.

⚠️ 지연 데이터 스트리밍을 사용하는 경우 이 가이드의 끝부분에 있는 SSR 스트리밍 및 스트림 변환 패턴도 구현했는지 확인해야 합니다.

데이터 로딩을 활용하는 방법은 데이터 로딩 가이드를 참고합니다.

Router 생성

라우터는 서버와 클라이언트 모두에 존재하므로 두 환경에서 일관된 방식으로 생성하는 것이 중요합니다. 가장 쉬운 방법은 서버 및 클라이언트 진입점 파일 모두에서 import하고 호출할 수 있도록 공유 파일에 createRouter 함수를 노출하는 것입니다.

React

src/router.tsx
import { createRouter as createTanstackRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export function createRouter() {
return createTanstackRouter({ routeTree })
}

declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof createRouter>
}
}

Solid

src/router.tsx
import { createRouter as createTanstackRouter } from '@tanstack/solid-router'
import { routeTree } from './routeTree.gen'

export function createRouter() {
return createTanstackRouter({ routeTree })
}

declare module '@tanstack/solid-router' {
interface Register {
router: ReturnType<typeof createRouter>
}
}

서버에서 애플리케이션 렌더링

현재 URL에 필요한 모든 중요 데이터를 로드한 라우터 인스턴스가 있으므로 서버에서 애플리케이션을 렌더링할 수 있습니다.

defaultRenderHandler 사용

React

src/entry-server.tsx
import {
createRequestHandler,
defaultRenderHandler,
} from '@tanstack/react-router/ssr/server'
import { createRouter } from './router'

export async function render({ request }: { request: Request }) {
const handler = createRequestHandler({ request, createRouter })

return await handler(defaultRenderHandler)
}

Solid

src/entry-server.tsx
import {
createRequestHandler,
defaultRenderHandler,
} from '@tanstack/solid-router/ssr/server'
import { createRouter } from './router'

export async function render({ request }: { request: Request }) {
const handler = createRequestHandler({ request, createRouter })

return await handler(defaultRenderHandler)
}

renderRouterToString 사용

React

src/entry-server.tsx
import {
createRequestHandler,
renderRouterToString,
RouterServer,
} from '@tanstack/react-router/ssr/server'
import { createRouter } from './router'

export function render({ request }: { request: Request }) {
const handler = createRequestHandler({ request, createRouter })

return handler(({ request, responseHeaders, router }) =>
renderRouterToString({
request,
responseHeaders,
router,
children: <RouterServer router={router} />,
}),
)
}

Solid

src/entry-server.tsx
import {
createRequestHandler,
renderRouterToString,
RouterServer,
} from '@tanstack/react-router/ssr/server'
import { createRouter } from './router'

export function render({ request }: { request: Request }) {
const handler = createRequestHandler({ request, createRouter })

return handler(({ request, responseHeaders, router }) =>
renderRouterToString({
request,
responseHeaders,
router,
children: <RouterServer router={router} />,
}),
)
}

참고: createRequestHandler 메서드는 웹 API 표준 Request 객체가 필요하고, handler 메서드는 웹 API 표준 Response 프로미스를 반환합니다.

Express처럼 자체 Request 및 Response 객체를 사용하는 서버 프레임워크를 사용한다면 한 객체를 다른 객체로 변환해야 합니다. 이러한 구현의 형태는 예제를 참고합니다.

클라이언트에서 애플리케이션 렌더링

클라이언트에서는 훨씬 간단합니다.

  • 라우터 인스턴스를 만듭니다.
  • <RouterClient /> 컴포넌트를 사용해 애플리케이션을 렌더링합니다.

React

src/entry-client.tsx
import { hydrateRoot } from 'react-dom/client'
import { RouterClient } from '@tanstack/react-router/ssr/client'
import { createRouter } from './router'

const router = createRouter()

hydrateRoot(document, <RouterClient router={router} />)

Solid

src/entry-client.tsx
import { hydrate } from 'solid-js/web'
import { RouterClient } from '@tanstack/solid-router/ssr/client'
import { createRouter } from './router'

const router = createRouter()

hydrate(() => <RouterClient router={router} />, document.body)

이렇게 설정하면 애플리케이션이 서버에서 렌더링된 후 클라이언트에서 하이드레이트됩니다.

스트리밍 SSR

스트리밍 SSR은 가장 현대적인 SSR 유형으로, 서버에서 렌더링하는 대로 HTML 마크업을 클라이언트에 지속적이고 점진적으로 보내는 과정입니다. 중요한 첫 페인트를 디하이드레이트하고 다시 하이드레이트할 수 있을 뿐 아니라, 우선순위가 낮거나 응답 시간이 느린 마크업과 데이터를 초기 렌더링 후 같은 요청에서 클라이언트로 스트리밍할 수 있다는 점에서 기존 SSR과 개념적으로 약간 다릅니다.

이 패턴은 데이터를 느리게 가져오거나 높은 지연 시간이 필요한 페이지에 유용할 수 있습니다. 예를 들어 서드 파티 API에서 데이터를 가져와야 하는 페이지가 있다면 중요한 초기 마크업과 데이터를 클라이언트로 스트리밍한 다음, 확인되는 대로 덜 중요한 서드 파티 데이터를 클라이언트로 스트리밍할 수 있습니다.

[!NOTE] defaultStreamHandler 또는 renderRouterToStream 중 하나를 사용하는 한 이 스트리밍 패턴은 모두 자동으로 처리됩니다.

defaultStreamHandler 사용

React

src/entry-server.tsx
import {
createRequestHandler,
defaultStreamHandler,
} from '@tanstack/react-router/ssr/server'
import { createRouter } from './router'

export async function render({ request }: { request: Request }) {
const handler = createRequestHandler({ request, createRouter })

return await handler(defaultStreamHandler)
}

Solid

src/entry-server.tsx
import {
createRequestHandler,
defaultStreamHandler,
} from '@tanstack/solid-router/ssr/server'
import { createRouter } from './router'

export async function render({ request }: { request: Request }) {
const handler = createRequestHandler({ request, createRouter })

return await handler(defaultStreamHandler)
}

renderRouterToStream 사용

React

src/entry-server.tsx
import {
createRequestHandler,
renderRouterToStream,
RouterServer,
} from '@tanstack/react-router/ssr/server'
import { createRouter } from './router'

export function render({ request }: { request: Request }) {
const handler = createRequestHandler({ request, createRouter })

return handler(({ request, responseHeaders, router }) =>
renderRouterToStream({
request,
responseHeaders,
router,
children: <RouterServer router={router} />,
}),
)
}

Solid

src/entry-server.tsx
import {
createRequestHandler,
renderRouterToStream,
RouterServer,
} from '@tanstack/solid-router/ssr/server'
import { createRouter } from './router'

export function render({ request }: { request: Request }) {
const handler = createRequestHandler({ request, createRouter })

return handler(({ request, responseHeaders, router }) =>
renderRouterToStream({
request,
responseHeaders,
router,
children: <RouterServer router={router} />,
}),
)
}

스트리밍 디하이드레이션/하이드레이션

스트리밍 디하이드레이션/하이드레이션은 마크업을 넘어 서버에서 클라이언트로 모든 지원 데이터를 디하이드레이트하고 스트리밍한 후 도착 시 다시 하이드레이트할 수 있게 하는 고급 패턴입니다. 서버에서 초기 마크업을 렌더링하는 데 사용한 기반 데이터를 애플리케이션에서 추가로 사용하거나 관리해야 할 때 유용합니다.

데이터 직렬화

SSR을 사용할 때 서버와 클라이언트 사이에 전달하는 데이터는 네트워크 경계를 넘어 전송하기 전에 직렬화해야 합니다. TanStack Router는 JSON.stringify/JSON.parse를 넘어 일반적인 데이터 타입을 지원하는 매우 가벼운 직렬화기를 사용해 이 직렬화를 처리합니다.

기본적으로 다음 타입을 지원합니다.

  • undefined
  • Date
  • Error
  • FormData

기본적으로 지원해야 할 다른 타입이 있다고 생각하면 TanStack Router 저장소에 issue를 등록합니다.

Map, Set, BigInt 같은 더 복잡한 데이터 타입을 사용하는 경우 타입 정의가 정확하고 데이터가 올바르게 직렬화 및 역직렬화되도록 사용자 지정 직렬화기를 사용해야 할 수 있습니다. 현재 더 강력한 직렬화기와 애플리케이션에 맞게 직렬화기를 사용자 지정하는 방법을 모두 개발하고 있습니다. 도움을 주고 싶다면 issue를 등록합니다.