generateStaticParams 이용해 SSR 에서 SSG로 넘어가자
개발 블로그 개발 여정
nextjs
Thu Jun 13 2024
이 글은 개발 블로그 개발 여정의 게시글이예요
해당 포스트는 NextJS를 이용하여 개발 블로그를 만들며 작성한 포스트입니다.
기술 블로그의 전체 코드는 🪢 yonglog github 에서 확인 하실 수 있습니다.
- 1 . 개발블로그 제작을 시작하며
- 2 . CI/CD 파이프라인 구성하기
- 3 . tailwind 환경 설정 및 디자인 레퍼런스 찾기
- 4 . / 경로 레이아웃 , 페이지 디자인 생성하기
- 5 . [BUG] Build 시 발생하는 typeError 해결하기
- 6 . MDX를 사용하기 위한 라이브러리들 설치 및 환경 설정
- 7 . Post들이 저장된 FileSystem 파싱하기
- 8 . PageNavigation UI 만들기
- 9 . tag , series Identifier 만들기
- 10 . / 경로 UI 디자인 하기
- 11 . 서버 컴포넌트에 Dynamic Routing 추가하기
- 12 . Post/page.tsx 생성하기
- 13 . 마크다운 파일 코드블록 꾸미기
- 14 . [BUG] Vercel 에 환경 변수 추가하기
- 15 . Loading Suspense 구현하기
- 16 . generateStaticParams 이용해 SSR 에서 SSG로 넘어가자
- 17 . SSG를 이용한 블로그에서 테마 변경하기
- 18 . 인터렉티브한 사이드바 만들기
- 19 . 기술블로그에 giscus를 이용하여 댓글 기능을 추가하기
- 20 . 기술 블로그의 SEO를 최적화 하기 위한 방법 Part1
- 21 . 기술 블로그의 SEO를 최적화 하기 위한 방법 Part2
- 22 . 바닐라 자바스크립트로 깃허브 OAuth 를 구현해보자
- 23 . 라이브러리 없이 깃허브 API를 이용해 댓글창을 구현해보자
- 24 . 리팩토링 : Promise 패턴을 이용하여 비동기 처리중인 전역 객체에 접근하기
- 25 . NextJS로 만든 기술블로그에서 검색 기능 구현하기
현재 상황을 먼저 짚고 가보자
현재 블로그 포스트 글에 대한 postId
주소로 다이나믹 라우팅 시켜 mdx
파일들을 이용해 페이지를 렌더링 하는 컴포넌트이다.
해당 컴포넌트는 dynamic rendering
을 사용하고 있기 때문에 매번 라우팅 될 때 마다 라우팅 경로에 따라 컴포넌트가 호출된다.
이로 인해 어떤 문제가 발생했었는지 생각해보자
Suspense 컴포넌트 도입 전 발생하던 문제
Suspense 컴포넌트 도입 전
[postId]
경로에 접근했을 때 해당 포스트에 대한 서버에서의 렌더링 결과를 받기 전까지 loading.tsx
가 렌더링 되도록 해줬을 때
page.tsx
의 서버 렌더링 결과가 끝나고 클라이언트한테 요청이 도착하기 전까지 loading.tsx
가 먼저 렌더링 되고
page.tsx
가 클라이언트에게 도착했을 때, 화면이 잠시 멈춰있다가 page.tsx
의 렌더링 결과가 나타나는 모습 을 볼 수 있다.
화면이 왜 잠시 멈춰있는가 생각을 해본다면 문제는 page.tsx
가 서버에서 렌더링 된 후 클라이언트에게 렌더링 결과인 RSCP
를 넘기고 RSCP
를 클라이언트 창에서 호출하여 클라이언트의 인터페이스를 변경 할 때 발생한다.
MDXRemote
컴포넌트에서 rehype-pretty-code
에서 코드 블록을 포맷팅 하는 단계에서 전체 렌더링을 block
하여 코드 블록이 포맷팅 되는 동안 이런 문제들이 발생하고 있었다.
저번 게시글에서 이야기하였지만
rehype-pretty-code
에선 비동기적으로 테마를 가져오거나 , 코드 문법에 따라 포맷팅을 다르게 하기 위해 코드에 대한 정보를 가져오는 비동기 과정이 존재한다.
MDXRemote
컴포넌트 자체는 동기적인 컴포넌트이지만 내부 함수에서async/await
혹은Promise chaining
으로 이뤄진 부분이 모두resolve
되어야 컴포넌트 호출이 완료되기 때문에 이런block
과정이 존재했던 것으로 생각한다.
Suspense 컴포넌트를 도입하여 로딩 상태를 렌더링 하도록 했다.
클라이언트의 화면에서 아무런 화면이 뜨지 않고 빈 화면만 렌더링 하는 것은 매우 치명적인 단점이다.
그래서 나는 MDXRemote
컴포넌트의 렌더링이 모두 완료되기 전에 로딩 상태를 보여줄 컴포넌트를 이용해 Suspense
컴포넌트로 감싸주었다.
Suspense를 도입한 후의 모습, 초기 로딩 상태가 렌더링 되고 있다.
그렇다면 모든 문제가 해결된 것일까 ?
사실은 그렇지 않다. Suspense
컴포넌트를 도입하여 렌더링 전에 보여줄 컴포넌트를 우선적으로 보여줌으로서 UX
는 높혔을 지 모르지만
여전히 게시글의 내용이 보여지기 까지 매우 긴 로딩 시간이 존재한다는 사실은 동일 하다.
왜 이런 문제가 발생했을지 생각해보자 : 다이나믹 렌더링의 한계점
이는 [postId]/page.tsx
의 렌더링 방식인 다이나믹 렌더링의 한계로 인해 발생한다.
라우팅 경로인 postId
에 따라 file system
에서 mdx
데이터를 가져오는 getPostContent
도 무거운 작업이고
가져온 mdx
파일을 MDXRemote
컴포넌트로 클라이언트 단에서 호출하는 작업은 더욱 무거운 작업이다.
이렇게 무거운 작업을 클라이언트가 요청 할 때 마다 수행하는 행위는 매우 비효율적인 작업 방식이다.
다이나믹 렌더링이 필요한 상황에는 적절한 방식이겠지만 build time
시 렌더링 시 사용할 mdx
파일들이 미리 정해져있는 블로그에서 해당 방식은 비효율적인 방법이다.
차라리 build time
시 모든 처리들을 미리 해두고 , 클라이언트의 요청이 일어날 때 마다 미리 준비해둔 정보들을 이용해 렌더링을 처리하는 방식이 훨씬 맞을 것이다.
어떻게 해결할까 ? : generateStaticParams
generateStaticParams
기본적으로 page.tsx
파일은 props
로 해당 경로의 params , searchParameter
를 동적으로 받는다.
app/[postId]/page.tsx
에서 /1
에 대한 request
가 서버 측에 도달했다면 서버에서 렌더링 되는 page.tsx
는 {params : 1}
을 props
로 받았을 것이다.
이렇게 동적으로 시행되는 라우팅에 대해 렌더링 하는 방식이 dynamic routing , rendering
방식이다.
그렇다면 나의 블로그의 경우엔 해당 방식이 올바른 방식인지 생각해볼 필요가 있다.
내 블로그 게시글들은 결국 게시글에 사용 될 postId
경로에 대한 라우팅만 지원을 할 것이다.
그러니 모든 경로에 대해 적용되는 다이나믹 라우팅 이 아닌 postId
에 대해서만 라우팅을 지원하면 된다.
이는 generateStaticParams
메소드를 이용해 해결 할 수 있다.
🪢 NextJS-generateStaticParams API
generateStaticParams
메소드에 대한 공식문서의 설명을 살펴보자
During
next dev
, generateStaticParams will be called when you navigate to a route.During
next build
, generateStaticParams runs before the corresponding Layouts or Pages are generated.
개발 모드에선 잘 작동하지 않지만, 배포시에 layout.tsx , page.tsx
에게 제공 할 params
들을 모두 생성해둔다고 한다.
즉 , generateStaticParams
는 build time
때 라우팅 경로를 미리 생성해둔다.
??? : ㅋㅋㅋ 엥 ? 아니 정적으로 경로들 미리 생성해두는게 무슨 의미가 있나요?
generateStaticParams
로 생성해둔 라우팅 경로의 장점은 build time
때 미리 생성해둔 라우팅 경로들에 대해서 pre-render
를 해두고 static params
에 대한 접근 시 pre-render
해둔 페이지를 반환한다는 것이다.
이렇게 build time
때 미리 페이지들을 생성해두는 방식을 Static Server Generation (SSG)
이라고 한다.
이전
page routing
방식에선getStaticPath , getStaticProps
메소드 등을 이용해SSG
를 구현해뒀다면
app routing
방식에선generateStaticParams
로 정적 라우팅 경로들을 생성해두기만 하면build time
시pre-render
해두는 방식으로 변경되었다.
SSG 기능 추가 전 지표들을 살펴보기
기능 추가 전,후로 성능을 얼마나 향상 시켰는지를 확인하기 위해 지표들을 설정해두자
SSG 기능 추가 전 화면의 모습
SSG 기능 추가 전 전체 네트워크 소요 시간
generateStaticParams
는 배포 모드일 때에만 작동되기 때문에 현재 vercel
에 배포된 게시글을 대상으로 지표를 삼아봤다.
서버에서 page.tsx
를 렌더링 하여 RSCP
를 생성하고 클라이언트에게 보내는기까지 걸리는 시간 476.71ms
, 클라이언트 단에서 RSCP
를 이용해 페이지를 띄우는데까지 걸리는 시간 3820ms
, 총 4.3 초정도의 시간이 걸렸다.
해당 포스트를 작성 하기 전 블로그의 배경색을 변경해주었기에 위 이미지들과 배경색이 다를 수 있다. :)
generateStaticParams 추가하기
다음과 같이 generateStaticParams
란 함수를 page.tsx
에서 정의해주고 export
로 해당 모듈을 보내준다.
generateStaticParams
에서 반환하는 반환값은 해당 페이지에서 이용할 params
에 정적으로 할당 할 값들을 {key : string}
형태로 정의한 배열값들이여야 한다.
나는 이전에 정의한 getAllPost
로 모든 포스트들을 가져오고 모든 포스트들의 postId
들을 정적으로 정의해주었다.
이렇게 정적으로 정의된 라우팅 경로들에 대해선 build time
시 생성 되며, 다이나믹 라우팅 시 정적으로 정의 된 경로에 대한 요청이라면 pre-render
해둔 페이지를 반환 할 것이다.
vercel
에 재배포 한 후 잘 작동하는지 확인해보자
generateStaticParams
는 라우트 경로가 여러개인multiple dynamic
에서도 사용 가능하다. (예를 들어[series]/[postId]
와 같이)이렇게 라우팅 경로가 여러개이면서 각 경로마다
generateStaticParams
를 이용한다면child segment
는parent segment
의generateStaticParams
가 호출 될 때 마다 매 번 호출된다고 한다.
vercel 의 build log 를 살펴보자
vercel의 프로젝트 build log
vercel
에선 배포 하기 위한 과정을 Build Logs
창에서 확인 할 수 있는데 해당 창을 보면 게시글 18개들에 대해서 SSG
가 일어난 모습을 볼 수 있다.
나는 앱 라우팅 방식이라 generateStaticParams
를 이용했으나 page routing
방식에서 사용하는 getStaticPath , getStaticProps
등을 이용해 pre render
되었다고 한다.
로깅된 결과에선
static HTML
문서를 생성해뒀다고 하지만reqeust , response
과정에선HTML
문서로 빠르게 변환 가능한RSCP
파일을 주고 받는다.클라이언트 측에서 실행했어야 할
RSCP
번들을 서버 측에서 미리 실행해 생성해둔RSCP
파일을 보내준다.
아마 app routing
방식이 보일러 플레이트 코드가 많은 page routing
방식의 sugar syntax
버전이기에 generateStaticParmas
도 getStaticPath , getStaticProps
로 변환되어 배포되는 듯 하다.
SSG 기능 추가 후 라우팅 시 모습 SSG 기능 추가 후 네트워크 시간
진짜 말도 안되게 빨라졌다.
기능 추가 전 wating for server response
, 즉 클라이언트가 다이나믹 라우팅 방식으로 요청한 postId
에 대해서 서버에서 렌더링 하는 시간은 476ms
였다.
하지만 SSG 기능 추가 후에는 9.37ms
로 50배나 빨라졌다.
아무래도 다이나믹 렌더링 시에는 매 렌더링 마다 getAllPost
를 호출하는 getPostContent
메소드를 호출한 후 렌더링 하기 때문에 긴 시간이 걸렸으나
SSG
로 구성되어 있는 페이지의 경우엔 단순히 미리 생성해둔 페이지를 건내주면 되기 때문에 시간이 매우 단축되었다.
Content download
시간 , 즉 서버에서 생성한 RSCP
가 클라이언트의 인터페이스에 렌더링 되기 까지의 시간은 SSG
도입 전엔 3820ms
였으나 현재는 0.65ms
이다. 약 3800
배나 빨라졌다.
MDXRemote
에서 mdx
파일을 rehype-pretty-code
를 이용해 반환하는데 걸리는 시간이 build time
때 모두 수행되었기 때문에 시간이 매우 단축 된 것으로 보인다.
결국 SSG
를 이용하면 서버에서의 렌더링 시간이 없기 때문에 단순히 네트워크가 오고 가는 시간 + a(SSG 파일을 처리하는 시간) 만큼의 시간만 소요되고
즉시 클라이언트의 인터페이스에서 렌더링 되기 때문에 시간이 크게 단축된다.
전체 소요시간은 다이나믹 렌더링 시 4300ms
에서 SSG
에서 12.57ms
로 342배나 빨라졌다.