Suspense
React의 Suspense는 비동기적인 데이터 로딩을 보다 자연스럽게 처리할 수 있도록 도와줌
특정 컴포넌트가 데이터를 가져오는 동안 로딩 상태(Loading State)를 보여줌
핵심 개념
1. 비동기 렌더링 지원
- React가 데이터를 기다리지 않고 UI를 먼저 그린 뒤, 데이터가 준비되면 다시 렌더링함
2. 자동적인 Fallback UI
- 데이터가 로딩 중일 때, 특정 UI(로딩 화면 등)를 부여줄 수 있음.
3. Concurrent Mode와 호환
- React의 Concurrent Rendering에서 자연스럽게 동작해, 사용자 경험 향상시킴
사용법
import React, { Suspense, lazy } from 'react';
// lazy를 사용하여 컴포넌트를 동적으로 로드
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
export default App;
lazy()를 사용해 LazyComponent를 동적으로 가져옴
Suspense를 감싸고 fallback 속성으로 로딩 UI 제공
LazyComponent가 로드되기 전에는 "Loading..." 메시지가 표시됨
Suspense와 함께 Error Boundary를 사용해 에러를 잡아주는 것이 좋음
import { ErrorBoundary } from "react-error-boundary";
function ErrorFallback({ error }) {
return <div>에러 발생: {error.message}</div>;
}
function App() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<div>Loading...</div>}>
<Post />
</Suspense>
</ErrorBoundary>
);
}
(이 블로그의 관련된 글: Error Boundary란? )
Suspense의 단점
1. 여러 Suspense 컴포넌트를 중첩하거나, 트리 구조로 사용할 경우
각 Suspense가 독립적으로 로딩 상태를 관리하므로 데이터 준비 시점이 다름
로딩 화면(fallback)이 여러 번 표시되거나, 비일관적인 UI 경험(뚝 끊기는 UX)이 발생할 수 있음
<Suspense fallback={<div>Loading user...</div>}>
<UserProfile />
</Suspense>
<Suspense fallback={<div>Loading posts...</div>}>
<UserPosts />
</Suspense>
UserProfile과 UserPosts가 개별적으로 데이터 로딩하는 경우
하나의 컴포넌트가 먼저 데이터 로딩을 마쳐도 다른 컴포넌트는 여전히 로딩 UI 표시 중일 수 있음
import { Suspense, SuspenseList } from "react";
<SuspenseList revealOrder="together">
<Suspense fallback={<div>Loading user...</div>}>
<UserProfile />
</Suspense>
<Suspense fallback={<div>Loading posts...</div>}>
<UserPosts />
</Suspense>
</SuspenseList>
(*주의!! SuspenseList는 React 18 이후에 제거 되었음. 참고: https://github.com/facebook/react/issues/22771)
SuspenseList의 revealOrder 속성을 사용해서 로딩 순서를 제어할 수 있음
together: 모든 컴포넌트 데이터 로딩이 끝난 후 동시에 렌더링
forward: 위에서부터 순서대로 렌더링. 아래 컴포넌트의 데이터가 먼저 fullfilled 되어도 위 원소가 먼저 렌더링 되어야 함.
2. Suspense는 Effect 또는 이벤트 핸들러 내부에서 가져오는 데이터 감지하지 않음.
일반적인 fetch() 요청이나 Redux, Zustand 같은 상태 관리 라이브러리와 바로 호환되지 않음
GraphQL, React Query, SWR 같은 라이브러리가 Suspense와 호환되도록 제공하는 옵션을 사용해야 함
Suspense로 Data Fetch하기
[1] React Query 사용하기
import { Suspense } from "react";
import { useQuery } from "@tanstack/react-query";
const fetchData = async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
return res.json();
};
function Post() {
const { data } = useQuery({
queryKey: ["post"],
queryFn: fetchData,
suspense: true, // Suspense 사용!
});
return <h1>{data.title}</h1>;
}
function App() {
return (
<Suspense fallback={<div>Loading post...</div>}>
<Post />
</Suspense>
);
}
export default App;
useQuery에서 suspense: true를 설정하면, 데이터가 로딩될 때 Suspense가 fallback을 보여줌
데이터가 도착하면 Post 컴포넌트가 정상적으로 렌더링 됨
[2] use() 훅 사용
import { Suspense, use } from "react";
// API 호출 후 Promise 반환
const fetchData = fetch("https://jsonplaceholder.typicode.com/posts/1").then((res) => res.json());
function Post() {
const data = use(fetchData); // Suspense가 Promise를 감지하고 로딩 상태 관리
return <h1>{data.title}</h1>;
}
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Post />
</Suspense>
);
}
export default App;
React 18+에서 추가된 use() 훅
fetch()의 반환값은 Promise 형태 (https://developer.mozilla.org/ko/docs/Web/API/Window/fetch)
따라서, fetchData의 반환은 Promise 형태
React18+에서 use()가 추가되기 전에는 Promise 데이터를 자동으로 감지하는 내장 훅을 제공하지 않았음
React는 기본적으로 Promise의 상태(pending, fulfilled, rejected)를 직접 추적하지 않음.
따라서, 이전 버전에서는 useEffect(), useState()를 사용해서 Promise를 직접 관리해야 했음
(React docs. use. https://ko.react.dev/reference/react/use)
fetchData를 use로 감싸면, use가 Promise를 감지함.
Promise 결과가 fullfilled 될 때까지 Suspense가 fallback UI를 표시
isLoading 같은 상태 변수나, throw promise 같은 로직이 필요 없음
[3] Next.js에서 fetch 함께 사용
// app/page.tsx (Next.js 서버 컴포넌트)
import { Suspense } from "react";
// 서버에서 실행되며 Next.js가 자동으로 캐싱
async function fetchPost() {
const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
return res.json();
}
// 서버 컴포넌트에서는 async function을 직접 사용할 수 있음
async function Post() {
const data = await fetchPost(); // Next.js가 자동으로 캐싱
return <h1>{data.title}</h1>;
}
export default function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Post />
</Suspense>
);
}
Next.js에서 '서버 컴포넌트'로 만들 경우 Suspense에서 fetch()를 바로 사용할 수 있음
서버 컴포넌트는 비동기 함수(async)를 직접 사용할 수 있기 때문
fetch()를 직접 사용하면 Next.js에서 자동으로 캐싱함.
서버 컴포넌트에서 데이터를 미리 가져와서 렌더링 결과만 클라이언트 컴포넌트에 전달하는 방식으로 사용
(참고) Susepnse 대신 Fetcher 컴포넌트 직접 만들기
카카오엔터 FE에서는 Suspense 컴포넌트 대신 비슷한 구조로 Fetcher를 만듦
next12 버전을 사용하고 있어서 React18로 업데이트할 수 없어, React18에 새로 추가된 Suspense 못씀
Fetcher 컴포넌트에서 API 에러 발생 시 에러를 throw하고, 이를 잡아서 에러를 처리할 수 있음
ApiErrorBoundary는 Fetcher에서 발생한 제한된 영역 내에서 Fetcher의 에러를 처리함
처리 가능한 영역에 해당하지 않는 에러는 rethrow함.
rethrow된 에러는 루트 레벨에 위치한 GlobalErrorBoundary에서 처리함
1. 전체 화면에 에러를 보여줘야 하는 경우
2. ApiErrorBoundary에서 처리하지 못해서 rethrow된 경우
3. ApiErrorboundary로 감싸져 있지 않은 컴포넌트에서 에러 발생한 경우
Fetcher - 참고한 글:
React의 Error Boundary를 이용해 효과적으로 에러 처리, 카카오엔터 FE기술블로그
https://fe-developers.kakaoent.com/2022/221110-error-boundary/
2025.02.10 추가
useEffect()로 Data Fetch
부제: useEffect()와 Suspense로 로딩 상태를 관리하는 것의 차이점은?
import { useState, useEffect } from "react";
function MyComponent() {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsLoading(true);
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((res) => res.json())
.then((data) => {
setData(data);
setIsLoading(false);
});
}, []);
if (isLoading) return <div>Loading...</div>;
return <h1>{data.title}</h1>;
}
1. useEffect() 훅으로 데이터를 불러옴
2. 로딩 상태를 관리하기 위해 isLoading 상태 변수를 만듦
3. 데이터 불러오는 동안 isLoading: true, 다 부르고 나면 isLoading: false로 바꾸는 방식
이런 조건부 렌더링은 여러 비동기 데이터를 동시에 다루면 조건부 렌더링 로직이 복잡해질 수 잇음
반면에,
Suspense는 fallback 속성으로 fallback UI를 정의해서, 데이터 로딩을 기다리는 동안 보여줌
로딩 컴포넌트를 직접 렌더링하지 않고, 선언적으로 관리할 수 있음
(선언형 코드는 개발자가 '어떻게 할지' 세부적 절차를 지정하는 게 아니라 '무엇을 할지'만 정의하면 됨)
(자세한 설명은 Error Boundary란? https://developer-dreamer.tistory.com/159 참고)
출처
React docs. Suspense https://ko.react.dev/reference/react/Suspense
React에서 SuspenseList는 어떻게 동작하나요? https://ted-projects.com/react-internals-deep-dive-25
useEffect를 이용해 로딩 상태 관리하는 방법과 Suspense를 활용하는 방법에 대한 차이점을 설명해주세요. 82번. https://maeil-mail.kr
'개발자 강화 > 프론트엔드' 카테고리의 다른 글
[매일메일] 민감한 데이터는 어디에 저장해야 할까? (FE.250211) (0) | 2025.02.11 |
---|---|
[공부] BFF API란? (1) | 2025.02.10 |
[매일메일] React의 Concurrent Mode(동시성 모드) (FE.250207) (0) | 2025.02.08 |
[매일메일] React의 메모이제이션? (FE.241223/250206) (0) | 2025.02.06 |
[매일메일] Error Boundary란? (FE.250205) (1) | 2025.02.06 |