목차
배경 및 문제 정의
블로그에 시스템 업데이트 내역이 쌓이면서 사용성 관점에서 눈에 밟히는 세 가지 문제를 발견했다.
- 메인 화면 텍스트 과다: 메인 페이지 하단 System Logs 섹션에 최근 업데이트 로그 5개 전문이 그대로 노출되어 첫 페이지가 너무 길어지고 불필요한 시각적 피로감을 유발했다.
- Changelog 탐색 한계: 전체 변경 이력을 보여주는
/changelog페이지는 필터 없이 날짜순으로만 나열되어 있었다. “디자인 수정 내용만 골라보기” 같은 유형별 탐색이 불가능해 정보를 찾아보기가 곤혹스러웠다. - 블로그와 이력 단절: Changelog 업데이트 기록과 개발 비하인드를 적은 블로그 글 사이에 연계 고리가 없었다. 이 때문에 업데이트 내용의 상세한 기술적 구현 맥락을 바로 찾기 어려웠다.
메인 화면 피로감은 낮추고, 전체 이력 페이지 필터 기능을 강화하며, 두 공간을 유기적으로 잇는 동선 설계 작업을 시작했다.
협업 및 의사결정 과정
이번 개선은 내가 아이디어를 제안하면, AI 파트너 Antigravity가 이를 구체적인 코드로 구현하고 다듬어 나가는 방식으로 이루어졌다.
- 1단계 (아코디언 구조 제안): 메인 화면 업데이트 기록을 클릭하면 열리는 접이식 아코디언 구조로 압축하자고 의견을 냈다. Antigravity는 복잡한 자바스크립트 라이브러리 대신 HTML5 표준 규격
<details>와<summary>태그만 활용해 가볍고 직관적인 구조를 완성했다. - 2단계 (Changelog 피아노 건반형 필터 바 기획): Changelog 페이지에 필터 버튼을 얹는 기획 단계에서, 낱개로 떨어진 배지형 버튼 대신 피아노 건반처럼 한 줄로 연결된 형태의 정갈한 레이아웃을 제안했다. 버튼 텍스트를 영문 대문자(위)와 국문 설명(아래)으로 세로 정렬하고, 유형별 로그 개수(예:
Feature (15))를 실시간으로 가져와 표기하도록 요청했다. - 3단계 (색상 스펙 동기화 및 뱃지 연동): 활성화 색상으로 장미색(Rose 600)을 유지하되, 홈 화면 수직 도트 라인 및 뱃지(Badge) 색상도 필터 액티브 색상에 맞춰 1:1로 연동하도록 주문했다. Antigravity는 신규 장미색을 디자인 스펙 파일(palette.astro)에 등록하고, 뱃지 스타일러(
getTypeBadge)와 홈 도트 헬퍼(getDotColor) 컬러 매핑 코드를 일치시켰다. - 4단계 (SEO 최적화 및 푸터 정비): 자주 업데이트되는 시스템 이력 페이지를 검색 엔진이 더 자주 크롤링하고 사용자가 쉽게 찾을 수 있도록 푸터(Footer) Logs & Critique 열에 System Changelog 링크를 노출시켰다. Changelog 맨 밑에는 하드코딩 링크 대신 공통 컴포넌트인
<Button>을 이식해 홈으로 복귀하는 동선까지 보완했다. - 5단계 (상호 자동 연동 도입): 마지막으로 두 영역의 단절을 해결하려 Frontmatter 메타데이터 참조 구조를 설계했다. 블로그 포스트와 변경 이력 항목이 서로의 식별자(
relatedUpdate,relatedBlog)를 가질 때 양방향 링크 카드 및 버튼을 그리도록 바인딩했다. 블로그 하단에서 변경 이력으로 넘어올 때 해당 카테고리가 자동으로 활성화되도록 URL 파라미터(?filter=...) 연동 스크립트도 더했다.
핵심 코드 구현 분석
1. 메인 화면 아코디언화 (SystemLogs.astro)
HTML5 네이티브 요소 <details>를 뼈대로 잡고, Tailwind CSS group-open 지시어를 사용해 아코디언이 펼쳐질 때 화살표 아이콘이 회전하도록 설계했다. 브라우저 기본 삼각형 마커는 CSS로 숨겨 시각적 군더더기를 뺐다.
<!-- src/components/home/SystemLogs.astro 일부 -->
{recentUpdates.map((update) => (
<div class="relative group border-b border-slate-100/50 last:border-0 pb-6 mb-6 last:pb-0 last:mb-0">
<!-- 동적으로 연동되는 도트 색상 -->
<span class={`absolute -left-[32.5px] top-2 w-2.5 h-2.5 rounded-full border-2 border-white shadow-sm ring-1 ring-matcha-50 ${getDotColor(update.data.type)} group-hover:scale-125 transition duration-300`}></span>
<details class="w-full group/details">
<summary class="list-none outline-none cursor-pointer flex flex-col">
<div class="flex items-center gap-2 flex-wrap pr-6 relative select-none">
<!-- 동적 뱃지 테마 적용 -->
<Badge size="micro" variant={getBadgeVariant(update.data.type)}>v{update.data.version}</Badge>
<span class="font-bold text-matcha-900 text-sm md:text-base group-hover:text-matcha-600 transition tracking-tight flex-grow">{update.data.title}</span>
<span class="text-[10px] text-slate-400 font-mono mr-2">{update.data.date.toLocaleDateString('ko-KR')}</span>
<!-- 열림/닫힘에 반응하는 화살표 -->
<div class="text-slate-400 group-hover:text-matcha-700 transition-colors group-open/details:rotate-180 duration-300">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 transform transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
</summary>
<div class="mt-3 text-[11px] md:text-xs text-slate-500/90 leading-relaxed break-keep border-t border-slate-100/80 pt-3 whitespace-pre-line">
{update.body}
</div>
</details>
</div>
))}
2. 피아노 건반형 필터 구성 (changelog.astro)
버튼이 틈 없이 밀착되어 하나의 기계식 스위치 툴바처럼 보이도록 개별 보더 대신 부모 컨테이너에 보더와 overflow-hidden을 주고, 내부 자식 간 분할선(divide-x)을 그어 정돈했다.
<!-- src/pages/changelog.astro 일부 -->
<div class="max-w-4xl mx-auto mb-16 px-4 relative z-10">
<div class="flex flex-col sm:flex-row border border-slate-200/60 rounded-2xl overflow-hidden shadow-sm divide-y sm:divide-y-0 sm:divide-x divide-slate-200/60">
<!-- 전체보기 버튼 (실시간 개수 반영) -->
<button
data-filter="all"
class="changelog-filter-card flex-1 py-3 px-4 transition-all duration-300 bg-matcha-900 text-white active-filter text-center flex flex-col justify-center items-center group/card"
>
<span class="text-xs md:text-sm font-black tracking-tight">{typeLabels.all.en} ({sortedUpdates.length})</span>
<span class="text-[9px] font-semibold opacity-70 mt-0.5 transition-opacity label-ko">{typeLabels.all.ko}</span>
</button>
<!-- 동적 카테고리 버튼들 -->
{existingTypes.map((type) => (
<button
data-filter={type}
class="changelog-filter-card flex-1 py-3 px-4 transition-all duration-300 bg-brand-card/75 hover:bg-white text-slate-700 text-center flex flex-col justify-center items-center group/card"
>
<span class="text-xs md:text-sm font-black tracking-tight">{typeLabels[type].en} ({countByType[type] || 0})</span>
<span class="text-[9px] font-semibold opacity-50 mt-0.5 transition-opacity label-ko">{typeLabels[type].ko}</span>
</button>
))}
</div>
</div>
3. 활성 상태 클래스 스왑 스크립트
필터 건반을 누를 때마다 정의된 themes 색상 맵에서 브랜드 컬러 배경을 꺼내 클래스를 스왑하는 코드다. className 전체를 선언된 defaultClasses와 스왑해 동적 스크립트 실행 시 스타일이 깨지는 현상을 방지했다.
// src/pages/changelog.astro 하단 스크립트 중 일부
const themes: Record<string, string> = {
all: "bg-matcha-900 text-white active-filter",
major: "bg-matcha-500 text-white active-filter",
feature: "bg-brand-gemini text-white active-filter",
design: "bg-rose-600 text-white active-filter",
fix: "bg-[#8C6239] text-white active-filter",
content: "bg-brand-jiyu text-white active-filter",
};
const defaultClasses = "bg-brand-card/75 hover:bg-white text-slate-700 text-center";
filterCards.forEach((card) => {
card.addEventListener("click", () => {
const filter = (card as HTMLElement).dataset.filter || "all";
activeFilter = filter;
filterCards.forEach((c) => {
const el = c as HTMLElement;
const cFilter = el.dataset.filter || "all";
const koLabel = el.querySelector(".label-ko");
if (cFilter === activeFilter) {
const activeTheme = themes[cFilter] || themes.all;
el.className = `changelog-filter-card flex-1 py-3 px-4 transition-all duration-300 text-center flex flex-col justify-center items-center group/card ${activeTheme}`;
if (koLabel) {
koLabel.classList.remove("opacity-50");
koLabel.classList.add("opacity-70");
}
} else {
el.className = `changelog-filter-card flex-1 py-3 px-4 transition-all duration-300 flex flex-col justify-center items-center group/card ${defaultClasses}`;
if (koLabel) {
koLabel.classList.remove("opacity-70");
koLabel.classList.add("opacity-50");
}
}
});
// 실제 타임라인 항목 필터링 제어
items.forEach((item) => {
const el = item as HTMLElement;
if (activeFilter === "all" || el.dataset.type === activeFilter) {
el.style.display = "flex";
el.style.opacity = "1";
} else {
el.style.display = "none";
el.style.opacity = "0";
}
});
});
});
// URL 파라미터 필터 적용 (?filter=feature 등)
const urlParams = new URLSearchParams(window.location.search);
const filterParam = urlParams.get("filter");
if (filterParam) {
const targetCard = document.querySelector(`.changelog-filter-card[data-filter="${filterParam}"]`);
if (targetCard) {
(targetCard as HTMLElement).click();
}
}
4. 양방향 자동 연동 시스템 (Frontmatter 및 동적 파라미터 제어)
두 페이지를 개별 개편하는 데 그치지 않고, 글과 시스템 이력을 연결해 유기적인 동선을 설계했다.
1) content.config.ts 스키마 설정
Astro 데이터 유효성 검증과 자동 완성을 위해 스키마 명세에 상호 참조용 메타데이터 필드를 선택 사항(optional())으로 추가했다.
- 블로그 스키마에는 연결할 업데이트 버전(
relatedUpdate)을 등록할 수 있다. - 업데이트 스키마에는 연결할 블로그 Slug(
relatedBlog)를 등록할 수 있다.
// src/content.config.ts 일부
const blogCollection = defineCollection({
loader: glob({ pattern: '**/[^_]*.md', base: "./src/content/blog" }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
tags: z.array(z.string()),
category: z.enum(['AI Coding', 'ARM Windows', 'Electronics', 'Essay']),
relatedUpdate: z.string().optional(), // 연결된 업데이트 버전
}),
});
const updatesCollection = defineCollection({
loader: glob({ pattern: '**/[^_]*.md', base: "./src/content/updates" }),
schema: z.object({
date: z.date(),
version: z.string(),
type: z.enum(['feature', 'design', 'fix', 'content', 'major']),
title: z.string(),
relatedBlog: z.string().optional(), // 연결된 블로그 포스트 slug
}),
});
2) 블로그 본문 하단의 업데이트 연동 카드 (blog/[...slug].astro)
블로그 상세 페이지에서 포스트의 relatedUpdate를 파싱해 참조 데이터가 있으면 업데이트 컬렉션에서 릴리즈 정보 카드를 그려준다.
이때 이동할 주소에 /changelog 대신 /changelog?filter=${type}를 붙여, 해당 업데이트 유형의 건반 필터가 활성화된 상태로 랜딩되도록 설계했다.
<!-- src/pages/blog/[...slug].astro 일부 -->
---
const { entry } = Astro.props;
const { Content } = await render(entry);
// 연결된 업데이트 정보 탐색
let relatedUpdateEntry = null;
if (entry.data.relatedUpdate) {
const updates = await getCollection("updates");
relatedUpdateEntry = updates.find((u) => u.data.version === entry.data.relatedUpdate);
}
---
<main>
<div class="max-w-4xl mx-auto px-6 py-24">
<article class="prose max-w-none">
<Content />
</article>
{relatedUpdateEntry && (
<div class="mt-16 p-6 rounded-2xl bg-brand-card border border-matcha-100/50 hover:border-matcha-350 transition duration-300 shadow-sm flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div class="space-y-1">
<span class="text-[9px] font-mono font-black text-matcha-500 uppercase tracking-widest">Related System Release</span>
<h4 class="text-base font-bold text-matcha-900">
v{relatedUpdateEntry.data.version} - {relatedUpdateEntry.data.title}
</h4>
<p class="text-xs text-slate-500 font-medium">이 포스트의 작업 내용이 반영된 릴리즈 변경 사항 이력입니다.</p>
</div>
<a
href={`/changelog?filter=${relatedUpdateEntry.data.type}`}
class="px-4 py-2 bg-matcha-900 text-white rounded-xl text-xs font-bold shadow-sm hover:bg-matcha-850 hover:-translate-y-0.5 transition duration-300 whitespace-nowrap"
>
변경 이력 보러가기 →
</a>
</div>
)}
</div>
</main>
3) 변경 이력 카드 내 개발 비하인드 링크 (changelog.astro)
Changelog 타임라인 카드에도 relatedBlog 값이 있으면 자동으로 상세 기록으로 유도하는 “개발 일지 읽기” 버튼이 노출되도록 마크업을 연동했다.
<!-- src/pages/changelog.astro 일부 -->
{update.data.relatedBlog && (
<div class="mt-4 pt-3 border-t border-slate-100/50 flex items-center justify-between">
<span class="text-[10px] text-slate-400 font-medium">이 변경 사항과 관련된 비하인드가 있습니다.</span>
<a
href={`/blog/${update.data.relatedBlog}`}
class="inline-flex items-center gap-1 text-[11px] font-bold text-matcha-800 hover:text-matcha-600 transition"
>
개발 일지 읽기
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
</svg>
</a>
</div>
)}
이 상호 참조 연결로 사용자는 간결한 릴리즈 노트를 훑어보다가 “개발 일지 읽기” 버튼을 눌러 비하인드 스토리와 논리 전개 과정을 탐독할 수 있다.
결론 및 교훈
이번 리팩토링은 화면 복잡도를 걷어내고 싶다는 불편함을 흘려보내지 않고, AI와 소통하며 설계 구조를 정교화해 나갔기에 성공할 수 있었다.
아코디언 아이디어에서 시작해 다차원 건반 툴바를 기획하고, 디자인 시스템 스펙 동기화와 푸터 링크 구조 정비까지 마쳤다. 사용성 개선은 거창한 라이브러리보다 세심한 사용자 배려와 동선을 일치시키는 집념에서 출발한다는 당연한 교훈을 다시 한번 체감했다.