
웹 개발에서 성능 최적화만큼이나 중요한 것이 바로 '사용자가 느끼는 속도'이다. 오늘은 단순히 데이터를 불러오는 것을 넘어, 대기 시간마저도 사용자 경험(UX)의 일부로 승화시키는 스켈레톤 UI에 대해 깊이 있게 탐구하고 정리해 보았다.

1. 스켈레톤 UI란 무엇인가?
스켈레톤 UI는 데이터가 로드되기 전, 실제 콘텐츠가 들어갈 자리에 미리 박스나 선 등으로 레이아웃의 '뼈대'를 그려주는 기법이다. 과거에는 화면 중앙에 빙글빙글 돌아가는 '스피너(Spinner)'를 주로 사용했지만, 이는 사용자에게 "나는 지금 기다리고 있다"는 부정적인 인식을 강조하는 경향이 있다.
반면 스켈레톤 UI는 다음과 같은 장점을 가진다.
- 심리적 안정감: 화면의 전체적인 구조를 미리 보여줌으로써 콘텐츠가 곧 나타날 것이라는 예측 가능성을 제공한다.
- 레이아웃 시프트(CLS) 방지: 데이터가 로드된 후 갑자기 요소들이 툭툭 튀어나오며 화면 밀림 현상이 발생하는 것을 막아준다.
- 인지적 로드 감소: 사용자가 로딩 후 어디에 시선을 두어야 할지 미리 준비하게 만든다.
보통 정적 스켈레톤, 동적 스켈레톤으로 구분된다.
정적 스켈레톤은 아래 이미지와 같이 가장 일반적인 형태의 스켈레톤 화면이다. 와이어프레임 형태의 화면이 로딩 상황에서 특별한 모션 없이 정지된 상태로 나타난다.

동적 스켈레톤은 아래 이미지와 같이 정적 스켈레톤 화면에 그라데이션이나 움직임이 추가된 형태의 UI이다. 움직임이 추가됨에 따라 사용자는 로딩중인 것을 인지하기 더 쉽다.

2. Next.js에서의 적용 방식 (App Router 기준)
Next.js는 서버 컴포넌트와 스트리밍(Streaming) 기능을 지원하기 때문에 스켈레톤 UI를 적용하기에 최적화된 환경을 제공한다.
1️⃣ loading.tsx를 통한 자동 스트리밍
Next.js App Router에서는 폴더 구조 내에 loading.tsx 파일만 만들어두면, 해당 경로의 페이지 컴포넌트가 로드되는 동안 자동으로 이 파일을 보여준다. 이는 별도의 상태 관리가 필요 없어 매우 편리하다.
export default function Loading() {
return (
<div className="p-4 space-y-4">
{/* 타이틀 스켈레톤 */}
<div className="h-8 w-1/3 bg-gray-200 rounded animate-pulse" />
{/* 카드 리스트 스켈레톤 */}
{[1, 2, 3].map((i) => (
<div key={i} className="h-32 w-full bg-gray-100 rounded-lg animate-pulse" />
))}
</div>
);
}
2️⃣ Suspense를 이용한 정밀 제어
페이지 전체가 로딩되는 것이 아니라, 특정 API 호출이 늦어지는 컴포넌트만 따로 스켈레톤을 처리하고 싶을 때 사용한다. 이를 통해 '중요한 정보'는 먼저 보여주고, '부수적인 정보'는 로딩 중으로 표시하는 전략적 렌더링이 가능하다.
<Suspense fallback={<CardSkeleton />}>
<SlowDataComponent />
</Suspense>
3. 스켈레톤 UI를 더 쉽고 우아하게 적용하는 방법
직접 CSS를 짜는 것도 좋지만, 생산성과 퀄리티를 위해 다음과 같은 전략을 사용하는 것이 효율적이다.
- Tailwind CSS의 animate-pulse 활용: 별도의 라이브러리 설치 없이도 부드러운 깜빡임 효과를 줄 수 있어 가장 가성비가 좋다.
- 이미지 플레이스홀더 (next/image): Next.js의 Image 컴포넌트에서 placeholder="blur" 속성을 사용하면, 별도의 스켈레톤 없이도 이미지가 저해상도에서 고해상도로 부드럽게 전환되는 효과를 얻을 수 있다.
- Shadcn/ui 또는 Radix UI: 이미 잘 만들어진 Skeleton 프리미티브를 제공하므로, 프로젝트의 디자인 시스템에 맞춰 스타일만 수정하면 즉시 실전 투입이 가능하다.
- 추가적으로 react-loading-skelton과 같은 라이브러리를 활용하는 방법이다.

회고
스켈레톤 UI를 단순히 '그려 넣는 것'에 그치지 않고, 깊이 있게 고민해 본 결과 몇 가지 중요한 교훈을 얻었다.
첫째, 실제 데이터와의 '싱크(Sync)'가 핵심이다. 스켈레톤은 회색 박스이지만, 그 크기와 여백(Margin/Padding)은 실제 데이터가 들어왔을 때와 정확히 일치해야 한다. 만약 1px이라도 차이가 나면 데이터가 로드되는 순간 화면이 미세하게 '덜컥'거리는 불쾌한 경험을 주게 된다.
둘째, '반짝임(Shimmer)' 효과의 속도 조절이다. 너무 빠르게 깜빡이면 사용자의 눈을 피로하게 만들고, 너무 느리면 정지된 화면처럼 느껴진다. 보통 1.5초에서 2초 사이의 부드러운 루프가 가장 적당하다는 것을 실험을 통해 느꼈다.
셋째, 모든 곳에 남발하지 말아야 한다. 초고속으로 로드되는 페이지에 굳이 스켈레톤을 넣으면 오히려 화면이 번쩍거리는 역효과를 낸다. 약 200~300ms 이상의 지연이 예상되는 곳에만 전략적으로 배치하는 것이 진정한 최적화이다.
어느 덧 부트캠프에서의 마지막 TIL이다.
부트캠프 회고와 프로젝트 회고로 다시 돌아오겠습니다 ~ 야호


'TIL' 카테고리의 다른 글
| [TIL] useEffect와 useLayoutEffect 차이 (0) | 2026.05.21 |
|---|---|
| [134일차] Tanstack Infinite Queries 톺아보기 (0) | 2026.03.24 |
| [125일차] Tanstack Query (0) | 2026.03.10 |
| [122일차] 모노레포와 멀티레포 (0) | 2026.03.05 |
| [119일차] 프론트엔드 FSD 아키텍처 (0) | 2026.02.28 |