Changelog의 재구성:대시보드 통합과 아코디언 매트릭스 뷰

개발 내부 릴레이션 대시보드를 일반 사용자 공간인 Changelog로 흡수 통합하고, SQL 테이블 감성의 아코디언 뷰를 입혀 SEO 패널티 해소와 사용자 탐색 경험 고도화를 동시에 달성한 리팩토링 기록.

CategoryAI Coding
Brewed on2026.07.18

목차


배경 및 문제 정의

이전 개선 작업을 통해 메인 화면 아코디언화와 Changelog 건반형 필터를 구축했으나, 콘텐츠의 다차원 연결 구조를 한눈에 관리하고 검증하려는 욕심이 생겼다.

버전 업데이트(updates), 개발 비하인드(blog), 그리고 AI의 차 시음 비평(tasting)이 유기적으로 연계되어 있는지 모니터링하기 위해 실험실(Kitchen) 내부에 독립된 릴레이션 대시보드 페이지(/kitchen/status)를 임시 구축해 테스트를 진행했다.

이 과정에서 두 가지 중요한 문제점이 대두되었다.

  1. SEO 중복 콘텐츠(Duplicate Content) 패널티: 임시 구축한 대시보드가 메인 변경 이력 페이지(/changelog)와 거의 100% 동일한 데이터 소스(updates, blogs, tastings)를 사용했기에, 검색 엔진에 두 페이지가 동시에 노출될 경우 중복 콘텐츠 패널티를 받아 사이트 전반의 검색 순위가 깎일 우려가 있었다.
  2. 독립 페이지 운영의 비효율성: 개발자가 아닌 일반 사용자나 독자들은 실험실 카테고리의 내부 대시보드 존재 자체를 인지하기 어렵다. 정작 릴리즈 내용과 연계 콘텐츠(Blog, Tasting)를 가장 쉽고 편리하게 이동하며 읽어야 할 대상은 사이트의 방문자들이었다.

이에 임시 대시보드의 고밀도 매트릭스 뷰를 메인 서비스 공간인 /changelog로 완전히 흡수 통합하는 디자인/구조적 대규모 마이그레이션을 결정했다.


협업 및 의사결정 과정

이번 작업 역시 AI 파트너 Antigravity와 머리를 맞대고 코드를 설계해 나가는 즐거운 페어 프로그래밍 과정이었다.

  • 1단계 (데이터 매핑 모듈화): status.astro 내부에 수동으로 얽혀 있던 백업용 릴레이션 매핑 데이터를 외부 TypeScript 모듈(src/data/manualRelations.ts)로 완전히 추출했다. 데이터 레이어와 뷰 레이어를 명확하게 분리하여 코드 청결도를 향상시켰다.
  • 2단계 (Changelog로의 흡수 통합): 중복 페이지를 제거하고 /changelog에 표 형태의 릴레이션 대시보드를 직접 이식하는 데 합의했다. 기존의 피아노 건반형 필터바 기능은 그대로 유지하면서, 하단 타임라인을 아코디언 매트릭스 테이블로 교체하는 복합 구조를 그렸다.
  • 3단계 (브랜드 라이트 테마로의 옷 갈아입히기): 실험실 테마의 다크 코딩창 스타일을 말차 브랜드의 편안하고 따뜻한 종이 감성 테마에 맞추어 라이트 스타일로 변환했다. 테이블 배경은 순백색(bg-white)으로, 아코디언 디테일 배경은 메인 화면의 Brewing 카드 배경에 쓰인 맑은 미색(bg-brand-card/90)을 그대로 가져와 시각적 일치감을 높였다.
  • 4단계 (정밀 정렬 및 정돈): Astro 6/7 Content Layer에서 파일명(entry.id)으로 빌드 데이터를 정렬할 때 문자열 비교 오류(v1.10.0이 v1.2.0보다 먼저 나오는 현상)를 방지하기 위해, 파일명 숫자를 파싱하는 내츄럴 소팅 알고리즘을 도입하고 최신 버전이 가장 위에 오도록 정렬 순서를 가다듬었다.

핵심 코드 구현 분석

1. 파일명 기반 내츄럴 정렬 (Natural Sort)

버전 식별 파일명에서 메이저, 마이너, 패치 번호를 정규식으로 추출하여 비교하는 헬퍼 함수를 구축했다. 이를 통해 최신 릴리즈가 항상 상단에 위치하는 내림차순 리스트를 무결하게 구현했다.

// src/pages/changelog.astro 일부
const parseVersion = (id: string) => {
  const match = id.match(/v?(\d+)-(\d+)-(\d+)/);
  if (!match) return [0, 0, 0];
  return [
    parseInt(match[1] || '0', 10),
    parseInt(match[2] || '0', 10),
    parseInt(match[3] || '0', 10)
  ];
};

const sortedUpdates = allUpdates.sort((a, b) => {
  const versionA = parseVersion(a.id);
  const versionB = parseVersion(b.id);
  
  if (versionB[0] !== versionA[0]) return versionB[0] - versionA[0];
  if (versionB[1] !== versionA[1]) return versionB[1] - versionA[1];
  return versionB[2] - versionA[2];
});

2. SQL 테이블 뷰 및 아코디언 레이아웃

메인 릴리즈 정보를 보여주는 tr과, 클릭 시 확장되는 디테일 정보 tr을 형제 요소로 결합하여 하나의 완성도 높은 표 구조를 생성했다. 연동 상태(Blog, Tasting)는 체크 표시()로 요약해 시각적 밀도를 확보했다.

<!-- src/pages/changelog.astro 일부 -->
<tbody class="divide-y divide-matcha-100">
  {matrix.map((row, idx) => (
    <>
      <!-- 메인 릴리즈 정보 행 -->
      <tr 
        class="changelog-item hover:bg-matcha-50/10 transition duration-200 cursor-pointer"
        data-type={row.update.data.type}
        data-idx={idx}
        data-target={`detail-${idx}`}
      >
        <td class="text-center font-mono align-middle">{totalReleases - idx}</td>
        <td class="font-mono font-bold align-middle text-center">v{row.update.data.version}</td>
        <td class="align-middle text-center">
          <span class={`px-2 py-0.5 rounded text-[9px] font-bold uppercase ${getTypeBadge(row.update.data.type)}`}>
            {row.update.data.type}
          </span>
        </td>
        <td class="text-matcha-900 font-bold truncate align-middle max-w-[100px] md:max-w-[360px]">{row.update.data.title}</td>
        <td class="align-middle text-center font-mono">{row.update.data.date.toISOString().split('T')[0]}</td>
        <td class="text-center align-middle font-bold">{row.blog ? '✓' : '-'}</td>
        <td class="text-center align-middle font-bold">{row.tasting ? '✓' : '-'}</td>
        <td class="text-center align-middle">
          <!-- 아코디언 제어 화살표 -->
        </td>
      </tr>

      <!-- 아코디언 상세 정보 펼침 행 -->
      <tr id={`detail-${idx}`} class="hidden bg-brand-card/90 border-l-2 border-matcha-500/80 detail-row">
        <td colspan="8" class="py-5 px-12 align-middle">
          <div class="space-y-4 text-xs font-mono text-slate-500">
            <!-- 블로그 기사 & tasting 비평 링크 렌더링 -->
            <!-- 상세 릴리즈 설명 본문 (Release Notes) -->
          </div>
        </td>
      </tr>
    </>
  ))}
</tbody>

3. 필터와 아코디언의 인터랙션 통합 제어

기존 건반형 필터바 클릭 시 유형별 필터링이 정상 동작하면서도, 해당 유형과 맞지 않아 숨겨지는 카드가 있다면 사전에 열려 있던 아코디언 디테일 행을 강제로 접어주는(hidden 복구) 연동 로직을 심었다. 이를 통해 레이아웃이 어긋나거나 껍데기만 노출되는 예외를 사전에 방지했다.

// src/pages/changelog.astro 하단 스크립트 일부
filterCards.forEach((card) => {
  card.addEventListener("click", () => {
    const filter = (card as HTMLElement).dataset.filter || "all";
    activeFilter = filter;

    items.forEach((item) => {
      const el = item as HTMLElement;
      const idx = el.dataset.idx;
      const detailRow = document.getElementById(`detail-${idx}`);
      const arrowIcon = el.querySelector('.arrow-icon');

      if (activeFilter === "all" || el.dataset.type === activeFilter) {
        el.style.display = "table-row";
      } else {
        el.style.display = "none";
        // 필터 조건에 부합하지 않아 숨겨지는 오픈 아코디언은 강제로 리셋
        if (detailRow) {
          detailRow.classList.add('hidden');
          arrowIcon?.classList.remove('rotate-180');
          el.classList.remove('bg-matcha-50/10');
        }
      }
    });
  });
});

결론 및 교훈

이번 리팩토링은 단순히 새로운 기능을 화면에 뿌리는 것을 넘어, **“이 기능이 사이트 전체의 가치를 훼손하지 않는가?”**라는 질문을 던져보는 좋은 기회였다.

개발 단계에서 편하다는 이유로 분리했던 상태 페이지를 SEO 관점과 일반 독자의 시선에서 바라본 결과, 메인 Changelog와 합치는 최선의 의사결정을 내릴 수 있었다.

또한, 쑥색의 차분하고 정돈된 컴퓨터 창 테마에 브랜디드 라이트 컬러를 결합한 테이블은 디자인적으로도 사이트의 완성도를 크게 끌어올려 주었다. 앞으로 시스템이 확장됨에 따라 이 표 형태의 대시보드 뷰가 변경 이력 탐색의 단단한 주춧돌이 되어줄 것이다.

Related System Release

v2.5.0 - Changelog Dashboard Integration & UI Polish

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

변경 이력 보러가기 →
🍵

Brewed by JIYU

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