Next.js에서는 기본적으로 pre-rendering 방식을 제공한다. 프리렌더링이란 클라이언트단에서 HTML이 생성되는 것이 아니라 각각의 페이지의 리액트 컴포넌트를 HTML로 미리 생성해두는 것을 의미한다. 대표적인 프리렌더링 방식의 렌더링으로 SSR
과 SSG
가 있으며, HTML이 미리 생성되었기 때문에 초기 렌더링 시간이 빠르고 SEO에 유리하다는 장점을 가지고 있다.
이렇게 유저가 사이트의 UI 볼 수 있는 타이밍을 TTV(Time To View)
라고 한다. 하지만 생성된 화면은 화면을 그리기 위한 최소한의 자바스크립트 코드만 HTML으로 변환되어 보여지기 때문에 사용자 인터랙션 기능을 위해선 페이지 로드 후 자바스크립트 코드가 적용되는 단계가 필요하다. 이 단계를 hydration
이라고 한다. 이 단계가 없으면 화면에 버튼이 보이지만, 링크 버튼을 눌러도 이동하지 않고, 버튼을 눌러도 동작하지 않게 된다. 유저가 버튼을 클릭했을 때, 동작하기 시작하는 타이밍 즉, 자바스크립트 인터렉션이 동작하는 타이밍을 TTI(Time To Interact)
라고 한다.
CNA(create-next-app)을 이용해 프로젝트를 생성하는 경우, 프리렌더링 방식이 기본이기 때문에 start 전에 build를 실행시켜 .next 파일을 생성해주어야 한다.
SSR와 SSG는 HTML문서를 미리 생성한다는 공통점이 있지만 언제 생성해두는지에 따라 나뉜다.
SSG
: 빌드 타임에 생성SSR
: 유저가 리퀘스트시 생성SSR은 유저의 요청이 들어올 때 (페이지에 처음 진입하는 경우, 창 포커싱이 바뀌는 경우 등) 서버에서 HTML을 생성한다. 서버에서 하는 일이 많아지기 때문에 서버의 부하가 증가하는 단점이 있지만, 업데이트되는 데이터를 반영하지만 유저와의 인터렉션이 크게 없는 페이지인 경우 빠른 TTV로 원활한 UX를 제공할 수 있다.
✔︎ 마이페이지와 같이 데이터가 너무 잦게 바뀌지는 않지만 유저마다 다른 데이터를 보여줘야 하는 경우,
getServerSideProps
를 이용해 SSR을 구현할 수 있다.
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 } }
}
export default Page
SSG는 빌드시에 이미 파일을 만들고 접속하는 유저들에게 같은 화면을 보여주기 때문에, 데이터 변동이 없는 페이지를 만드는데 적합하다. 반면, 데이터가 변동되는 경우 재빌드와 재배포가 필요하기 때문에 동적 컨텐츠에 부적합하다.
✔︎ ex) 소개페이지나, 정보성 글 등 SEO를 필요로 하고 많은 인터렉션이 없는 경우
외부 데이터가 없는 경우, 자연스럽게 Static HTML로 생성되지만, 외부 데이터가 필요한 경우 Next.js에서 제공하는 getStaticProps
를 사용해 빌드타임에 해당 데이터를 포함해 HTML을 만들어둘 수 있다.
export default function Home(props) { ... }
export async function getStaticProps() {
// Get external data from the file system, API, DB, etc.
const data = ...
// The value of the `props` key will be
// passed to the `Home` component
return {
props: ...
}
}
✅ 개발 모드에서는 SSG도 매 유저 리퀘스트마다 갱신된다.
SSG은 빠르고 서버의 부담이 줄어들고, 검색 엔진 최적화까지 된다는 장점이 있지만 데이터가 업데이트 되지 않는 다는 점이 단점이 있다. 이와 같은 SSG의 단점을 보완하기 위해 생긴 것이 ISR이다. 빌드 시점에 페이지가 생성된다는 점은 같지만, 일정 시간이 지난 후 페이지를 re-build해 페이지를 새로 생성하면서 최신 데이터로 업데이트된다.
SSG와 같이 getStaticProps
를 사용하지만, revalidate 시간을 직접 설정해 재빌드되는 주기를 정할 수 있다.
function Blog({ posts }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// revalidation is enabled and a new request comes in
export async function getStaticProps() {
const res = await fetch('https://.../posts')
const posts = await res.json()
return {
props: {
posts,
},
// Next.js will attempt to re-generate the page:
// - When a request comes in
// - At most once every 10 seconds
revalidate: 10, // In seconds
}
}
10초간 화면에 진입하는 모든 유저가 같은 화면을 보게되고 10초 뒤에 재빌드과정을 시작한다. 10초 뒤, 성공시에는 새로 들어온 유저는 새로 업데이트된 화면을 볼 수 있지만, 실패시 이전 화면으로 계속 보여진다.
클라이언트 사이드에서 HTML을 생성하는 CSR도 무조건 좋다 나쁘다의 이분법적인 구조로 나눌 수 없다. 최초 요청시 페이지의 HTML과 자바스크립트를 포함한 각종 리소스를 받아오기 때문에 초기 로딩 속도가 느리다는 단점이 있지만, 이후 페이지 이동시 빠른 UX 제공한다. 리액트 컴포넌트가 HTML로 변경되는 시점이 클라이언트라는 점에서 SEO에는 적합하지 않지만, 필요시 그때 그때 데이터를 받아오기 때문에 유저 인터랙션에 따른 데이터 업데이트가 잦은 경우 CSR을 사용하는 것이 더 유리하다.
특히 Next.js에서는 SWR을 이용해 데이터를 캐싱해 더욱 알뜰하게 받아올 수 있도록 도와준다.
Next.js에서 기본적으로 프리렌더링 방식을 제공한다고 했지만, 상황에 따라 적합한 렌더링 방식을 혼용할 수 있다는 게 Next.js의 가장 큰 장점이다. 만드려는 페이지의 특성을 고려해 어떤 렌더링 방식이 적합할 지 적절히 골라 사용하는 것이 중요하다.
참고하면 좋은 글
Next.js 공식문서
[FE] SSR(Server-Side-Rendering) 그리고 SSG(Static-Site-Generation) (feat. NEXT를 중심으로)
CSR, SSR만 알고 있었는데 ISR에 대해 알 수 있어서 너무 좋았어요. 좋은 글 감사합니다 :)