이 글은 nextjs 공식 문서 내용을 번역하고 모아둔 글 입니다.
기본적으로 Next.js는 모든 페이지를 사전 랜더링(Pre-Rendering)합니다. 이는 Next.js가 페이지마다 HTML을 미리 생성하고, 클라이언트 측 JavaScript로 모두 처리하지 않고 미리 생성하는 것을 의미합니다. 사전 랜더링은 성능과 SEO 향상에 도움이 될 수 있습니다.
생성된 각 HTML은 해당 페이지에 필요한 최소한의 JavaScript 코드와 연결됩니다. 페이지가 브라우저에 의해 로드되면 해당 페이지의 JavaScript 코드가 실행되어 페이지를 완전히 인터랙티브하게 만듭니다. (이 과정을 하이드레이션(hydration)이라고 합니다.)
Check That Pre-rendering Is Happening
다음 단계를 수행하여 사전 렌더링이 발생하는지 확인할 수 있습니다.
1. 브라우저에서 JavaScript를 비활성화
2. 네트워크 요청에서 HTML 파일 확인
Next.js가 없는 일반적인 React.js 앱의 경우, 사전 렌더링이 없으므로 JavaScript를 비활성화하면 앱을 볼 수 없습니다.
Two forms of Pre-rendering
Next.js에는 Static Generation과 Server-side Rendering 두 가지 형태의 사전 랜더링이 있습니다. 이들의 차이점은 페이지의 HTML을 언제 생성하는가에 있습니다.
- Static Generation : HTML은 빌드 타임에 생성되며 각 요청마다 재사용됩니다.
- Server-side Rendering : HTML은 각 요청마다 생성됩니다.
중요한 점은 Next.js가 각 페이지에 대해 사용할 사전 랜더링 형태를 선택할 수 있도록 해준다는 것입니다. 대부분의 페이지에서 Static Generation을 사용하고 다른 페이지에서 Server-side Rendering을 사용하여 "하이브리드" Next.js 앱을 만들 수 있습니다.
성능 문제 때문에 Static Generation을 Server-side Rendering보다 권장합니다. 정적으로 생성된 페이지는 CDN에 캐시될 수 있으며 추가 구성 없이 성능을 향상시킬 수 있습니다. 그러나 일부 경우에는 Server-side Rendering이 유일한 옵션일 수 있습니다.
Static Generation 또는 Server-side Rendering과 함께 Client-side 데이터 가져오기를 사용할 수도 있습니다. 즉, 페이지의 일부분은 완전히 클라이언트 측 JavaScript로 렌더링될 수 있습니다.
Static Generation
만약 페이지가 Static Generation을 사용한다면, 페이지 HTML은 빌드 타임에 생성됩니다. 이는 프로덕션에서 next build를 실행할 때 페이지 HTML이 생성된다는 것을 의미합니다. 이 HTML은 각 요청마다 재사용됩니다. 또한 CDN에 캐시될 수 있습니다.
Next.js에서는 데이터가 있는 경우와 없는 경우 모두 페이지를 정적으로 생성할 수 있습니다.
Static Generation without data
기본적으로 Next.js는 데이터를 가져오지 않고 정적 생성(Static Generation)을 사용하여 페이지를 사전 랜더링합니다.
function About() {
return <div>About</div>
}
export default About
위와 같은 정적 페이지는 사전 랜더링을 위해 외부 데이터를 가져올 필요가 없습니다. 위와 같은 경우 Next.js는 빌드 타임에 페이지당 하나의 HTML 파일을 생성합니다.
Static Generation with data
일부 페이지는 사전 랜더링을 위해 외부 데이터를 가져와야 할 수 있습니다. 두 가지 시나리오가 있으며 둘 중 하나 또는 둘 다가 적용될 수 있습니다. 각 경우에 Next.js에서 제공하는 다음 함수를 사용할 수 있습니다:
- 페이지 콘텐츠가 외부 데이터에 따라 달라지는 경우 : getStaticProps를 사용
- 페이지 경로가 외부 데이터에 따라 달라지는 경우: getStaticPaths를 사용(보통 getStaticProps와 함께 사용됨).
Scenario 1: 페이지 콘텐츠가 외부 데이터에 의존 할 때
예를 들어, 블로그 페이지는 CMS(컨텐츠 관리 시스템)에서 블로그 글 목록을 가져와야 할 수 있습니다.
// TODO: 이 페이지를 사전 랜더링하기 위해서는 posts(API 엔드포인트를 호출하여)를 가져와야합니다.
export default function Blog({ posts }) {
return (
<ul>
{posts.map((post) => (
<li>{post.title}</li>
))}
</ul>
)
}
이 데이터를 사전 랜더링(pre-render)하려면, Next.js는 같은 파일에서 getStaticProps라는 async 함수를 내보낼 수 있도록 허용합니다. 이 함수는 빌드 타임에 호출되며, 가져온 데이터를 페이지의 props로 전달하여 사전 랜더링합니다.
export default function Blog({ posts }) {
// Render posts...
}
// This function gets called at build time
export async function getStaticProps() {
const res = await fetch('https://.../posts')
const posts = await res.json()
// { props: { posts } }를 반환함으로써, 빌드 타임에 Blog 컴포넌트는 posts를 prop으로 받게 됩니다.
return {
props: {
posts,
},
}
}
Scenario 2: 페이지 경로가 외부 데이터에 의존 할 때
Next.js는 동적 라우트를 가진 페이지를 생성할 수 있도록 허용합니다. 예를 들어, id에 따라 단일 블로그 게시물을 보여주는 pages/posts/[id].js
라는 파일을 만들 수 있습니다. 이렇게 하면 posts/1
에 액세스할 때 id가 1인 블로그 게시물을 표시할 수 있습니다.
그러나, 어떤 id를 빌드 타임에 사전 랜더링하려는지는 외부 데이터에 따라 달라질 수 있습니다.
예를 들어, 데이터베이스에 id가 1인 블로그 게시물 하나만 추가한 경우, 빌드 타임에는 posts/1만 사전 랜더링하려고 할 것입니다.
나중에 id가 2인 두 번째 게시물을 추가할 경우, posts/2도 사전 랜더링하려고 할 것입니다.
따라서 사전 랜더링할 페이지 경로는 외부 데이터에 따라 달라집니다. 이를 처리하기 위해 Next.js는 동적 페이지(pages/posts/[id].js의 경우)에서 getStaticPaths라는 async 함수를 내보낼 수 있도록 허용합니다. 이 함수는 빌드 타임에 호출되며, 어떤 경로를 사전 랜더링하려는지 지정할 수 있습니다.
// 이 함수는 빌드 타임에 호출됩니다.
export async function getStaticPaths() {
// 외부 API 엔드포인트를 호출하여 포스트를 가져옵니다.
const res = await fetch('https://.../posts')
const posts = await res.json()
// posts 데이터를 기반으로 사전 랜더링하려는 경로 가져옵니다
const paths = posts.map((post) => ({
params: { id: post.id },
}))
// 빌드 타임에 이러한 경로만 사전 랜더링합니다. { fallback: false }는 다른 경로는 404 페이지를 보여주어야 함을 의미합니다.
return { paths, fallback: false }
}
또한 pages/posts/[id].js
에서는 이 id를 가진 포스트의 데이터를 가져와 페이지를 사전 랜더링하도록 getStaticProps
를 내보내야합니다.
export default function Post({ post }) {
// Render post...
}
export async function getStaticPaths() {
// ...
}
// This also gets called at build time
export async function getStaticProps({ params }) {
// params contains the post `id`.
// If the route is like /posts/1, then params.id is 1
const res = await fetch(`https://.../posts/${params.id}`)
const post = await res.json()
// Pass post data to the page via props
return { props: { post } }
}
위처럼 getStaticPaths와 getStaticProps를 함께 사용하게 되는 경우
1. 빌드 실행시 `getStaticPaths` 를 실행, API를 호출해 데이터를 받습니다.
2. getStaticPaths에서 응답받은 params를 통해, 상세데이터를 호출합니다.
3. 상세데이터를 호출받고 난 이후, 이 데이터를 기반으로 HTML을 미리 만듭니다.
getStaticProps 함수는 서버 측에서만 실행됩니다. 클라이언트 측에서는 절대 실행되지 않습니다. 브라우저를 위한 JS 번들에 포함되지 않기 때문에 직접적인 데이터베이스 쿼리와 같은 코드를 작성해도 브라우저로 전송되지 않습니다.
빌드가 완료되고 나면, .next/server/pages/posts
디렉토리에 위 사진처럼, 빌드시점에 만들어진 html 페이지를 볼 수 있다.
HTML 파일에 내용들이 잘 들어간 것이 확인되며,
훌륭하게 동작한다.
When should I use Static Generation?
가능하다면 정적 생성(데이터가 있거나 없는 경우 모두)을 사용하는 것이 좋습니다. 이렇게 하면 페이지를 한 번 빌드하고 CDN에서 제공할 수 있으므로, 서버가 모든 요청마다 페이지를 렌더링하는 것보다 훨씬 빠릅니다.
마케팅 페이지, 블로그 게시물 및 포트폴리오, 전자상거래 상품 목록, 도움말 및 문서 등 다양한 유형의 페이지에 대해 정적 생성을 사용할 수 있습니다.
사용자 요청 이전에 이 페이지를 사전 랜더링할 수 있을까? 에 대해 긍정적인 대답을 할 수 있다면 정적 생성을 선택해야 합니다.
하지만, 페이지를 사용자 요청 이전에 사전 랜더링할 수 없는 경우 정적 생성은 좋은 선택이 아닙니다. 페이지가 자주 업데이트되어 콘텐츠가 매 요청마다 변경되는 경우 등이 그러한 경우입니다.
이러한 경우 다음 중 하나를 사용할 수 있습니다.
- 클라이언트 측 데이터 가져오기와 함께 정적 생성 사용
페이지의 일부를 사전 랜더링하지 않고 클라이언트 측 JavaScript를 사용하여 이를 채울 수 있습니다. 이 접근 방식에 대해 자세히 알아보려면 데이터 가져오기 문서를 확인하세요.
- 서버 사이드 랜더링 사용
Next.js는 모든 요청마다 페이지를 사전 랜더링합니다. CDN에 캐시할 수 없으므로 느리지만, 사전 랜더링된 페이지는 항상 최신 상태입니다.
Server-side Rendering (SSR, Dynamic Rendering)
페이지가 서버 사이드 랜더링을 사용하는 경우, 페이지 HTML은 매 요청마다 생성됩니다.
페이지에 서버 사이드 랜더링을 사용하려면 getServerSideProps라는 async 함수를 내보내야 합니다. 이 함수는 모든 요청에서 서버에 의해 호출됩니다.
예를 들어, 페이지가 자주 업데이트되는 데이터(외부 API에서 가져온)를 사전 랜더링해야 하는 경우, 이 데이터를 가져오고 아래와 같이 페이지로 전달하는 getServerSideProps를 작성할 수 있습니다
export default function Page({ data }) {
// Render data...
}
// This gets called on every request
export async function getServerSideProps() {
// Fetch data from external API
const res = await fetch(`https://.../data`)
const data = await res.json()
// Pass data to the page via props
return { props: { data } }
}
getServerSideProps는 getStaticProps와 유사하지만, 차이점은 getServerSideProps가 빌드 시간이 아닌 매 요청마다 실행된다는 것입니다.
getServerSideProps는 요청 시간에 호출되기 때문에 해당 매개변수(context)는 요청에 특정한 매개변수를 포함합니다.
getServerSideProps는 요청 시간에 가져와야 하는 데이터를 가진 페이지를 사전 렌더링해야 할 때에만 사용해야 합니다.
getStaticProps보다 Time to first byte (TTFB)가 느릴 수 있으며, 서버는 매 요청마다 결과를 계산해야 하므로 결과를 CDN에서 캐시할 수 없습니다. 추가적인 구성이 필요합니다.
SSR이 적용된 파일의 경우 ./next/server/pages/posts/[id].js
가 생성 되었습니다.
그리고, 해당 파일에 다음과 같이 정의되어 있습니다.
// ./next/server/pages/posts/[id].js
"use strict";
(() => {
var exports = {};
exports.id = 646;
exports.ids = [646];
exports.modules = {
/***/ 669:
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__),
/* harmony export */ "getServerSideProps": () => (/* binding */ getServerSideProps)
/* harmony export */ });
/* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(997);
/* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__);
const PostPage = ({ post })=>{
return /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)("div", {
children: [
/*#__PURE__*/ react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx("h1", {
children: post.title
}),
/*#__PURE__*/ react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx("p", {
children: post.body
})
]
});
};
async function getServerSideProps({ params }) {
if (!params?.id) return {
props: {
post: {}
}
};
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`);
const post = await res.json();
return {
props: {
post
}
};
}
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (PostPage);
/***/ }),
/***/ 997:
/***/ ((module) => {
module.exports = require("react/jsx-runtime");
/***/ })
};
;
// load runtime
var __webpack_require__ = require("../../webpack-runtime.js");
__webpack_require__.C(exports);
var __webpack_exec__ = (moduleId) => (__webpack_require__(__webpack_require__.s = moduleId))
var __webpack_exports__ = (__webpack_exec__(669));
module.exports = __webpack_exports__;
})();
빌드된 코드를 보면, 복잡하게 되어있지만, IIFE로 실생되는 코드가 몇개 보이고, 내부에 getServerSideProps
바인딩 되고 실행되는 코드와 함께, getServerSideProps
가 정의된 것을 확인할 수 있습니다.
이 부분이, 요청이 들어 왔을 때, 런타임에 저 getServerSideProps
가 실행된다는 것을 유추할 수 있습니다.
Client-side Rendering
만약 데이터를 사전 렌더링할 필요가 없는 경우, 다음과 같은 전략(클라이언트 측 렌더링이라고 함)을 사용할 수도 있습니다:
- 외부 데이터가 필요하지 않은 페이지 부분은 정적으로 생성(사전 렌더링)합니다.
- 페이지가 로드될 때, JavaScript를 사용하여 클라이언트에서 외부 데이터를 가져와 남은 부분을 채웁니다.
이 접근 방식은 사용자 대시보드 페이지와 같은 페이지에 적합합니다. 대시보드는 개인적이며 사용자별 페이지이므로 SEO는 관련이 없으며 페이지를 사전 렌더링할 필요가 없습니다. 데이터가 자주 업데이트되므로 요청할 떄 마다 데이터 페칭이 필요합니다.
'Nextjs' 카테고리의 다른 글
[Nextjs] Build Time vs Runtime (0) | 2023.03.09 |
---|---|
[Nextjs] Nextjs 렌더링 (0) | 2023.03.09 |