Skip to content

스켈레톤 (Skeleton) ​

데이터가 오기 전에 표 · 목록 · 카드의 자리를 회색 면으로 미리 잡아, 화면에 들어갈 때 내용이 덜컹거리지 않게 합니다.

CSS 전용 · Web / Mobile · 2026-10-21 신설

규칙 (강제) · 코딩 에이전트용
  • 기존 로딩 표시를 대체하지 않습니다. 스피너 · 「불러오는 중…」 한 줄은 그대로 두고, 그 아래 자리를 스켈레톤이 잡습니다. 목적은 도착 순간의 덜컹(높이 변화 · 밀림)을 없애는 것입니다.
  • 「첫 로딩」에만 그립니다. 이미 그려진 목록을 필터 · 새로고침으로 다시 부를 때는 이전 내용을 그대로 둡니다. 기준 상태는 「첫 응답이 왔다(성공 · 실패 무관)」 한 가지 — employeesLoaded · rosterSettled · leaveBalanceSettled 처럼 finally 에서 켭니다. 실패 때 켜지 않으면 스켈레톤이 영원히 남습니다.
  • 빈 상태 문구는 로딩이 끝난 뒤에만. x-if="items.length === 0" 만 보고 「…없어요」를 그리면 로딩 중에 빈 상태가 먼저 뜹니다. x-if="loaded && items.length === 0" 로 씁니다.
  • 「있을 때만 나오는」 카드에는 그리지 않습니다 — 없는 사람에게 스켈레톤을 그리면 카드가 떴다 사라지는 새 덜컹이 생깁니다. 꼭 필요하면 지난번 결과를 기억해 있던 사람에게만 그립니다(직원 앱 홈 「지금 할 일」 readHadTodos).
  • 색은 토큰 한 가지 — --color-skeleton(라이트 neutral-75, 다크는 면보다 한 단계 밝은 회색). 면에 따라 변형을 붙입니다.
    • 흰 면(카드 · 표 · 흰 화면) — 기본, 변형 없음.
    • 회색 면(bg-bg-alternative 카드 · 직원 앱 셸 바탕) — ds-skeleton--on-sunken. neutral-75 는 회색 바탕(neutral-50)과 4 차이라 사라집니다.
    • 떠 있는 면(모달 · 팝오버 · 메뉴) — ds-skeleton--on-elevated. .ds-menu · .ds-modal · 데스크톱 채팅 창 안은 DS 가 자동으로 바꿔 줍니다.
    • 팔레트 · hex · 인라인 색으로 회색을 칠하지 않습니다(bg-neutral-100 animate-pulse 손수 만든 블록 금지).
  • 폭은 Tailwind w-*, 높이 · 모서리 · 원 지름은 변수로. .ds-skeleton 은 width 를 선언하지 않아 w-24 · w-3/4 가 그대로 먹습니다. 높이는 이 파일이 레이어 밖이라 h-* 유틸이 지므로 --ds-skeleton-h · --ds-skeleton-radius · --ds-skeleton-size 를 씁니다(스피너와 같은 확장 규칙).
  • 표는 <tr class="ds-skeleton-row"> 에 칸마다 .ds-skeleton 하나. 칸 · 행마다 폭이 조금씩 달라지는 규칙이 DS 에 있으니 폭을 손으로 주지 않습니다. 고정 열(tbl-v2-sticky-*) 클래스는 실제 행과 똑같이 붙입니다. 체크박스 칸은 ds-skeleton--box.
  • 움직임은 페이드 한 가지(옅어졌다 돌아옴, 1.6초). 반짝임(shimmer) · 그라데이션 띠를 만들지 않습니다. prefers-reduced-motion 이면 멈춥니다.
  • 반응형 숨김(hidden md:block)은 감싸는 요소에. .ds-skeleton 의 display: block 이 레이어 밖이라 같은 요소의 hidden 유틸을 이겨, 모바일에서도 보입니다(2026-10-21 직원 앱 도입 화면에서 발견).
  • 스켈레톤 묶음에는 aria-hidden="true" — 화면 읽기 도구에는 기존 「불러오는 중」 문구가 상태를 알립니다.
  • 새 모양은 변수만 덮습니다. design-system.css 에 .ds-skeleton--<이름> 을 추가해 --ds-skeleton-* 만 바꾸고, 이 페이지 변형 표에 행을 더합니다. 앱 main.css 에 정의하지 않습니다.

Interactive playground ​

옵션을 바꾸면 미리보기와 코드가 함께 바뀌어요.
미리보기 테마
실제 DS CSS 로 렌더해요. 색은 라이트 neutral-75, 다크는 면보다 한 단계 밝은 회색이에요(미리보기 테마로 비교). 회색 면 위에서는 --on-sunken, 모달 · 메뉴 같은 떠 있는 면 안에서는 --on-elevated(.ds-menu · .ds-modal · 데스크톱 채팅 창 안은 자동)를 씁니다.
<!-- 첫 응답 전에만 — 빈 상태 행은 로딩이 끝난 뒤에 그린다 -->
<template x-if="!rowsLoaded">
  <template x-for="i in 4" :key="'sk-' + i">
    <tr class="ds-skeleton-row">
      <td class="cell"><span class="ds-skeleton ds-skeleton--box"></span></td>
      <template x-for="c in 5" :key="c"><td class="cell"><span class="ds-skeleton"></span></td></template>
    </tr>
  </template>
</template>
<template x-if="rowsLoaded && rows.length === 0">…빈 상태…</template>

언제 쓰나 ​

상황쓰는 것
표 · 카드 · 목록의 모양을 미리 알고, 데이터 전후로 높이가 바뀜스켈레톤 + (있으면) 기존 스피너
「없어요」 빈 상태가 로딩 중에 먼저 뜸스켈레톤 + 빈 상태 조건에 로딩 끝남 추가
모양을 모르는 영역 전체가 빔(앱 부팅 · iframe 임베드)영역 링 — 스켈레톤 아님
버튼 · 컨트롤만 바쁨인라인 아이콘 스피너
진행률을 앎(N/M)카운트 배너
내용이 몇 줄인 짧은 팝업스피너 한 번으로 충분 — 스켈레톤 불필요

사용법 ​

표 (Table 1) — <tbody> 안, 실제 행 바로 아래.

html
<template x-if="!employeesLoaded">
  <template x-for="i in 12" :key="'emp-sk-' + i">
    <tr class="ds-skeleton-row">
      <td class="cell tbl-v2-sticky-1"><span class="ds-skeleton ds-skeleton--box"></span></td>
      <td class="cell tbl-v2-sticky-2"><span class="ds-skeleton"></span></td>
      <template x-for="c in 9" :key="c"><td class="cell"><span class="ds-skeleton"></span></td></template>
    </tr>
  </template>
</template>
<template x-if="employeesLoaded && employees.length === 0">
  <tr><td colspan="12" class="cell text-center text-label-muted">직원 데이터가 없어요</td></tr>
</template>

목록 (아바타 + 두 줄) — 메신저 대화 목록.

html
<template x-if="!loaded && conversations.length === 0">
  <div aria-hidden="true">
    <template x-for="i in 8" :key="'conv-sk-' + i">
      <div class="flex items-center gap-3 py-2.5">
        <span class="ds-skeleton ds-skeleton--circle"></span>
        <div class="flex-1 min-w-0 flex flex-col gap-2">
          <span class="ds-skeleton ds-skeleton--title w-24"></span>
          <span class="ds-skeleton w-3/4"></span>
        </div>
      </div>
    </template>
  </div>
</template>

회색 카드 안 — 결재 탭 연차현황(bg-bg-alternative).

html
<template x-if="!leaveBalanceSettled">
  <div class="bg-bg-alternative rounded-xl p-4 flex flex-col gap-2.5" aria-hidden="true">
    <span class="ds-skeleton ds-skeleton--on-sunken w-16"></span>
    <span class="ds-skeleton ds-skeleton--title ds-skeleton--on-sunken w-3/5"></span>
  </div>
</template>

기존 로딩 표시와 함께 — 직원 카드 팝오버. 링 + 「불러오는 중…」은 그대로, 아래 자리만 스켈레톤.

html
<div x-show="state === 'loading'">
  <div class="flex items-center gap-2"><span class="ds-spinner ds-spinner--sm"></span><span>불러오는 중…</span></div>
  <div class="flex flex-col gap-3" aria-hidden="true">
    <span class="ds-skeleton w-10"></span>
    <div class="flex justify-between"><span class="ds-skeleton w-20"></span><span class="ds-skeleton w-24"></span></div>
  </div>
</div>

적용 현황 ​

2026-10-21 1차(P1) — 진입마다 겪는 큰 덜컹 9곳.

앱화면자리기준 상태
admin직원 관리표 12행 · 통계 핀 숫자employeesLoaded · statsReady
admin팀 관리팀 트리 8줄 · 명단 표 10행(두 표)teamsLoaded · rosterSettled
admin근태 현황 › 연차표 10행 · 요약 숫자 3개leaveBalancesLoaded
공용(admin · team-manager · 직원 앱)직원 카드 팝오버소속 팀 · 병동 배정 줄$store.employeeCard.state
직원 앱홈「지금 할 일」 카드(지난번에 있던 계정만)loaded · expectTodos
직원 앱결재 › 신청·결재연차현황 카드(회색 면)leaveBalanceSettled
직원 앱결재 › 내 신청·결재 현황묶음 1개(제목 + 신청 줄 3개)loaded
공용(직원 앱 · 채팅 위젯)메신저 대화 목록대화 줄 8개loaded
직원 앱급여명세서연도 줄 + 월 카드 4장(화면을 먼저 열도록 바꿈)payslipsLoading

2026-10-21 2차(P2) — 자주 열지만 덜컹이 중간이거나 이미 스피너가 있던 18곳.

앱화면자리기준 상태
admin역할 관리줄 카드 8개(.ds-list-card 껍데기)loadedOnce.roles
admin병동 관리좌측 목록 6줄 · 요약 숫자 · 표 5행loadedOnce.wards
admin근태 현황 › 외근 · 근로시간 마감 · 정정 요청표 행loadedOnce.offsite · .closings · .corrections
admin설정 › 직무 · 근무 그룹 · 근로형태표 카드(근무 그룹은 그룹 카드 2장)loadedOnce.jobDuties · .shiftGroups · .workPatterns
admin설정 › 조직 설정「불러오는 중」 아래 설정 카드 2장(실제 머리 · 본문 껍데기)orgInfo
admin스케줄러링 아래 근무표 격자(이름 열 + 14칸 × 10줄)loading
admin연차 잔여 상세 모달「불러오는 중…」 아래 표 4행leaveDetailLoading
직원 앱근무 기록월 합계 숫자 4칸 · 일별 카드 2장workLog · workLogDayLoading
직원 앱팀 근태링 아래 팀원 줄 6개loading
공용(직원 앱 · 웹 앱 모달)내 정보라벨 · 값 줄(화면을 먼저 열도록 바꿈)myInfoLoading
직원 앱공지(소식 전체) · 드라이브 · 알림링 · 「로딩 중」 아래 목록 줄noticesLoading · drive.loading · notificationsLoading
공용(admin · team-manager · 직원 앱)결재선 패널안내 한 줄 아래 단계 줄 3개apsLoading

2026-10-21 3차(P3) — 5곳.

앱화면자리기준 상태
admin예약된 변경링 아래 표 8행(768 이상) · 카드 4장(768 미만)loading && rows.length === 0
admin직원 앱 도입링 아래 「도입 흐름」 단계 카드 · 「지금 할 일」 카드loading && !summary
admin전자 서명연동 상태 전 목록 머리 + 표, 목록 전 표 행loadedOnce.esignStatus · .esign
admin결재선 › 팀별 결재선링(첫 줄) 아래 팀 줄 6개teamLineLoading && !teamLineOverview
공용(admin 등)직원 정보 수정 모달「편집」 즉시 같은 크기 껍데기 + 메뉴 · 입력칸 자리, 준비되면 실제 패널로editOpening

공통 상태 하나 — admin 은 목록별 「첫 응답이 왔다」를 loadedOnce.<목록> 한 객체에 모았어요. 로더의 finally 에서 켭니다.

변형 ​

클래스모양값
.ds-skeleton줄(본문 한 줄)높이 12 · 모서리 4
.ds-skeleton--title제목 줄 · 이름높이 18
.ds-skeleton--block버튼 · 큰 숫자 · 입력칸높이 44 · 모서리 8
.ds-skeleton--circle아바타 · 썸네일지름 --ds-skeleton-size(기본 40)
.ds-skeleton--box체크박스 칸16 × 16 · 가운데
.ds-skeleton--on-sunken회색 면 위--color-skeleton-sunken
.ds-skeleton--on-elevated떠 있는 면 위--color-skeleton-elevated
tr.ds-skeleton-row표 행칸 · 행마다 폭 40~76% 로 흔듦

색 ​

토큰라이트다크
--color-skeletonneutral-75 #f1f1f1 (흰 면과 14 차이)neutral-600 40% + neutral-800 ≈ #343434 (면 #252525 과 15 차이)
--color-skeleton-sunkenneutral-100 #e5e5e5 (회색 면 #f5f5f5 위)neutral-700 #2c2c2c (면 #1b1b1b 위)
--color-skeleton-elevatedneutral-75 (떠 있는 면도 흰색)neutral-600 50% + neutral-700 ≈ #3b3b3b (면 #2c2c2c 위)

다크에서 라이트처럼 「한 단계 어둡게」 하면 면과 구분이 안 됩니다. 다크는 면보다 밝게, 차이는 라이트와 비슷하게(약 15) 맞췄어요. 700 단계를 그대로 쓰면 카드 면과 7 차이라 거의 보이지 않았습니다.

규격 ​

변수기본쓰는 곳
--ds-skeleton-h12px높이 — h-* 유틸 대신
--ds-skeleton-radius--radius-4모서리
--ds-skeleton-size40px--circle 지름
--ds-skeleton-bg--color-skeleton면 색 — 변형이 바꾼다. 화면에서 직접 덮지 않는다

단일 출처 ​

  • CSS · 토큰: packages/static/styles/design-system.css 의 .ds-skeleton 블록과 --color-skeleton*(라이트 @theme static · 다크 [data-theme="dark"]).
  • 앱 main.css 에 복사본을 두지 않습니다.

변경 이력 ​

  • 2026-10-21 — 컴포넌트 · 토큰 신설, P1 9곳 적용, 이 페이지 신설 → 변경 이력