Claude Code로 접근성 있는 브레드크럼 구현하기
Claude Code로 브레드크럼, JSON-LD, aria-current, 모바일 CSS와 테스트까지 구현합니다.
브레드크럼은 제목 위에 놓이는 작은 링크처럼 보이지만, 실제 서비스에서는 사이트 구조, 내부 링크, 접근성, 구조화 데이터, 모바일 표시가 한 번에 얽히는 컴포넌트입니다. 겉으로는 멀쩡해도 스크린 리더가 현재 페이지를 알 수 없거나, JSON-LD에 상대 URL이 들어가거나, 긴 제목 때문에 모바일 첫 문단이 밀릴 수 있습니다.
ClaudeCodeLab 템플릿에서 Claude Code에 단순히 “breadcrumb를 만들어줘”라고 요청했을 때 첫 결과는 Home > Blog > Title을 그렸지만, aria-current 누락, JSON-LD 절대 URL 누락, 모바일 줄바꿈, slug 그대로 노출 문제가 남았습니다. 그래서 구현 전에 완료 조건을 명확히 주는 것이 중요합니다.
이 글은 React, Next.js 스타일, Astro 사이트에서 쓸 수 있는 접근성 있는 브레드크럼을 Claude Code로 스캐폴딩하고 리뷰하는 방법을 다룹니다. 함께 보면 좋은 글은 SEO 최적화, 접근성 구현, Astro 개발, React 개발입니다.
설계 기준
브레드크럼은 뒤로 가기 버튼이 아닙니다. 계층을 보여 주고, 상위 페이지로 이동하게 하며, 검색 엔진에 페이지 관계를 전달합니다. WAI-ARIA APG의 Breadcrumb Pattern은 브레드크럼을 라벨이 있는 navigation landmark 안에 두고 현재 페이지 링크에 aria-current="page"를 쓰는 방식을 설명합니다.
구조화 데이터는 schema.org BreadcrumbList를 따릅니다. BreadcrumbList는 웹페이지의 연결된 목록이며, position으로 순서를 명확히 합니다. Google 검색 표시까지 고려한다면 Google Search Central breadcrumb structured data도 확인해야 합니다.
| 결정 | 정할 내용 | 흔한 실패 |
|---|---|---|
| 라벨 | 제목, 카테고리명, 사전 중 무엇을 쓸지 | slug가 그대로 보임 |
| URL | HTML 링크와 JSON-LD URL 규칙 | 구조화 데이터에 상대 경로가 들어감 |
| 현재 페이지 | 마지막 항목을 링크로 둘지 텍스트로 둘지 | 색상만으로 현재 위치를 표현 |
| 모바일 | 중간 항목을 접을지 여부 | 여러 줄로 밀림 |
| 다국어 | locale prefix와 번역 라벨 | 다른 언어 라벨이 섞임 |
Claude Code 프롬프트
React/Next.js 또는 Astro 사이트용 브레드크럼을 구현해 주세요.
요구사항:
- items는 { label: string; href: string }[]로 받습니다.
- 마지막 항목에는 aria-current="page"를 추가합니다.
- nav aria-label="브레드크럼"을 사용합니다.
- 구분자는 aria-hidden="true"로 숨깁니다.
- 같은 items 배열에서 JSON-LD BreadcrumbList를 생성합니다.
- JSON-LD URL은 siteUrl로 절대 URL을 만듭니다.
- pathname에서 items를 만드는 유틸리티를 추가합니다.
- slug는 사람이 읽기 쉽게 바꾸고 라벨 사전으로 덮어쓸 수 있게 합니다.
- 모바일에서는 중간 항목을 접고 현재 페이지는 읽을 수 있게 유지합니다.
- Vitest로 root, nested path, localized label, query string을 테스트합니다.
- 구현 후 접근성과 구조화 데이터 체크리스트를 제시합니다.
이 프롬프트는 Claude Code가 장식용 UI가 아니라 검증 가능한 컴포넌트를 만들도록 합니다. 큰 변경이라면 리뷰 워크플로 체크리스트로 한 번 더 점검하세요.
React 컴포넌트
components/Breadcrumb.tsx를 만듭니다. siteUrl은 https://example.com처럼 끝 슬래시 없이 넘깁니다. 화면과 JSON-LD를 같은 items에서 생성하면 카테고리 변경 때 불일치가 줄어듭니다.
import type { ReactNode } from "react";
export type BreadcrumbItem = {
label: string;
href: string;
};
type BreadcrumbProps = {
items: BreadcrumbItem[];
siteUrl: string;
ariaLabel?: string;
};
function toAbsoluteUrl(siteUrl: string, href: string) {
return new URL(href, siteUrl).toString();
}
function Separator(): ReactNode {
return (
<span className="breadcrumb__separator" aria-hidden="true">
/
</span>
);
}
export function Breadcrumb({
items,
siteUrl,
ariaLabel = "Breadcrumb",
}: BreadcrumbProps) {
if (items.length <= 1) return null;
const jsonLd = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: items.map((item, index) => ({
"@type": "ListItem",
position: index + 1,
item: {
"@id": toAbsoluteUrl(siteUrl, item.href),
name: item.label,
},
})),
};
return (
<>
<nav className="breadcrumb" aria-label={ariaLabel}>
<ol className="breadcrumb__list">
{items.map((item, index) => {
const isCurrent = index === items.length - 1;
return (
<li className="breadcrumb__item" key={item.href}>
{index > 0 ? <Separator /> : null}
{isCurrent ? (
<span className="breadcrumb__current" aria-current="page">
{item.label}
</span>
) : (
<a className="breadcrumb__link" href={item.href}>
{item.label}
</a>
)}
</li>
);
})}
</ol>
</nav>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
</>
);
}
라우트에서 items 만들기
import type { BreadcrumbItem } from "@/components/Breadcrumb";
export type BreadcrumbLabels = Record<string, string>;
function titleize(segment: string) {
return decodeURIComponent(segment)
.replace(/[-_]+/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase());
}
export function buildBreadcrumbs(
pathname: string,
labels: BreadcrumbLabels = {},
): BreadcrumbItem[] {
const cleanPath = pathname.split(/[?#]/)[0].replace(/\/+$/, "") || "/";
const segments = cleanPath.split("/").filter(Boolean);
const items: BreadcrumbItem[] = [
{ label: labels["/"] ?? "Home", href: "/" },
];
let href = "";
for (const segment of segments) {
href += `/${segment}`;
items.push({
label: labels[href] ?? labels[segment] ?? titleize(segment),
href,
});
}
return items;
}
Next.js에서는 route params, CMS, frontmatter 중 무엇이 정답인지 Claude Code에 알려 주세요. Astro에서는 Astro.url.pathname과 content collection의 title을 함께 쓰면 자연스럽습니다.
import { Breadcrumb } from "@/components/Breadcrumb";
import { buildBreadcrumbs } from "@/lib/breadcrumbs";
const siteUrl = "https://claudecodelab.com";
export default async function ArticlePage() {
const pathname = "/ko/blog/claude-code-breadcrumb-navigation";
const labels = {
"/": "홈",
"/ko": "한국어",
"/ko/blog": "글",
"/ko/blog/claude-code-breadcrumb-navigation":
"Claude Code로 접근성 있는 브레드크럼 구현하기",
};
const items = buildBreadcrumbs(pathname, labels);
return (
<main>
<Breadcrumb items={items} siteUrl={siteUrl} ariaLabel="브레드크럼" />
<h1>Claude Code로 접근성 있는 브레드크럼 구현하기</h1>
</main>
);
}
모바일 CSS
.breadcrumb {
margin-block: 0 1rem;
font-size: 0.875rem;
color: #4b5563;
}
.breadcrumb__list {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
list-style: none;
margin: 0;
padding: 0;
}
.breadcrumb__item {
align-items: center;
display: inline-flex;
min-width: 0;
}
.breadcrumb__link {
color: #2563eb;
text-decoration: underline;
text-underline-offset: 0.15em;
}
.breadcrumb__current {
color: #111827;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.breadcrumb__separator {
color: #9ca3af;
margin-inline: 0.35rem;
}
@media (max-width: 640px) {
.breadcrumb__list {
flex-wrap: nowrap;
}
.breadcrumb__item:not(:first-child):not(:nth-last-child(-n + 2)) {
display: none;
}
.breadcrumb__item:nth-last-child(2)::after {
color: #9ca3af;
content: "...";
margin-inline: 0.35rem;
}
.breadcrumb__current {
max-width: 58vw;
}
}
테스트
import { describe, expect, it } from "vitest";
import { buildBreadcrumbs } from "./breadcrumbs";
describe("buildBreadcrumbs", () => {
it("returns only Home for the root path", () => {
expect(buildBreadcrumbs("/")).toEqual([{ label: "Home", href: "/" }]);
});
it("builds nested breadcrumbs and ignores query strings", () => {
expect(buildBreadcrumbs("/blog/claude-code?page=2")).toEqual([
{ label: "Home", href: "/" },
{ label: "Blog", href: "/blog" },
{ label: "Claude Code", href: "/blog/claude-code" },
]);
});
it("uses localized labels when provided", () => {
expect(
buildBreadcrumbs("/ko/blog/claude-code-breadcrumb-navigation", {
"/": "홈",
"/ko": "한국어",
"/ko/blog": "글",
"/ko/blog/claude-code-breadcrumb-navigation": "브레드크럼 구현",
}),
).toEqual([
{ label: "홈", href: "/" },
{ label: "한국어", href: "/ko" },
{ label: "글", href: "/ko/blog" },
{ label: "브레드크럼 구현", href: "/ko/blog/claude-code-breadcrumb-navigation" },
]);
});
});
E2E에서는 nav[aria-label], 단 하나의 aria-current="page", 파싱 가능한 JSON-LD, 절대 URL, 모바일 겹침 여부를 확인합니다. 자세한 흐름은 Playwright 테스트가 이어지는 주제입니다.
실무 유스케이스
첫째, 블로그와 문서 사이트입니다. 독자는 홈 > 글 > Claude Code > 제목을 통해 주제 페이지로 돌아갈 수 있습니다.
둘째, 커머스와 유료 교육 페이지입니다. 홈 > 교육 > Claude Code > 팀 워크숍은 튜토리얼 트래픽을 교육 및 상담으로 자연스럽게 연결합니다.
셋째, SaaS 관리 화면입니다. 조직 > 프로젝트 > 설정 > 결제처럼 깊은 화면에서 사용자가 어느 범위를 수정하는지 명확해집니다.
넷째, 다국어 사이트입니다. route prefix, 표시 라벨, JSON-LD name을 모두 같은 언어로 맞춰야 합니다.
실패 사례와 체크리스트
흔한 실패는 현재 페이지를 색상만으로 표현하는 것, JSON-LD에 상대 URL을 넣는 것, slug를 그대로 보여 주는 것, 모바일에서 세 줄 이상으로 줄바꿈되는 것, 화면용 배열과 JSON-LD 배열을 따로 두는 것입니다.
공개 전에는 aria-label, aria-current, 숨겨진 구분자, BreadcrumbList, 1부터 시작하는 position, 절대 URL, 표시와 JSON-LD의 일치, 모바일 표시, locale 라벨, Rich Results Test 또는 Search Console 검증을 확인하세요.
CTA와 검증 결과
브레드크럼은 작은 컴포넌트지만 콘텐츠 비즈니스에서는 회유와 전환에 영향을 줍니다. ClaudeCodeLab에서는 글에서 무료 치트시트, 제품, Claude Code 교육 및 상담으로 이어지는 흐름을 보조합니다.
이 글의 코드는 React 컴포넌트, route helper, Vitest 예제로 나눠 확인했습니다. 가장 효과적인 결정은 화면 표시와 JSON-LD를 같은 items 배열에서 생성한 것입니다. 별도 배열을 쓰던 초안에서는 카테고리명 변경 때 불일치가 생겼지만, 단일 데이터 소스로 바꾸자 Claude Code 리뷰도 단순해졌습니다.
정리
좋은 브레드크럼은 링크 사이에 기호를 넣는 작업이 아닙니다. Claude Code에 라벨, 라우팅, aria-current, JSON-LD, 절대 URL, 모바일 동작, 테스트를 한 번에 요구하고, 공식 문서 기준으로 비판적으로 검토하세요.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Claude Code 권한 세이프티 래더: 통제력을 잃지 않고 allow 넓히기
read-only에서 제한 편집, 검증 명령, deploy 확인까지 권한을 단계적으로 넓히는 방법.
Claude Code Small PR Proof Pack: 작은 PR을 리뷰 가능한 상태로 만드는 증거 세트
Claude Code의 작은 PR에 diff, 검증, 공개 URL, CTA 경로, rollback을 붙이는 실무 체크리스트.
Claude Code 커밋 전 리뷰 게이트: diff, 테스트, 공개 URL, CTA 확인
Claude Code 작업을 커밋하기 전에 diff 범위, build, 공개 URL, Gumroad 링크, 상담 CTA, 테스트 누락과 무관한 파일을 확인하는 방법입니다.