안녕하세요 웹 YB 임지수입니다!
다들 데이터를 불러올 때 로딩 처리에 대해 한 번쯤 고민해 보신 적 있지 않으신가요?
저도 프로젝트를 진행하면서 언제 로딩 UI를 띄울지, 어떻게 띄울지에 대해 고민했던 적이 많았습니다.
그 과정에서 React의 Suspense라는 기능을 알게 되었습니다.
Suspense를 활용하면 효율적인 로딩 처리뿐만 아니라 UX 개선까지 자연스럽게 이어질 수 있었습니다!!
이번 글에서는 Suspense를 중심으로 기존의 비동기 처리 방식부터 Suspense를 어떻게 사용하고 주의할 점은 어떤 게 있는지까지 알아보려고 합니다.

📍 React에서 비동기 처리 방식
Fetch-on-render
컴포넌트 렌더링 후 데이터 가져오기 → 데이터 가져오는 동안 빈 상태나 로딩 화면 보임
Fetch-then-render
데이터 먼저 가져오고 컴포넌트 렌더링 → 데이터 페칭 완료까지 렌더링 지연
Render-as-you-fetch
데이터 가져오는 동시에 컴포넌트 렌더링 → Suspense 사용할 때 이 방식 활용
리액트에서 비동기 데이터를 불러와야 할 때 보통은 useEffect와 useState를 조합해 아래처럼 작성합니다.
import { useEffect, useState } from 'react';
function Albums() {
const [albums, setAlbums] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/albums')
.then(res => res.json())
.then(data => {
setAlbums(data);
setLoading(false);
});
}, []);
if (loading) return <p>로딩 중...</p>;
return (
<ul>
{albums.map(album => (
<li key={album.id}>{album.title}</li>
))}
</ul>
);
}
이 방식은 많은 분들이 흔히 사용하는 패턴입니다.
하지만 이렇게 작성하다 보면 몇 가지 문제에 부딪히게 되는데요,,
‼️Side Effect 문제가 발생합니다
→ fetch()는 렌더링 외부에서 일어나는 부수 효과입니다.
→ useEffect는 렌더링 이후에 실행되기 때문에 데이터가 오기 전까지 화면에 빈 컴포넌트나 깜빡임이 보일 수 있습니다.
→ 컴포넌트가 언마운트되기 전에 fetch 응답이 오면 메모리 누수 경고 발생할 수 있습니다.
🤔 기존 방식의 한계
Suspense 없이 로딩을 처리하려면 다음과 같은 문제점이 생깁니다
- 매 컴포넌트마다 loading, setLoading 상태를 따로 선언해야 함
- useEffect로 fetch 후 상태 변경을 일일이 관리해야 함
- 여러 컴포넌트가 동시에 로딩될 경우, 컴포넌트 단위로 분리된 로딩 처리가 어려움
- 로딩 UI가 중복되고 유지 보수가 어려워짐
💉 Suspense가 해결 가능
React 18부터는 Suspense를 활용한 비동기 컴포넌트 처리가 가능합니다.
import { Suspense } from 'react';
import Albums from './Albums';
function App() {
return (
<Suspense fallback={<p>로딩 중...</p>}>
<Albums />
</Suspense>
);
}
이렇게 Suspense는 비동기적으로 로딩되는 컴포넌트를 감싸서 로딩 중에는 fallback UI를 보여주고 데이터가 준비되면 본 UI를 렌더링 합니다.
컴포넌트 자체에 loading 상태를 따로 관리하지 않아도 되고 로딩 처리를 선언적으로 분리할 수 있어 코드도 깔끔해집니다.
import { Suspense } from 'react';
import UserProfile from './UserProfile';
import UserPosts from './UserPosts';
function App() {
return (
<>
<Suspense fallback={<p>프로필 로딩 중...</p>}>
<UserProfile />
</Suspense>
<Suspense fallback={<p>게시글 로딩 중...</p>}>
<UserPosts />
</Suspense>
</>
);
}
이렇게 컴포넌트 단위로 개별적인 로딩 UI를 구성할 수 있어 각 영역마다 자연스러운 사용자 경험을 제공할 수 있습니다.
👍🏻 Suspense의 장점
① 로딩 UI를 선언적으로 관리 → fallback으로 간단히 로딩 화면 처리 가능
② 컴포넌트 단위 제어 → 하위 컴포넌트 개별 로딩 처리 가능
③ 깜빡임 최소화 → 렌더 전에 fetch 전략으로 깜빡임 없는 UI
④ 라이브러리와 시너지 → React Query, Next.js 등과 함께 사용하면 진가 발휘
🌟Suspense 사용 시 주의할 점
- 기본 fetch()만으론 Suspense를 사용할 수 없습니다.
→ Suspense는 컴포넌트가 필요한 데이터를 아직 불러오지 못한 상태를 감지하고, 그동안 fallback UI를 보여주는 방식입니다.
React Query, Relay와 같이 Suspense를 지원하는 데이터 패칭 라이브러리를 사용하는 것이 필요합니다.
import { useQuery } from '@tanstack/react-query';
const fetchAlbums = async () => {
const response = await fetch('/api/albums');
if (!response.ok) {
throw new Error('Failed to fetch albums');
}
return response.json();
};
function Albums() {
const { data } = useQuery(['albums'], fetchAlbums, {
suspense: true,
});
return (
<ul>
{data.map((album) => (
<li key={album.id}>{album.title}</li>
))}
</ul>
);
}
위의 코드에서 useQuery는 Suspense와 함께 작동하도록 되어 있습니다.
suspense: true를 추가하면 React Query는 데이터 로딩 중 Suspense의 fallback UI를 자동으로 표시하고 데이터가 준비되면 컴포넌트를 렌더링합니다.
- Next.js 같이 서버에서 렌더링 하는 프레임워크에서는 완전히 자유롭지 않다.
→ 서버 사이드 렌더링 환경에서는 Suspense 지원이 아직 제한적이거나 실험적인 경우가 많아 사용 전 공식 문서를 확인하거나 테스트해 보는 것을 추천드립니다.
- fallback에는 복잡한 로직을 넣지 않는다.
→ Suspense의 fallback은 단순 대체 UI를 보여주기 용도이기 때문에 그 안에서 상태를 변경하거나 API 호출을 하는 등 복잡한 로직은 피하는 게 좋습니다.
<Suspense fallback={setLoading(true)}> // 상태 변경 X
Suspense가 아직 모든 상황에서 완벽한 해결책은 아니지만 꽤 유용한 도구라고 생각합니다.
특히 React Query 같은 라이브러리와 함께 사용할 때 유용하게 활용할 수 있으니 관심 있으시면 같이 적용해 보는 걸 추천드려요!
이 방식 말고도 다른 여러 방식이 있겠지만, 하나의 선택지로 활용해 보시면 좋을 거 같습니다 ~ ~😊
🔗 참고
<Suspense> – React
The library for web and native user interfaces
react.dev
'4주차' 카테고리의 다른 글
| useEffect vs useLayoutEffect (0) | 2025.05.13 |
|---|---|
| 변성 가족 알아보기 (공변성, 반공변성, 이변성, 불변성) (0) | 2025.05.13 |
| TypeScript에서 같은 이름의 타입을 여러 개 생성하면 어떻게 될까? (0) | 2025.05.13 |
| API 폴더? 서비스 폴더? 구조 분리가 중요한 이유 (0) | 2025.05.13 |
| 사용자 정의 훅 (Custom Hook) vs 고차 컴포넌트 (Higher Order Component) (0) | 2025.05.13 |