Skeleton
로딩 중 구조 유지 플레이스홀더 3계층 — variant 4종(text/rect/rounded/circle)·animation 3종(pulse/wave/none)·tone 3종(subtle/default/strong) 프리미티브, 6종 프리셋(Text·Media·Card·List·Table·Image), SkeletonLoadable a11y 래퍼. 모션은 컴포지터 친화(opacity/transform)이며 prefers-reduced-motion에서 정적입니다.
마지막 업데이트 2026-06-13
한눈에#
Skeleton은 세 계층으로 구성됩니다. ① 형태·모션·크기를 책임지는 프리미티브
Skeleton, ② 실제 UI 실루엣을 조합한 6종 프리셋(SkeletonText·SkeletonMedia·
SkeletonCard·SkeletonList·SkeletonTable·SkeletonImage), ③ 로딩→완료 전환과
보조기술 알림을 한 번에 처리하는 접근성 래퍼 SkeletonLoadable입니다. 골격 자체는
장식이라 보조기술이 읽지 않고, 상태 알림은 컨테이너에서 단 한 번 발생합니다.
- 프리미티브 3축
- 프리셋 6종
- SkeletonLoadable
- reduced-motion 정적
실제 콘텐츠와 같은 형태·크기로 자리를 잡아 로드 후 레이아웃 시프트(CLS)를 막습니다
사용 시점#
페치 동안 실제 콘텐츠와 같은 형태로 자리를 유지해야 하면 Skeleton — 진행률·단순 대기는 다른 컴포넌트입니다.
데이터 페치 동안 실제 콘텐츠와 같은 형태·크기로 자리를 유지해 CLS를 막을 때
골격 형태는 실제 콘텐츠와 같게 잡으세요 — 다르게 잡으면 로드 후 시프트가 생겨 목적을 잃습니다.
플레이그라운드#
컨트롤로 props를 조작하면 미리보기와 코드가 실시간 갱신됩니다.
import { Skeleton } from '@wds/ui-web';
<Skeleton />1계층 · 프리미티브 Skeleton
형태(variant)·모션(animation)·크기를 책임지는 단일 블록입니다. 모든 프리셋이 이 프리미티브만 조합해 만들어집니다.
변형#
- text — 높이 1줄(
space.4) +radius.sm. 문장·라벨 자리 - rect —
radius.md블록. 카드·썸네일 자리 - rounded —
radius.lg블록. 부드러운 미디어·패널 자리 - circle —
radius.full. 아바타·아이콘 자리
variant는 radius와 기본 높이만 정하고 애니메이션과 직교합니다 — 어떤 형태든 세 모션과 자유롭게 조합됩니다.
모션#
- pulse(기본) — opacity 1 ↔ 0.5 호흡(1.6s). 컴포지터 친화·저비용
- wave —
surface70% 셰머 밴드가 좌→우로 스치는transform애니메이션. 라이트/다크 모두에서 또렷하게 보이도록 그라데이션을 튜닝했습니다 - none — 정적 플레이스홀더. 모션 없이 자리만 유지
세 모션 모두 transform/opacity만 움직여 레이아웃·페인트를 유발하지 않습니다.
prefers-reduced-motion: reduce에서는 pulse·wave 애니메이션이 완전히 정지하고
정적 플레이스홀더만 남습니다(필수 폴백).
색 강도#
배경 표면과 대비가 약하면 골격이 잘 안 보입니다. tone으로 토큰에 매핑된 강도를
고르세요(임의 색이 아니라 semantic 토큰). 기본값은 흰 카드·페이지에서도 또렷한 default입니다.
- subtle —
surface-muted(gray-100/700). 이미 강조된 표면 위에서 은은하게 - default(기본) —
border(gray-200/700). 흰 카드·페이지에서도 또렷 - strong —
border-strong(gray-300/600). 대비가 약한 표면에서 최대 가시성
tone은 6종 프리셋에도 동일하게 전파됩니다 — 예: <SkeletonCard tone="strong" />.
크기#
전용 size prop은 없고 편의 width/height 를 제공합니다 — 숫자는 px로 변환,
문자열은 그대로 통과합니다. 호출부 style이 항상 최종 우선이라
style={{ aspectRatio: '16 / 9' }} 같은 세밀 제어와도 충돌하지 않습니다.
실제 콘텐츠와 같은 크기를 지정해야 로드 후 레이아웃 시프트(CLS)가 없습니다.
상태#
골격은 인터랙션 상태가 없습니다(장식·aria-hidden 고정). 유일한 변화는 모션
모디파이어이며, reduced-motion에서 정적이 됩니다. 상태/훅이 없어 RSC에서
그대로 렌더됩니다.
Props#
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
variant | 'text' | 'rect' | 'rounded' | 'circle' | 'rect' | 자리표시 형태 — radius·기본 높이 프리셋이 달라집니다 |
animation | 'pulse' | 'wave' | 'none' | 'pulse' | 로딩 모션 — pulse(호흡)·wave(셰머)·none(정적). reduced-motion에서 정지 |
tone | 'subtle' | 'default' | 'strong' | 'default' | 색 강도 — 토큰 매핑(subtle gray-100·default gray-200·strong gray-300). 다크는 자동 반전 |
width | number | string | — | 편의 너비 — 숫자는 px, 문자열은 그대로. style이 우선 |
height | number | string | — | 편의 높이 — 숫자는 px, 문자열은 그대로. style이 우선 |
…rest | HTMLAttributes<HTMLDivElement> | — | style(세밀 크기)·className 등 네이티브 속성 전달. ref는 루트 div로 |
2계층 · 프리셋
흔한 UI 실루엣을 미리 조합한 6종입니다. 모두 프리미티브 Skeleton만 합성하며
(서로를 임포트하지 않아 빌드가 분리됨), animation prop이 자식 전체로 전파됩니다.
SkeletonText#
문단 자리표시 — 여러 줄을 렌더하고 마지막 줄을 짧게 끊어 자연스러운 텍스트 블록을 흉내 냅니다.
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
lines | number | 3 | 렌더할 줄 수 — 최소 1로 보정 |
lastLineWidth | string | '60%' | lines > 1 일 때 마지막 줄 너비 — 단락 끝맺음 흉내 |
animation | 'pulse' | 'wave' | 'none' | 'pulse' | 자식 Skeleton 전체에 전파되는 모션 |
…rest | HTMLAttributes<HTMLDivElement> | — | className·style 등 전달 |
SkeletonMedia#
미디어 오브젝트 자리표시 — 아바타 + 텍스트 줄 조합. 리스트 행·코멘트·유저 셀 같은 아바타-텍스트 패턴에 씁니다.
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
avatarShape | 'circle' | 'rounded' | 'circle' | 아바타 형태 — circle 또는 rounded 프리미티브 |
avatarSize | number | string | '2.5rem' | 아바타 가로/세로 크기 — 숫자는 px, 문자열은 그대로 |
lines | number | 2 | 오른쪽 텍스트 줄 수 — 최소 1로 보정 |
lastLineWidth | string | '60%' | lines > 1 일 때 마지막 줄 너비 |
animation | 'pulse' | 'wave' | 'none' | 'pulse' | 자식 Skeleton 전체에 전파되는 모션 |
…rest | HTMLAttributes<HTMLDivElement> | — | className·style 등 전달 |
SkeletonCard#
Card 실루엣 — 상단 미디어 블록(16 / 9) + 타이틀 줄 + 본문 줄. 그리드/피드 카드
로딩에 씁니다.
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
media | boolean | true | 상단 미디어 블록 표시 여부 |
mediaAspectRatio | string | '16 / 9' | 미디어 블록 가로세로 비율 — CSS aspect-ratio 값 |
lines | number | 2 | 본문 라인 수 — 0이면 타이틀만 |
animation | 'pulse' | 'wave' | 'none' | 'pulse' (Skeleton 기본 상속) | 자식 Skeleton 전체에 전파되는 모션 |
…rest | HTMLAttributes<HTMLDivElement> | — | className·style 등 전달 |
SkeletonList#
반복 행 리스트 — 각 행은 원형 아바타 + 텍스트 줄. divided로 행 사이 1px 구분선을
넣을 수 있습니다. 컨테이너 루트에 aria-hidden이 붙어 행마다가 아니라 한 번만
숨겨집니다.
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
items | number | 3 | 반복 행 수 — 최소 1로 보정 |
avatar | boolean | true | 행 앞 원형 아바타 표시 여부 |
lines | number | 2 | 행당 텍스트 줄 수 — 최소 1로 보정 |
animation | 'pulse' | 'wave' | 'none' | 'pulse' | 자식 Skeleton 전체에 전파되는 모션 |
divided | boolean | false | 행 사이 1px 구분선(color.border) |
…rest | HTMLAttributes<HTMLDivElement> | — | className·style 등 전달 |
SkeletonTable#
데이터 테이블 실루엣(Carbon DataTableSkeleton 스타일) — 헤더 행 + 바디 셀 그리드.
셀 너비는 난수가 아니라 (r+c) % 4 결정론적 패턴이라 SSR/CSR 마크업이 일치합니다.
긴 표는 셀마다 알리지 말고 컨테이너에서 한 번 알리세요(접근성 절 참고).
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
rows | number | 5 | 바디 행 수 — 최소 1로 보정 |
columns | number | 4 | 컬럼 수 — 최소 1로 보정 |
header | boolean | true | 헤더 행 표시 여부 |
animation | 'pulse' | 'wave' | 'none' | 'pulse' | 자식 Skeleton 전체에 전파되는 모션 |
…rest | HTMLAttributes<HTMLDivElement> | — | style(gridTemplateColumns 병합)·className 등 전달 |
SkeletonImage#
이미지/썸네일 자리표시 — aspect-ratio + radius variant + 중앙 장식 글리프(산 아이콘).
글리프는 aria-hidden 장식이라 보조기술이 읽지 않습니다.
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
aspectRatio | string | '16 / 9' | 이미지 종횡비 — CSS aspect-ratio 값 |
radius | 'none' | 'sm' | 'md' | 'lg' | 'full' | 'md' | 모서리 반경 변형 — radius 토큰 매핑 |
icon | boolean | true | 중앙 장식 이미지 글리프 표시 여부 |
width | number | string | — | 명시적 너비 — 숫자는 px, 문자열은 그대로 |
animation | 'pulse' | 'wave' | 'none' | 'pulse' (Skeleton 기본 상속) | 내부 Skeleton 모션 |
…rest | HTMLAttributes<HTMLDivElement> | — | className·style 등 전달 |
3계층 · SkeletonLoadable
로딩→완료 전환과 보조기술 알림을 한 곳에서 처리하는 접근성 캡스톤 래퍼입니다.
loaded 한 prop으로 골격을 보여줄지 실콘텐츠를 보여줄지 결정하고, 완료 시
실콘텐츠를 fade-in으로 노출합니다.
wrap-and-reveal 패턴#
placeholder(골격 트리)와 children(실콘텐츠)을 함께 넘기면, 래퍼가 로딩 동안에는
placeholder를, 완료 후에는 children을 렌더합니다. 알림 채널과 콘텐츠 채널을
분리한 것이 핵심입니다 — 알림 문구는 전용 visually-hidden 라이브 리전
(role="status" + aria-live="polite")에만 담고, placeholder/children은 그 밖에
둬서 스크린리더가 콘텐츠 전체를 읽어버리지 않게 합니다. 콘텐츠 영역에는 aria-busy가
붙어 로딩 상태를 기술하므로, 수백 개 골격 도형마다 알리지 않습니다.
<SkeletonLoadable
loaded={!isLoading}
announce="목록을 불러오는 중"
placeholder={<SkeletonList items={5} />}
>
<NotificationList items={data} />
</SkeletonLoadable>
loaded={false}—placeholder를 렌더하고, 전용 라이브 리전이announce텍스트를 내보내며, 콘텐츠 영역은aria-busy="true"loaded={true}—children을 fade-in으로 렌더하고, 라이브 리전을 비워(콘텐츠를 읽지 않음) 콘텐츠 영역을aria-busy="false"로 전환announce— 로딩 중 스크린리더 문구. 기본'콘텐츠를 불러오는 중'
이 래퍼는 상태/훅이 없어 RSC에서 렌더되며, 로딩 여부(loaded)는 호출부가 소유합니다.
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
loaded | boolean | — | true면 children(실콘텐츠), false면 placeholder 렌더 |
placeholder | ReactNode | — | !loaded 상태에서 렌더할 스켈레톤 트리 (필수) |
children | ReactNode | — | loaded 상태에서 렌더할 실콘텐츠 (필수) |
announce | string | '콘텐츠를 불러오는 중' | 로딩 중 스크린리더 알림 문구 — 전용 라이브 리전 텍스트로 노출 |
…rest | Omit<HTMLAttributes<div>, 'children'> | — | className·style 등 전달. ref는 루트 div로 |
접근성#
골격 도형은 장식, 알림은 컨테이너 한 곳에서 — 가 핵심 계약입니다.
- 장식 도형은
aria-hidden— 프리미티브Skeleton은 항상aria-hidden="true"이고,SkeletonImage의 중앙 글리프도 마찬가지입니다. 또한 모든 프리셋의 컨테이너 루트가aria-hidden이라 장식 서브트리 전체가 접근성 트리에서 단일 노드로 숨겨집니다 — 빈 노드가 트리를 어지럽히지 않습니다 - 상태 알림은
SkeletonLoadable이 한 번만 — 전용 visually-hidden 라이브 리전 (role="status"+aria-live="polite")이announce텍스트만 담고,placeholder/실콘텐츠는 라이브 리전 밖에 둬 콘텐츠가 통째로 읽히지 않습니다. 콘텐츠 영역의aria-busy가 "불러오는 중 → 완료"를 기술합니다. 골격을 직접 쓸 때 로딩을 알리려면 이 래퍼로 감싸거나 Spinner(role="status")를 병행하세요 - 긴 리스트·테이블은 컨테이너에서 한 번 —
SkeletonList/SkeletonTable의 수십~수백 도형마다 알리지 마세요. 셀·행은aria-hidden이고, 알림은 바깥SkeletonLoadable컨테이너에서 한 번만 발생합니다. 라이브 영역 폭주를 막는 핵심입니다 - reduced-motion 폴백 필수 —
prefers-reduced-motion: reduce에서 pulse·wave가 완전히 정지하고 정적 플레이스홀더만 남습니다. 컴포지터 친화 속성(opacity/transform)만 움직여 저비용입니다 - 포커스 불가 — 골격은 인터랙티브 요소가 아니라 탭 순서에 들어가지 않습니다.
포커스 가능한 자리표시가 필요하면 실제 컨트롤을
disabled로 두세요
토큰#
프리미티브·프리셋 모두 component 토큰 없이 semantic을 직접 소비합니다(신설 기준 §4 미충족).
| 속성 | 토큰 |
|---|---|
| 바탕(fill) — tone | subtle color.surface-muted · default color.border · strong color.border-strong |
| text 변형 | 높이 space.4 + radius.sm |
| rect 변형 | radius.md |
| rounded 변형 | radius.lg |
| circle 변형 | radius.full |
| wave 셰머 밴드 | color.surface 70% color-mix 그라데이션(양 끝 transparent 감쇠) |
| 리스트 구분선 | color.border (divided 1px) |
| 이미지 글리프 | color.text-subtle (장식 — 모든 tone에서 대비 확보) |
| 모션 이징 | ease.standard (pulse·wave 공통) |
| 모션 주기 | 1.6s 리터럴 — duration 토큰(≤slow 400ms) 범위 밖, AgentCard 선례 |