본문으로 건너뛰기

SSR

Vue Query는 서버에서 여러 쿼리를 프리페치한 다음 해당 쿼리를 queryClient로 _디하이드레이션_하는 기능을 지원합니다. 즉, 서버는 페이지 로드 시 즉시 사용할 수 있는 마크업을 미리 렌더링할 수 있으며, JS를 사용할 수 있게 되는 즉시 Vue Query가 라이브러리의 모든 기능을 사용하도록 해당 쿼리를 업그레이드하거나 _하이드레이션_할 수 있습니다. 여기에는 해당 쿼리가 서버에서 렌더링된 이후 stale 상태가 된 경우 클라이언트에서 다시 가져오는 작업도 포함됩니다.

Nuxt.js 사용하기

Nuxt 3

먼저 plugins 디렉터리에 다음 내용으로 vue-query.ts 파일을 생성합니다:

import type {
DehydratedState,
VueQueryPluginOptions,
} from '@tanstack/vue-query'
import {
VueQueryPlugin,
QueryClient,
hydrate,
dehydrate,
} from '@tanstack/vue-query'
// Nuxt 3 app aliases
import { defineNuxtPlugin, useState } from '#imports'

export default defineNuxtPlugin((nuxt) => {
const vueQueryState = useState<DehydratedState | null>('vue-query')

// Modify your Vue Query global settings here
const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 5000 } },
})
const options: VueQueryPluginOptions = { queryClient }

nuxt.vueApp.use(VueQueryPlugin, options)

if (import.meta.server) {
nuxt.hooks.hook('app:rendered', () => {
vueQueryState.value = dehydrate(queryClient)
})
}

if (import.meta.client) {
hydrate(queryClient, vueQueryState.value)
}
})

이제 페이지에서 onServerPrefetch를 사용해 일부 데이터를 프리페치할 준비가 되었습니다.

  • queryClient.query, queryClient.infiniteQuery 또는 suspense를 사용하여 필요한 모든 쿼리를 프리페치합니다
export default defineComponent({
setup() {
const queryClient = useQueryClient()
const { data } = useQuery({
queryKey: ['test'],
queryFn: fetcher,
})

onServerPrefetch(async () => {
await suspense()
})

return { data }
},
})

Nuxt 2

먼저 plugins 디렉터리에 다음 내용으로 vue-query.js 파일을 생성합니다:

import Vue from 'vue'
import { VueQueryPlugin, QueryClient, hydrate } from '@tanstack/vue-query'

export default (context) => {
// Modify your Vue Query global settings here
const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 5000 } },
})

if (process.server) {
context.ssrContext.VueQuery = queryClient
}

if (process.client) {
Vue.use(VueQueryPlugin, { queryClient })

if (context.nuxtState && context.nuxtState.vueQueryState) {
hydrate(queryClient, context.nuxtState.vueQueryState)
}
}
}

이 플러그인을 nuxt.config.js에 추가합니다

module.exports = {
...
plugins: ['~/plugins/vue-query.js'],
}

이제 페이지에서 onServerPrefetch를 사용해 일부 데이터를 프리페치할 준비가 되었습니다.

  • nuxt 컨텍스트를 가져오려면 useContext를 사용합니다
  • queryClient의 서버 측 인스턴스를 가져오려면 useQueryClient를 사용합니다
  • queryClient.query, queryClient.infiniteQuery 또는 suspense를 사용하여 필요한 모든 쿼리를 프리페치합니다
  • queryClientnuxtContext로 디하이드레이션합니다
// pages/todos.vue
<template>
<div>
<button @click="refetch">Refetch</button>
<p>{{ data }}</p>
</div>
</template>

<script lang="ts">
import {
defineComponent,
onServerPrefetch,
useContext,
} from '@nuxtjs/composition-api'
import { useQuery, useQueryClient, dehydrate } from '@tanstack/vue-query'

export default defineComponent({
setup() {
// Get QueryClient either from SSR context, or Vue context
const { ssrContext } = useContext()
// Make sure to provide `queryClient` as a second parameter to `useQuery` calls
const queryClient =
(ssrContext != null && ssrContext.VueQuery) || useQueryClient()

// This will be prefetched and sent from the server
const { data, refetch, suspense } = useQuery(
{
queryKey: ['todos'],
queryFn: getTodos,
},
queryClient,
)
// This won't be prefetched, it will start fetching on client side
const { data2 } = useQuery(
{
queryKey: ['todos2'],
queryFn: getTodos,
},
queryClient,
)

onServerPrefetch(async () => {
await suspense()
ssrContext.nuxt.vueQueryState = dehydrate(queryClient)
})

return {
refetch,
data,
}
},
})
</script>

보인 것처럼 일부 쿼리를 프리페치하고 다른 쿼리는 클라이언트에서 가져오도록 해도 괜찮습니다. 즉, 특정 쿼리에 queryClient.query 또는 suspense를 추가하거나 제거하여 서버가 어떤 콘텐츠를 렌더링할지 여부를 제어할 수 있습니다.

Vite SSR 사용하기

DOM에서 직렬화할 수 있도록 VueQuery 클라이언트 상태를 vite-ssr과 동기화합니다:

// main.js (entry point)
import App from './App.vue'
import viteSSR from 'vite-ssr/vue'
import {
QueryClient,
VueQueryPlugin,
hydrate,
dehydrate,
} from '@tanstack/vue-query'

export default viteSSR(App, { routes: [] }, ({ app, initialState }) => {
// -- This is Vite SSR main hook, which is called once per request

// Create a fresh VueQuery client
const queryClient = new QueryClient()

// Sync initialState with the client state
if (import.meta.env.SSR) {
// Indicate how to access and serialize VueQuery state during SSR
initialState.vueQueryState = { toJSON: () => dehydrate(queryClient) }
} else {
// Reuse the existing state in the browser
hydrate(queryClient, initialState.vueQueryState)
}

// Mount and provide the client to the app components
app.use(VueQueryPlugin, { queryClient })
})

그런 다음 Vue의 onServerPrefetch를 사용하여 원하는 컴포넌트에서 VueQuery를 호출합니다:

<!-- MyComponent.vue -->
<template>
<div>
<button @click="refetch">Refetch</button>
<p>{{ data }}</p>
</div>
</template>

<script setup>
import { useQuery } from '@tanstack/vue-query'
import { onServerPrefetch } from 'vue'

// This will be prefetched and sent from the server
const { refetch, data, suspense } = useQuery({
queryKey: ['todos'],
queryFn: getTodos,
})

onServerPrefetch(suspense)
</script>

팁, 요령 및 주의 사항

성공한 쿼리만 디하이드레이션에 포함됩니다.

오류가 있는 모든 쿼리는 디하이드레이션에서 자동으로 제외됩니다. 즉, 기본 동작은 이러한 쿼리가 서버에서 로드된 적이 없는 것처럼 처리하여 일반적으로 대신 로딩 상태를 표시하고 queryClient에서 쿼리를 재시도하는 것입니다. 이는 오류와 관계없이 발생합니다.

때로는 이 동작이 바람직하지 않을 수 있으며, 특정 오류나 쿼리에 대해서는 올바른 상태 코드가 포함된 오류 페이지를 대신 렌더링하고 싶을 수 있습니다. 그런 경우 queryClient.query를 사용하고 모든 오류를 포착하여 수동으로 처리하세요.

stale 상태는 서버에서 쿼리를 가져온 시점부터 측정됩니다

쿼리가 stale 상태로 간주되는지는 쿼리가 dataUpdatedAt된 시점에 따라 달라집니다. 여기서 주의할 점은 이 기능이 올바르게 작동하려면 서버의 시간이 정확해야 하지만, UTC 시간이 사용되므로 시간대는 영향을 주지 않는다는 것입니다.

staleTime의 기본값이 0이므로 기본적으로 페이지를 로드할 때 백그라운드에서 쿼리를 다시 가져옵니다. 특히 마크업을 캐시하지 않는 경우, 이러한 이중 가져오기를 방지하기 위해 더 높은 staleTime을 사용하는 것이 좋을 수 있습니다.

stale 상태인 쿼리를 이렇게 다시 가져오는 방식은 CDN에서 마크업을 캐시할 때 완벽하게 어울립니다! 서버에서 페이지를 다시 렌더링하지 않아도 되도록 페이지 자체의 캐시 시간을 적절히 길게 설정하면서, 사용자가 페이지를 방문하는 즉시 데이터가 백그라운드에서 다시 가져와지도록 쿼리의 staleTime은 더 짧게 구성할 수 있습니다. 페이지는 일주일 동안 캐시하되, 데이터가 하루보다 오래되었다면 페이지 로드 시 자동으로 다시 가져오도록 할 수도 있습니다.

서버의 높은 메모리 사용량

요청마다 QueryClient를 생성하는 경우, Vue Query는 이 클라이언트를 위한 격리된 캐시를 생성하며 이 캐시는 gcTime 기간 동안 메모리에 유지됩니다. 해당 기간에 요청 수가 많으면 서버의 메모리 사용량이 높아질 수 있습니다.

서버에서 gcTime의 기본값은 Infinity이며, 이는 수동 가비지 컬렉션을 비활성화하고 요청이 완료되면 자동으로 메모리를 정리합니다. Infinity가 아닌 gcTime을 명시적으로 설정하면 캐시를 조기에 정리할 책임이 있습니다.

더 이상 필요하지 않은 캐시를 비우고 메모리 사용량을 줄이려면 요청을 처리하고 디하이드레이션된 상태를 client로 보낸 후 queryClient.clear() 호출을 추가할 수 있습니다.

또는 더 작은 gcTime을 설정할 수 있습니다.