본문으로 건너뛰기

서버 렌더링 및 하이드레이션

Lit Query는 Lit SSR과 @tanstack/lit-query에서 다시 내보내는 TanStack Query Core 하이드레이션 API를 결합하여 서버 렌더링과 함께 사용할 수 있습니다.

이 가이드의 실행 가능한 소스는 SSR 예제입니다.

흐름

서버 렌더링은 세 단계로 구성됩니다:

  1. 요청별 QueryClient를 생성합니다.
  2. 서버에서 쿼리를 프리페치하고 해당 클라이언트로 Lit HTML을 렌더링합니다.
  3. 캐시를 HTML로 디하이드레이션한 다음, 클라이언트 앱이 렌더링되기 전에 브라우저 QueryClient를 하이드레이션합니다.

사용자 또는 요청 간에 하나의 서버 QueryClient를 절대 공유하지 마세요.

서버 프리페치 및 렌더링

import { render } from '@lit-labs/ssr'
import { collectResult } from '@lit-labs/ssr/lib/render-result.js'
import { html } from 'lit'
import { QueryClient, dehydrate, noop } from '@tanstack/lit-query'
import { createDataQueryOptions } from './api.js'
import './app.js'

async function renderPage() {
const apiBaseUrl = 'https://example.com'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
},
},
})

await queryClient.query(createDataQueryOptions(apiBaseUrl)).catch(noop)

const appHtml = await collectResult(
render(
html`<ssr-app
api-base-url=${apiBaseUrl}
.queryClient=${queryClient}
></ssr-app>`,
),
)

const dehydratedState = dehydrate(queryClient)

return { appHtml, dehydratedState }
}

서버는 property binding을 사용하여 동일한 client를 Lit 요소에 전달합니다. 이를 통해 createQueryController가 서버 렌더링 중 프리페치된 캐시를 읽을 수 있습니다. SSR 중에 쿼리 함수가 fetch를 호출한다면 브라우저 기준 상대 URL에 의존하지 말고 절대 API origin을 전달합니다.

클라이언트 하이드레이션

import '@lit-labs/ssr-client/lit-element-hydrate-support.js'
import { QueryClient, hydrate, type DehydratedState } from '@tanstack/lit-query'

const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
},
},
})

const dehydratedState = JSON.parse(
document.getElementById('__QUERY_STATE__')?.textContent ?? 'null',
) as DehydratedState

queryClient.mount()
hydrate(queryClient, dehydratedState)

const appElement = document.querySelector('ssr-app') as
| (HTMLElement & { queryClient?: QueryClient })
| null

if (!appElement) {
throw new Error('Expected the SSR app element to exist before hydration.')
}

appElement.queryClient = queryClient
await import('./app.js')

클라이언트를 수동으로 마운트했다면 페이지가 언로드될 때 언마운트합니다:

window.addEventListener(
'pagehide',
() => {
queryClient.unmount()
},
{ once: true },
)

컴포넌트 패턴

SSR 예시는 queryClient 속성을 사용할 수 있게 된 후에만 컨트롤러를 생성합니다:

import { LitElement } from 'lit'
import {
createQueryController,
type QueryClient,
type QueryResultAccessor,
} from '@tanstack/lit-query'
import { createDataQueryOptions, type DataResponse } from './api.js'

class SsrApp extends LitElement {
static properties = {
apiBaseUrl: { attribute: 'api-base-url' },
queryClient: { attribute: false },
}

apiBaseUrl = ''
queryClient?: QueryClient
private dataQuery?: QueryResultAccessor<DataResponse, Error>

protected override willUpdate(): void {
if (!this.dataQuery && this.queryClient) {
this.dataQuery = createQueryController(
this,
createDataQueryOptions(this.apiBaseUrl),
this.queryClient,
)
}
}
}

이 명시적 클라이언트 패턴은 연결된 DOM provider에서 클라이언트를 찾는 대신 renderer가 클라이언트를 생성하므로 SSR에 유용합니다.

직렬화

디하이드레이션된 상태를 JSON으로 HTML에 삽입하고, script 태그를 벗어날 수 있는 문자를 이스케이프합니다. 예제 서버는 빌드된 HTML 템플릿에서 __QUERY_STATE_JSON__을 교체하기 전에 작은 직렬화 도구를 사용합니다.

Lit Query는 TanStack Query Core의 dehydratehydrate를 다시 내보냅니다. 서버 프리페치 후 dehydrate(queryClient)를 사용하여 캐시 상태를 캡처합니다. 브라우저에서 해당 상태를 파싱하고, 새로운 QueryClient를 생성하고, hydrate(queryClient, dehydratedState)를 호출하고, 서버에서 렌더링된 요소에 client를 할당한 다음, 프리페치된 캐시를 사용할 수 있는 상태로 업그레이드되도록 그 이후에만 Lit component를 import합니다.