블로그를 만들기 시작하며 가장 먼저 들었던 생각이 ‘포스팅을 작성하거나 수정할 때마다 빌드를 다시 하고싶지 않다’ 였다. 그렇다고 블로그에서 SEO를 포기하고 client-side rendering 페이지를 만들 수는 없었다. 한편 server-side rendering (getServerSideProps를 활용한) 페이지는 매번 최신의 데이터를 보여줄 수야 있겠지만, ‘거의’ 바뀌지 않을 페이지를 모든 요청 마다 새로 렌더링 하는 건 서버 자원의 낭비다.
다행히 Nextjs에는 ISR 방식이 있었고, **v12.2.0
**부터는 ‘On-Demand ISR’을 제공하고 있었다. 결론적으로는 On-Demand ISR을 활용해서 내가 원하는 방식으로 블로그를 구현할 수 있었다.
ISR, Incremental Static Regeneration
‘점진적 정적 재생성’ 정도로 해석할 수 있을 것 같다. 의미 그대로 최초 빌드 시에 생성된 정적 페이지(SSG)를 시간이 흐르고 나서 다시 생성 하는 방식이다. ISR 방식으로는 일부분의 수정을 위해 전체 앱을 재빌드 할 필요없이 ‘페이지 단위’의 정적 페이지 재생성이 가능한 것이다.
근본적으로는 SSG이기 때문에 동일하게 getStaticProps를 사용하면 된다. 거기에 revalidate라는 prop만 추가하면 되는데 ‘트리거 발생 후 얼마 뒤에 재생성을 진행할지’ 시간을 설정하면 된다.
이때 재생성의 트리거는 해당 페이지에 대한 요청, 즉 해당 페이지에 트래픽이 발생한 시점이다. 따라서 아래의 예시는 ‘방문자가 해당 페이지에 접근하고 나서 60초 뒤에 해당 페이지를 재생성 한다’를 의미한다.
export async function getStaticProps() {
const res = await fetch('https://.../posts');
const posts = await res.json();
return {
props: {
posts,
},
revalidate: 60,
};
}
실제 동작 과정은 다음과 같을 것이다.
- 페이지에 방문자가 발생하면 최초 빌드 시에 생성되어 캐싱 된 정적 페이지가 서빙 된다.
- 60초 이내로 또다른 방문자가 발생해도 여전히 같은 페이지가 서빙 된다.
- 60초가 지나면 nextjs가 백그라운드에서 해당 페이지를 재생성 한다.
- 새로운 페이지가 생성되면 기존 캐시를 무효화하고, 재생성된 페이지가 서빙 된다.
- (새로운 방문자 발생하면 위의 과정을 반복)
트래픽이 발생한 시점에만 재성성이 트리거 되니 서버 리소스 사용이 줄어 합리적인 방식이라고 할 수 있다. 그러나 여전히 탐탁치 않은 지점들이 있었다.
- 트리거를 유발한 최초 방문자는 revalidate 시간이 지나고 새로고침을 하지 않으면 재생성 된 페이지를 볼 수 없다.
- 페이지에 업데이트 된 내용이 전혀 없는 경우에도 트래픽이 발생하여 조건을 충족하면 계속해서 동일한 페이지를 재생성 해야 한다. 서버 자원의 낭비다.
사실 1번의 경우 글을 수정한 다음 내가 해당 페이지를 한 번 방문하면 다음 방문자들에게 업데이트는 반영 되겠지만, 여전히 2번 문제는 피할 수가 없다.
내가 원하는 블로그를 만들기에는 반쪽 짜리 해결책인 것 같다.
On-Demand ISR
ISR인데 on-demand라면, 요청이 있을 때만 정적 페이지를 재생성할 수 있을 것 같았고, 실제로도 그랬다.
그럼 그 재생성 ‘요청’을 어떻게 서버에 전달할까? 당연하게도 api를 통해서 가능하다.
// pages/api/revalidate.js
export default async function handler(req, res) {
try {
await res.revalidate('/path-to-revalidate');
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).send('Error revalidating');
}
}
이처럼 api를 작성해주면 이제 /api/revalidate 를 호출하는 것만으로 해당 페이지를 재생성 할 수 있게 된다. (현재는 필요 시 클라이언트에서 직접 revalidate api를 호출하도록 해놨지만, 웹훅을 사용하여 DB에 업데이트가 발생하면 api를 호출하는 방식으로 변경해도 좋을 것 같다)
동작 순서는 다음과 같다.
- 포스트 수정 후, revalidate api 호출
- nextjs가 백그라운드에서 해당 페이지를 재생성
- 페이지 생성이 완료되면 기존 캐시를 무효화하고 새로운 페이지 서빙
on-demand ISR을 활용하면 직접 api를 호출하는 경우 외에는 페이지를 재생성 할 일이 없으니 서버 자원도 필요한 때에만 사용할 수 있다.
이제 정적 페이지 생성을 통해 빠른 서빙과 SEO를 챙길 수도 있고, 새로운 데이터를 추가하거나 부분적인 수정 사항이 생겨도 전체 앱을 재빌드 할 필요도 없어졌고, 서버 자원의 낭비 없이 필요한 페이지만 업데이트 할 수도 있다.
getStaticPaths Fallback
ISR을 적용할 때 고려해야할 부분이 하나 더 있다.
기존에 존재하던 포스트를 수정하는 경우 해당 페이지의 경로가 getStaticPaths에 의해 최초 빌드 시 정적 생성되어 있으니 문제가 없지만, 새롭게 포스트를 추가하는 경우 해당 포스트의 경로는 빌드 시점에 getStaticPaths에 의해 생성된 경로 목록에 존재하지 않는다.
따라서 getStaticPaths의 fallback 설정을 통해 ISR로 새롭게 추가된 경로에 접근 했을 때 화면 처리를 어떻게 할 것인지에 대한 적절한 설정이 필요하다.
- fallback:
false
최초 빌드 시 getStaticPaths에 포함되지 않은 모든 경로에 대한 요청에 404 페이지를 응답한다. 새로운 포스트 작성 후 재빌드 없이 해당 경로에 접근할 수 있어야 하는 현재 목적과 맞지 않다. - fallback:
blocking
getStaticProps에 포함되지 않은 경로인 경우, 백그라운드에서 해당 페이지의 렌더링(SSR) 진행 후 완료되어 브라우저에 html이 응답될 까지 그 상태로 대기하다가 완성된 페이지를 보여 준다. - fallback:
true
페이지 컴포넌트에서는 next router의 isFallback 프로퍼티를 활용하여 fallback 버전의 화면을 구성할 수 있다. getStaticProps에 포함되지 않은 경로인 경우, 이 fallback 버전의 페이지를 보여주다가 브라우저가 해당 페이지의 JSON을 응답받으면 해당 데이터로 페이지를 다시 그린다.
blocking
, true
둘 다 최초 동작 하여 경로를 추가한 이후로는 정적 경로 목록에 포함되기 때문에 이후부터는 기존에 정적 생성된 페이지들과 동일하게 동작한다.
이 블로그의 경우 처음에는 아무것도 화면에 표시되지 않은 채 기다려야하는 사용자 경험을 고려해서 fallback을 true로 설정하고 로딩 문구를 보여주도록 했다. 그런데 다시 생각해보니 이미 revalidate가 완료된 시점에 해당 페이지는 재생성이 되었을 터였다. 따라서 없는 경로에 대한 요청이 발생하더라도 해당 페이지가 완성될 때까지 기다리는 시간이 거의 필요없기 때문에 오히려 blocking 모드로 어떤 화면 전환도 발생하지 않도록 하는 것이 더 좋겠다는 판단이 들었다.
피드백은 언제나 감사합니다.
오류 정정, 질문, 토론거리를 자유롭게 댓글로 남겨주세요.
블로그 소스코드에 대한 리뷰, PR도 적극 환영합니다!
► Github Repository 바로가기