Nuxt 웹 앱에서 SWR(ISR) 사용하기
제가 대단한 것이 아니라 Nitro가 대단한 것입니다.
지금 당신이 보고 있는 이 블로그는 ISR을 사용하고 있다. ISR은 Incremental Static Regeneration, 라는 표현의 약자다. 아마도 예전엔 Nuxt에서 이 ISR이 SWR과 같은 의미였던 것 같다. 그럼 또 SWR은 뭘까? ISR은 Next.js가 제시한 렌더링 방식 중 하나를 일컫는 말이고, SWR은 Stale-While-Revalidate, 라는 의미로 웹 표준 용어이다. 같은 이름의 HTTP Header도 존재한다.
어쨌든 Nuxt에서 이 두 가지는 같은 의미로 사용된다. 사용 방법 예시 코드는 SWR을 기준으로 써보겠다.
// nuxt.config.ts
export default defineNuxtConfig({
// ... 기타 설정
routeRules: {
'/': { swr: 60 }, // 포스트 목록: 1분마다 갱신
'/post/**': { swr: 3600 }, // 개별 포스트: 1시간마다 갱신
}
})
자, 보기만 해도 굉장히 간단하지 않은가? nuxt.config.ts 파일에 routeRules라는 필드만 명시해주면 된다.
이렇게 설정을 바꾸면 아래 페이지 컴포넌트는 어떻게 렌더링이 될까?
<script setup lang="ts">
import type { PostListItem } from '~~/shared/types/post-list-item.types'
import type { CMSResponse } from '~~/shared/types/response.types'
import PostCard from '~/components/post-card.vue'
const config = useRuntimeConfig()
const { data } = await useFetch<CMSResponse<{ posts: PostListItem[] }>>(`${config.public.tyangeCmsApiBase}/posts`)
const postList = computed<PostListItem[]>(() => {
if (!data?.value) {
return []
}
return data.value.data.posts
})
async function handleNavigateToPost(postId: string) {
await navigateTo(`/post/${postId}`)
}
</script>
<template>
<div class="mb-12 flex w-full flex-col gap-5">
<PostCard v-for="item in postList" :key="item.post_id" :item="item" @click="handleNavigateToPost(item.post_id)" />
</div>
</template>
routeRules에 swr or isr을 명시하면 빌드 타임에 이 페이지 컴포넌트의 useFetch를 실행한다.
이 실행의 결과물로 얻어진 Post List를 Nuxt는, 정확히 말해 Nuxt 웹 앱을 서비스하는 Nitro 서버가 캐싱한다. 유저가 이 Post List 페이지에 들어왔을 때 정적인 HTML로 제공할 수 있도록 준비해놓는다는 표현이 더 맞을 것 같다. 결과적으로 유저가 Post List 페이지에 진입해서 얻는 것은 이 블로그가 배포되는 시점에 '이미 만들어진' Post List다.
중요한 건 Nitro 서버가 정적인 HTML을 생성해 가지고 있진 않는다는 것이다. 물론, 진짜 SSG, 그러니까 prerender 필드를 사용하면 얘기는 달라진다. 그때는 prerender 필드가 지정된 경로는 빌드 타임에 모두 정적인 'HTML 파일'로 만들어진다. swr or isr 을 사용할 때는 얘기가 다르다. 이 옵션은 유저가 실제 페이지에 대한 HTTP 요청을 보내기 전까지는 HTML을 생성하지 않는다. 유저가 실제로 페이지에 진입해 트래픽이 발생하면 그때 docuement 응답으로 '갓 생성한' HTML을 반환한다.
(발생한 트래픽에 대한 document 응답으로 HTML을 반환하는 건 SSG나 SWR(ISR)이 똑같다. 따라서, SEO에 있어서는 서로 다른 게 없다.)
또 중요한 건, 우리가 swr 을 사용한 페이지 컴포넌트(경로)는 Post List 페이지 컴포넌트가 가진 useFetch 를 렌더링 될 때마다 실행하지 않는다는 거다. 따라서 배포 이후 내가 새로운 Post를 써서 Post List 데이터가 업데이트 되어도 Post List는 변하지 않는다.
그럼 최신 Post List를 보여주려면 어떻게 해야할까? 여러가지 방법이 있지만, 가장 직관적인 방법은 다시 빌드를 해서 배포를 하는 것이다. 웹 훅 같은 걸 이용해서 새로운 Post를 생성할 때마다 다시 빌드, 배포하면 새 Post를 쓸 때마다 새로운 Post 리스트를 보여줄 수 있다.
내가 선택한 것은 웹 훅을 사용하는 방법이 아니라 swr 을 사용하는 거였다. 이 필드에 지정된 숫자, 그러니까 60이라면 배포 이후 60초가 지난 후 Nuxt가, 정확히는 Nuxt 웹 앱을 serving하는 Nitro 서버가 이 Post List 페이지가 'stale'한 상태라고 인식한다.
이 60초가 지난 상황이라도 useFetch 가 저절로 다시 실행되지는 않는다. 이 상황에서 유저가 Post List 페이지에 진입해 트래픽을 발생시키면 그때 다시 useFetch 가 실행된다. useFetch 만 실행되는 건 아니다. 이 Post List 페이지 컴포넌트 전체가 다시 렌더링 된다. Nitro 서버는 이 렌더링된 결과물을 다시 캐시하고 60초 동안(어쩌면 더 긴 시간 동안) 동일한 Post List를 보여주게 된다.
정말 놀라운 건, 이 모든 과정을 Nuxt 웹 앱이 실행되는 Nitro(Node.js) 서버가 알아서 해준다는 것이다. 개발자는 그저 routeRules 에 어떤 route를 렌더링 전략만 정해주면 된다. 심지어 build 스크립트도 수정할 필요가 없다.
제약이 있다면 이런 SWR 기능은 Nitro, 그러니까 Nuxt 웹 앱을 Node.js 서버 위에서 직접 구동할 때에만 사용할 수 있다. GitHub Pages, S3 + CloudFront, Firebase Hosting, Netlify 등 "파일만 업로드" 방식에서는 작동이 어렵다.
너무 간단해서 나는 이 모든 게 그저 흑마법처럼 느껴진다. 지금 swr 을 적용한 나의 블로그는 아주 빠르고 안정적으로 내 블로그 포스트를 보여주고 있다. 만족스럽다.