← 목록으로 돌아가기

Tina CMS API 과부하 해결기: ISR로 캐싱 최적화

Tina CMS Next.js ISR 캐싱

Next.js ISR과 웹훅으로 API 호출량 줄이고 캐싱 성능 개선하기

이전 글에서 Tina CMS를 도입해 컨텐츠 관리 워크플로우를 개선한 이야기를 나눴는데요. 모든 게 순조롭게 돌아가던 어느 날, Tina 측에서 예상치 못한 메일을 받았습니다.

귀하의 프로젝트에서 API 호출이 과도하게 발생하고 있습니다…

사실 부끄러운 이야기지만, generateStaticParams로 모든 페이지를 빌드 타임에 미리 생성하는 방식으로 구현해놨던 게 문제였어요. 마케터들이 글 하나를 완성하기까지 평균 10-15번 저장하는데, 매번 수백 개의 전체 컨텐츠를 다시 빌드하니 API 호출량이 기하급수적으로 늘어났죠.

// 기존 방식: 빌드 시 모든 페이지 생성
export async function generateStaticParams() {
  const posts = await getAllPosts() // 매번 전체 컨텐츠 조회
  return posts.map((post) => ({ slug: post.slug }))
}

이 문제를 계기로 ISR (Incremental Static Regeneration) 도입을 고민하게 되었습니다. ISR의 핵심 장점들이 우리 상황에 딱 맞았거든요:

  • 온디맨드 생성: 빌드 단계에서 컨텐츠를 미리 만들지 않고 요청할 때만 생성
  • 선택적 재생성: 변경된 페이지만 다시 생성하므로 API 호출량 대폭 감소
  • 캐시 무효화: 웹훅을 통해 수정된 페이지의 캐시만 지워서 즉시 반영

개선 과정

1단계: SSG에서 ISR로 전환

먼저 generateStaticParams를 제거하고 revalidate = false로 설정해서 수동 revalidate만 사용하도록 변경했습니다.

2단계: 웹훅 API 구현

GitHub 웹훅을 받아서 변경된 컨텐츠만 선택적으로 revalidate하는 API를 구현했습니다.

// app/api/webhook/route.ts
export async function POST(request: Request) {
  const res: PushEvent = await request.json();

  // 변경된 컨텐츠 파일들만 revalidate
  res.commits.forEach(commit => {
    [...commit.added, ...commit.modified].forEach(async filePath => {
      if (filePath.startsWith("content/contents")) {
        const { frontmatter } = await parseContentFile(filePath);
        revalidatePath(`/content/${frontmatter.slug}`);
      }
    });
  });

  return Response.json({ revalidated: true });
}

3단계: 배포 환경 설정

Vercel 배포 시 웹훅 API가 컨텐츠 파일을 읽을 수 있도록 next.config.js에 설정을 추가했습니다. CMS 특성상 컨텐츠를 런타임에 동적으로 읽어야 하는데, Next.js는 import로 직접 참조되는 파일만 번들에 포함하기 때문에 outputFileTracingIncludes 설정으로 미리 알려줘야 해요.

// next.config.js
module.exports = {
  outputFileTracingIncludes: {
    // 해당 폴더 파일들을 번들에 포함
    "/api/webhook": ["./content/contents/**/*"],
  },
}

4단계: 검색 인덱스 최적화

기존에는 빌드 시 Tina Cloud API를 호출해서 검색 인덱스를 생성했는데, 로컬 파일 시스템을 직접 읽도록 변경했습니다. 빌드 타임에는 컨텐츠 파일에 접근할 수 있어서 API 호출 없이도 인덱스를 만들 수 있더라구요. (하하)

// 로컬 파일 시스템 직접 읽기
const contents = readdirSync("content/contents").map((file) =>
  readFileSync(file, "utf-8"),
)

개선 결과

캐싱 전후 응답 시간이 2.47초에서 437ms로 약 6배 성능이 향상되었습니다!

prev

캐싱 전: 2.47초

after

캐싱 후: 437ms

ISR과 웹훅 도입으로 API 호출량은 대폭 줄이면서도 사용자 경험은 크게 향상되었어요. 마케터들이 컨텐츠를 수정하면 즉시 프로덕션에 반영되는 것은 물론이고, 더 이상 API 제한을 걱정할 필요도 없어졌습니다.

추가로 Vercel 설정에서 컨텐츠 폴더 변경 시에는 빌드를 하지 않도록 했어요. 기존에는 글 하나만 수정해도 전체 사이트를 다시 빌드했는데, 이제는 빌드 없이 GitHub 웹훅을 통해 해당 페이지만 revalidate하니까 훨씬 효율적이죠.

마무리

사실 Tina에서 API 과부하 메일을 받았을 때는 정말 당황스러웠어요. 수정한 후에도 혹시 또 문제가 생길까 봐 며칠간 메일함을 수시로 확인했던 기억이 나네요. 하지만 이 경험 덕분에 Next.js ISR을 제대로 이해하게 되었고, 더 나은 아키텍처를 구축할 수 있었습니다.