Skeleton UI란?
Skeleton UI는 콘텐츠가 로드되는 동안 사용자에게 시각적인 힌트를 제공하는 로딩 방식입니다.
빈 화면 대신 콘텐츠의 레이아웃을 회색 블록 등의 형태로 미리 보여줌으로써, 사용자 경험을 보다 부드럽게 만듭니다.
Skeleton UI의 핵심 개념
- 로딩 상태를 시각적으로 표현하여 사용자가 무작정 기다리는 느낌을 줄임
- 콘텐츠 배치를 미리 보여줌으로써 사용자 경험(UX) 개선
- 화면 전환 및 로딩 경험을 더 자연스럽고 직관적으로 제공
Skeleton 필요성
굳이 필요한 이유가 무엇일까?
과거에는 로딩 스피너(Spinner)나 프로그레스 바(Progress Bar)를 사용하여 로딩 상태를 알렸습니다. 하지만 이러한 방식은 사용자가 ‘기다리고 있다’는 인상을 강하게 줍니다. Skeleton UI는 이러한 문제를 개선하여 로딩 중에도 콘텐츠가 준비되고 있다는 느낌을 줄 수 있습니다. 저 역시 사용자 경험 측면에서 Skeleton UI는 꼭 필요한 요소라고 생각합니다.
Skeleton UI 유형
1.정적 콘텐츠 및 이미지 스켈레톤 화면
텍스트, 이미지, 버튼 등의 위치와 크기를 미리 지정해 둔 상태에서, 회색 블록(Placeholder)으로 표현하는 방식입니다.로딩이 완료될 때까지 스켈레톤 UI가 그대로 유지되며, 애니메이션 효과 없이 정적인 형태를 유지합니다.
- 뉴스 기사 앱에서 제목, 본문, 썸네일 이미지를 스켈레톤으로 미리 배치하는 경우
import Skeleton from "react-loading-skeleton";
function StaticSkeleton() {
return (
<div>
<Skeleton width={300} height={20} />
<Skeleton width={250} height={15} />
<Skeleton width={400} height={200} />
</div>
);
제목, 설명, 이미지 자리를 스켈레톤으로 미리 만들어 놓는 방식
2.애니메이션 스켈레톤 화면
스켈레톤 UI가 페이드 인/아웃 효과 또는 움직이는 그라데이션 효과를 가지는 유형입니다.
사용자가 로딩 상태를 더 직관적으로 인식할 수 있도록 흐르는 효과(Shimmer Effect) 가 들어가는 경우가 많습니다.
- 페이스북, 인스타그램에서 뉴스피드 로딩 시 회색 블록이 반짝반짝 흐르는 효과
import Skeleton from "react-loading-skeleton";
function AnimatedSkeleton() {
return (
<div>
<Skeleton width={300} height={20} baseColor="#ddd" highlightColor="#eee" />
<Skeleton width={250} height={15} baseColor="#ddd" highlightColor="#ccc" />
<Skeleton width={400} height={200} />
</div>
);
}
baseColor, highlightColor를 설정하면 애니메이션 효과(흐르는 느낌의 로딩)가 들어감
3.프레임 디스플레이 스켈레톤 스크린
웹페이지 또는 앱 전체의 프레임(레이아웃)만 미리 로드하고, 내부 콘텐츠는 나중에 불러오는 방식입니다.헤더, 사이드바, 푸터 등의 기본 레이아웃을 먼저 표시하고, 콘텐츠는 개별적으로 로딩되는 특징을 가집니다.
- 유튜브에서 페이지를 로딩할 때, 비디오 썸네일과 제목 자리는 미리 표시되고, 썸네일 이미지는 나중에 로드
import Skeleton from "react-loading-skeleton";
function FrameSkeleton() {
return (
<div>
{/* 네비게이션 바 */}
<Skeleton height={50} />
{/* 사이드바와 메인 콘텐츠 영역 */}
<div style={{ display: "flex" }}>
<Skeleton height={600} width={200} />
<div style={{ marginLeft: 20 }}>
<Skeleton width={400} height={30} />
<Skeleton width={350} height={20} count={3} />
</div>
</div>
</div>
);
}
페이지의 큰 틀(네비게이션 바, 사이드바 등)을 먼저 보여주고, 내부 콘텐츠는 이후 로딩하는 방식
Skeleton UI 성능 최적화 방법은?
Skeleton UI는 사용자 경험을 개선하는 도구지만, 잘못 사용하면 성능 저하를 초래할 수 있습니다.이를 방지하기 위해 다음과 같은 최적화 방법을 고려해야 합니다.
성능 최적화 전략
- Skeleton UI 최소화: 너무 많은 요소를 Skeleton UI로 로드하면 렌더링 부담이 증가합니다. 중요한 영역에만 적용하는 것이 좋습니다.
- 로딩 시간 최적화: Skeleton UI를 3~5초 이상 유지하면 사용자가 답답함을 느낄 수 있습니다. 네트워크 요청 속도를 고려하여 적절한 시간을 설정해야합니다.
- Lazy Loading 적용:React의 Suspense와 lazy를 활용하여 필요한 시점에만 Skeleton UI를 보여줍니다.
- GPU 가속 활용:CSS 애니메이션을 transform: translateZ(0);와 함께 사용하면 GPU 가속을 활용하여 렌더링 성능을 향상시킬 수 있습니다.
Skeleton UI를 프로젝트에 적용해보자!
Skeleton UI 적용 라이브러리: react-loading-skeleton
yarn add react-loading-skeleton

기본적인 사용법
import Skeleton from "react-loading-skeleton";
//react-loading-skeleton 패키지에서 Skeleton 컴포넌트를 가져오는 코드
import "react-loading-skeleton/dist/skeleton.css";
<Skeleton width={100} height={20} />
실제 프로젝트 컴포넌트에 적용하기: PopularStoreSkeleton 컴포넌트

문제점: 로딩 상태 처리 부재
첫 번째 코드에서 PopularStoreSkeleton 컴포넌트는 초기 상태에서 로딩 스켈레톤을 보여주지만 실제 데이터를 로딩하는 동작이나 시간이 없었기 때문에, 사용자가 아무것도 로딩되지 않은 상태에서 스켈레톤을 볼 수 있는 것처럼 보였습니다. 이 상태에서는 실제 데이터가 언제 로딩되는지에 대한 피드백이 없어서 UI의 비동기 데이터 처리에 대한 예상이 어려웠습니다.
문제의 원인
- 로딩 처리 부재: 데이터가 비동기적으로 로드되기 전에 UI가 바로 렌더링되었기 때문에 스켈레톤만이 화면에 보였습니다. 실제 데이터가 로딩되기까지 기다리는 코드가 없어서 stores의 데이터가 없었고, 초기 UI에서만 스켈레톤이 표시됐습니다.
- 상태 변경을 위한 설정 부족: useState로 stores 상태를 정의했지만, 그 값은 초기 상태에서 빈 배열로 설정되어 있었습니다. 데이터가 로드될 때 이 값을 변경해주는 로직이 없었기 때문에 로딩 상태를 제대로 처리하지 못했습니다.
export default function PopularStoreSkeleton() {
return (
<HomeContentsWrapper>
<Text typo="subtitle200">
우리동네 <span>HOT</span>한 매장
</Text>
<PopularStoreWrap>
{Array(2)
.fill(null)
.map((_, index) => (
<PopularStoreItem key={index}>
<Skeleton width={120} height={120} borderRadius={10} />
<PopularStoreText>
<Text typo="subtitle300">
<Skeleton width={100} />
</Text>
<Text typo="body400">
<Skeleton width={150} />
</Text>
<NewStoreBadgeWrap>
<Skeleton width={80} height={24} borderRadius={12} />
<Skeleton width={120} height={24} borderRadius={12} />
</NewStoreBadgeWrap>
</PopularStoreText>
</PopularStoreItem>
))}
</PopularStoreWrap>
</HomeContentsWrapper>
);
}

개선된 코드
두 번째 코드에서는 비동기 로딩 상태를 처리하기 위해 setTimeout을 사용하여 5초 후에 데이터를 로딩하는 방식으로 개선되었습니다. isLoading 상태를 관리하여 데이터가 로딩되었는지 여부를 추적하고 이를 통해 로딩 중일 때는 스켈레톤을 표시하고, 로딩이 완료되면 실제 데이터를 렌더링하는 방식으로 UI가 개선되었습니다.
주요 개선 사항
- 로딩 상태 관리
- isLoading이라는 상태 변수를 추가하여 데이터가 로딩 중인지 아닌지를 확인합니다.
- 로딩 중일 때는 스켈레톤을 보여주고, 데이터가 로딩되면 실제 데이터를 표시합니다.
- setTimeout을 사용한 데이터 로딩
- useEffect 훅 내에서 setTimeout을 사용하여 5초 뒤에 데이터를 로딩합니다.
- 5초 후, 실제 데이터를 setStores로 설정하고 setIsLoading(false)로 로딩 상태를 종료합니다.
- 실제 데이터 렌더링
- isLoading이 false일 때, 실제 데이터를 렌더링합니다. 이때 stores 배열에 담긴 데이터를 사용하여 매장 정보를 표시합니다.
export default function PopularStoreContainer() {
const [isLoading, setIsLoading] = useState(true);
const [stores, setStores] = useState<Store[]>([]);
useEffect(() => {
setTimeout(() => {
setStores([
{
id: 1,
name: "알로하",
address: "서울 강남구 논현동",
image: Alo,
},
{
id: 2,
name: "마이뷰티독 강남정",
address: "서울 강남구 역삼동",
image: MyBE,
},
]);
setIsLoading(false);
}, 5000);
}, []);
return (
<HomeContentsWrapper>
<Text typo="subtitle200">
우리동네 <span style={{ color: `${colors.red100}` }}>HOT</span>한 매장
</Text>
<PopularStoreWrap>
{isLoading
? Array(2).fill(null).map((_, index) => (
/* 스켈레톤 UI 컴포넌트 */
))
: stores.map((store) => (
/* 실제 데이터 렌더링 컴포넌트 */
))}
</PopularStoreWrap>
</HomeContentsWrapper>
);
}
개선된 코드의 흐름
- 초기 상태 설정
- isLoading이 true일 때는 Skeleton을 사용하여 로딩 중임을 사용자에게 알립니다.
- 데이터 로딩 처리
- useEffect 훅을 통해 컴포넌트가 렌더링된 후 5초 뒤에 실제 데이터를 setStores로 설정합니다. 이때 setIsLoading(false)를 호출하여 로딩 상태를 변경합니다.
- 렌더링
- isLoading이 false로 변경되면, stores 배열의 데이터를 기반으로 실제 매장 정보를 화면에 표시합니다.
- 매장 이미지, 이름, 주소와 함께 해당 매장에 대한 배지를 추가로 표시합니다.
이번 리팩토링을 통해 스켈레톤 UI는 단순한 시각적 요소가 아니라 사용자에게 현재 로딩 중이라는 "명확한 신호"를 제공하는 중요한 도구임을 배울 수 있었습니다. 특히 비동기 데이터 처리와 컴포넌트 상태(state) 관리가 밀접하게 연관되어야 진정한 의미에서 자연스러운 사용자 경험(UX)을 만들 수 있다는 것을 느꼈던 것 같습니다.
- 로딩 상태 관리 및 UI에 반영하는 중요성
→ 사용자는 애매한 화면보다 로딩 중임을 정확히 인지할 때 더 좋은 경험을 합니다. - 컴포넌트 설계 시 데이터 흐름과 상태 동기화까지 고려해야 한다는 점
→ 비동기 작업을 단순 호출이 아니라 사용자 인터랙션 관점에서도 고민하게 되었습니다. - 타입스크립트를 통해 데이터 구조를 명확히 정의하면 유지보수가 쉬워진다는 것
→ 예상치 못한 에러를 줄이고 코드를 보는 사람에게도 명확한 의도를 전달할 수 있습니다.
또한 앞으로는 API 통신이나 데이터 로딩이 필요한 컴포넌트를 설계할 때 항상 '로딩 상태 → 성공 상태' 흐름을 자연스럽게 고려하는 습관을 가져가려고 합니다.
'refactor' 카테고리의 다른 글
| [반려견 미용 견적 서비스] refactor4 - 로그인 로직 (4) | 2025.06.20 |
|---|---|
| [반려견 미용 견적 서비스] refactor 3 - 상태관리 (4) | 2025.05.16 |
| [반려견 미용 견적 서비스] refactor 1 - SEO 설정 (0) | 2025.04.19 |
| [반려견 미용 견적 서비스] PEAUTY 회고 및 리팩토링 방향성 (0) | 2025.04.16 |