728x90
반응형
리액트에서 에러바운더리를 쓰는 이유에 대해서 알아봅시다.
그 이유에 대해 알아보기 전에, 에러 바운더리를 간단히 설명하자면 아래와 같아요.
📒 선언적으로 에러를 처리할 수 있게 해주는 리액트 컴포넌트
기존에 명령형으로 처리했던 로직을 선언적으로 처리할 수 있도록 도와주는데요.
더 자세히 알아봅시다.
왜 사용해요?
사실, 에러 바운더리를 사용하지 않아도 기본적인 에러처리는 가능해요.
const MyComponent = () => {
const {
data,
error,
} = useQuery(...);
if (error) {
return <p>오류가 발생했어요!</p>
}
return (
<div>
{data.data.map(...)}
</div>
);
}
- 위와 같은 코드는 명령형 코드로 분류돼요.
- 명령형 코드가 나쁜 건 아닙니다.
- 하지만, 명령에 대한 코드를 작성하게 되면 코드 라인이 점점 증가하고 복잡도가 상승합니다.
에러 바운더리를 쓰면 간편하고 선언적인 오류처리 코드를 작성할 수 있어요.
시작하기
Class형 컴포넌트 제작
- 에러 바운더리 컴포넌트는 react package에서 import 해서 바로 사용할 수 없어요.
- react의 class형 컴포넌트의 메소드를 오버라이딩해서 만들 수 있어요.
- 고로, 함수형 컴포넌트를 사용해 에러 바운더리를 구현할 수 없습니다. !
예시 코드
import {
Component,
ComponentType,
createElement,
ReactNode,
ErrorInfo,
} from 'react';
type ErrorBoundaryState = {
hasError: boolean;
error: Error | null;
};
type FallbackProps = {
error: Error | null;
};
type ErrorBoundaryProps = {
// fallback 용도의 컴포넌트는 Error 정보를 props로 받을 수 있는 컴포넌트여야 한다.
fallback: ComponentType<FallbackProps>;
children: ReactNode;
};
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = {
hasError: false, // 오류가 발생했는지 여부를 state 에 저장!
error: null, // 발생한 오류의 정보를 state 상태로 저장 !
};
}
/*
getDerivedStateFromError 메소드는 하위 컴포넌트에서 오류의 정보를 return을 통해서 State에 저장하는 역할을 합니다.
error 파라미터는 발생한 오류의 정보를 담고 있습니다.
렌더링 도중 자식 컴포넌트가 오류를 던질 때 React가 호출합니다.
*/
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return {
hasError: true,
error, // 자식 컴포넌트가 throw한 에러
};
}
/* componentDidCatch 메소드는 오류 정보와 상세 정보를 파라미터로 얻을 수 있습니다.
주로 오류를 로깅해야 할때 해당 메소드에 접근해서 로깅할 수 있습니다.
*/
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
// error : 자식 컴포넌트가 throw한 에러, errorInfo : 오류에 대한 추가 정보가 포함된 객체
console.log({ error, errorInfo });
}
render() {
const { state, props } = this;
const { hasError, error } = state;
const { fallback, children } = props;
const fallbackProps: FallbackProps = {
error,
};
// fallback 컴포넌트 측에서 오류 정보를 props로 받을 수 있도록 설정
const fallbackComponent = createElement(fallback, fallbackProps);
// 오류 발생 여부를 체크하여, 오류가 발생했을때 조건부 렌더링 처리를 해줍니다.
return hasError ? fallbackComponent : children;
}
}
- 각 메소드마다 역할이 다 있어요.
- 어차피 복붙해서 사용하면서 깨닫는 거니까 지금 당장 세부적으로 알지 못해도 괜찮습니다.
사용예시는 아래와 같아요.
const ChildComponent = () => {
return (
...
);
}
const ParentComponent = () => {
return (
<ErrorBoundary
fallback={(({ error }) => {
// ErrorBoundary에서 캐치된 오류 정보를 매개변수를 통해서 받을 수 있기에, 오류별 케이스 처리가 가능합니다.
if (error.status === 404) {
return <p>404 오류입니다.</p>
}
else if (error.status === 500) {
return <p>500 오류에요!</p>
}
})}
>
<ChildComponent />
</ErrorBoundary>
);
}
- 오류에 대한 정보를 fallback에서 매개변수로 받을 수 있기 때문에,
- 오류 케이스 별로 조건부 렌더링이 가능해집니다 !
에러바운더리로 감싸진 자식 컴포넌트 에서 에러가 발생하면, 아래와 같은 순서를 거쳐요.
1. `getDerivedStateFromError` 가 호출됩니다.2. 내부 상태 변수인 `hasError` 가 true 로 변경됩니다.3. 에러가 발생하여 `fallbackComponent` 가 렌더링 됩니다.
react-error-boundary
- class형 컴포넌트가 낯설다면, 라이브러리를 이용합시다 !
- https://www.npmjs.com/package/react-error-boundary
const Example = () => {
const logError = (error: Error, info: ErrorInfo) => {
console.log(error, info);
};
const resetError = () => {
// ErrorableComponent가 다시 렌더링될 수 있도록 에러 바운더리 상태를 재설정
// 예를 들어 "재시도" 버튼을 눌렀을때 사용
// 단순 상태변경 (hasError: true -> false)는 라이브러리에서 내부적으로 해주지만, 추가적인 작업이 필요할때 여기에 로직 추가할 수 있음
console.log('resetError');
};
return (
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={resetError} onError={logError}>
<ErrorableComponent />
</ErrorBoundary>
);
};
useErrorBoundary
- 위의 라이브러리를 사용해서, 에러 바운더리를 명령형으로도 구현할 수도 있어요.
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 <>예시</>;
};
export default ErrorableComponent;
react-query와 함께 사용하는 에러 바운더리
- react-query는 에러 바운더리 사용을 지원합니다 !
- useErrorBoundary 를 true로 설정해주면 됩니다.
import { useQuery } from '@tanstack/react-query';
const { data } = useQuery({
queryKey: ['some key'],
...
useErrorBoundary: true, // 상위 컴포넌트에서 감싼 Error Boundary의 처리 허용여부
});
const fetchData = async () => {
// return something
};
const ChildComponent = () => {
const { data } = useQuery({
queryKey: ['some key'],
queryFn: fetchData,
useErrorBoundary: true, // 상위 컴포넌트에서 감싼 Error Boundary의 처리 허용여부
});
return (
<div>
<h1>제목: {data.title}</h1>
<p>글 내용: {data.content}</p>
</div>
);
};
const ParentComponent = () => {
return (
<ErrorBoundary
fallback={({ error }) => {
const status = error?.status;
switch (status) {
case 404:
return <p>404 오류가 발생했습니다!</p>;
case 500:
return <p>서버 오류에요!</p>;
}
}}
>
<ChildComponent />
</ErrorBoundary>
);
};
에러 바운더리가 잡지 않는 에러
- 에러 바운더리라고 해서 모든 에러를 잡는 것은 아니에요.
- 잡지 못하는 에러들은 아래와 같습니다.
- 이벤트 핸들러
- 비동기적인 코드
- 서버 사이드 렌더링
- 자식에서가 아닌 에러 경계 자체에서 발생하는 에러
사전 지식
try {
function throwErrorFn() {
throw new Error("will it be catched?");
}
setTimeout(throwErrorFn, 1000);
} catch (e) {
console.log(e);
}
- 과연 위의 코드에서 error가 catch문에서 잡힐까요 ? → NO!
💡
자바스크립트에서 함수들은 메인 콜 스택에 쌓이고 차례대로 실행(push-pop) 되는데요.
setTimeout 함수는 별도의 공간인 테스트큐에 들어가고, 메인 콜스택이 다 비워져야, 테스트큐에 있던 setTimeout 함수가 메인 콜스택으로 들어와실행하게 됩니다.
동기식 언어인 자바스크립트 엔진에서 비동기처럼 코드를 작성할 수 있는 이유 기도 합니다.
setTimeout함수가 메인 콜 스택에 왔을 땐, setTimeout을 감싸고 있던 try-catch문은 이미 메인 콜스택에서 실행되어 pop된 상태이기 때문에, 에러를 던지더라도 받아줄 수 있는 try-catch 문이 없는 상태가 됩니다.
이벤트 핸들러
- 자바스크립트에서 이벤트 핸들러는 dom node에 이벤트를 직접 등록합니다.
- 이런 이벤트는 root DOM 에 등록이 됩니다. (이벤트 위임)
- 즉, 에러 바운더리 내에 이벤트가 존재하지 않기 때문에, 포착할 수 없는 것 !
비동기적 코드
- 비동기적인 코드는 별도의 테스크 큐에서 기다렸다가, 메인 콜스택에 있는 함수가 모두 실행이 되어서야 동작합니다.
- 즉, 에러 바운더리 실행이 다 끝나고 난 뒤 실행된다는 뜻 !
export default function App() {
return (
);
}
function Children() {
const [todos, setTodos] = useState([]);
useEffect(() => {
(async () => {
try {
const res = await axios.get(
"<https://jsonplaceholder.typicode.com/todos21231>"
);
setTodos(res);
} catch (e) {
throw new Error("is it ?"); // 비동기 내에 throw한 에러는 포착할 수 없어요.
}
})();
});
return click;
}
- 그렇다면, 어떻게 잡을 수 있을까요?
- 에러바운더리 내에서 throw 를 일으켜야 합니다 !
function Children() {
const [todos, setTodos] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
(async () => {
try {
const res = await axios.get(
"<https://jsonplaceholder.typicode.com/todos21231>"
);
setTodos(res.data);
} catch (e) { // 비동기 외부에서 에러를 캐치하라는 뜻 !
setError(e);
}
})();
}, []);
if (error) {
throw error; // 에러 상태가 있으면 에러를 던집니다
}
return click;
}
서버사이드 렌더링
- getDerivedStateFromError 가 상태 변화가 존재하는 브라우저 환경 에서 만 실행되기 때문에, 서버에서 그려지는 컴포넌트에서는 실행할 수 없어요.
try {
throw new Error("Error");
} catch (e) {
throw e;
}
- try 에서 에러가 발생하고, catch 에서 잡혔는데 이것을 다시 던진다면 해당 catch 에서 잡을 수 없습니다.
- 이것을 잡기 위해서는 다른 try/catch 가 필요 합니다.
try {
try {
throw new Error("Error");
} catch (e) {
throw e;
}
} catch (e) {
console.log(e);
}
- 에러 바운더리도 이와 동일합니다.
- 에러 바운더리 에서 다시 에러가 throw 된다면, 에러를 다시 상위 컴포넌트로 던지고
- 그 상위엔 에러 바운더리가 있어야 만, 에러를 포착할 수 있어요.
참고 블로그
https://yiyb-blog.vercel.app/posts/error-boundary-with-react-query https://yogjin.tistory.com/122#5.
https://fe-developers.kakaoent.com/2022/221110-error-boundary/ https://happysisyphe.tistory.com/66
728x90
반응형
'🌳Frontend > react' 카테고리의 다른 글
React에서 에러 처리 종류 (Axios Interceptor, Error Boundary, createBrowserRouter 의 errorElement 처리) (0) | 2025.01.11 |
---|---|
React에서 state를 정의할 때 고려해야 할 점 (feat. SSOT) (0) | 2024.02.25 |
React 컴포넌트 타입인 ReactElement vs JSX.Element vs ReactNode 차이와 예시 (0) | 2024.02.24 |
Next.js에서 Firebase 파이어베이스로 구글 로그인 기능 구현 (0) | 2023.10.14 |
Next.js 와 파이어베이스(Firebase) 연동하기 (0) | 2023.10.14 |