본문으로 건너뛰기

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을 구현하려면 다음 유틸리티가 필요합니다.

<!-- ::start:framework -->

React

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

Solid

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

<!-- ::end:framework -->

자동 서버 히스토리​

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

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

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

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

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

Router 생성​

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

<!-- ::start:framework -->

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>
}
}

<!-- ::end:framework -->

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

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

defaultRenderHandler 사용

<!-- ::start:framework -->

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)
}

<!-- ::end:framework -->

renderRouterToString 사용

<!-- ::start:framework -->

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} />,
}),
)
}

<!-- ::end:framework -->

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

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

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

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

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

<!-- ::start:framework -->

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)

<!-- ::end:framework -->

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

스트리밍 SSR​

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

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

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

defaultStreamHandler 사용

<!-- ::start:framework -->

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)
}

<!-- ::end:framework -->

renderRouterToStream 사용

<!-- ::start:framework -->

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} />,
}),
)
}

<!-- ::end:framework -->

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

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

데이터 직렬화​

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

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

  • undefined
  • Date
  • Error
  • FormData

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

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