Suspense
- 비동기 작업이 완료될 때까지 UI를 일시적으로 다른 화면으로 보여주면서, 작업이 끝나면 진짜 내용을 보여주는 컴포넌트.
- 화면에 뭔가를 불러오는 중임을 자연스럽게 보여주는 역할을 함.
사용 이유
- 코드 스플리팅 (필요한 컴포넌트만 동적으로 불러오기)
- 데이터 로딩 (서버에서 API 결과 받아오기)
- 이미지나 폰트 같은 자원 로딩
사용 방법
import React, { Suspense } from 'react';
const LazyComponent = React.lazy(() => import('./MyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspnese>
);
}
- fallback은 로딩 중 보여줄 UI
- LazyComponent가 다 불러와지면 진짜 컴포넌트로 교체됨
핵심 아이디어
- Suspens 안에서 렌더링 중인 컴포넌트가 아직 준비 안 됐으면 Promise를 던짐
- Suspense는 이걸 받아서 fallback UI를 보여줌
- Promise가 끝나면 다시 렌더링해서 실제 내용을 보여줌
ErrorBoundary
- 자식 컴포넌트 트리에서 발생한 JS 오류를 잡아
- 앱 전체가 다운되지 않도록 하고
- fallback UI를 보여주는 컴포넌트
ErroBoundary가 잡을 수 있는 에러
- 렌더링 중 발생한 오류
- 라이프사이클 메서드에서 발생한 오류
- 생성자에서 발생한 오류
잡을 수 없는 것들
- 이벤트 핸들러 내부 에러
- 비동기 호출 (setTimeout, fetch, Promise.catch)
- 서버사이드 렌더링 중 오류
- 에러 바운더리 내부의 에러
기본 형태
컴포넌트 트리에서 발생한 에러를 잡아내고, 에러가 발생했을 때 fallback UI(문제가 발생했습니다 메시지)를 대신 보여주는 에러 바운더리
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 다음 렌더링에서 fallback UI를 보여주도록 상태를 업데이트
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 에러 로깅 서비스로 보내기
console.error('Error caught by ErrorBoundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h2>문제가 발생했습니다.</h2>;
}
return this.props.children;
}
}
construtor(props) {
super(props);
this.state = { hasError: false };
}
- 초기 상태를 정의하는 부분
- hasError를 false로 설정 -> 아직 에러가 없다는 뜻
static getDerivedStateFromError(error) {
return { hasError: true };
}
- 에러가 발생하면 호출되는 정적 생명주기 메서드
- 에러가 발생하면 상태를 업데이트해 다음 render()에서 fallback UI를 보여주게 함
componentDidCatch(error, errorInfo) {
console.error('Error caught by ErrorBoundary:', error, errorInfo);
}
- 실제로 에러 정보를 캡처하고 처리하는 메서드
- console.error()로 에러를 출력하고 있지만,
- 실제로는 Sentry, LogRocket, Datadog 같은 서비스로 로그를 보내는 데 사용함
render() {
if (this.state.hasError) {
return <h2>문제 발생</h2>;
}
return this.props.children;
}
- 에러 발생하면 fallback UI 렌더링
- 아니면 자식 컴포넌트 렌더링
사용 예시
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
- MyComponent 안에서 에러가 발생하면, 전체 앱이 크래시되지 않고 오류 메시지 보임
React 16 이상부터 제공되는 클래스형만 ErrorBoundary 기능 지원함.
React 18+에서는 useErrorBoundary() 같은 서드파티 라이브러리 혹은 react-error-boundary를 사용하면 함수형 방식으로도 에러 바운더리를 구현할 수 있음
함수형으로 ErrorBoundary 구현하기 (라이브러리 사용)
npm install react-error-boundary
사용 예시
import React from 'react';
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role='alert'>
<p>문제가 발생했습니다: </p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>다시 시도</button>
</div>
);
}
function BuggyComponent() {
throw new Error('error 발생');
}
export default function App() {
return (
<ErrorBoundary
FallbackComponent={Errorfallback}
onReset={() => {
// 상태 초기화 또는 리다이렉트
}}
>
<BuggyComponent />
</ErrorBoundary>
);
}
ErrorBoundary
- 함수형 컴포넌트에서 사용 가능한 에러 경계
FallbackComponent
- 에러 발생 시 보여줄 UI 컴포넌트
onReset
- 다시 시도 클릭 시 호출되는 함수
Suspense + ErrorBoundary
import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
// 로딩 중일 때 보여줄 컴포넌트
const Loading = () => <p>로딩 중...</p>;
// 에러 발생 시 보여줄 컴포넌트
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role='alert'>
<p>문제가 발생</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>다시 시도</button>
</div>
);
}
// React.Lazy로 동적 import
const LazyComponent = React.lazy(() => import('./MyComponent'));
export default function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
console.log('ErrorBoundary reset');
}}
>
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
);
}
export default function MyComponent() {
throw new Error('error');
return <div>정상 내용</div>;
}
Suspense + 비동기 데이터 fetch + 에러 처리
- axios로 데이터 fetch
- Suspens로 로딩 상태 표시
- react-error-boundary로 에러 처리
- 선언적 API 형태 유지
유틸: fetchResource.ts
export function fetchUser(userId: string) {
const promise = axios
.get(url)
.then(res => res.data);
return wrapPromise(promise);
}
// Suspense와 함께 쓰기 위한 유틸
function wrapPromise(promise: Promise<any>) {
let status = 'pending';
let result;
let suspender = promise.then(
res => {
status = 'success';
result = res;
},
err => {
status = 'error';
result = err;
}
);
return {
read() {
if (status === 'pending') {
throw suspender; // Suspense fallback이 보임
} else if (status === 'error') {
throw result;
} else if (status === 'success') {
return result;
}
}
};
}
Suspense의 특별한 처리 방식
- suspender는 Promise 객체
- 렌더링 중에 Promise가 던져지면, Suspense를 통해 처리하라는 특별한 규칙을 갖고 있음
- 로딩 중이라고 해석함
내부 처리
- 컴포넌트 렌더링 중 throw Promise 발생
- React는 이를 감지하고, 가장 가까운 <Suspense>로 fallback UI 보여줌
- 해당 Promise가 resolve되면, 자동으로 컴포넌트 다시 렌더링
export default function UserProfile({ resource }) {
const user = resource.read(); // Suspens에 의해 로딩 또는 성공 상태
return (
<div>
<h2>👤 사용자 정보</h2>
<p>이름: {user.name}</p>
<p>이메일: {user.email}</p>
<p>전화번호: {user.phone}</p>
</div>
);
}
const userResource = fetchUser('1'); // ID 1번 사용자 fetch
function ErrorFallback({ error, resetErrorBoundary }: any) {
return (
<div role="alert">
<p>❌ 문제가 발생했습니다:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>다시 시도</button>
</div>
);
}
export default function App() {
return (
<>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<p>로딩 중...</p>}>
<UserProfile resource={userResource} />
</Suspense>
</ErrorBoundary>
</>
);
}
Suspense + ErrorBoundary + React Query
QueryClientProvider
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true, // suspense 모드 활성화
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<ErrorBoundary>
<React.Suspense fallback={<h2>로딩 중...</h2>}>
<Post />
</React.Suspense>
</ErrorBoundary>
</QueryClientProvider>
);
}
export default App;
ErrorBoundary 컴포넌트
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
console.error(error, info);
}
render() {
if (this.state.hasError) {
return <h2>문제 발생</h2>
}
return this.props.children;
}
}
export default ErrorBoundary;
데이터 패칭 및 사용 컴포넌트
const fetchPost = async () => {
const { data } = await axios.get(url);
return data;
};
export default function Post() {
const { data } = useQuery(['post', 1], fetchPost); // suspense가 true면 로딩 상태 자동 관리
return (
<div>
<h2>{data.title}</h2>
<p>{data.body}</p>
</div>
);
}
'카테캠 > 2단계' 카테고리의 다른 글
| [7/22] useMutation으로 바꾸기 (3) | 2025.08.06 |
|---|---|
| [7/22] useMutation으로 바꾸기 (0) | 2025.07.23 |
| [7/22] React Query - queryClient (0) | 2025.07.22 |
| [7/22] React Query - useMutation (1) | 2025.07.22 |
| [7/22] React Query - useQuery (0) | 2025.07.22 |