Next.js + S3 버킷 정적 배포 관련 이슈 해결기 (feat. SSG)

💛 nalsae·2023년 12월 12일
4

🔥 트러블 슈팅

목록 보기
5/7
post-thumbnail

 이번 글에서는 "Grow Story" 프로젝트를 진행하던 중에 마주친 이슈와 그 해결 과정에 대해 소개하고자 한다.

🐛 문제 발생 배경

 본격적인 개발에 앞서 임시 라우팅까지 구현해놓고 배포 테스트를 하던 때였다. 당시 우리 팀은 S3 버킷을 이용한 프론트 배포를 염두에 두고 있었다. 사실 지금 돌이켜보면 프로젝트 성격만 잘 고려하여 배포 방법을 정했다면 마주치지 않았을 이슈였던 것 같다. 하지만 그 당시 Next.js를 거의 처음 경험해보는 입장이었기 때문에 SSR, SSG 등의 개념에 무지하여 이번 글에서 소개할 이슈를 마주하게 되었다.

 "Grow Story" 프로젝트는 Next.js 13 버전의 App Router 방식으로 라우팅을 구현했다. 따라서 위의 사진과 같이 app 폴더 하위에 라우팅을 위한 페이지 폴더를 위치시켰다. 여기까지는 아무 문제 없이 순조롭게 개발을 진행할 수 있었다. 문제는 테스트 배포 시 발생했다.

 Next.js는 기본적으로 빌드 시에 프로젝트에 포함된 라우트별 tsx 파일을 정적 HTML 파일로 생성한다. 이를 위해 GitHub Pages를 이용하여 CI/CD를 정상적으로 완료했고, S3 버킷에 빌드 파일이 업로드되는 것도 확인했다. 그런데 실제 배포 URL에서 테스트해보니 몇몇 페이지에 접속하면 404 에러가 발생하는 것이었다. 그리고 404 에러가 발생하는 페이지들의 공통점은 동적 라우팅을 적용했다는 것이다.


🔨 해결 과정

📌 서버리스 함수를 이용해볼까?

 그렇다면 동적 라우팅을 적용한 페이지들의 경우 빌드 시에 서버측에서 먼저 생성할 방법이 없는 것일까? 문제 해결을 위해 서버리스 함수를 찾아보기 시작했다. SSG를 위해서는 Next.js 12버전의 getStaticPathsgetStaticProps를 활용해볼 수 있겠다. Next.js 13버전에서는 getStaticPaths의 경우 generateStaticParams로, getStaticProps의 경우 Next.js에 자체적으로 내장된 fetch API의 두 번째 인수로 cache를 설정하는 것으로 변경되었다. 공식 문서에서 발췌한 자세한 내용은 다음과 같다.

fetch('https://url', option)

export default async function Page() {
 // This request should be cached until manually invalidated.
 // Similar to `getStaticProps`.
 // `force-cache` is the default and can be omitted.
 const staticData = await fetch(`https://...`, { cache: 'force-cache' });

 // This request should be refetched on every request.
 // Similar to `getServerSideProps`.
 const dynamicData = await fetch(`https://...`, { cache: 'no-store' });

 // This request should be cached with a lifetime of 10 seconds.
 // Similar to `getStaticProps` with the `revalidate` option.
 const revalidatedData = await fetch(`https://...`, {
   next: { revalidate: 10 },
 });

 return <div>...</div>;
}

 이를 활용하여 다음과 같은 방식으로 해결을 시도해보았다.

// 빌드 시 최초 1회만 호출되어 서버로부터 응답 받은 값에 따라 정적인 파라미터를 생성
export async function generateStaticParams() {
 const data = await fetch('http://localhost:9999/user');

 const users = await data.json();

	// 반환 값은 항상 { 파라미터 이름: 값 } 객체를 담은 배열 형식이어야 함
	// ex) [{ id: 1 }, { id: 2 } ... ]
 return users.map((user: { id: number }) => ({ id: String(user.id) }));
}

// 빌드 시 생성된 모든 정적 파라미터에 해당하는 html 파일 생성
// ex) garden/1.html, garden/2.html ...
export default async function Garden({
 	params: { id },
 }: {
   params: { id: string };
 }) {
 return <div>정원 {id} 페이지입니다!</div>;
}

 시도 결과 위와 같이 SSG 방식으로 빌드 시에 데이터를 페칭하여 1~10까지의 사용자에 해당하는 정원 페이지를 생성할 수 있었다. 그러나 문제는 "Grow Story" 정원 페이지의 특성 상 실시간으로 사용자가 회원가입을 할 때마다 새롭게 페이지가 생성되어야 한다. 하지만 위와 같은 서버리스 함수는 빌드 시에 서버측에서 1회 호출된다. 결국 실시간으로 동적 라우팅을 적용한 페이지를 생성할 수 없다는 본질적인 문제가 해결되지 않은 것이다. 여기서 프로젝트의 성격을 다시 한 번 고찰해보는 시간을 가지게 되었다. 그 결과 어차피 사용자와 상호작용해야 하는 페이지가 많을 수밖에 없는 구조라면 배포 방식을 변경해야 할 것 같다는 결론이 나왔다. S3 버킷은 정적 배포 방식이기 때문에 실시간으로 페이지를 생성해야 하는 프로젝트 특성상 한계점이 명확할 수밖에 없기 때문이다.

💡 배포 방식을 변경하자!

 결국 맹점은 Next.js에서 빌드 시에 백엔드 서버로 요청을 보낼 프론트 서버가 필요하다는 것이었다. 이를 위해서는 클라이언트를 위한 서버를 하나 더 생성해야 하는데, 백엔드의 부담을 늘리고 싶지 않기도 했거니와 비용 문제가 마음에 걸렸다.

 그래서 결론적으로 프론트 서버를 지원하는 Vercel을 배포 툴로 선택했고, 이번 글의 주제인 동적 라우팅 페이지 생성 이슈를 말끔히 해결할 수 있었다. 기존에 활용하고자 했던 S3 버킷에는 사용자의 프로필 이미지, 식물 카드나 게시글 이미지 등 동적으로 사용자가 등록하는 리소스를 저장하는 방식으로 운용하게 되었다.


🤔 느낀 점

 결국 뭐든지 설계가 중요함을 이번에도 뼈저리게 느낀다. 설계 과정에서 사용자와의 상호작용이 많다는 서비스 특성을 고려했다면 애초에 배포 툴로 Vercel을 선택해서 시간 낭비를 줄일 수 있었겠다는 아쉬움이 크다. 또한 시간 부족으로 프로젝트에 바로 도입하느라 Next.js의 동작 방식을 잘 이해하지 못했다는 것도 불찰이다. 기술 스택 도입 전에 관련 개념과 동작 방식에 대한 이해가 선행되어야 함을 느꼈다.

profile
𝙸'𝚖 𝚊 𝚍𝚎𝚟𝚎𝚕𝚘𝚙𝚎𝚛 𝚝𝚛𝚢𝚒𝚗𝚐 𝚝𝚘 𝚜𝚝𝚞𝚍𝚢 𝚊𝚕𝚠𝚊𝚢𝚜. 🤔

0개의 댓글