Claude Code 스켈레톤 로딩 실전 가이드: React, CLS, 접근성
Claude Code로 스켈레톤 로딩을 구현하는 방법. React 예제, CLS 대응, 접근성, 실패 사례를 다룹니다.
스켈레톤 로딩은 데이터가 아직 도착하지 않았을 때 화면의 대략적인 뼈대를 먼저 보여주는 UI 패턴입니다. 쉽게 말하면 이미지, 제목, 설명, 버튼이 들어갈 자리를 미리 잡아 두는 방식입니다.
스피너는 “처리 중”이라는 사실만 알려 줍니다. 스켈레톤은 “어떤 종류의 콘텐츠가 어디에 나타날지”까지 보여 줍니다. 이미지, 광고, API 응답이 이미 확보된 공간 안에 들어오면 화면이 갑자기 밀리는 일을 줄일 수 있습니다. 이런 시각적 안정성은 web.dev의 Cumulative Layout Shift와 Core Web Vitals 설명과도 연결됩니다.
이 글에서는 Claude Code에 스켈레톤 로딩을 맡길 때의 프롬프트, 복사해서 실행할 수 있는 React 예제, CSS 동작, 접근성, 실패 사례, 간단한 검증 방법을 정리합니다. 관련 내용은 Claude Code 성능 최적화, 이미지 지연 로딩, 접근성 구현 워크플로도 함께 참고하세요.
먼저 작업을 나누기
스켈레톤은 회색 사각형 몇 개를 놓는 일이 아닙니다. 실제 기능에서는 로딩, 성공, 빈 상태, 오류 상태가 필요하고, 이 상태들이 같은 레이아웃 안에서 안정적으로 전환되어야 합니다. Claude Code에 “예쁜 스켈레톤을 만들어줘”라고만 말하면 shimmer 효과는 그럴듯하지만 오류 UI, 움직임 감소 설정, 스크린 리더 안내가 빠질 수 있습니다.
flowchart LR
P["Claude Code 프롬프트"] --> S["비슷한 크기의 스켈레톤"]
S --> D["실제 데이터"]
D --> E["빈 상태"]
D --> X["오류 상태"]
S --> A["aria-busy / status"]
S --> M["prefers-reduced-motion"]
S --> C["CLS 확인"]
처음에는 범위와 확인 조건을 함께 전달합니다.
Read the existing card/list components before editing.
Implement skeleton loading only for the article cards list.
Keep the skeleton dimensions close to the loaded content.
Handle loading, empty, error, and success states.
Respect prefers-reduced-motion and avoid layout shift.
Add a small Playwright check if the project already uses Playwright.
Do not change unrelated styles, routing, or data fetching.
prefers-reduced-motion은 사용자가 운영체제나 브라우저에서 움직임을 줄이도록 설정했는지 확인하는 CSS 조건입니다. 강한 shimmer 애니메이션은 일부 사용자에게 부담이 될 수 있으므로 MDN의 prefers-reduced-motion을 확인하고 정적인 대안을 넣어 둡니다.
실제로 유용한 사용 사례
스켈레톤 로딩은 사용자가 곧 어떤 콘텐츠가 나올지 이해할 수 있지만, 아직 데이터는 보여줄 수 없는 화면에서 특히 효과적입니다.
| 사용 사례 | 미리 잡을 공간 | 주의할 점 |
|---|---|---|
| 글 카드 목록 | 썸네일, 두 줄 제목, 요약, 태그 | 미디어 높이를 고정해 카드가 튀지 않게 한다 |
| 대시보드 | KPI 카드, 차트 영역, 최근 활동 | 일부 숫자만 먼저 보여 오해를 만들지 않는다 |
| 쇼핑몰 상품 목록 | 상품 이미지, 이름, 가격, 평점 | 갱신 중 오래된 가격이나 재고를 믿게 만들지 않는다 |
| 관리자 테이블 | 헤더, 행, 작업 버튼 영역 | 행 수가 크게 바뀌면 페이지네이션도 함께 점검한다 |
| 상담/콘텐츠 랜딩 페이지 | 사례 카드, CTA, FAQ | CTA가 늦게 나타나면 전환 경로가 아래로 밀린다 |
Masa가 ClaudeCodeLab 상담 흐름에서 테스트했을 때 가장 효과가 컸던 부분은 글 카드와 CTA 영역의 높이를 먼저 고정한 것이었습니다. 반대로 스켈레톤을 실제 콘텐츠보다 크게 만들면 로딩 완료 시 화면이 위로 튑니다. 스켈레톤은 장식이 아니라 최종 레이아웃과 맞추는 약속입니다.
복사해서 실행하는 React 예제
아래 코드는 Vite + React + TypeScript 프로젝트의 src/App.tsx에 붙여 넣어 실행할 수 있습니다. 실제 API 대신 setTimeout으로 지연을 만들고 성공, 빈 상태, 오류 상태를 버튼으로 바꿔 봅니다. Claude Code에 실제 저장소를 수정하게 할 때도 이 정도로 상태를 명확히 전달하는 편이 좋습니다.
import { useEffect, useState } from "react";
import "./skeleton-demo.css";
type Article = {
id: number;
title: string;
description: string;
tag: string;
};
type LoadState = "loading" | "success" | "empty" | "error";
const demoArticles: Article[] = [
{
id: 1,
title: "Claude Code로 안전한 UI 변경 만들기",
description: "기존 컴포넌트를 먼저 읽고 카드 목록의 로딩 경험만 개선합니다.",
tag: "UX",
},
{
id: 2,
title: "CLS를 늘리지 않는 이미지 자리 잡기",
description: "실제 데이터가 오기 전에 미디어, 제목, 요약의 높이를 고정합니다.",
tag: "Performance",
},
{
id: 3,
title: "접근 가능한 로딩 상태",
description: "aria-busy, status 메시지, 움직임 감소 설정을 함께 사용합니다.",
tag: "A11y",
},
];
function SkeletonLine({ width = "100%" }: { width?: string }) {
return <span className="sk-line" style={{ width }} aria-hidden="true" />;
}
function ArticleCardSkeleton() {
return (
<article className="article-card is-skeleton" aria-hidden="true">
<div className="sk-media" />
<div className="article-card__body">
<SkeletonLine width="46%" />
<SkeletonLine />
<SkeletonLine width="86%" />
<SkeletonLine width="32%" />
</div>
</article>
);
}
function ArticleCard({ article }: { article: Article }) {
return (
<article className="article-card">
<div className="article-card__media">{article.tag}</div>
<div className="article-card__body">
<p className="article-card__tag">{article.tag}</p>
<h2>{article.title}</h2>
<p>{article.description}</p>
</div>
</article>
);
}
export default function App() {
const [state, setState] = useState<LoadState>("loading");
const [articles, setArticles] = useState<Article[]>([]);
useEffect(() => {
const timer = window.setTimeout(() => {
setArticles(demoArticles);
setState("success");
}, 1200);
return () => window.clearTimeout(timer);
}, []);
const reloadAs = (nextState: LoadState) => {
setState("loading");
setArticles([]);
window.setTimeout(() => {
setArticles(nextState === "success" ? demoArticles : []);
setState(nextState);
}, 700);
};
return (
<main className="demo-shell">
<div className="demo-toolbar" aria-label="표시 상태 변경">
<button onClick={() => reloadAs("success")}>성공</button>
<button onClick={() => reloadAs("empty")}>빈 상태</button>
<button onClick={() => reloadAs("error")}>오류</button>
</div>
<section
aria-busy={state === "loading"}
aria-describedby="article-list-status"
className="article-grid"
>
<p id="article-list-status" className="sr-only" role="status">
{state === "loading" ? "글 목록을 불러오는 중입니다" : "글 목록 로딩이 완료되었습니다"}
</p>
{state === "loading" &&
Array.from({ length: 3 }).map((_, index) => (
<ArticleCardSkeleton key={index} />
))}
{state === "success" &&
articles.map((article) => (
<ArticleCard key={article.id} article={article} />
))}
{state === "empty" && (
<div className="state-panel">아직 표시할 글이 없습니다.</div>
)}
{state === "error" && (
<div className="state-panel" role="alert">
글 목록을 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.
</div>
)}
</section>
</main>
);
}
접근성에서 중요한 점은 스켈레톤 자체를 지나치게 읽히지 않는 것입니다. 회색 줄이 세 개 있다는 사실은 의미 있는 정보가 아닙니다. 위 예제에서는 스켈레톤 카드에aria-hidden을 넣고, 목록 상태만 하나의role="status"로 조용히 알립니다. 긴급하지 않은 상태 변화에는 MDN의 ARIA status role을 참고하면 됩니다.
크기와 움직임을 고정하는 CSS
다음 CSS를src/skeleton-demo.css로 저장합니다. 핵심은 로딩 중과 로딩 후의min-height, 미디어 높이, 본문 여백이 크게 달라지지 않게 하는 것입니다. shimmer는 과하지 않게 두고, 움직임 감소 설정에서는 애니메이션을 멈춥니다.
:root {
color: #18212f;
background: #f6f7f9;
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
button {
min-height: 40px;
border: 1px solid #b8c2d6;
border-radius: 8px;
background: #ffffff;
color: #18212f;
padding: 0 14px;
font-weight: 700;
}
.demo-shell {
width: min(1040px, calc(100% - 32px));
margin: 40px auto;
}
.demo-toolbar {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 18px;
}
.article-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
}
.article-card {
min-height: 316px;
overflow: hidden;
border: 1px solid #d7deea;
border-radius: 8px;
background: #ffffff;
}
.article-card__media,
.sk-media {
display: grid;
min-height: 148px;
place-items: center;
background: #dfe7f3;
color: #39506f;
font-weight: 800;
}
.article-card__body {
display: grid;
gap: 10px;
padding: 18px;
}
.article-card__tag {
color: #3b6b4f;
font-size: 0.875rem;
font-weight: 800;
}
.article-card h2 {
min-height: 56px;
margin: 0;
font-size: 1.16rem;
line-height: 1.45;
}
.article-card p {
margin: 0;
line-height: 1.7;
}
.sk-line,
.sk-media {
border-radius: 8px;
background: linear-gradient(90deg, #d9e0ea 25%, #edf1f7 37%, #d9e0ea 63%);
background-size: 240% 100%;
animation: skeleton-shimmer 1.4s ease-in-out infinite;
}
.sk-line {
display: block;
height: 16px;
}
.state-panel {
min-height: 180px;
display: grid;
place-items: center;
border: 1px solid #d7deea;
border-radius: 8px;
background: #ffffff;
padding: 24px;
text-align: center;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
}
@keyframes skeleton-shimmer {
from {
background-position: 120% 0;
}
to {
background-position: -120% 0;
}
}
@media (prefers-reduced-motion: reduce) {
.sk-line,
.sk-media {
animation: none;
background: #d9e0ea;
}
}
이 CSS는 화려한 빛 효과보다 레이아웃 안정성을 우선합니다. 스켈레톤 줄의 폭을 너무 무작위로 만들면 실제 콘텐츠가 나타날 때 다른 디자인으로 바뀐 것처럼 느껴집니다. 제목 두 줄, 요약 두 줄, 고정 미디어 영역처럼 완성 화면과 가까운 기준을 먼저 잡는 편이 좋습니다.
Playwright로 최소한의 깨짐 확인
이미 Playwright를 쓰는 프로젝트라면 작은 테스트 하나를 추가합니다. 실제 사용자 환경의 CLS를 완전히 증명하지는 못하지만, 리뷰 전에 명백한 퇴행을 잡을 수 있습니다.
import { expect, test } from "@playwright/test";
test("article skeleton keeps a stable card area", async ({ page }) => {
await page.goto("/");
await expect(page.getByText("글 목록을 불러오는 중입니다")).toBeAttached();
await expect(page.locator(".is-skeleton")).toHaveCount(3);
const firstBox = await page.locator(".article-card").first().boundingBox();
expect(firstBox?.height).toBeGreaterThan(280);
await page.getByRole("button", { name: "오류" }).click();
await expect(page.getByRole("alert")).toContainText("불러오지 못했습니다");
});
실제 CLS는 이미지, 광고, 웹폰트, 서드파티 스크립트, 네트워크, 기기 상태의 영향을 받습니다. 이 테스트는 빠른 경고 장치로 두고, 더 강한 판단은 web.dev의 CLS 가이드와 실제 운영 데이터를 함께 봅니다.
흔한 실패와 함정
첫 번째 실패는 스켈레톤 크기가 최종 콘텐츠와 맞지 않는 것입니다. 로딩 중에는 좋아 보이지만 실제 카드가 나타나는 순간 높이가 크게 바뀝니다. 이미지에는width, height, aspect-ratio 중 하나를 정하고, 광고와 CTA도 렌더링 전에 자리를 잡습니다.
두 번째는 아주 빠른 요청에도 항상 스켈레톤을 보여 주는 것입니다. 보통 100ms 안에 끝나는 요청이라면 오히려 깜빡임이 생깁니다. 실무에서는 “300ms를 넘으면 표시”, “첫 로드에서만 표시”, “새로고침 중에는 기존 데이터를 유지” 같은 규칙을 둡니다.
세 번째는 접근성 안내가 너무 많은 것입니다. 카드마다role="status"를 넣으면 같은 메시지가 반복될 수 있습니다. 라이브 메시지는 목록 단위로 모으고, 시각적 모양은 보조 기술에서 숨깁니다.
네 번째는 실패 상태를 만들지 않는 것입니다. API 오류가 나도 스켈레톤이 계속 보이면 사용자는 아직 로딩 중이라고 생각합니다. 오류, 빈 상태, 재시도 버튼을 별도 상태로 설계합니다.
다섯 번째는 제품 판단을 Claude Code에 맡기는 것입니다. Claude Code는 파일을 읽고 코드를 만들 수 있지만, 어떤 CTA를 우선할지, 광고 공간을 얼마나 예약할지, 무엇을 먼저 보여줄지는 사람이 결정해야 합니다.
Claude Code에 리뷰시키기
구현 후에는 바로 공개하지 말고 리뷰 모드로 전환합니다.
Review only the skeleton loading changes.
Check whether loaded content and skeleton content reserve similar space.
Check loading, success, empty, and error states.
Check reduced-motion behavior and ARIA announcements.
Point out any code that may increase CLS or create repeated screen reader messages.
Return findings with file names and exact lines.
이 프롬프트는 Claude Code를 구현자에서 비판적 리뷰어로 바꾸기 위한 것입니다. 글 사이트에서는 광고, 관련 글, CTA, 지연 로딩 이미지가 같은 시각 흐름에 영향을 줍니다. CSS 스타일링 가이드와 테스트 전략을 함께 사용해 시각 확인, 스크린 리더 확인, 회귀 테스트를 분리하세요.
수익 흐름과의 관계
스켈레톤 로딩은 UX뿐 아니라 수익 흐름도 보호합니다. 상담 CTA, 상품 카드, 뉴스레터 폼, 광고 영역이 늦게 나타나 본문을 밀어내면 독자는 읽던 위치를 잃거나 의도하지 않은 곳을 누를 수 있습니다. 신뢰와 전환 모두에 좋지 않습니다.
개인 개발자는 무료 Claude Code 치트시트로 UI 변경 전후 확인 절차를 먼저 고정해 보세요. 팀 단위로 스켈레톤, 이미지 지연 로딩, Core Web Vitals, 접근성을 표준 워크플로로 만들고 싶다면 Claude Code 교육 및 도입 상담에서 기존 저장소를 기준으로 개선 순서와 프롬프트, 컴포넌트, 검증 항목을 정리할 수 있습니다.
정리
좋은 스켈레톤 로딩은 기다림을 숨기는 효과가 아닙니다. 최종 화면에 가까운 공간을 미리 확보하고, 불확실성을 줄이며, 눈에 띄는 레이아웃 이동을 줄이는 설계입니다. Claude Code를 사용할 때는 대상 컴포넌트, 상태, 크기, 접근성 요구사항, 검증 명령을 한 번에 전달하세요.
이 흐름을 실제로 시험해 보니 가장 효과적인 것은 미디어와 제목 높이를 먼저 고정하고, 목록 단위로 로딩 상태를 알리며, 움직임 감소 사용자에게 애니메이션을 멈추는 것이었습니다. shimmer 모양만 먼저 다듬으면 빈 상태와 오류 상태가 뒤로 밀려 리뷰 시간이 늘어납니다. 구현과 검증을 함께 요청하는 편이 공개 가능한 품질에 더 가깝습니다.
무료 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, 테스트 누락과 무관한 파일을 확인하는 방법입니다.