본문 바로가기
카테캠/2단계

[7/23] Suspense & ErrorBoundary

by 쪼꼬에몽 2025. 7. 23.

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를 통해 처리하라는 특별한 규칙을 갖고 있음
  • 로딩 중이라고 해석함 

내부 처리

  1. 컴포넌트 렌더링 중 throw Promise 발생
  2. React는 이를 감지하고, 가장 가까운 <Suspense>로 fallback UI 보여줌
  3. 해당 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