스켈레톤 UI란? 실무에서 쓰는 구현 방법 2가지 정리

👉🏼 KIM·2026년 4월 1일

1. 스켈레톤 UI란?

목록 UI를 구현하다 보면 데이터 로딩 동안 화면이 비거나 레이아웃이 밀리는 상황을 자주 마주친다.
스켈레톤 UI(Skeleton UI)는 그 순간을 채우기 위한 로딩 패턴으로, 실제 콘텐츠와 비슷한 형태의 회색 블록을 먼저 보여준다.
뼈대(skeleton)만 먼저 그려두고, 데이터가 준비되면 실제 콘텐츠로 교체하는 방식이다.


2. 왜 써야 하는가?

기존 방식의 문제

[데이터 로딩 중] → 빈 화면 or 스피너 → [콘텐츠 등장]

빈 화면이나 스피너는 사용자 입장에서 "뭔가 고장난 건가?" 하는 불안감을 준다.
또한 콘텐츠가 갑자기 나타나면서 레이아웃이 툭툭 밀리는 현상(Layout Shift)이 발생한다.

스켈레톤의 장점

[데이터 로딩 중] → 스켈레톤(뼈대) → [콘텐츠 등장]
항목효과
체감 속도실제보다 빠르게 느껴짐
레이아웃 안정성자리를 미리 잡아두어 Layout Shift 없음
사용자 불안감"로딩 중"임을 시각적으로 인지시킴
UX 완성도서비스 전체의 완성도가 올라 보임

3. 언제 쓰는가?

스켈레톤은 모든 로딩에 쓰는 게 아니다. 상황에 맞게 선택해야 한다.

상황권장 방식
로딩이 300ms 이상 걸릴 때스켈레톤
목록, 카드, 랭킹 등 반복 구조스켈레톤
버튼 클릭 후 즉시 응답스피너 or 비활성화 처리
페이지 전체 전환스피너 or 프로그레스바

4. 구현 방법 2가지

방법 A — 별도 스켈레톤 마크업 교체

스켈레톤 전용 HTML을 따로 만들어두고, 로딩 시작 시 삽입 → 완료 시 실제 콘텐츠로 교체하는 방식.

흐름

API 호출 시작 → 스켈레톤 HTML 삽입 → 데이터 수신 → 실제 콘텐츠 HTML로 교체

HTML (스켈레톤 전용)

<div class="card-skeleton">
  <div class="skeleton skeleton--thumbnail"></div>
  <div class="skeleton skeleton--title"></div>
  <div class="skeleton skeleton--desc"></div>
</div>

CSS

.skeleton {
  position: relative;
  display: block;
  overflow: hidden;
  background: #ececec;
  border-radius: 4px;
}

/* shimmer 애니메이션 */
.skeleton::after {
  content: "";
  position: absolute;
  top: 0;
  left: -120px;
  width: 80px;
  height: 100%;
  background: linear-gradient(
    90deg,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, 0.7) 50%,
    rgba(255, 255, 255, 0) 100%
  );
  animation: shimmer 1.4s infinite;
}

@keyframes shimmer {
  0%   { left: -120px; }
  100% { left: 100%; }
}

.skeleton--thumbnail { width: 100%; height: 180px; }
.skeleton--title     { width: 70%; height: 16px; margin-top: 12px; }
.skeleton--desc      { width: 50%; height: 12px; margin-top: 8px; }

JS

async function loadCard() {
  // 스켈레톤 삽입
  container.innerHTML = skeletonHTML;

  const data = await fetchData();

  // 실제 콘텐츠로 교체
  container.innerHTML = renderCard(data);
}

장점

  • CSS가 짧고 단순하다
  • 스켈레톤 모양을 자유롭게 만들 수 있다

단점

  • 마크업이 바뀌면 스켈레톤 HTML도 같이 수정해야 한다
  • 컴포넌트마다 별도 파일이 필요하다

방법 B — CSS 클래스 토글

기존 실제 마크업에 .skeleton 클래스만 추가/제거하는 방식.
별도 스켈레톤 HTML이 필요 없다.

흐름

페이지 렌더링 → section에 .skeleton 클래스 추가 → 데이터 수신 → .skeleton 클래스 제거

HTML (실제 마크업 그대로 사용)

<!-- 로딩 중: skeleton 클래스 있음 -->
<section class="card-section skeleton">
  <h2 class="card-title">인기 상품</h2>
  <ul class="card-list">
    <li class="card-item">
      <span class="card-rank">1</span>
      <span class="card-name">상품명</span>
    </li>
  </ul>
</section>

<!-- 로딩 완료: skeleton 클래스 제거 -->
<section class="card-section">
  ...
</section>

CSS

/* 스켈레톤 상태일 때 하위 요소를 덮어씀 */
.skeleton .card-title,
.skeleton .card-rank,
.skeleton .card-name {
  position: relative;
  overflow: hidden;
  background: #ececec;
  color: transparent;        /* 텍스트 숨기기 */
  border-radius: 4px;
  pointer-events: none;      /* 클릭 막기 */
}

.skeleton .card-title  { width: 80px; height: 18px; }
.skeleton .card-rank   { width: 16px; height: 14px; }
.skeleton .card-name   { width: 74%; height: 14px; }

/* shimmer */
.skeleton .card-title::after,
.skeleton .card-rank::after,
.skeleton .card-name::after {
  content: "";
  position: absolute;
  top: 0; left: -120px;
  width: 80px; height: 100%;
  background: linear-gradient(
    90deg,
    rgba(255,255,255,0) 0%,
    rgba(255,255,255,0.7) 50%,
    rgba(255,255,255,0) 100%
  );
  animation: shimmer 1.4s infinite;
}

@keyframes shimmer {
  0%   { left: -120px; }
  100% { left: 100%; }
}

JS

async function loadCard() {
  section.classList.add('skeleton');

  const data = await fetchData();
  renderCard(data);

  section.classList.remove('skeleton');
}

장점

  • 별도 스켈레톤 HTML이 필요 없다
  • 마크업이 바뀌어도 HTML은 수정 불필요 (CSS만 조정)
  • PHP처럼 서버에서 마크업을 미리 그리는 환경에 특히 적합

단점

  • CSS가 길어진다 (하위 요소 전부 스타일 덮어써야 함)
  • color: transparent, pointer-events: none 등 예외 처리가 많아진다

5. 방법 선택 기준

상황추천 방법
AJAX로 콘텐츠 전체를 동적으로 삽입방법 A (마크업 교체)
PHP 등 서버에서 마크업을 미리 렌더링방법 B (클래스 토글)
컴포넌트 구조가 자주 바뀜방법 B
스켈레톤 모양을 세밀하게 커스텀해야 함방법 A

6. shimmer 애니메이션 원리

스켈레톤의 핵심인 shimmer(반짝임) 효과는 ::after 가상 요소로 구현한다.

[회색 박스 - overflow: hidden]
       ↑
  [흰색 빛줄기 - position: absolute]
  left: -120px → left: 100% 로 이동 (animation)

overflow: hidden 덕분에 빛줄기가 박스 밖으로 나가면 잘려서 보이지 않는다.
이 빛줄기가 왼쪽에서 오른쪽으로 반복 이동하면서 shimmer 효과가 만들어진다.


7. 실무 팁

키워드 너비를 다르게 줘서 자연스럽게 만들기

모든 항목의 너비가 똑같으면 부자연스러워 보인다.
nth-child로 항목마다 너비를 다르게 주면 실제 텍스트처럼 보인다.
방법 B(클래스 토글)에서는 요소에 color: transparent와 background가 이미 적용되어 있으므로, width를 직접 덮어쓰면 된다.

.skeleton .card-name                           { width: 74%; }
.skeleton .card-item:nth-child(2n) .card-name  { width: 62%; }
.skeleton .card-item:nth-child(3n) .card-name  { width: 84%; }
.skeleton .card-item:nth-child(5n) .card-name  { width: 56%; }

로딩 완료 후 부드럽게 전환

스켈레톤에서 실제 콘텐츠로 바뀔 때 transition을 주면 덜 튀어보인다.

.card-section {
  transition: opacity 0.2s ease;
}
.card-section.skeleton {
  opacity: 0.8;
}

접근성 처리

스켈레톤 UI에서 접근성의 핵심은 스켈레톤 요소 하나하나를 스크린 리더에게 설명하는 게 아니라,
컨테이너가 현재 로딩 중이라는 상태만 전달하는 것이다.
aria-busy="true"는 "이 영역은 아직 준비 중"이라는 신호를 스크린 리더에 보내고, 내부 콘텐츠 읽기를 보류하게 한다.

<section aria-busy="true" aria-label="콘텐츠 로딩 중">
// 완료 후: 로딩 끝났음을 알림
section.setAttribute('aria-busy', 'false');
section.removeAttribute('aria-label');

정리

항목내용
정의로딩 중 실제 콘텐츠 형태를 흉내낸 회색 플레이스홀더
목적체감 속도 향상, Layout Shift 방지, UX 완성도
핵심 기술::after 가상 요소 + shimmer 애니메이션
구현 방식별도 마크업 교체 / CSS 클래스 토글
선택 기준동적 삽입이면 마크업 교체, 서버 렌더링이면 클래스 토글

스켈레톤 UI는 단순한 로딩 효과가 아니다.
화면을 미리 채워두는 것 자체가 사용자에게 "지금 잘 되고 있다"는 신호를 주는 UX 장치다.

profile
프론트는 순항중 ¿¿

0개의 댓글