디버깅 일기: Astro 태그 시스템의 4중 함정

1. 시작: 간단해 보였던 ‘태그’ 기능

블로그에 태그 기능을 추가하는 것은 간단해 보였다.

  1. 블로그 목록과 본문에 태그 뱃지를 표시한다. (<span>)
  2. 태그 뱃지를 클릭하면(/tags/[tag]/), 해당 태그의 글만 모아본다.

하지만 이 뱃지를 <span>에서 <a> 링크로 바꾸는 순간, 4단계의 디버깅 릴레이가 시작되었다.

2. 함정 1: 레이아웃 폭발 (<a> in <a>)

증상: 태그 뱃지를 <a>로 바꾸자마자 블로그 목록(blog/index.astro)의 카드 레이아웃이 완전히 깨졌다.

원인: HTML 표준 위반. 나는 카드 전체를 /blog/[slug]/로 가는 <a> 태그로 감싸고 있었다. 그 안에 /tags/[tag]/로 가는 또 다른 <a> 태그를 넣으려 한 것이다. 링크(<a>) 안에 링크(<a>)를 중첩하는 것은 허용되지 않는다.

해결: 카드 전체를 감싸던 <a><div>로 변경하고, 대신 글 제목(<h3>)에만 <a> 링크를 걸어 문제를 해결했다.

3. 함정 2: “태그: #undefined” (Props vs Params)

증상: 레이아웃을 고치고 태그를 클릭하니 /tags/astro로 이동은 했지만, 페이지 제목이 태그: #undefined로 나왔다.

원인: Astro의 동적 라우트([tag].astro)에서 URL의 동적 값(astro)을 가져오는 변수명을 착각했다. Astro.props를 사용했는데, 정답은 Astro.params였다.

해결: const { tag } = Astro.props;const { tag } = Astro.params;로 수정하여 URL 파라미터를 정상적으로 가져왔다.

4. 함정 3: 빈 목록 (대소문자 + 서버 캐시)

증상: 제목은 태그: #astro로 제대로 나왔지만, 글 목록이 비어 있었다.

원인 1 (대소문자): URL 파라미터(tag)는 "astro"(소문자)였지만, .md 파일의 frontmatter에는 tags: ["Astro"](대문자)로 저장되어 있었다. JavaScript는 이 둘을 다르다고 판단하여 filter가 실패했다.

해결 1: getStaticPathsfilteredPosts 로직 양쪽에서 .toLowerCase()를 적용해, 링크와 데이터를 모두 소문자로 통일했다.

원인 2 (서버 캐시): …하지만 그래도 목록이 비어 있었다.

해결 2: npm run dev 서버가 getCollection의 내용을 캐시하고 있어, 태그가 없던 ‘옛날’ 데이터를 계속 사용하고 있었다. Ctrl+C개발 서버를 완전히 껐다 켜니, 비로소 새 데이터를 읽어와 목록이 정상적으로 표시되었다.

5. 함정 4: 레이아웃의 귀환 (잘못된 복붙)

증상: 태그 목록은 나왔지만, 태그 페이지(/tags/astro)의 레이아웃이 ‘함정 1’처럼 다시 깨졌다.

원인: ‘함정 3’의 필터링 로직을 수정하면서, [tag].astro의 템플릿(HTML) 부분에 ‘함정 1’에서 해결했던 div 카드 코드가 아니라, 예전의 ‘깨지는 a 카드 코드’를 잘못 복사해 붙여넣었다.

해결: [tag].astro의 템플릿도 blog/index.astro와 동일한 div 카드 구조로 수정하여 최종 해결했다.

6. 결론

간단한 태그 기능 하나가 HTML 표준(<a> in <a>), Astro 라우팅(params), 데이터 처리(대소문자), 개발 환경(캐시)까지 모든 레이어를 건드렸다. 디버깅은 한 번에 하나씩 가설을 세우고 검증하는 과정임을 다시 느꼈다.

블로그 목록으로 돌아가기