Error boundary와 Suspense의 이름만 보고 어떤 역할을 하는 친구들인지 추론해보자!
error boundary라는 이름만 보면, 에러를 처리하는 방법중 하나일 거 같다.
error boundary를 해석해보면, 에러 경계선이다.
이를 통해 추론해보면
에러를 발생했을 때 처리해주는 역할을 한다 → 어떤 에러를 처리할까? → 리액트에서 발생하는 에러들 (데이터 패칭 에러, 렌더링시 발생하는 에러 등등…) → 어떻게 처리할까? →에러 경계선을 설정해주어, 에러가 상위로 전파되는 것을 막자
Suspense를 해석하면 “영화·드라마·소설 등의 줄거리의 전개가 관객이나 독자에게 주는 불안감과 긴박감. 순화어는 긴장감', 박진감'."이다. 사실 이 뜻만 가지고 추론하기에는 너무 힘들어서 지피티에게 Suspense의 뜻을 물어보니 “긴장감”, “불안정한 기다림”, “결과를 기다리는 상태” 라고 대답했다.
이제는 추론을 해 볼 수 있을 거 같다. Suspense는 비동기 동작에서 발생하는 로딩 상태?를 처리해주는 친구인 거 같다.
로딩을 처리해주는 역할을 한다 → 어떤 로딩을 처리할까 → 비동기 작업에서 발생하는 로딩(데이터 패칭) → 어떻게 처리할까? → 새로운 컴포넌트로 로딩을 처리한다? (기존 명령형 방식에서 선언형 방식으로 변경)
내가 추론한 내용을 토대로, 해당 기능을 사용해서 만들 수 있는 작은 기능을 계획해보자.
이제 내가 추론한게 맞는지 검증해보고, 작은 기능을 만들기 위해 필요한 지식을 학습해보자.
UI의 일부분에 존재하는 자바스크립트 에러가 전체 애플리케이션을 중단시켜서는 안 됩니다. React 사용자들이 겪는 이 문제를 해결하기 위해 React 16에서는 에러 경계(“error boundary”)라는 새로운 개념이 도입되었습니다.
에러 경계는 하위 컴포넌트 트리의 어디에서든 자바스크립트 에러를 기록하며 깨진 컴포넌트 트리 대신 폴백 UI를 보여주는 React 컴포넌트입니다. 에러 경계는 렌더링 도중 생명주기 메서드 및 그 아래에 있는 전체 트리에서 에러를 잡아냅니다.
Error Boundary 는 하위 컴포넌트에서 발생하는 자바스크립트 에러를 잡아서 fallback UI를 보여주는 React 컴포넌트다.
React의 렌더링 과정 안에서 발생한 ‘동기적’ 에러는 대부분 포착할 수 있다.
동기적 에러은 다음과 같다.
Error Boundary는 다음과 같은 에러는 포착하지 않습니다.
- 이벤트 핸들러
- 비동기적 코드 (예:
setTimeout혹은requestAnimationFrame콜백)- 서버 사이드 렌더링
- 자식에서가 아닌 에러 경계 자체에서 발생하는 에러
왜 동작하지 않는지 궁금하다면 시지프의 블로그를 살펴보자
https://happysisyphe.tistory.com/66
Error Boundary는 자바스크립트의
catch {}구문과 유사하게 동작하지만 컴포넌트에 적용됩니다. 오직 클래스 컴포넌트만이 에러 경계가 될 수 있습니다.
에러 경계는 트리 내에서 하위에 존재하는 컴포넌트의 에러만을 포착합니다.
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false }; // 에러 상태 초기값
}
// 하위 컴포넌트에서 에러가 발생하면 리액트에서 자동으로 getDerivedStateFromError를 실행한다.
// 상태 값을 바꾸고 componentDidCatch함수를 실행한다.
static getDerivedStateFromError(error) {
return { hasError: true }; // 에러 발생 시 상태 변경
}
// 실제로 에러 정보를 외부 서비스에 전송하거나 콘솔에 출력하는 등의 작업을 이 메서드에서 처리한다.
// 렌더링에는 관여하지 않는다.
componentDidCatch(error, errorInfo) {
logErrorToMyService(error, errorInfo); // 에러 로깅
}
// componentDidCatch함수 실행다음 실행되는 함수로, 폴백 UI를 보여줄지, children을 보여줄 지 결정한다.
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>; // 폴백 UI
}
return this.props.children; // 정상 렌더링
}
}
ErrorBoundary 아래 컴포넌트에서 에러가 발생하면 React는 자동으로 getDerivedStateFromError() → componentDidCatch() → render() 순서로 처리한다.
실제로 Error Boundary를 사용할 때는 에러를 처리하고 싶은 컴포넌트 상위에 작성한다.
<ErrorBoundary fallback={<p>Something went wrong</p>}>
<Profile />
</ErrorBoundary>
Profile에서 발생하는 에러를 ErrorBoundary에서 처리한다.
React의 에러 처리 메서드가 클래스 전용이다!
Error Boundary를 만들기 위해서는 다음 두 메서드를 사용해야 한다.
static getDerivedStateFromError(error)componentDidCatch(error, info)이 두 메서드는 클래스 컴포넌트의 전용 생명주기 메서드다. 함수형 컴포넌트에서는 이 메서드들을 사용할 수 없어서 클래스형 컴포넌트만 사용가능하다.
error boundary를 함수 컴포넌트로 작성할 수 있는 방법은 없습니다. 하지만 error boundary 클래스를 직접 작성할 필요는 없습니다. 예를 들어
react-error-boundary를 대신 사용할 수 있습니다.
리액트 공식문서에도 나온 만큼 react-error-boundary 라이브러리를 사용하자..
try / catch 는 명령형 코드에서만 동작한다.
try {
showButton();
} catch (error) {
// ...
}
그러나 React 컴포넌트는 선언적이며 무엇을 렌더링할지 구체화한다.
Error Boundary는 React의 선언적인 특성을 보존하고 예상한 대로 동작gksek. 예를 들어 트리의 깊숙한 어딘가에 있는setState에 의해 유발된 componentDidUpdate 메서드에서 에러가 발생하더라도 가장 가까운 에러 경계에 올바르게 전달된다.
function App() {
return (
<ErrorBoundary fallback={"에러가 발생했습니다."}>
<Children />
</ErrorBoundary>
);
}
function Children() {
throw new Error("will it be catched?");
return (
<div>
자식 컴포넌트입니다.
</div>
);
}

성공적으로, 에러가 처리되었다!
그렇다면 throw new Error를 setTimeout에 넣어서 실행하면 어떻게 될까?
function Children() {
function throwErrorFn() {
throw new Error("will it be catched?");
}
setTimeout(throwErrorFn, 1000);
return <div> 자식 컴포넌트입니다. </div>;
}

정상적으로 <div>자식 컴포넌트입니다.</div>가 화면에 표시되는 이유는 setTimeout 내부 함수가 비동기적으로 실행되기 때문이다.
setTimeout은 현재 실행 중인 콜 스택이 모두 비워진 후, 즉 이벤트 루프의 다음 사이클에서 등록된 콜백을 실행한다.
따라서 Children 컴포넌트는 먼저 정상적으로 렌더링되고 화면에 출력된 뒤, 1초 후에 setTimeout의 콜백 함수가 실행되며 에러가 발생하게 된다.
즉, 에러는 렌더링이 완료된 이후에 발생하므로, “자식 컴포넌트입니다”라는 문구는 정상적으로 화면에 표시된다.
<Suspense>는 자식 요소를 로드하기 전까지 화면에 대체UI Fallback를 보여줍니다.
import { Suspense } from 'react';
import Albums from './Albums.js';
export default function ArtistPage({ artist }) {
return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<Loading />}>
<Albums artistId={artist.id} />
</Suspense>
</>
);
}
function Loading() {
return <h2>🌀 Loading...</h2>;
}
import {use} from 'react';
import { fetchData } from './data.js';
export default function Albums({ artistId }) {
const albums = use(fetchData(`/${artistId}/albums`));
return (
<ul>
{albums.map(album => (
<li key={album.id}>
{album.title} ({album.year})
</li>
))}
</ul>
)<;
}
Suspense가 가능한 데이터만이 Suspense 컴포넌트를 활성화합니다. 아래와 같은 것들이 해당합니다.
- Relay와 Next.js 같이 Suspense가 가능한 프레임워크를 사용한 데이터 가져오기.
lazy를 활용한 지연 로딩 컴포넌트.use를 사용해서 캐시된 Promise 값 읽기.
Suspense는 Effect 또는 이벤트 핸들러 내부에서 가져오는 데이터를 감지하지 않습니다. (Error Boundary가 비동기를 처리하지 못하는 거랑 같은 맥락)
위의Albums컴포넌트에서 데이터를 로딩하는 정확한 방법은 프레임워크마다 다릅니다.Suspense가 가능한 프레임워크를 사용하는 경우, 프레임워크의 데이터 불러오기 관련 문서에서 자세한 내용을 확인할 수 있습니다.
use()는 비동기적으로 데이터를 가져오는 동안 컴포넌트를 일시 중단(suspend) 시켜, Suspense로 로딩 UI를 보여주도록 트리거할 수 있다.
// data.js
export function fetchData() {
return fetch('/api/data')
.then(res => res.json());
}
// Component.jsx
import { use } from 'react';
import { fetchData } from './data';
export default function MyComponent() {
const data = use(fetchData()); // Suspense가 이걸 감지
return <div>{data.message}</div>;
}
기존 useEffect나 useState로 관리하지 않고도 Suspense와 함께 자연스럽게 로딩 처리가 되도록 구현할 수 있다.
use 훅을 사용하지 않고, 원래했던 방식으로 데이터를 패칭해도 Suspense에 잡히지 않는다.
기본적으로 Suspense 내부의 전체 트리는 하나의 단위로 취급된다. 예를 들어, 이러한 구성 요소 중 하나라도 어떤 데이터에 의해 지연되더라도 모든 구성 요소가 함께 로딩 표시로 대체된다.
<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>
export default function Biography({ artistId }) {
const bio = use(fetchData(`/${artistId}/bio`));
return (
<section>
<p className="bio">{bio}</p>
</section>
);
}
export default function Albums({ artistId }) {
const albums = use(fetchData(`/${artistId}/albums`));
return (
<ul>
{albums.map(album => (
<li key={album.id}>
{album.title} ({album.year})
</li>
))}
</ul>
);
}
Biography와 Albums 모두 어떤 데이터를 서버에서 가져온다. 하지만 두 구성 요소는 같은 단일 Suspense 아래에 그룹화되어 있기 때문에 항상 동시에 함께 그려지게 된다.
이렇게 Suspense를 세분화하여 사용하면 두 컴포넌트가 로딩될 때까지 기다리는 것을 막을 수 있다.
<Suspense fallback={<BigSpinner />}>
<Biography />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums />
</Panel>
</Suspense>
</Suspense>
이 변경으로 Biography를 보여줄 때 Albums가 로딩될 때까지 기다릴 필요가 없다.
직접 작성한 코드
function fetchData() {
return fetch("https://jsonplaceholder.typicode.com/todos/1").then(
(res) => res.json()
);
}
export function MyComponent() {
const data = use(fetchData());
return <div>{data.title}</div>;
}
function App(){
return (
<div>
<Suspense fallback="로딩중...">
<MyComponent />
</Suspense>
</div>
)
}
이렇게 구현하고 실행을 해보니

무한 로딩에 빠진다..
문제 원인을 파악해보니, use훅의 동작원리 때문이었다.
use는 인자로 받은 Promise가 resolve될 때까지 컴포넌트 렌더링을 지연시킨다. 그리고 Promise가 resolve되면 컴포넌트를 다시 렌더링한다. 이때 컴포넌트가 다시 실행되면서 use의 인자로 넘긴 Promise가 매번 새로 생성되면, 매 렌더링마다 새로운 Promise를 만나게 되어 무한 로딩에 빠지게 된다.
따라서 use(fetch(...))처럼 직접 Promise를 생성하는 게 아니라, 외부에서 메모이제이션된 Promise를 넘겨주는 방식으로 작성해야 무한 루프를 방지할 수 있다.
에러 해결
let cachedPromise;
export function fetchData() {
if (!cachedPromise) {
cachedPromise = fetch("https://jsonplaceholder.typicode.com/todos/1").then(
(res) => res.json()
);
}
return cachedPromise;
}
export default function MyComponent() {
const data = use(fetchData());
return <div>{data.title}</div>;
}
cachedPromise 전역 변수를 만들어서, fetchData가 재실행되어도 새로운 promise 만들지 않게 하여 promise를 메모이제이션을 진행했다.
이렇게 되면 위에서 발생한 무한 렌더링 문제를 해결할 수 있다.

쨘
https://ko.legacy.reactjs.org/docs/error-boundaries.html
https://happysisyphe.tistory.com/66
https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
머싯다...