블로그를 처음 시작할 때, SEO의 장점을 얻기 위해 SSR(Server Side Rendering) 방식으로 페이지를 생성하는 것이 좋다고 생각했습니다. 하지만 Vercel의 Edge Function과 캐싱을 적극 활용하는 SSR을 구현했지만, Vercel의 Cold Start 문제로 인해 첫 실행 시 페이지 로딩 속도가 느려지는 문제가 있었습니다.
그래서 블로그 포스팅 페이지를 정적 생성 방식으로 전환하기로 결정했습니다. 단순 정적 생성 방식(SSG)는 페이지를 미리 생성하여 서버에서 제공하는 방식으로, 페이지 로딩 속도가 빠르고 SEO에도 유리합니다. 하지만 이 방식은 페이지가 추가되는 경우마다 전체 페이지를 다시 빌드해야 하므로, 빌드 시간이 길어지는 단점이 있었습니다. 그래서 Next.js의
ISR(Incremental Static Regeneration)
기능을 활용하여 정적 페이지를 생성하되, 일정 시간마다 백그라운드에서 자동으로 갱신하는 방식을 선택했습니다. 자세한 내용은 Next.js의 Incremental Static Regeneration(ISR) 완벽 가이드를 참고하시면 좋습니다.
그 결과, 페이지 로딩 속도가 크게 개선되고, SEO에도 유리한 효과를 얻을 수 있었습니다. 또한, 페이지가 추가되더라도 전체 페이지를 다시 빌드할 필요가 없어 빌드 시간이 단축되었습니다. 이 포스트에서는 블로그 포스팅 페이지를 ISR로 전환한 이유와 효과, 그리고 빌드 에러를 해결한 방법에 대해 자세히 설명하겠습니다.
기존의 SSR(Server Side Rendering) 방식은 페이지를 요청할 때마다 서버에서 데이터를 가져와서 렌더링하는 방식입니다. 간략히 설명하자면, generateMetadata와 getPost 메서드를 사용하여 포스트의 메타데이터와 내용을 가져오는 방식입니다. 이 방식은 SEO에 유리하지만, edge function의 서버 상태에 따라 페이지 로딩 속도가 느려지는 문제가 있었습니다.
export async function generateMetadata(props: { params: Promise<{ post: string }>;}): Promise<Metadata> { const params = await props.params; const markdownsource = await PostService().getPostMeta({ // 포스트 메타데이터 가져오기 post_id: params.post, }); return GenerateMeta({ meta: markdownsource, param: params.post });}const Post = async ({ params }: { params: Promise<{ post: string }> }) => { const { post } = await params; const [markdownsource] = await Promise.all([ PostService().getPost({ post_id: post }), // 포스트 내용 가져오기 PostService().viewIncrement({ post_id: post.split("-")[0] }), // 포스트 조회수 증가 ]); return ( <> {/* ... */} </> );};export default Post;
하였습니다. 이 방식은 페이지를 정적으로 생성하되, 일정 시간마다 백그라운드에서 자동으로 갱신하는 방식입니다. 이를 통해 페이지 로딩 속도를 개선하고, SEO에도 유리한 효과를 얻을 수 있었습니다.
데이터를 가져오는 방식은 기존과 동일하게 유지하되, generateStaticParams 메서드를 사용하여 동적으로 생성할 포스트의 경로를 지정합니다. 이 메서드는 빌드 시에만 실행되며, 이후에는 ISR 방식으로 페이지를 갱신합니다. 또한, generateMetadata 메서드는 여전히 서버 측에서 실행되므로 SEO에도 유리합니다. 저는 relativedate를 2일로 설정하여 2일마다 페이지를 갱신하도록 설정하였습니다. 이 설정은 블로그의 특성에 따라 조정할 수 있습니다.
이제 포스트 페이지는 다음과 같은 구조로 변경되었습니다.
/app/blog/[post]/page.tsx
export const revalidate = 172800; // 2 daysexport const dynamicParams = true; // 동적 파라미터 사용export async function generateStaticParams() { const Links = await PostService().getLinks(); // param 목록 가져오는 API 호출 const posts: string[] = []; Links.map((category) => { // 내부 로직을 통해 param 목록을 생성 category.posts.map((post) => { posts.push(post.post_id); }); }); return posts.map((post) => ({ post: post, }));}// generateMetadata는 유지const Post = async ({ params }: { params: Promise<{ post: string }> }) => { const { post } = await params; const markdownsource = await PostService().getPost({ post_id: post.split("-")[0], }); return ( <> <ViewIncrement postId={post.split("-")[0]} /> {/* CSR 컴포넌트로 분리 */} {/* ... */} </> );};export default Post;
제 페이지에는 좌측에 Google Analytics API를 통해 조회수를 보여주는 컴포넌트가 있습니다. 이 컴포넌트는 페이지가 로드될 때마다 조회수를 증가시키고, Google Analytics API를 호출하여 조회수를 가져오는 방식으로 구현되어 있었습니다. 하지만 ISR로 전환하면서 이 컴포넌트에서 문제가 발생했습니다. 바로
이 에러는 Google Analytics API의 동시 요청 수를 초과했을 때 발생하는 에러입니다. ISR로 전환하면서 페이지가 빌드될 때마다 Google Analytics API를 호출하게 되어, 동시 요청 수를 초과하게 된 것입니다. 이 문제를 해결하기 위해서는 Google Analytics API를 클라이언트 측에서 호출하도록 변경해야 합니다.
빌드 시점에 Promise.all을 사용하여 비동기 처리를 하던 부분에서 에러가 발생했습니다. 이 문제는 180개가 넘는 요청을 Promise.all로 처리하면서 발생했습니다. 이 문제를 해결하기 위해서는 Promise.all을 사용하지 않고, 각 요청을 순차적으로 처리하는 방식으로 변경해야 합니다. 하지만 이 경우에는 성능 저하가 발생할 수 있으므로, 비동기 처리를 적절히 조절하는 것이 중요합니다.
저는 사용자가 포스트를 요청할 때마다 조회수를 증가시키는 로직을 클라이언트 컴포넌트로 분리하여, 페이지 로딩 시점에 클라이언트 사이드로 요청하도록 변경하였습니다. 이렇게 하면 페이지 로딩 시점에 비동기 처리를 하지 않게 되어, 빌드 시점에 발생하는 에러를 해결할 수 있었습니다.
const markdownsource = await PostService().getPost({ post_id: post.split("-")[0] });<ViewIncrement postId={post.split("-")[0]} />; // 포스트 조회수 증가를 클라이언트 컴포넌트로 분리
Vercel에서 빌드 시간을 측정해보니, 기존 SSR 방식에서는 약 98초가 소요되었으나, ISR 방식으로 전환한 후에는 132초로 층가하였습니다. 하지만, 전체 페이지를 다시 빌드할 필요가 없으므로, 페이지가 추가되더라도 빌드 시간이 크게 증가하지 않습니다. 또한, ISR 방식은 페이지를 정적으로 생성하므로, 페이지 로딩 속도가 빨라지는 효과도 있습니다.
이번 ISR 전환으로 인해 페이지의 성능이 크게 개선되었습니다. 특히, 사용자 경험이 향상되었으며, SEO 최적화에도 긍정적인 영향을 미쳤습니다. 추가적으로, 데이터 업데이트 주기를 조정하여 더 나은 사용자 경험을 제공할 수 있게 되었습니다.
SSR 상태의 좌측 페이지, ISR을 적용한 페이지를 비교해보면, 페이지 로딩 속도가 크게 개선된 것을 확인할 수 있습니다. 아래 이미지는 ISR을 적용한 페이지의 성능을 보여줍니다.
SSR 방식에서는 페이지를 요청할 때마다 서버에서 데이터를 가져와서 렌더링하는 방식이었습니다. 이로 인해 페이지 로딩 속도가 느려지는 문제가 있었습니다. 아래 이미지는 SSR 방식의 성능을 보여줍니다.
서버 응답 대기시간만 4.12초가 소요되었습니다. 이로 인해 페이지 로딩 속도가 느려지는 문제가 있었습니다.