거의 1년만에 블로그에 글을 쓴다.
next.js로 웹사이트를 10개 가까이 만들어서 배포해봤음에도, 아직까지 헷갈리는 데이터 fetching과 렌더링에 대해 한번 마음 먹고 정리해보려고 한다. 기본적으로는 docs의 내용을 읽기 쉽게 번역 및 정리해서 올리는 것이 목표이며, 코드가 포함된 실습은 docs 링크를 통해 접근할 수 있다.
next.js의 data fetching은 당신이 만드는 애플리케이션의 사용 목적에 따라서 컨텐츠를 렌더링할 수 있는 다양한 방식들을 제공한다. 프리 렌더링을 제공하는 Server-side Rendering(SSR)과 Static-site Generation(정적 생성, SSG), 그리고 런타임에 컨텐츠를 생성하고 업데이트하는 Incremental Static Regeneration(ISR)이 있다.
기본적으로 next.js에서는 모든 페이지에서 프리 렌더링을 한다. 이 뜻은, 클라이언트 사이드의 자바스크립트가 이 모든 것을 하는 대신에, next.js가 모든 페이지에서 HTML을 미리 생성한다는 말이다. 프리 렌더링은 더 높은 성능과 SEO (검색엔진 최적화)를 제공할 수 있다.
위 단계에서 생성된 각각의 HTML은, 그 페이지를 위해 필요한 최소한의 자바스크립트 코드들과 연관되어 있다. 어떤 페이지가 브라우저에 의해 로드되면, 그 페이지의 자바스크립트 코드들이 동작하고, 페이지를 백퍼센트 인터랙티브하게 만든다. 이 과정을 ‘하이드레이션hydration’ 이라고 한다. (물에 흠뻑 젖는 것처럼, 자바스크립트로 페이지가 물든다고 생각하면 편할 듯.)
Docs에 의하면, 크롬에서 자바스크립트를 끄고 next.js로 만들어진 예시 사이트에 접속해도, 사이트가 완전히 동작하진 않지만 필수 UI들이 보여지고 있다. 이는 next.js가 웹 애플리케이션을 정적 HTML로 프리렌더링하고, 자바스크립트를 실행하지 않아도 앱의 UI를 볼 수 있게 렌더링해주기 때문이다. 만약 웹앱이 퓨어한 React.js로 쓰여졌다면, 프리렌더링을 하지 않기 때문에 자바스크립트가 비활성화 된 경우 앱을 볼 수 없다. (Docs에서 테스트해볼 수 있다.)
다만 여기에서 주의할 점은, next.js라 하더라도 앱의 첫 화면에 Link 컴포넌트와 같은 인터랙티브한 컴포넌트가 존재할 경우, 이는 자바스크립트가 로드된 다음에 활성화된다.
프리 렌더링에는 두가지 종류가 있다. 1) Static Generation (정적 생성), 그리고 2) Server-side Rendering (서버사이드 렌더링). 둘의 차이는 웹페이지를 위한 HTML을 언제 생성하느냐에 있다.
Static Generation (정적 생성)은 빌드 타임에 HTML을 생성하는 프리 렌더링 방법이다. 이렇게 프리 렌더링 된 HTML은 매 요청마다 재사용된다. next build 명령을 통해 웹 앱을 프로덕션 레벨로 빌드했을 경우, 이 시점에 HTML이 렌더링 되고, 각각의 클라이언트들이 요청할 때마다 이미 만들어진 해당 HTML이 재사용된다.
Server-side Rendering은 매 요청마다 HTML을 생성하는 프리 렌더링 방법이다. 그저 각각의 클라이언트들이 요청을 하는 시점에 매번 HTML을 생성한다. npm run dev 나 yarn dev를 통해 실행된 개발 환경에서는, 페이지들이 Static Generation을 사용하고 있더라도 모든 페이지들이 매 요청마다 프리렌더링 된다.
next.js를 사용하는 개발자는 매 페이지마다 어떤 렌더링 방법을 사용할지 선택할 수 있다. 대부분의 페이지에 Static Generation(정적 생성)을 사용하고, 나머지에는 SSR (Server-side Rendering)을 사용하는 하이브리드 next.js 앱을 만들 수도 있다.
next.js에서는 가능하면 Static Generation을 추천한다. (데이터가 있든 없든). 왜냐하면, 당신의 페이지가 한번에 빌드되고 CDN에 의해 서빙되는 것이, 매 요청마다 서버가 페이지를 렌더하게 만드는 것보다 훨씬 빠르기 때문이다. Static Generation은 다음 예시들을 포함한 많은 상황들에서 사용할 수 있다. 마케팅 페이지, 블로그 포스트, 이커머스 (쇼핑몰) 제품 목록, 도움이나 기록을 위한 페이지 등.
그러니까, 어떤 페이지를 설계하기 전에 스스로 물어봐야 한다. “내가 이 페이지를 유저가 요청을 보내기 전에 렌더링해도 될까?” 만약 이 질문에 대한 답이 yes라면, 그냥 Static Generation을 선택하면 된다.
반면에, 유저의 요청 이전에 페이지를 프리 렌더링 할 수 없는 경우에는 Static Generation은 좋은 방법이 아니다. 만약 해당 페이지가 자주 업데이트되는 데이터를 보여줘야 하거나, 매 요청마다 페이지의 컨텐츠가 바뀌는 경우라면. (예를 들어 주식 등의 실시간 그래프 및 차트인 경우?) 이러한 경우에는 Server-side Rendering을 써라. 이게 더 느리지만, 프리 렌더링 된 페이지들이 항상 최신 상태로 유지될 것이다. 아니면 아예 프리 렌더링을 하지 않고, 자주 업데이트되는 데이터를 관리하기 하기 위해서 클라이언트 사이드의 자바스크립트를 사용할 수도 있다.
외부에서 가져올 데이터가 필요 없는 페이지의 경우, 이 페이지들은 앱이 프로덕션을 위해 빌드될 때 자동으로 정적 생성될 것이다. 그러나 몇몇 페이지들은 첫 데이터 페칭 없이는 HTML을 렌더링할 수 없는 경우도 있을 것이다. 만약 당신이 빌드 타임에 파일 시스템에 접근하거나, 외부 API에 데이터 요청을 보내거나, 당신의 데이터베이스에 쿼리를 날려야 하는 경우, Next.js는 즉시 사용 가능한 방법으로 이러한 경우들도 지원한다. 이 경우가 데이터가 있는 Static Generation을 의미한다.
next.js에서 어떤 페이지 컴포넌트를 export할 때, getStaticProps라고 불리는 async function도 같이 익스포트 할 수 있다.
이렇게 할 경우, getStaticProps는 프로덕션 단계에서의 빌드 타임에 실행되고, 이 함수 안에서 외부의 데이터를 페칭할 수 있으며, 이 데이터들이 그 페이지의 prop으로 전달된다. 기본적으로, getStaticProps는 Next.js에게 다음과 같이 말한다. “야, 이 페이지는 데이터 의존성이 좀 있어. 그니까 빌드 타임에 이 페이지를 프리 렌더링 할려면, 이 데이터들 먼저 리졸브 해줘야돼.” (노트: 개발 환경에서는 getStaticProps가 매 요청마다 실행된다.)
파일 시스템을 이용해서 데이터를 웹 페이지에 추가할 수 있다. 각각의 블로그 포스트는 마크다운 파일이 될 것이다. (자세한 실습 예제는 여기를 참고한다.)
어떤 데이터를 가져오는 함수를 통해 리턴된 반환값을 getStaticProps 함수 내의 props 객체 안에 넘기면, 데이터를 해당 함수가 위치한 페이지 컴포넌트의 프롭으로 보내줄 수 있다.
getStaticProps에 대해 알아야 할 몇 가지 에센셜이 있다.
파일 시스템을 이용해서 데이터를 가져오는 것 말고도, 외부 API 엔드포인트를 통해 데이터를 가져올 수 있다. 또한 데이터베이스에 직접 쿼리를 보낼 수도 있다. 이 두 가지 경우 모두 가능하다. 왜냐하면 getStaticProps는 ‘오직 서버 사이드에서만’ 실행되기 때문이다. 절대로 클라이언트 사이드에서는 실행되지 않는다. 이는 브라우저의 JS 번들 안에 포함되지도 않는다. 이 뜻은, 쿼리들이 브라우저에 보내지기 전에 다이렉트로 데이터베이스에 쿼리를 보낼 수 있는 코드를 작성할 수 있다는 뜻이다.
개발 환경에서 getStaticProps는 매 요청마다 실행된다. 프로덕션 모드에서는 getStaticProps는 빌드 타임에 실행된다. 그러나 이 행동은 getStaticPaths를 통한 fallback key를 이용해 향상될 수 있다. 왜냐하면 빌드 타임에 실행된다는 것은, 이 상황에서 쿼리 파라미터나 http 헤더와 같은, ‘요청 중일 때만 가능한 데이터’는 이용할 수 없다는 것을 의미한다.
getStaticProps는 페이지에서만 익스포트 될 수 있다. 만약 페이지가 아닌 경우 getStaticProps를 익스포트 할 수 없다. 이러한 제한의 여러 이유 중 하나는, 리액트는 요구되는 데이터들이 페이지 렌더링 이전에 모두 갖춰져 있어야 하기 때문이다.
만약 당신이 유저의 리퀘스트 이전에 페이지를 프리렌더링 할 수 없는 상황이라면 Static Generation, 즉 정적 생성 자체가 좋은 아이디어가 아니다. 앞서 말했듯 만약 어떤 페이지가 자주 업데이트 되는 데이터나, 매 요청마다 변경되는 데이터를 보여주어야 하는 경우 말이다. 이런 경우에는 서버 사이드 렌더링을 이용하거나, 프리 렌더링을 스킵해라.
빌드 타임이 아닌 요청 시 데이터를 가져올 필요가 있다면 서버사이드 렌더링을 이용할 수 있다. 클라이언트의 매 페이지 요청마다, 데이터가 fetch 되고 HTML이 생성된다. 서버사이드 렌더링을 하기 위해서는 페이지에 getStaticProps 대신에 getServerSideProps를 사용한다.
getServerSideProps는 사용자의 ‘요청 시’ 호출되기 때문에, context라는 ‘리퀘스트 특정적인’ 파라미터를 포함해야 한다. getServerSideProps는 사용자 요청 시에만 데이터가 프리 렌더링 되어야 하는 페이지에만 사용해야 한다. 서버가 모든 요청의 결과를 계산해야 하기 때문에 TTFB(Time to first byte)가 getStaticProps보다 느려질 것이다. 그리고 추가적인 설정 없이는 이에 대한 요청 결과값은 CDN에 의해 캐싱되지 않는다.
만약 데이터를 프리렌더링 하지 않아도 되는 상황이라면, 클라이언트 사이드 렌더링을 해도 된다. 외부 데이터를 필요로 하지 않는 정적으로 생성된 페이지인 경우 또는 페이지가 로드될 때, 클라이언트 측에서 자바스크립트를 이용해 외부 데이터를 가져오고 나머지 부분들을 만들어내는 경우
즉 정리하자면, 데이터 없는 Static Generation (정적 생성)을 하고, 클라이언트 사이드에서 데이터를 가져오는 방식이다. 이 경우는 유저 대시보드 페이지와 같은 경우에 잘 맞는다. 왜냐하면 대시보드는 프라이빗하고, 유저 특정적인 페이지이고, SEO랑 상관 없어서 페이지가 프리렌더링 되지 않아도 된다. 데이터가 자주 업데이트 되고, 이는 요청 시점에 데이터 fetching을 해야 한다는 것이다.
next.js 팀은 SWR이라는 데이터 fetching 리액트 훅을 만들었다. 만약 클라이언트 사이드에서 데이터 fetching을 할 거라면 이 훅을 강력 추천한다. 이는 캐싱, 재확인, 포커스 트래킹, 간격을 두고 다시 가져오기 등을 지원한다.
next.js 는 사이트를 빌드하고 나서 정적 페이지들을 생성하거나 업데이트할 수 있도록 해줄 수도 있다. Incremental Static Regeneration (ISR) 은 static-generation (정적 생성)을 per-page basis (매 페이지마다를 기반으로), 모든 페이지들을 다시 빌드할 필요 없이 가능하게 한다. ISR은 수백만개의 페이지를 스케일링하는 동안, '정적인 것'의 장점들을 얻을 수 있다.
ISR은 사이트를 재배포하지 않고도 컨텐츠를 생성하거나 수정할 수 있게 해준다. 더 좋은 성능과, 향상된 보안, 더 빠른 빌드 타임이라는 세 가지 장점이 있다.
정적 페이지들은 Vercel의 Edge Network상의 모든 리전에서, 생성된 페이지들을 캐싱함으로써 일관되게 빠른 속도를 유지할 수 있고, 내구성 있는 스토리지에 파일들을 보존할 수 있다.
ISR의 렌더 함수들은 페이지들을 생성하기 위해 사용되며, 새로 들어오는 리퀘스트들에는 접근 권한이 없다. 이는 향상된 보안을 위해, 우연히 일어나는 유저 데이터의 캐싱을 막아준다.
페이지들은 빌드를 통해서가 아니라, 어떤 API를 통해서나 어떤 리퀘스트에 의해 생성을 달리할 수 있다. 이는 애플리케이션이 성장하는데에 있어서 빌드 타임을 빠르게 유지해 준다.
next.js 애플리케이션을 빌드하거나, Build Output API와 함께 커스텀한 솔루션을 사용할 때, ISR 렌더 함수 (또는 Serverless나 Edge)를 정의할 수 있고, 이는 정적 페이지를 생성하거나 수정할 수 있게 해준다.
next.js는 getStatidProps를 revalidate와 함께 사용하는 경우, Vercel에서 자동으로 ISR 렌더 함수를 생성한다. 만약 Build Output API를 사용하는 경우, 이는 프리렌더 함수라고 불린다. 이 함수들은 새로 들어오는 리퀘스트에는 접근 권한이 없이 때문에, 유저 데이터를 우연하게 캐싱하는 경우를 방지해주며, 보안을 향상시킬 수 있다.
ISR과 Cache-Control 헤더 (s-maxage나 stale-with-error를 포함)는 데이터 소스에 대해 더 적은 리퀘스트를 만듦으로써 백엔드의 로드를 줄여준다. 그러나, 이 두가지 사이에는 구조적인 주요 차이점이 존재한다.
Shared Global Cache
ISR 렌더 함수의 아웃풋을 위한 캐시는 글로벌하게 배분된다. 캐시 MISS
의 경우에는, 하나의 글로벌 버킷 안의 값들을 찾는다. 이는 ISR이 빌트인 '캐시 방어'를 자동으로 가지고 있다는 것을 뜻한다. 이는 캐시의 HIT
ratio를 향상시킨다. 만약 cache-control
헤더만을 사용한다면, 설계에 의해서 캐시는 만료되고 리전을 가로질러 공유되지 않는다.
300ms Global Purges
온디맨드이든 배경에서든 재생성이 트리거되면, ISR 함수들은 재실행되고, 모든 리전들은 300ms 내에 최신의 컨텐츠와 함께 최신의 상태로 가져와진다.
Intant Rollbacks
ISR은 배포 사이에 생성된 페이지들을 보존한다. 이는 즉각적으로 롤백할 수 있다는 것을 뜻하며, 이전에 생성된 페이지들을 상실하지 않는다는 것을 뜻한다.
ISR은 HTTP에 기반한 캐싱 삽입과 관련된 이슈들을 추상화하고, 전역적 퍼포먼스와 사용성을 위한 추가 기능들을 더한다. 그리고 더 좋은 개발자 경험을 제공한다.
Note: experimental-edge 런타임은 현재 ISR과 호환되지 않는다. 그렇지만 cache-control 헤더를 수동으로 세팅해서 stale-while-revalidate를 레버리지 할 수 있다.
ISR을 이용하기 위해서는, revalidate 프롭을 getStaticProps에 전달한다.
function Blog({ posts }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
// 이 함수는 서버사이드의 빌드 타임에 호출된다.
// 만약 revalidation이 활성화되어 있고, 새로운 요청이 들어오면
// 이 함수는 서버리스 함수에서 다시 호출될 수 있다.
export async function getStaticProps() {
cosnt res = await fetch('https://.../posts');
const posts = await res.json();
return {
props: {
posts,
}
// next.js 는 페이지를 재생성하기 위해 시도할 것이다:
// - 새로운 리퀘스트가 들어오면
// - 10초마다 한 번씩
revalidate: 10, // 몇초마다 할 것인지
}
}
// 이 함수는 서버 사이드의 빌드 타임에 호출된다.
// 만약 path 가 생성되지 않았을 경우,
// 서버리스 함수에서 다시 호출될 수 있다.
export async function getStaticPaths() {
const res = await (fetch('https://.../posts');
const posts = await res.json();
// posts에 기반하여 프리렌더링 하고 싶은 path들을 가져온다
const paths = posts.map((post) => ({
params: { id: post.id },
}));
// 빌드 타임에 이 path들만 프리렌더링 할 것이다.
// { fallback: blocking } 은 페이지들을 서버렌더링 한다
// path가 존재하지 않을 경우에만!
return { paths, fallback: 'blocking' }
}
export default Blog;
페이지에 요청이 만들어지면, 이는 빌드 타임에 프리렌더링 된 것이다. 그리고 이는 맨 처음에는 캐싱된 페이지를 보여준다.
특정 경로에 대한 리퀘스트가 생성되지 않았다면, next.js는 가장 첫 리퀘스트에 페이지를 서버렌더링 할 것이다. 그 이후의 리퀘스트들은 캐시를 통해 static file 들을 서빙한다. Vercel에서의 ISR은 글로벌하게 캐시를 보존하며, 롤백을 핸들링할 수 있다.
Note: 업스트림 데이터 프로바이더가 기본적으로 캐싱을 제공하고 있는지 확인해라. 어쩌면 이를 비활성화해야할 수도 있다. 그렇지 않으면 revalidation은 새로운 데이터를 가져와 ISR 캐시를 업데이트하는 것이 불가능할 수 있다. 캐싱은
Cache-Control
헤더를 반환하는 경우, CDN (요청되고 있는 엔드포인트)에서 일어나고 있을 수 있다.
만약 revalidate
타임을 60으로 설정하면, 모든 유저들이 똑같이 생성된 버전의 사이트를 1분 동안 봐야 한다. 캐시를 무효화하는 유일한 방법은 어떤 사람이 1분이 지난 뒤에 페이지에 방문하는 것 밖에 없다.
v12.2.0
부터 next.js는 온디맨드 ISR을 지원한다. 특정한 페이지에서 수동으로 next.js 캐시를 삭제할 수 있다. 이는 headless CMS에서 새로운 컨텐츠가 생기거나 수정되었을 때나, 가격, 설명, 카테고리, 리뷰 등의 이커머스 메타데이터가 변경된 경우 사이트를 업데이트하기 쉬워진다.
온디맨드 revalidation을 사용하기 위해서는 getStaticProps
내에서, revalidate를 지정하지 않아도 된다. 만약 revalidate
가 발생하면, next.js는 기본값인 false (no revalidation)
을 사용하고, revalidate()가 온디맨드로 호출되었을 경우에만 revalidate를 실행할 것이다.
Note: 미들웨어에는 On-Demand ISR 요청이 실행되지 않는다. 대신에, revalidate()를 revalidate되길 원하는 특정한 페이지에서 호출해라. 예를들면, 당신이 pages/blog/[slug].js 를 가지고 있고, /post-1 을 /blog/post-1로 다시 작성한 경우, 당신은 res.revalidate('/blog/post-1')을 실행해야 할 수 있다.
우선, next.js 애플리케이션만 알 수 있는 시크릿 토큰을 하나 만든다. 이 시크릿은 API Route를 revalidation하기 위핸 허가되지 않은 접근을 방지한다. 당신은 라우트에 아래와 같은 URL 스트럭쳐로 접근할 수 있다. (또는 수동으로 접근하든지, webhook으로 접근하든지.)
https://<your-site.com>/api/revalidate?secret=<token>
그 다음, 해당 시크릿을 애플리케이션의 환경 변수로 저장한다. 그리고 revalidation API Route를 생성한다.
// pages/api/revalidate.js
export default async function handler(req, res) {
// 유효한 리퀘스트인지 확인하기 위해 시크릿을 확인한다
if (req.query.secret !== process.env.MY_SECRET_TOKEN) {
return res.status(401).json({ message: 'Invalid token' })
}
try {
// 이는 다시 작성된 pathrk dkslfk, 실제 유효한 path여야 한다
// e.g. for "/blog/[slug]" 는 "/blog/post-1" 과 같은 형태여야 한다.
await res.revalidate('/path-to-revalidate')
return res.json({ revalidated: true })
} catch (err) {
// 만약 이 과정에서 에러가 발생할 경우, next.js는
// 가장 마지막으로 성공적으로 생성된 페이지를 보여준다
return res.status(500).send('Error revalidating');
}
}
next dev
명령으로 로컬에서 실행중일 경우, getStaticProps
가 매 요청마다 실행된다. 온디맨드 ISR 설정이 맞는지 확인하고 싶다면, next build
와 next start
명령어를 통해 프로덕션 빌드 후 프로덕션 서버를 켜서 확인할 수 있다. 그러면, 정적 페이지들이 성공적으로 revalidate 되고 있는지 확인할 수 있다.
만일 background regeneration 과정에서 getStaticProps
안에 에러가 있을 경우, 가장 최근에 성공적으로 생성된 페이지가 계속해서 보여지게 된다. 다음에 뒤따르는 요청에서, next.js는 getStatidProps
호출을 재시도한다.
export default function getStaticProps() {
// 만약 해당 요청이 uncaught 에러를 발생시키면, next.js는
// 지금 보여지고 있는 페이지를 무효화하지 않는 대신
// 다음 요청때 getStaticProps를 재시도한다
const res = await fetch('https://.../posts');
const posts = await res.json();
if (!res.ok) {
// 만약 서버에러가 발생할 경우, 다음번 성공적인 리퀘스트 전까지
// cache가 업데이트 되지 않을 수 있도록
// 리턴 대신 에러를 던지고 싶을 때가 있다
throw new Error(`Failed to fetch posts, received status ${res.status}`);
}
// 만약 리퀘스트가 성공하면, 포스트를 리턴하고
// 매 10초마다 revalidate한다
return {
props: {
posts,
},
revalidate: 10,
}
}
아래의 예시들은 온디맨드 또는 배경 작업으로서 Build Output API나 next.js와 함께 ISR을 어떻게 사용하는지를 보여준다.
아래의 예시는 next.js 페이지를 다이나믹 라우트를 통해 ISR을 사용해서 생성하는 법을 알려준다. getStaticProps
는 Vercel에 배포될 경우 ISR 렌더 함수 (서버리스)로 사용된다.
export default function Page({ posts }) }
return posts[0].title;
}
export async function getStaticProps() {
return {
props: {
posts: [
{
title: 'Hello World!',
},
],
},
},
revalidate: 10, // In seconds
}
export async function getStaticPaths() {
return {
paths: [
{
params: { id: '1' },
},
],
fallback: 'blocking',
}
}