개발 slecs

Astro 동적 라우팅을 콘텐츠 컬렉션으로 전환해 빌드 안정성 확보

목차

Astro 프로젝트의 데이터 레이어를 db 직접 호출에서 getCollection으로 전면 교체한 작업이다.


왜 이걸 건드렸냐

처음엔 그냥 돌아가니까 냅뒀다. src/lib/db.ts에서 직접 DB를 물어 페이지를 그리는 방식이었는데, 빌드 시점엔 몰랐고 로컬에서도 멀쩡했다. 문제가 수면 위로 올라온 건 [...slug].astroc/[category].astro 두 파일에서 동적 라우팅을 유지보수하다가였다. 페이지 수가 늘어날수록 DB 커넥션 의존 코드가 빌드 파이프라인 곳곳에 박혀 있었고, 그게 결국 빌드 타임 fetch 실패와 런타임 에러가 뒤섞이는 지뢰밭이 됐다.

Astro의 설계 철학 자체가 "콘텐츠는 Content Collections로, 외부 I/O는 최소화"인데, 우리 코드는 거기서 꽤 벗어나 있었다. 팀원들이 새 페이지 하나 만들 때마다 db.ts를 들여다봐야 하는 구조가 온보딩 마찰로도 이어지고 있었다. 언젠가는 해야 할 일이었는데, 이번에 metric.ts API 라우트 쪽 버그를 잡다가 근본 원인이 동일한 DB 의존 패턴이란 걸 확인하고 한 번에 정리하기로 했다.


작업 내용 요약

파일 변경 전 변경 후
src/lib/db.ts 페이지용 쿼리 함수들 포함 순수 DB 유틸만 남기거나 역할 최소화
src/pages/[...slug].astro db.ts 직접 import → 런타임 fetch getCollection + getEntry 패턴으로 교체
src/pages/c/[category].astro DB 쿼리로 카테고리 필터링 getCollection 필터 함수로 교체
src/pages/api/metric.ts DB 의존 로직 혼재 API 역할만 남기고 데이터 접근 분리

핵심은 getStaticPaths에서 getCollection을 쓰도록 바꾼 것이다. 대략 이런 형태다.

// before
export async function getStaticPaths() {
  const posts = await db.query('SELECT * FROM posts WHERE published = 1');
  return posts.map(post => ({ params: { slug: post.slug }, props: { post } }));
}

// after
import { getCollection } from 'astro:content';

export async function getStaticPaths() {
  const posts = await getCollection('posts', ({ data }) => data.published);
  return posts.map(post => ({
    params: { slug: post.slug },
    props: { entry: post },
  }));
}

카테고리 페이지도 마찬가지다. DB에서 WHERE category = ?로 뽑아오던 걸 getCollection 필터로 대체하면 빌드 타임에 정적으로 모든 경우의 수를 결정할 수 있다.

// c/[category].astro
export async function getStaticPaths() {
  const all = await getCollection('posts', ({ data }) => data.published);
  const categories = [...new Set(all.map(p => p.data.category))];

  return categories.map(category => ({
    params: { category },
    props: { posts: all.filter(p => p.data.category === category) },
  }));
}

DB를 직접 치던 것 대비 코드량이 줄고, 타입도 Astro가 자동 추론해준다. 이게 팀 입장에서 제일 큰 이득이다. 전에는 DB 스키마 바뀌면 쿼리 함수, 타입 정의, 페이지 코드 세 곳을 동기화해야 했는데, 이제 Content Collection 스키마 하나만 관리하면 된다.


회고

이 작업을 하면서 가장 많이 든 생각은 "왜 처음부터 이렇게 안 짰냐"가 아니라 "왜 이게 이렇게 오래 방치됐냐"였다. 초기엔 DB 직접 연결이 더 유연해 보였을 것이고, 그 판단이 틀렸다고 보기도 어렵다. 콘텐츠 구조가 확정되기 전에 스키마를 강제하는 건 오버엔지니어링일 수 있으니까.

문제는 그 판단이 유효한 시점이 지났는데도 코드가 안 바뀌었다는 거다. 팀에서 이런 기술 부채는 보통 명시적으로 우선순위를 잡지 않으면 계속 밀린다. 이번엔 버그 수정 티켓이 트리거가 됐는데, 사실 그것도 좋은 방법이다. 버그를 fix하면서 근본 구조를 같이 정리하면 리뷰어도 컨텍스트가 있고, "왜 이렇게 큰 PR이냐"는 질문에도 설명이 쉽다.

metric.ts는 API 라우트라 getCollection으로 완전히 교체하진 않았다. 런타임에 동적으로 뭔가를 기록해야 하는 엔드포인트이기 때문에 DB 접근 자체를 없앨 순 없다. 다만 페이지 렌더링용 데이터를 API 라우트 안에서 같이 처리하던 로직은 걷어냈다. 역할 분리가 목적이었으니까.

코드리뷰 때 팀원한테 "이거 getCollection이 빌드 타임에만 동작하는 거 알고 쓰는 거 맞죠?" 확인받았다. 당연한 거지만 런타임에도 동적 데이터가 필요한 부분이 섞여 있으면 혼란이 생기기 쉬운 포인트라 짚어두길 잘했다. 앞으로 비슷한 구조 손볼 때도 "이 데이터가 빌드 타임에 확정 가능한가, 아닌가"를 먼저 물어보는 게 기준이 될 것 같다.

끝.


🛒 이 글과 어울리는 추천 상품

*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.

댓글 0

첫 댓글 달아줘.