렌더링 모드
Nuxt는 다양한 렌더링 모드를 지원합니다. 유니버설 렌더링, 클라이언트 사이드 렌더링뿐만 아니라 하이브리드 렌더링과 애플리케이션을 CDN 엣지 서버에서 렌더링할 수 있는 기능도 제공합니다.
브라우저와 서버 모두 JavaScript 코드를 해석하여 Vue.js 컴포넌트를 HTML 요소로 변환할 수 있습니다. 이 과정을 렌더링이라고 합니다. Nuxt는 유니버설 렌더링과 클라이언트 사이드 렌더링을 모두 지원합니다. 두 가지 접근 방식에는 각각 장단점이 있으며, 이에 대해 다룰 예정입니다.
기본적으로 Nuxt는 유니버설 렌더링을 사용하여 더 나은 사용자 경험, 성능, 그리고 검색 엔진 인덱싱 최적화를 제공합니다. 하지만 한 줄의 설정으로 렌더링 모드를 변경할 수 있습니다.
유니버설 렌더링
이 단계는 PHP나 Ruby 애플리케이션에서 수행하는 전통적인 서버 사이드 렌더링과 유사합니다. 브라우저가 유니버설 렌더링이 활성화된 URL을 요청하면, Nuxt는 서버 환경에서 JavaScript(Vue.js) 코드를 실행하고 완전히 렌더링된 HTML 페이지를 브라우저에 반환합니다. 페이지가 미리 생성된 경우, Nuxt는 캐시에서 완전히 렌더링된 HTML 페이지를 반환할 수도 있습니다. 사용자는 클라이언트 사이드 렌더링과 달리 애플리케이션의 초기 콘텐츠 전체를 즉시 받을 수 있습니다.
HTML 문서가 다운로드되면, 브라우저가 이를 해석하고 Vue.js가 문서를 제어합니다. 한 번 서버에서 실행되었던 동일한 JavaScript 코드가 이제 클라이언트(브라우저)에서 다시 백그라운드로 실행되어(따라서 유니버설 렌더링) 리스너를 HTML에 바인딩함으로써 상호작용성을 가능하게 합니다. 이를 하이드레이션이라고 합니다. 하이드레이션이 완료되면, 페이지는 동적 인터페이스와 페이지 전환과 같은 이점을 누릴 수 있습니다.
유니버설 렌더링을 사용하면 Nuxt 애플리케이션이 클라이언트 사이드 렌더링의 이점을 유지하면서도 빠른 페이지 로드 속도를 제공할 수 있습니다. 또한, 콘텐츠가 이미 HTML 문서에 존재하므로 크롤러가 오버헤드 없이 인덱싱할 수 있습니다.
서버에서 렌더링되는 부분과 클라이언트에서 렌더링되는 부분은 무엇인가요?
유니버설 렌더링 모드에서 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는 하이드레이션 과정에서 웹 애플리케이션의 상호작용성을 유지합니다.
- 검색 엔진 최적화: 유니버설 렌더링은 페이지의 전체 HTML 콘텐츠를 브라우저에 전통적인 서버 애플리케이션처럼 전달합니다. 웹 크롤러는 페이지의 콘텐츠를 직접 인덱싱할 수 있으므로, 빠른 인덱싱이 필요한 콘텐츠에 유니버설 렌더링이 적합합니다.
서버 사이드 렌더링의 단점:
- 개발 제약: 서버와 브라우저 환경은 동일한 API를 제공하지 않으므로, 양쪽 모두에서 원활하게 실행될 수 있는 코드를 작성하는 것이 까다로울 수 있습니다. 다행히도 Nuxt는 코드가 어디서 실행되는지 판단할 수 있도록 가이드라인과 특정 변수를 제공합니다.
- 비용: 페이지를 실시간으로 렌더링하려면 서버가 실행 중이어야 하므로, 전통적인 서버처럼 월별 비용이 추가됩니다. 하지만 브라우저가 클라이언트 사이드 내비게이션을 담당하기 때문에 유니버설 렌더링 덕분에 서버 호출이 크게 줄어듭니다. 엣지 사이드 렌더링을 활용하면 비용을 절감할 수 있습니다.
유니버설 렌더링은 매우 유연하여 거의 모든 사용 사례에 적합하며, 특히 콘텐츠 중심 웹사이트에 적합합니다: 블로그, 마케팅 웹사이트, 포트폴리오, 이커머스 사이트, 마켓플레이스 등.
클라이언트 사이드 렌더링
기본적으로 전통적인 Vue.js 애플리케이션은 브라우저(즉, 클라이언트)에서 렌더링됩니다. 그런 다음, Vue.js는 브라우저가 모든 JavaScript 코드를 다운로드하고 파싱한 후 현재 인터페이스를 생성하는 명령을 실행하여 HTML 요소를 생성합니다.
클라이언트 사이드 렌더링의 장점:
- 개발 속도: 전적으로 클라이언트 사이드에서 작업할 때는 코드의 서버 호환성을 걱정할 필요가 없습니다. 예를 들어,
window객체와 같은 브라우저 전용 API를 자유롭게 사용할 수 있습니다. - 저렴함: 서버를 실행하면 JavaScript를 지원하는 플랫폼에서 실행해야 하므로 인프라 비용이 추가됩니다. 클라이언트 전용 애플리케이션은 HTML, CSS, JavaScript 파일만 있으면 어떤 정적 서버에서도 호스팅할 수 있습니다.
- 오프라인: 코드가 전적으로 브라우저에서 실행되기 때문에, 인터넷이 없어도 잘 동작할 수 있습니다.
클라이언트 사이드 렌더링의 단점:
- 성능: 사용자는 브라우저가 JavaScript 파일을 다운로드, 파싱, 실행할 때까지 기다려야 합니다. 다운로드는 네트워크 상태에, 파싱과 실행은 사용자의 기기에 따라 시간이 걸릴 수 있어 사용자 경험에 영향을 줄 수 있습니다.
- 검색 엔진 최적화: 클라이언트 사이드 렌더링으로 전달된 콘텐츠를 인덱싱하고 업데이트하는 데는 서버 렌더링된 HTML 문서보다 더 많은 시간이 걸립니다. 이는 앞서 언급한 성능 저하와 관련이 있으며, 검색 엔진 크롤러는 첫 시도에서 인터페이스가 완전히 렌더링될 때까지 기다리지 않으므로, 순수 클라이언트 사이드 렌더링에서는 콘텐츠가 검색 결과에 표시되고 업데이트되는 데 더 많은 시간이 걸릴 수 있습니다.
클라이언트 사이드 렌더링은 인덱싱이 필요 없거나 사용자가 자주 방문하는 인터랙티브한 웹 애플리케이션에 적합합니다. 브라우저 캐싱을 활용하여 재방문 시 다운로드 단계를 건너뛸 수 있습니다. 예를 들어 SaaS, 백오피스 애플리케이션, 온라인 게임 등이 있습니다.
Nuxt에서 클라이언트 사이드 전용 렌더링을 활성화하려면 nuxt.config.ts에 다음과 같이 설정하세요:
export default defineNuxtConfig({
ssr: false
})
ssr: false를 사용하는 경우, ~/app/spa-loading-template.html에 앱이 하이드레이션될 때까지 렌더링할 로딩 화면용 HTML을 포함한 파일을 추가하는 것이 좋습니다.정적 클라이언트 렌더링 앱 배포
nuxt generate 또는 nuxt build --prerender 명령어로 정적 호스팅에 앱을 배포하면, Nuxt는 기본적으로 모든 페이지를 별도의 정적 HTML 파일로 렌더링합니다.
nuxt generate 또는 nuxt build --prerender 명령어로 앱을 프리렌더링하면, 출력 폴더에 서버가 포함되지 않으므로 서버 엔드포인트를 사용할 수 없습니다. 서버 기능이 필요하다면 nuxt build를 사용하세요.순수 클라이언트 사이드 렌더링을 사용하는 경우, 이 과정이 불필요할 수 있습니다. 단일 index.html 파일과 200.html, 404.html 폴백 파일만 있으면 충분하며, 정적 웹 호스트에 모든 요청에 대해 이 파일들을 제공하도록 설정할 수 있습니다.
이를 위해 라우트 프리렌더링 방식을 변경할 수 있습니다. 훅을 nuxt.config.ts에 다음과 같이 추가하세요:
export default defineNuxtConfig({
hooks: {
'prerender:routes' ({ routes }) {
routes.clear() // (기본값 외에는) 어떤 라우트도 생성하지 않음
}
},
})
이렇게 하면 세 개의 파일이 생성됩니다:
index.html200.html404.html
200.html과 404.html은 사용하는 호스팅 제공업체에서 유용하게 사용할 수 있습니다.
클라이언트 폴백 생성 건너뛰기
클라이언트 렌더링 앱을 프리렌더링할 때, Nuxt는 기본적으로 index.html, 200.html, 404.html 파일을 생성합니다. 하지만 빌드 시 이 파일들 중 일부(또는 전부)의 생성을 방지하려면 Nitro의 'prerender:generate' 훅을 사용할 수 있습니다.
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
}
}
}
}
})
하이브리드 렌더링
하이브리드 렌더링은 **라우트 규칙(Route Rules)**을 사용하여 라우트별로 서로 다른 캐싱 규칙을 적용하고, 서버가 특정 URL에 대한 새 요청에 어떻게 응답할지 결정할 수 있게 해줍니다.
이전에는 Nuxt 애플리케이션과 서버의 모든 라우트/페이지가 유니버설 또는 클라이언트 사이드 중 하나의 렌더링 모드만 사용할 수 있었습니다. 하지만 경우에 따라 일부 페이지는 빌드 시 생성하고, 다른 페이지는 클라이언트 사이드에서 렌더링해야 할 수 있습니다. 예를 들어, 콘텐츠 웹사이트에 관리자 섹션이 있는 경우, 모든 콘텐츠 페이지는 주로 정적으로 한 번만 생성되어야 하지만, 관리자 섹션은 회원가입이 필요하고 더 동적인 애플리케이션처럼 동작해야 합니다.
Nuxt는 라우트 규칙과 하이브리드 렌더링을 지원합니다. 라우트 규칙을 사용하면 nuxt 라우트 그룹에 대한 규칙을 정의하고, 렌더링 모드를 변경하거나 라우트별로 캐시 전략을 지정할 수 있습니다!
Nuxt 서버는 Nitro 캐싱 레이어를 사용하여 해당 미들웨어를 자동으로 등록하고 라우트를 캐시 핸들러로 감쌉니다.
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' }
}
})
라우트 규칙
사용할 수 있는 다양한 속성은 다음과 같습니다:
redirect: string- 서버 사이드 리디렉션을 정의합니다.ssr: boolean- 앱의 일부 섹션에서 HTML의 서버 사이드 렌더링을 비활성화하고,ssr: false로 브라우저에서만 렌더링하도록 만듭니다.cors: boolean-cors: true로 cors 헤더를 자동으로 추가합니다.headers로 출력을 오버라이드하여 커스터마이즈할 수 있습니다.headers: object- 사이트의 특정 섹션(예: 에셋)에 특정 헤더를 추가합니다.swr: number | boolean- 서버 응답에 캐시 헤더를 추가하고, 서버 또는 리버스 프록시에 설정 가능한 TTL(수명) 동안 캐시합니다. 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를 사용할 때는 지원되지 않습니다.예시:
엣지 사이드 렌더링
엣지 사이드 렌더링(ESR)은 Nuxt에 도입된 강력한 기능으로, CDN(콘텐츠 전송 네트워크)의 엣지 서버를 통해 Nuxt 애플리케이션을 사용자와 더 가까운 곳에서 렌더링할 수 있게 해줍니다. ESR을 활용하면 성능이 향상되고 지연 시간이 줄어들어, 더 나은 사용자 경험을 제공할 수 있습니다.
ESR을 사용하면 렌더링 프로세스가 네트워크의 '엣지', 즉 CDN의 엣지 서버로 이동합니다. ESR은 실제 렌더링 모드라기보다는 배포 타겟에 가깝다는 점에 유의하세요.
페이지에 대한 요청이 발생하면, 원래 서버까지 가지 않고 가장 가까운 엣지 서버가 요청을 가로챕니다. 이 서버가 페이지의 HTML을 생성하여 사용자에게 반환합니다. 이 과정은 데이터가 이동해야 하는 물리적 거리를 최소화하여 지연 시간을 줄이고 페이지를 더 빠르게 로드합니다.
엣지 사이드 렌더링은 Nuxt를 구동하는 Nitro 서버 엔진 덕분에 가능합니다. Nitro는 Node.js, Deno, Cloudflare Workers 등 다양한 플랫폼을 지원합니다.
현재 ESR을 활용할 수 있는 플랫폼은 다음과 같습니다:
- Cloudflare Pages - git 연동과
nuxt build명령어만으로 별도 설정 없이 사용 가능 - Vercel Edge Functions -
nuxt build명령어와NITRO_PRESET=vercel-edge환경 변수 사용 - Netlify Edge Functions -
nuxt build명령어와NITRO_PRESET=netlify-edge환경 변수 사용
하이브리드 렌더링은 라우트 규칙과 함께 엣지 사이드 렌더링을 사용할 때도 적용할 수 있습니다.
위에서 언급한 일부 플랫폼에 배포된 오픈 소스 예제를 살펴볼 수 있습니다: