렌더링 모드

Nuxt에서 사용할 수 있는 다양한 렌더링 모드에 대해 알아보세요.

Nuxt는 서로 다른 렌더링 모드인 유니버설 렌더링, 클라이언트 사이드 렌더링을 지원할 뿐만 아니라, 하이브리드 렌더링과 애플리케이션을 CDN 엣지 서버에서 렌더링할 수 있는 기능도 제공합니다.

브라우저와 서버 모두 JavaScript 코드를 해석하여 Vue.js 컴포넌트를 HTML 요소로 변환할 수 있습니다. 이 단계를 렌더링이라고 합니다. Nuxt는 유니버설 렌더링과 클라이언트 사이드 렌더링을 모두 지원합니다. 두 접근 방식은 각각 장단점이 있으며, 이에 대해 살펴보겠습니다.

기본적으로 Nuxt는 더 나은 사용자 경험, 성능, 검색 엔진 인덱싱 최적화를 위해 유니버설 렌더링을 사용하지만, 한 줄의 설정으로 렌더링 모드를 전환할 수 있습니다.

Universal Rendering

이 단계는 PHP나 Ruby 애플리케이션이 수행하는 전통적인 서버 사이드 렌더링과 유사합니다. 브라우저가 유니버설 렌더링이 활성화된 URL을 요청하면, Nuxt는 서버 환경에서 JavaScript(Vu e.js) 코드를 실행하고 완전히 렌더링된 HTML 페이지를 브라우저에 반환합니다. 또한 Nuxt는 페이지가 미리 생성된 경우 캐시에서 완전히 렌더링된 HTML 페이지를 반환할 수도 있습니다. 사용자는 클라이언트 사이드 렌더링과 달리 애플리케이션의 초기 콘텐츠 전체를 즉시 받게 됩니다.

HTML 문서가 다운로드되면 브라우저가 이를 해석하고, Vue.js가 문서의 제어권을 가져옵니다. 서버에서 한 번 실행되었던 동일한 JavaScript 코드가 이제 백그라운드에서 클라이언트(브라우저)에서 다시 실행되며, HTML에 리스너를 바인딩하여 상호작용을 가능하게 합니다(그래서 유니버설 렌더링이라고 부릅니다). 이를 **하이드레이션(Hydration)**이라고 합니다. 하이드레이션이 완료되면 페이지는 동적인 인터페이스와 페이지 전환과 같은 이점을 누릴 수 있습니다.

유니버설 렌더링을 사용하면 Nuxt 애플리케이션은 클라이언트 사이드 렌더링의 장점을 유지하면서도 빠른 페이지 로드 시간을 제공할 수 있습니다. 또한 콘텐츠가 이미 HTML 문서에 포함되어 있기 때문에 크롤러가 추가 작업 없이 이를 인덱싱할 수 있습니다.

무엇이 서버에서 렌더링되고, 무엇이 클라이언트에서 렌더링될까요?

유니버설 렌더링 모드에서 Vue 파일의 어떤 부분이 서버와/또는 클라이언트에서 실행되는지 궁금해하는 것은 자연스러운 일입니다.

app/app.vue
<script setup lang="ts">
const counter = ref(0) // 서버와 클라이언트 환경 모두에서 실행됨

const handleClick = () => {
  counter.value++ // 클라이언트 환경에서만 실행됨
}
</script>

<template>
  <div>
    <p>Count: {{ counter }}</p>
    <button @click="handleClick">
      Increment
    </button>
  </div>
</template>

초기 요청 시, counter ref는 <p> 태그 안에서 렌더링되기 때문에 서버에서 초기화됩니다. 이때 handleClick의 내용은 전혀 실행되지 않습니다. 브라우저에서의 하이드레이션 동안 counter ref가 다시 초기화됩니다. 이후 handleClick이 버튼에 바인딩되며, 따라서 handleClick의 본문은 항상 브라우저 환경에서 실행된다고 추론하는 것이 합리적입니다.

미들웨어페이지는 서버에서 실행되며, 하이드레이션 동안 클라이언트에서도 실행됩니다. 플러그인은 서버, 클라이언트 또는 둘 다에서 렌더링될 수 있습니다. 컴포넌트는 클라이언트에서만 실행되도록 강제할 수도 있습니다. 컴포저블유틸리티는 사용되는 컨텍스트에 따라 렌더링됩니다.

서버 사이드 렌더링의 장점:

  • 성능: 브라우저는 JavaScript로 생성된 콘텐츠보다 정적 콘텐츠를 훨씬 빠르게 표시할 수 있기 때문에, 사용자는 페이지 콘텐츠에 즉시 접근할 수 있습니다. 동시에 Nuxt는 하이드레이션 과정 동안 웹 애플리케이션의 상호작용성을 유지합니다.
  • 검색 엔진 최적화(SEO): 유니버설 렌더링은 고전적인 서버 애플리케이션처럼 페이지의 전체 HTML 콘텐츠를 브라우저에 전달합니다. 웹 크롤러는 페이지의 콘텐츠를 직접 인덱싱할 수 있으므로, 빠르게 인덱싱되기를 원하는 콘텐츠에는 유니버설 렌더링이 훌륭한 선택입니다.

서버 사이드 렌더링의 단점:

  • 개발 제약: 서버와 브라우저 환경은 동일한 API를 제공하지 않으며, 양쪽에서 매끄럽게 실행될 수 있는 코드를 작성하는 것은 까다로울 수 있습니다. 다행히도 Nuxt는 코드가 어디에서 실행되는지 판단하는 데 도움이 되는 가이드라인과 특정 변수를 제공합니다.
  • 비용: 페이지를 실시간으로 렌더링하려면 서버가 항상 실행 중이어야 합니다. 이는 전통적인 서버와 마찬가지로 월별 비용을 발생시킵니다. 그러나 브라우저가 클라이언트 사이드 내비게이션을 담당하기 때문에 유니버설 렌더링 덕분에 서버 호출은 크게 줄어듭니다. 엣지 사이드 렌더링을 활용하면 비용을 줄일 수 있습니다.

유니버설 렌더링은 매우 다재다능하여 거의 모든 사용 사례에 적합하며, 특히 콘텐츠 중심 웹사이트에 잘 어울립니다: 블로그, 마케팅 웹사이트, 포트폴리오, 이커머스 사이트, 마켓플레이스 등.

하이드레이션 불일치 없이 Vue 코드를 작성하는 더 많은 예시는 Vue 문서를 참고하세요.
브라우저 API에 의존하고 사이드 이펙트를 가지는 라이브러리를 import할 때는, 이를 import하는 컴포넌트가 클라이언트 사이드에서만 호출되도록 해야 합니다. 번들러는 사이드 이펙트를 포함하는 모듈의 import를 트리 셰이킹하지 않습니다.

Client-Side Rendering

기본적으로 전통적인 Vue.js 애플리케이션은 브라우저(또는 클라이언트)에서 렌더링됩니다. 그런 다음 브라우저가 현재 인터페이스를 생성하기 위한 명령을 포함한 모든 JavaScript 코드를 다운로드하고 파싱한 후, Vue.js가 HTML 요소를 생성합니다.

클라이언트 사이드 렌더링의 장점:

  • 개발 속도: 전적으로 클라이언트 사이드에서 작업할 때는 window 객체와 같은 브라우저 전용 API를 사용하는 등, 코드의 서버 호환성에 대해 걱정할 필요가 없습니다.
  • 저렴함: 서버를 실행하면 JavaScript를 지원하는 플랫폼에서 실행해야 하므로 인프라 비용이 발생합니다. 반면 클라이언트 전용 애플리케이션은 HTML, CSS, JavaScript 파일만으로 어떤 정적 서버에서도 호스팅할 수 있습니다.
  • 오프라인: 코드가 전적으로 브라우저에서 실행되기 때문에, 인터넷이 끊겨도 잘 동작할 수 있습니다.

클라이언트 사이드 렌더링의 단점:

  • 성능: 사용자는 브라우저가 JavaScript 파일을 다운로드, 파싱, 실행할 때까지 기다려야 합니다. 다운로드는 네트워크 상태에, 파싱과 실행은 사용자의 기기에 따라 시간이 걸릴 수 있으며, 이는 사용자 경험에 영향을 줄 수 있습니다.
  • 검색 엔진 최적화(SEO): 클라이언트 사이드 렌더링을 통해 전달된 콘텐츠를 인덱싱하고 업데이트하는 데는 서버 렌더링된 HTML 문서보다 더 많은 시간이 걸립니다. 이는 앞서 언급한 성능 문제와 관련이 있는데, 검색 엔진 크롤러는 첫 시도에서 인터페이스가 완전히 렌더링될 때까지 기다리지 않기 때문입니다. 순수 클라이언트 사이드 렌더링을 사용할 경우, 검색 결과 페이지에 콘텐츠가 표시되고 업데이트되는 데 더 많은 시간이 걸립니다.

클라이언트 사이드 렌더링은 인덱싱이 필요 없거나 사용자가 자주 방문하는 고도로 인터랙티브한 웹 애플리케이션에 적합한 선택입니다. 브라우저 캐싱을 활용하여 이후 방문 시 다운로드 단계를 건너뛸 수 있으며, 예를 들어 SaaS, 백오피스 애플리케이션, 온라인 게임 등이 이에 해당합니다.

Nuxt에서 nuxt.config.ts에 다음과 같이 설정하여 클라이언트 사이드 전용 렌더링을 활성화할 수 있습니다:

nuxt.config.ts
export default defineNuxtConfig({
  ssr: false,
})
ssr: false를 사용하는 경우, 앱이 하이드레이션될 때까지 렌더링할 로딩 화면용 HTML을 ~/spa-loading-template.html에 배치해야 합니다.
Read more in SPA Loading Template.

Deploying a Static Client-Rendered App

nuxt generate 또는 nuxt build --prerender 명령으로 앱을 정적 호스팅에 배포하는 경우, 기본적으로 Nuxt는 각 페이지를 개별 정적 HTML 파일로 렌더링합니다.

nuxt generate 또는 nuxt build --prerender 명령으로 앱을 프리렌더링하면, 출력 폴더에 서버가 포함되지 않기 때문에 어떤 서버 엔드포인트도 사용할 수 없습니다. 서버 기능이 필요하다면 대신 nuxt build를 사용하세요.

순수 클라이언트 사이드 렌더링만 사용하는 경우라면, 이는 불필요할 수 있습니다. 모든 요청에 대해 정적 웹 호스트가 제공하도록 설정할 수 있는 단일 index.html 파일과 200.html, 404.html 폴백만 필요할 수도 있습니다.

이를 위해서는 라우트가 프리렌더링되는 방식을 변경하면 됩니다. nuxt.config.tshooks에 다음을 추가하세요:

nuxt.config.ts
export default defineNuxtConfig({
  hooks: {
    'prerender:routes' ({ routes }) {
      routes.clear() // 기본값을 제외한 어떤 라우트도 생성하지 않음
    },
  },
})

이렇게 하면 다음 세 개의 파일이 생성됩니다:

  • index.html
  • 200.html
  • 404.html

200.html404.html은 사용 중인 호스팅 제공업체에서 유용하게 사용될 수 있습니다.

Skipping Client Fallback Generation

클라이언트 렌더링 앱을 프리렌더링할 때, Nuxt는 기본적으로 index.html, 200.html, 404.html 파일을 생성합니다. 그러나 빌드에서 이 파일들 중 일부(또는 전부)가 생성되지 않도록 해야 하는 경우, Nitro'prerender:generate' 훅을 사용할 수 있습니다.

nuxt.config.ts
export default defineNuxtConfig({
  ssr: false,
  nitro: {
    hooks: {
      'prerender:generate' (route) {
        const routesToSkip = ['/index.html', '/200.html', '/404.html']
        if (routesToSkip.includes(route.route)) {
          route.skip = true
        }
      },
    },
  },
})

Hybrid Rendering

하이브리드 렌더링은 **Route Rules(라우트 규칙)**을 사용하여 라우트별로 서로 다른 캐싱 규칙을 적용하고, 특정 URL에 대한 새 요청에 서버가 어떻게 응답해야 할지 결정할 수 있게 해줍니다.

이전에는 Nuxt 애플리케이션과 서버의 모든 라우트/페이지가 유니버설 또는 클라이언트 사이드 중 하나의 동일한 렌더링 모드를 사용해야 했습니다. 하지만 여러 경우에 일부 페이지는 빌드 시점에 생성될 수 있는 반면, 다른 페이지는 클라이언트 사이드 렌더링이 필요할 수 있습니다. 예를 들어, 관리자 섹션이 있는 콘텐츠 웹사이트를 생각해 보세요. 모든 콘텐츠 페이지는 기본적으로 정적이며 한 번만 생성되면 되지만, 관리자 섹션은 로그인 기능이 필요하고 더 동적인 애플리케이션처럼 동작해야 합니다.

Nuxt는 라우트 규칙과 하이브리드 렌더링을 지원합니다. 라우트 규칙을 사용하면 특정 Nuxt 라우트 그룹에 대한 규칙을 정의하고, 렌더링 모드를 변경하거나 라우트에 따라 캐시 전략을 지정할 수 있습니다!

Nuxt 서버는 Nitro 캐싱 레이어를 사용하여 해당 미들웨어를 자동으로 등록하고, 라우트를 캐시 핸들러로 감쌉니다.

nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // 홈페이지는 빌드 시점에 프리렌더링
    '/': { prerender: true },
    // 상품 페이지는 요청 시 생성, 백그라운드에서 재검증, API 응답이 변경될 때까지 캐시
    '/products': { swr: true },
    // 개별 상품 페이지는 요청 시 생성, 백그라운드에서 재검증, 1시간(3600초) 동안 캐시
    '/products/**': { swr: 3600 },
    // 블로그 목록 페이지는 요청 시 생성, 백그라운드에서 재검증, CDN에서 1시간(3600초) 동안 캐시
    '/blog': { isr: 3600 },
    // 개별 블로그 글 페이지는 다음 배포까지 한 번만 요청 시 생성되어 CDN에 캐시
    '/blog/**': { isr: true },
    // 관리자 대시보드는 클라이언트 사이드에서만 렌더링
    '/admin/**': { ssr: false },
    // API 라우트에 cors 헤더 추가
    '/api/**': { cors: true },
    // 레거시 URL 리다이렉트
    '/old-page': { redirect: '/new-page' },
  },
})

Route Rules

사용할 수 있는 다양한 속성은 다음과 같습니다:

  • redirect: string - 서버 사이드 리다이렉트를 정의합니다.
  • ssr: boolean - 앱의 일부 섹션에 대해 HTML 서버 사이드 렌더링을 비활성화하고, ssr: false로 브라우저에서만 렌더링되도록 합니다.
  • cors: boolean - cors: true로 cors 헤더를 자동으로 추가합니다. headers로 출력 값을 커스터마이즈할 수 있습니다.
  • headers: object - 사이트의 특정 섹션(예: 에셋)에 특정 헤더를 추가합니다.
  • swr: number | boolean - 서버 응답에 캐시 헤더를 추가하고, 서버 또는 리버스 프록시에 구성 가능한 TTL(time to live) 동안 캐시합니다. Nitro의 node-server 프리셋은 전체 응답을 캐시할 수 있습니다. TTL이 만료되면, 캐시된 응답이 전송되는 동시에 백그라운드에서 페이지가 재생성됩니다. true를 사용하면 MaxAge 없이 stale-while-revalidate 헤더가 추가됩니다.
  • isr: number | boolean - 동작은 swr와 동일하지만, 이를 지원하는 플랫폼(현재 Netlify 또는 Vercel)에서는 응답을 CDN 캐시에 추가할 수 있습니다. true를 사용하면, 콘텐츠는 다음 배포까지 CDN 내부에 유지됩니다.
  • prerender: boolean - 빌드 시점에 라우트를 프리렌더링하고, 이를 정적 에셋으로 빌드에 포함합니다.
  • noScripts: boolean - 사이트의 일부 섹션에 대해 Nuxt 스크립트와 JS 리소스 힌트 렌더링을 비활성화합니다.
  • appMiddleware: string | string[] | Record<string, boolean> - 애플리케이션의 Vue 앱 부분(즉, Nitro 라우트가 아닌) 내에서 페이지 경로에 대해 어떤 미들웨어를 실행하거나 실행하지 않을지 정의할 수 있습니다.

가능한 경우, 라우트 규칙은 최적의 성능을 위해 배포 플랫폼의 네이티브 규칙(Netlify와 Vercel이 현재 지원됨)에 자동으로 매핑됩니다.

하이브리드 렌더링은 nuxt generate를 사용할 때는 사용할 수 없습니다.

예시:

Nuxt Vercel ISR

Vercel에 하이브리드 렌더링으로 배포된 Nuxt 애플리케이션 예시입니다.

Edge-Side Rendering

엣지 사이드 렌더링(ESR)은 Nuxt에 도입된 강력한 기능으로, 콘텐츠 전송 네트워크(CDN)의 엣지 서버를 통해 사용자와 더 가까운 곳에서 Nuxt 애플리케이션을 렌더링할 수 있게 해줍니다. ESR을 활용하면 성능과 지연 시간을 개선하여 향상된 사용자 경험을 제공할 수 있습니다.

ESR에서는 렌더링 프로세스가 네트워크의 '엣지', 즉 CDN의 엣지 서버로 이동합니다. ESR은 실제 렌더링 모드라기보다는 배포 대상에 더 가깝다는 점에 유의하세요.

페이지에 대한 요청이 발생하면, 원본 서버까지 이동하는 대신 가장 가까운 엣지 서버에서 요청을 가로챕니다. 이 서버가 페이지의 HTML을 생성하여 사용자에게 다시 전송합니다. 이 과정은 데이터가 이동해야 하는 물리적 거리를 최소화하여 지연 시간을 줄이고 페이지를 더 빠르게 로드합니다.

엣지 사이드 렌더링은 Nuxt를 구동하는 서버 엔진Nitro 덕분에 가능합니다. Nitro는 Node.js, Deno, Cloudflare Workers 등 다양한 플랫폼을 지원합니다.

현재 ESR을 활용할 수 있는 플랫폼은 다음과 같습니다:

  • nuxt build 명령과 Git 연동을 사용하여 제로 설정으로 사용할 수 있는 Cloudflare Pages
  • nuxt build 명령과 NITRO_PRESET=vercel-edge 환경 변수를 사용하는 Vercel Cloud
  • nuxt build 명령과 NITRO_PRESET=netlify-edge 환경 변수를 사용하는 Netlify Edge Functions

하이브리드 렌더링은 라우트 규칙과 함께 엣지 사이드 렌더링을 사용할 때도 활용할 수 있습니다.