ComponentsP3 본문

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를 막을 때

대신 Spinner

진행률 없이 잠깐 도는 인라인 대기·0.5초 미만 짧은 대기(깜빡임만 늘림)

대신 Progress

0–100% 진행률·불확정 작업을 막대로 보여야 할 때

골격 형태는 실제 콘텐츠와 같게 잡으세요 — 다르게 잡으면 로드 후 시프트가 생겨 목적을 잃습니다.

플레이그라운드#

컨트롤로 props를 조작하면 미리보기와 코드가 실시간 갱신됩니다.

import { Skeleton } from '@wds/ui-web';

<Skeleton />

1계층 · 프리미티브 Skeleton

형태(variant)·모션(animation)·크기를 책임지는 단일 블록입니다. 모든 프리셋이 이 프리미티브만 조합해 만들어집니다.

변형#

text(한 줄) · rect(블록) · rounded(둥근 블록) · circle(아바타류)
  • text — 높이 1줄(space.4) + radius.sm. 문장·라벨 자리
  • rectradius.md 블록. 카드·썸네일 자리
  • roundedradius.lg 블록. 부드러운 미디어·패널 자리
  • circleradius.full. 아바타·아이콘 자리

variant는 radius와 기본 높이만 정하고 애니메이션과 직교합니다 — 어떤 형태든 세 모션과 자유롭게 조합됩니다.

모션#

라이트
pulse(호흡) · wave(셰머) · none(정적) — 라이트/다크 동시
  • pulse(기본) — opacity 1 ↔ 0.5 호흡(1.6s). 컴포지터 친화·저비용
  • wavesurface 70% 셰머 밴드가 좌→우로 스치는 transform 애니메이션. 라이트/다크 모두에서 또렷하게 보이도록 그라데이션을 튜닝했습니다
  • none — 정적 플레이스홀더. 모션 없이 자리만 유지

세 모션 모두 transform/opacity만 움직여 레이아웃·페인트를 유발하지 않습니다. prefers-reduced-motion: reduce에서는 pulse·wave 애니메이션이 완전히 정지하고 정적 플레이스홀더만 남습니다(필수 폴백).

색 강도#

배경 표면과 대비가 약하면 골격이 잘 안 보입니다. tone으로 토큰에 매핑된 강도를 고르세요(임의 색이 아니라 semantic 토큰). 기본값은 흰 카드·페이지에서도 또렷한 default입니다.

라이트
subtle · default(기본) · strong — 라이트/다크 동시
  • subtlesurface-muted(gray-100/700). 이미 강조된 표면 위에서 은은하게
  • default(기본) — border(gray-200/700). 흰 카드·페이지에서도 또렷
  • strongborder-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). 다크는 자동 반전
widthnumber | string편의 너비 — 숫자는 px, 문자열은 그대로. style이 우선
heightnumber | string편의 높이 — 숫자는 px, 문자열은 그대로. style이 우선
…restHTMLAttributes<HTMLDivElement>style(세밀 크기)·className 등 네이티브 속성 전달. ref는 루트 div로

2계층 · 프리셋

흔한 UI 실루엣을 미리 조합한 6종입니다. 모두 프리미티브 Skeleton만 합성하며 (서로를 임포트하지 않아 빌드가 분리됨), animation prop이 자식 전체로 전파됩니다.

SkeletonText#

문단 자리표시 — 여러 줄을 렌더하고 마지막 줄을 짧게 끊어 자연스러운 텍스트 블록을 흉내 냅니다.

기본 3줄 — 마지막 줄 60%로 단락 흉내
Prop타입기본값설명
linesnumber3렌더할 줄 수 — 최소 1로 보정
lastLineWidthstring'60%'lines > 1 일 때 마지막 줄 너비 — 단락 끝맺음 흉내
animation'pulse' | 'wave' | 'none''pulse'자식 Skeleton 전체에 전파되는 모션
…restHTMLAttributes<HTMLDivElement>className·style 등 전달

SkeletonMedia#

미디어 오브젝트 자리표시 — 아바타 + 텍스트 줄 조합. 리스트 행·코멘트·유저 셀 같은 아바타-텍스트 패턴에 씁니다.

원형 아바타 + 2줄 — 코멘트/유저 셀 실루엣
Prop타입기본값설명
avatarShape'circle' | 'rounded''circle'아바타 형태 — circle 또는 rounded 프리미티브
avatarSizenumber | string'2.5rem'아바타 가로/세로 크기 — 숫자는 px, 문자열은 그대로
linesnumber2오른쪽 텍스트 줄 수 — 최소 1로 보정
lastLineWidthstring'60%'lines > 1 일 때 마지막 줄 너비
animation'pulse' | 'wave' | 'none''pulse'자식 Skeleton 전체에 전파되는 모션
…restHTMLAttributes<HTMLDivElement>className·style 등 전달

SkeletonCard#

Card 실루엣 — 상단 미디어 블록(16 / 9) + 타이틀 줄 + 본문 줄. 그리드/피드 카드 로딩에 씁니다.

미디어 + 타이틀 + 본문 2줄 — 피드 카드 실루엣
Prop타입기본값설명
mediabooleantrue상단 미디어 블록 표시 여부
mediaAspectRatiostring'16 / 9'미디어 블록 가로세로 비율 — CSS aspect-ratio 값
linesnumber2본문 라인 수 — 0이면 타이틀만
animation'pulse' | 'wave' | 'none''pulse' (Skeleton 기본 상속)자식 Skeleton 전체에 전파되는 모션
…restHTMLAttributes<HTMLDivElement>className·style 등 전달

SkeletonList#

반복 행 리스트 — 각 행은 원형 아바타 + 텍스트 줄. divided로 행 사이 1px 구분선을 넣을 수 있습니다. 컨테이너 루트에 aria-hidden이 붙어 행마다가 아니라 한 번만 숨겨집니다.

3행 · 행당 2줄 — 알림/멤버 리스트 실루엣
Prop타입기본값설명
itemsnumber3반복 행 수 — 최소 1로 보정
avatarbooleantrue행 앞 원형 아바타 표시 여부
linesnumber2행당 텍스트 줄 수 — 최소 1로 보정
animation'pulse' | 'wave' | 'none''pulse'자식 Skeleton 전체에 전파되는 모션
dividedbooleanfalse행 사이 1px 구분선(color.border)
…restHTMLAttributes<HTMLDivElement>className·style 등 전달

SkeletonTable#

데이터 테이블 실루엣(Carbon DataTableSkeleton 스타일) — 헤더 행 + 바디 셀 그리드. 셀 너비는 난수가 아니라 (r+c) % 4 결정론적 패턴이라 SSR/CSR 마크업이 일치합니다. 긴 표는 셀마다 알리지 말고 컨테이너에서 한 번 알리세요(접근성 절 참고).

헤더 + 5행 × 4열 — 데이터 그리드 실루엣
Prop타입기본값설명
rowsnumber5바디 행 수 — 최소 1로 보정
columnsnumber4컬럼 수 — 최소 1로 보정
headerbooleantrue헤더 행 표시 여부
animation'pulse' | 'wave' | 'none''pulse'자식 Skeleton 전체에 전파되는 모션
…restHTMLAttributes<HTMLDivElement>style(gridTemplateColumns 병합)·className 등 전달

SkeletonImage#

이미지/썸네일 자리표시 — aspect-ratio + radius variant + 중앙 장식 글리프(산 아이콘). 글리프는 aria-hidden 장식이라 보조기술이 읽지 않습니다.

16/9 · radius md · 중앙 글리프 — 썸네일 실루엣
Prop타입기본값설명
aspectRatiostring'16 / 9'이미지 종횡비 — CSS aspect-ratio 값
radius'none' | 'sm' | 'md' | 'lg' | 'full''md'모서리 반경 변형 — radius 토큰 매핑
iconbooleantrue중앙 장식 이미지 글리프 표시 여부
widthnumber | string명시적 너비 — 숫자는 px, 문자열은 그대로
animation'pulse' | 'wave' | 'none''pulse' (Skeleton 기본 상속)내부 Skeleton 모션
…restHTMLAttributes<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타입기본값설명
loadedbooleantrue면 children(실콘텐츠), false면 placeholder 렌더
placeholderReactNode!loaded 상태에서 렌더할 스켈레톤 트리 (필수)
childrenReactNodeloaded 상태에서 렌더할 실콘텐츠 (필수)
announcestring'콘텐츠를 불러오는 중'로딩 중 스크린리더 알림 문구 — 전용 라이브 리전 텍스트로 노출
…restOmit<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) — tonesubtle 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 선례

관련#