1. 들어가기
현재 React에서 선언적으로 에러처리를 하려면 사용해야 하는 `ErrorBoundary` 컴포넌트를 정리하려고 한다.
- Class Component로 선언해서 사용하는 법
- 문서에서 알려주는 라이브러리를 이용해 Function Component로 사용하는 법
두 가지 방법을 알아보자.
근본 원리부터 차근차근 알고 싶다면 1번부터,
지금 바로 적용해야 한다면 2번부터 읽으면 된다.
2. Class Component 방식
에러바운더리를 사용할 때 가장 전통적인 방법이다.
다음은 문서에 나와있는 예시 코드이다.
주석으로
각 메서드 위에 호출 순서를 적어두었다.
간단하게 이런 과정을 거친다.
- `ErrorBoundary`로 감싸진 자식 컴포넌트에서 에러 발생 (`throw new Error`)
- `getDerivedStateFromError` 호출
- `hasError` 를 true로 변경
- `render` 호출
- `hasError`가 true라서 fallback 렌더링
- (로깅 필요시 선택적으로 선언) componentDidCatch(error, info) 호출
- Sentry 등으로 에러 로깅
// ErrorBoundary.tsx
import { Component, createElement } from 'react';
import { ErrorFallbackProps } from './ErrorFallback';
type Props = {
children: React.ReactNode;
fallback: React.ComponentType<ErrorFallbackProps>;
};
type State = {
error: Error | null;
};
export default class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { error: null };
}
// ErrorBoundary 내부 에러 상태를 초기화하여 children이 다시 렌더링될 수 있도록 함
// FallbackComponent의 onReset prop으로 넘김
reset() {
this.setState({ error: null });
}
// 2. 에러 잡히면 호출
static getDerivedStateFromError(error: State['error']) {
return { hasError: true, error };
}
// 1. children 렌더링(에러 발생)
// 3. getDerivedStateFromError에서 hasError가 true가 된 후 fallback 렌더링
render() {
const fallback = this.props.fallback;
if (this.state.error) {
return createElement(fallback, { error: this.state.error, onReset: this.reset.bind(this) });
}
return this.props.children;
}
// 4. fallback 렌더링 후 호출
// componentDidCatch(error, info) {
// logErrorToMyService(error, info.componentStack); // 에러 로깅 (ex. Sentry)
// }
}
에러바운더리 내부 주요 메서드
1. static getDerivedStateFromError(error)
렌더링 도중 자식 컴포넌트가 오류를 던질 때 React가 호출한다.
이를 통해 기존 UI를 지우는 대신 fallback UI를 표시할 수 있다.
일반적으로 에러바운더리에서 사용된다.
매개변수
- `error`: 자식 컴포넌트가 `throw`한 에러. 일반적으로 Error의 인스턴스가 되겠죠?
반환값
에러바운더리가 fallback UI를 렌더링 하도록 state를 변경해서 반환한다.
주의사항
- `static getDerivedStateFromError`는 순수함수여야 한다.
- 따라서 로깅은 `ComponentDidCatch`에서 수행해야 한다.
- 함수 컴포넌트에서 `static getDerivedStateFromError`에 대한 직접적인 대응은 아직 없다.
- react-error-boundary 라이브러리를 사용하자.
2. ComponentDidCatch(error, info)
렌더링 도중 자식 컴포넌트가 오류를 던질 때 React가 호출한다.
이를 통해 해당 오류를 로깅할 수 있다.
일반적으로 이 메서드는 에러바운더리에서 에러에 대한 응답으로 state를 업데이트하고 사용자에게 에러 메시지를 표시할 수 있는 `static getDerivedStateFromError`와 함께 사용된다.
매개변수
- `error`: 자식 컴포넌트가 `throw`한 에러
- `info`: 오류에 대한 추가 정보가 포함된 객체.
- 내부 `componentStack` 필드에 에러를 발생시킨 컴포넌트의 스택과 모든 상위 컴포넌트의 이름 및 소스위치가 포함된다.
반환값
아무것도 반환하지 않는다.
3. reset()
ErrorBoundary 내부 에러 상태를 초기화하여 children이 다시 렌더링 될 수 있도록 하는 메서드
FallbackComponent의 onReset prop으로 넘겨서 호출한다.
예를 들어 `홈으로 돌아가기 버튼`, `다시 요청하기 버튼` 등을 클릭할 때 함께 호출하면 된다.
Fallback UI Component와 적용예시
// ErrorFallback.tsx
import Button from '../../Button/Button';
export type ErrorFallbackProps = {
error: Error;
onReset: () => void;
};
const ErrorFallback = ({ error, onReset }: ErrorFallbackProps) => {
return (
<>
{error.message}에러가 발생했습니다!
<Button onClick={onReset}>이전으로</Button>
</>
);
};
export default ErrorFallback;
// Example.tsx
import ErrorBoundary from './ErrorBoundary';
import ErrorFallback from './ErrorFallback';
import ErrorableComponent from './ErrorableComponent';
const Example = () => {
return (
<ErrorBoundary fallback={ErrorFallback}>
<ErrorableComponent />
</ErrorBoundary>
);
};
export default Example;
// ErrorableComponent.tsx
import { useState } from 'react';
import Button from '../../Button/Button';
const ErrorableComponent = () => {
const [error, setError] = useState(false);
if (error) {
throw new Error('자식에서 던진 에러');
}
const throwError = () => {
setError(true);
};
return (
<>
<Button onClick={throwError}>렌더링 때 에러 발생시키기</Button>
</>
);
};
export default ErrorableComponent;
Fallback UI 컴포넌트 예시이다.
`ErrorBoundary` 내부에서 `createElement` 메서드로 위 `ErrorFallback` 컴포넌트를 렌더링 할 때 넘겨주는 `error`, `onReset` prop을 활용한다.
3. Function Component (react-error-boundary) 방법
사용법
Class Component 방식이 낯설어서 적용하기 힘들다면 라이브러리를 활용하자.
`react-error-boundary`라는 라이브러리를 이용하면 간단하게 ErrorBoundary를 사용할 수 있다.
// Example.tsx
import { ErrorBoundary } from 'react-error-boundary';
import ErrorFallback from './ErrorFallback';
import ErrorableComponent from './ErrorableComponent';
import { ErrorInfo } from 'react';
const Example = () => {
const logError = (error: Error, info: ErrorInfo) => {
// Do something with the error, e.g. log to an external API
console.log(error, info);
};
const resetError = () => {
// ErrorableComponent가 다시 렌더링될 수 있도록 에러 바운더리 상태를 재설정
// 예를 들어 "재시도" 버튼을 눌렀을때 사용
// 단순 상태변경 (hasError: true -> false)는 라이브러리에서 내부적으로 해주지만, 추가적인 작업이 필요할때 여기에 로직 추가할 수 있음
console.log('resetError');
};
return (
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={resetError} onError={logError}>
<ErrorableComponent />
</ErrorBoundary>
);
};
export default Example;
// ErrorFallback.tsx
import Button from '../../Button/Button';
export type ErrorFallbackProps = {
error: Error;
resetErrorBoundary: () => void;
};
const ErrorFallback = ({ error, resetErrorBoundary }: ErrorFallbackProps) => {
return (
<>
{error.message}에러가 발생했습니다!
<Button onClick={resetErrorBoundary}>이전으로</Button>
</>
);
};
export default ErrorFallback;
라이브러리가 제공하는 `ErrorBoundary`의 prop은
`FallbackComponent`, `onReset`, `onError` 등이 있다.
각각 클래스 컴포넌트 방식과는 이렇게 대응된다.
- `FallbackComponent` === 클래스 에러바운더리의 `fallback` prop
- `onReset` === `reset()` 메서드
- 라이브러리 내부적으로 error 상태를 초기화하고, 우리는 추가적인 로직을 넣어줄 수 있다.
- `onError` === `componentDidCatch` 메서드
- 로깅용
또한 클래스에서는 `getDerivedStateFromError` 에서 직접 에러 상태를 바꿔서 fallback UI를 렌더링 해야 했지만, 라이브러리에서는 내부적으로 해주기 때문에 구현할 필요가 없어 간단하다.
useErrorBoundary 훅
`ErrorBoundary`를 명령적으로 제어할 수 있는 훅도 제공한다.
`showBoundary` 와 같은 메서드를 이용해 명령적으로 에러바운더리를 렌더링 할 수 있다.
// ErrorableComponent.tsx
import Button from '../../Button/Button';
import { useErrorBoundary } from 'react-error-boundary';
const ErrorableComponent = () => {
const { showBoundary } = useErrorBoundary();
const throwError = () => {
showBoundary(new Error('자식에서 던진 에러'));
};
return (
<>
<Button onClick={throwError}>렌더링 때 에러 발생시키기</Button>
</>
);
};
export default ErrorableComponent;
위 클래스 에러바운더리 `ErrorableComponent` 예시에서
`error` 상태를 만들어서 강제로 리렌더링 후 에러를 throw 하는 로직을 볼 수 있다.
`showBoundary`도 똑같은 로직으로 구성된 메서드이다.
showBoundary 사용예시
렌더링 시 `useEffect`에서 get 요청을 했는데 error가 발생했을 때 호출하여 에러바운더리를 렌더링 할 수 있다.
// 렌더링 시 즉시 get 요청하는 컴포넌트
import { useEffect } from 'react';
import { useErrorBoundary } from 'react-error-boundary';
const fetch = () => {
return new Promise((resolve, reject) => setTimeout(() => reject(new Error('fetch 에러')), 1000));
};
const ErrorableComponent = () => {
const { showBoundary } = useErrorBoundary();
useEffect(() => {
const fetchData = async () => {
try {
await fetch();
} catch (error) {
showBoundary(error);
}
};
fetchData();
}, [showBoundary]);
return <>get 요청하는 컴포넌트</>;
};
export default ErrorableComponent;
만약 `showBoundary(error);` 대신 `throw error;` 를 작성했다면 잘못된 로직이다.
`useEffect` 내에서 `catch` 후에 다시 `throw` 하는 로직은 상위 컴포넌트로 전파되지 않는다.
즉 에러는 해당 catch 내에서만 잡히므로 의미 없는 로직이다.
대신 리렌더링을 강제하여 렌더링 시 에러를 throw 하는 `showBoundary`를 이용할 수 있다.
4. react-query와 errorBoundary를 함께 사용할 때
간단히 말해서 `throwOnError` 옵션, `QueryErrorResetBoundary` 컴포넌트를 이용한다.
throwOnError
요즘엔 직접 `useEffect` 내부에서 직접 `fetch` 함수를 호출하는 코드는 흔치 않다.
대신 서버 상태를 효과적으로 관리해 주는 react-query가 있기 때문이다.
그땐 `showBoundary` 대신 react-query에서 제공하는 `throwOnError` 옵션을 이용하자.
// ErrorableComponent.tsx
import { useQuery } from '@tanstack/react-query';
import { useEffect } from 'react';
const fetch = () => {
console.log('fetch');
return new Promise((resolve, reject) => setTimeout(() => reject(new Error('fetch 에러')), 100));
};
const ErrorableComponent = () => {
const { data } = useQuery({
queryKey: ['fetch'],
queryFn: fetch,
throwOnError: true, // 에러 발생 시 던진다.
});
return <>get 요청중...</>;
};
export default ErrorableComponent;
그럼 query시 발생하는 에러가 상위 컴포넌트로 던져지는 걸 보장할 수 있다.
QueryErrorResetBoundary
밑 움짤에서 reset을 해서 다시 `ErrorableComponent`를 렌더링 했지만 곧바로 error가 발생해서 다시 에러바운더리로 넘어가는 걸 볼 수 있다.
에러바운더리가 fallback UI를 렌더링 해서 ErrorableComponent가 unmount 되었는데 왜 ErrorableComponent 내부의 query의 error 상태가 초기화되지 않는 걸까?
이는 `useQuery`의 error 상태를 가지고 있는 캐시(queryCache)는 해당 컴포넌트 내부에 선언되는 것이 아니고 따로 관리되기 때문이다.
이에 대한 자세한 사항은 https://www.timegambit.com/blog/digging/react-query/01#querycache 을 참고해 보자
따라서 queryCache도 reset 해줘야 한다.
이때 사용하는 게 `QueryErrorResetBoundary`이다.
import { ErrorBoundary } from 'react-error-boundary';
import ErrorFallback from './ErrorFallback';
import ErrorableComponent from './ErrorableComponent';
import { ErrorInfo } from 'react';
import { QueryErrorResetBoundary } from '@tanstack/react-query';
const Example = () => {
...
return (
<QueryErrorResetBoundary>
// queryCache reset하기
{({ reset }) => (
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={reset} onError={logError}>
<ErrorableComponent />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
);
};
export default Example;
동작 원리는
를 참고하자.
5. 에러바운더리가 잡지 않는 에러
에러바운더리는 렌더링 시에 발생한 에러만 잡을 수 있다.
에러 경계는 다음과 같은 에러는 포착하지 않는다.
- 이벤트 핸들러
- 비동기적 코드 (예: setTimeout 혹은 requestAnimationFrame 콜백)
- 서버 사이드 렌더링
- 자식에서가 아닌 에러 경계 자체에서 발생하는 에러
각 아이템에 대한 예시는 다음 글들을 참고하자.
https://ko.legacy.reactjs.org/docs/error-boundaries.html#introducing-error-boundaries
https://happysisyphe.tistory.com/66
'프론트엔드' 카테고리의 다른 글
dynamic import를 이용한 lazy loading, code splitting 개념 정리 (1) | 2024.11.08 |
---|---|
[컨셉비] 2. 1차 스프린트 과정 정리 (4) | 2024.02.13 |
[컨셉비] 1. 프론트엔드 중간 합류와 각오 (2) | 2024.01.27 |
css 속성들을 이해하면서 Skeleton 만들기 (0) | 2024.01.26 |
vite + typescript + react 프로젝트에서 절대경로 설정하기 (4) | 2024.01.03 |