웹 페이지나 애플리케이션을 사용하면서 API 데이터를 불러오는 동안 흰색 화면만 나타났던 경험이 있었다. 이는 사용자 경험(UX)을 저해하는 주요 원인 중 하나이다. 이런 문제를 해결하기 위해 등장한 것이 바로 스켈레톤 컴포넌트이다.
스켈레톤 컴포넌트는 데이터를 로딩하는 동안 콘텐츠의 자리 표시자 역할을 한다. 로딩 중에도 화면이 비어 보이지 않도록 하고, 사용자가 콘텐츠를 기다리는 시간을 덜 지루하게 느끼도록 도와준다. 데이터 로딩 시간 동안 사용자에게 빈 화면 대신 콘텐츠의 레이아웃 힌트를 제공하는 것이 점점 더 중요해지고 있다.
https://ui.toast.com/weekly-pick/ko_20201110
스켈레톤 UI는 복잡하지 않으며, 간단한 상태 관리와 스타일링으로 구현할 수 있다. 위의 링크를 참고해봐도 좋다.
1. Home.jsx에서 API 호출 및 상태 관리
isLoading 이라는 상태 변수를 선언하여 로딩 상태를 관리한다. 데이터를 요청하기 전에는 로딩 상태를 활성화하고, 데이터를 가져온 후에는 로딩 상태를 비활성화한다.
import { useEffect, useState } from "react";
import Tab from "../components/home/Tab";
import List from "../components/home/List";
const DAYS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
const Home = () => {
const [webtoonList, setWebtoonList] = useState([]);
const [day, setDay] = useState(() => new Date().getDay());
const [isLoading, setIsLoading] = useState(true); // 로딩 상태 초기화
useEffect(() => {
const dayString = DAYS[day];
setIsLoading(true); // 로딩 상태 활성화
fetch(
`https://korea-webtoon-api-cc7dda2f0d77.herokuapp.com/webtoons?provider=NAVER&page=1&perPage=30&sort=ASC&updateDay=${dayString}`
)
.then((response) => {
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.json();
})
.then((data) => {
setWebtoonList(data.webtoons);
setIsLoading(false); // 로딩 상태 비활성화
})
.catch((error) => {
console.error("There was a problem with the fetch operation:", error);
setIsLoading(false); // 로딩 상태 비활성화
});
}, [day]);
const handleButtonClick = (e) => {
const selectedDay = Number(e.target.value);
setDay(selectedDay);
};
return (
<div className="home-container">
<Tab onHandleButtonClick={handleButtonClick} selectedBtn={day} />
{/* 로딩 상태 props 전달 */}
<List webtoonList={webtoonList} isLoading={isLoading} />
</div>
);
};
export default Home;
2. List.jsx에서 스켈레톤 컴포넌트 렌더링
isLoading 상태에 따라 데이터를 보여주거나 스켈레톤 컴포넌트를 렌더링 한다.
// List.jsx
import React from "react";
import style from "./list.module.scss";
import className from "classnames/bind";
import { Link } from "react-router-dom";
const cx = className.bind(style);
const List = ({ webtoonList, isLoading }) => {
return (
<div className={cx("list-container")}>
{isLoading
? Array.from({ length: 30 }).map((_, index) => (
<div key={index}>
<div className={cx("skeleton", "thumbnail")}></div>
<div className={cx("skeleton", "title")}></div>
<div className={cx("skeleton", "author")}></div>
<div className={cx("skeleton", "score")}></div>
</div>
))
: webtoonList.map((webtoon) => (
<Link key={webtoon.id} to={`/${webtoon.id}`} state={{ webtoon }}>
<img src={webtoon.thumbnail} alt="thumbnail" />
<div className={cx("title-text")}>{webtoon.title}</div>
<div className={cx("author-text")}>
{webtoon.authors.map((author, index) => (
<span key={index}>
{author} {index !== webtoon.authors.length - 1 ? "/" : null}
</span>
))}
</div>
<div className={cx("score-text")}>9.91</div>
</Link>
))}
</div>
);
};
export default List;
CSS를 적용해주면 결과는 아래와 같다.

'Frontend' 카테고리의 다른 글
| eslint 설정해보기 (5) | 2025.07.08 |
|---|---|
| 모노레포 구축하기 (1) | 2025.07.08 |
| No data to show 이후 그래프가 그려지지 않는 문제 (0) | 2025.06.30 |
| Ratio를 유지하는 이미지 컴포넌트 (0) | 2025.02.19 |
| 번들러가 왜 필요할까? (0) | 2025.01.22 |