URL 태그 필터:화면에 없는 태그 동적으로 해결하기

Astro 블로그에서 URL 파라미터로 특정 태그를 받았을 때, 화면에 없는 태그 버튼 때문에 필터링이 실패하는 버그를 해결하고 동적 돔의 상태 제어 문제를 보완한 기록.

CategoryAI Coding
Brewed on2026.07.18

발견된 문제: 랜딩 페이지 필터링 실패

Flavors 페이지 하단에는 다양한 글들의 태그를 모아둔 곁들임 찻잎 목록이 있습니다. 이 태그를 클릭하면 /blog?tag=태그명 주소로 이동하여 블로그 허브에서 필터링된 글들을 모아볼 수 있도록 기획했습니다.

그러나 일부 태그의 경우 블로그 허브로 이동은 하지만, 필터링이 적용되지 않고 모든 글이 그대로 보이는 문제가 있었습니다.


원인 분석: 화면에 렌더링되지 않은 태그 버튼

블로그 허브 페이지(blog/index.astro)는 UI 디자인의 복잡도를 낮추기 위해 사용 빈도가 높은 상위 10개의 태그만 서버사이드에서 버튼으로 미리 렌더링하고 있었습니다.

기존 스크립트는 주소창의 쿼리 스트링(?tag=...)을 파싱하여 다음과 같이 처리하고 있었습니다.

window.addEventListener("DOMContentLoaded", () => {
  const urlParams = new URLSearchParams(window.location.search);
  const tagParam = urlParams.get("tag");
  if (tagParam) {
    const targetBtn = Array.from(tagBtns).find(
      (b) => (b as HTMLElement).dataset.tag === tagParam,
    ) as HTMLElement;
    if (targetBtn) targetBtn.click();
  }
});

상위 10개에 들지 못해 화면에 렌더링되지 않은 태그가 주소로 들어올 경우, targetBtnundefined를 반환하면서 클릭 동작이 일어나지 않았고, 이에 따라 필터 적용 로직도 실행되지 않았습니다.


1차 수정 및 추가 문제 발견

1차 수정: 임시 태그 버튼 동적 생성

화면에 없는 태그가 유입되었을 때, 자바스크립트로 임시 활성 태그 버튼을 동적으로 생성하여 패널에 추가하는 방식을 적용했습니다.

if (targetBtn) {
  targetBtn.click();
} else {
  const tagsPanel = document.getElementById("tags-panel");
  if (tagsPanel) {
    const tempBtn = document.createElement("button");
    tempBtn.className = "tag-btn px-3 py-1.5 bg-matcha-100 text-matcha-800 rounded-lg text-[9px] font-bold transition";
    tempBtn.dataset.tag = tagParam;
    tempBtn.textContent = `#${tagParam}`;
    // ...버튼 추가 및 필터 적용
    tagsPanel.appendChild(tempBtn);
  }
}

추가 발견된 문제: 동적 엘리먼트의 상태 해제 누락

1차 수정 코드를 적용한 후 테스트하는 과정에서 새로운 문제가 발견되었습니다.

임시 생성된 태그 버튼이 활성화된 상태에서 사용자가 다른 기존 태그 버튼을 클릭했을 때, 새로 생긴 임시 버튼의 활성 클래스(bg-matcha-100)가 제거되지 않고 계속 초록색 활성 상태로 남아있었습니다.

기존의 클래스 해제 로직이 컴파일 시점에 미리 수집해 둔 정적 NodeList인 tagBtns 배열만 순회하며 비활성화 처리를 하고 있었기 때문에, 런타임에 동적으로 삽입된 임시 버튼은 해당 초기화 대상에서 누락되었던 것입니다.


최종 수정 및 보완

1. 실시간 돔 쿼리로 비활성화 범위 수정

이 문제를 해결하기 위해, 태그 버튼의 토글 해제 루프를 정적인 tagBtns 변수 대신 실시간 돔 상태를 조회하는 document.querySelectorAll(".tag-btn")로 변경했습니다.

  // 태그 버튼 제어
  tagBtns.forEach((btn) => {
    btn.addEventListener("click", (e) => {
      const target = e.currentTarget as HTMLElement;
      const tag = target.dataset.tag || "all";
      currentTag = tag;

-     tagBtns.forEach((b) => {
+     document.querySelectorAll(".tag-btn").forEach((b) => {
        b.classList.remove("bg-matcha-100", "text-matcha-800");
        b.classList.add("bg-slate-50", "text-slate-600", "border-slate-100");
      });
      target.classList.add("bg-matcha-100", "text-matcha-800");
      target.classList.remove("bg-slate-50", "text-slate-600", "border-slate-100");

      applyFilters();
    });
  });

이렇게 수정함으로써 런타임에 동적으로 추가된 버튼이 존재하더라도, 다른 버튼을 클릭했을 때 임시 버튼의 활성 클래스까지 안전하게 지워지도록 구현했습니다.

2. 필터링 비교 로직 정밀화

추가로 기존의 필터 비교식인 tags.includes(currentTag) 방식은 글의 전체 태그 문자열에 부분 포함되는 경우까지 매칭하는 허점이 있었습니다. (예: AI 필터 적용 시 AI Coding 태그를 가진 포스트가 의도치 않게 노출되는 현상)

이를 보완하기 위해 쉼표로 구분된 태그 목록을 배열로 분할한 뒤 정확히 일치하는 단어가 있는지만 확인하도록 판별식을 바꿨습니다.

const tagMatch = currentTag === "all" || 
  tags.split(",").map(t => t.trim().toLowerCase()).includes(currentTag.toLowerCase());

결론

Astro와 같은 정적 사이트 생성기 위에서 바닐라 JS로 인터랙티브한 돔을 직접 제어할 때, 정적인 데이터 바인딩과 런타임 동적 변화 간의 싱크 문제가 자주 발생합니다.

초기 바인딩된 정적 요소 배열만 갱신하던 로직을 실시간 돔 조회 방식으로 전환함으로써 런타임 예외 상태를 안전하게 차단할 수 있었습니다.

Related System Release

v2.3.0 - Flavors 페이지 고도화 및 태그 매칭/타이틀 분리 로직 적용

이 포스트의 작업 내용이 반영된 릴리즈 변경 사항 이력입니다.

변경 이력 보러가기 →
🍵

Brewed by JIYU

AI와 함께 코딩하고, ARM 윈도우 위에서 글을 씁니다.
기술의 스펙보다는 그 안에 담긴 논리(Logic)와 경험을 기록합니다.