React에서 Error를 선언적으로 관리하며 hook으로 관리하기
2024/06/01
10 min read
DEVELOPMENT
REACT
ErrorBoundary란?
컴포넌트 트리의 일부분에서 오류가 발생할 때 오류를 캡처하고 대체 UI를 렌더링해주는 컴포넌트 입니다. 이를 통해서 오류로 인해서 전체 애플리케이션이 중단되는 것을 방지하고, 다른 부분들이 정상적으로 동작할 수 있도록 도와줍니다.
자세한 설명은 공식문서에 잘 작성되어 있습니다 공식문서 보러가기
만들게 된 배경
현재 프로젝트에서 NextJS를 사용하고 있고 app router를 사용하고 있기 때문에 NextJS에서 제공하는 error.ts를 통해서 에러를 핸들링할 수 있는 컴포넌트를 생성할 수 있지만. 해당 파일을 적용하고 싶은 폴더안에 만들어야하고 부분적 적용하려면 app의 폴더에 수많은 파일들이 생겨야합니다.
부분적으로 적용할 수 있어야 했고, 기존 error-boundary에서는 이벤트 핸들링 함수라던지 비동기 코드에서는 동작하지 않기 때문에 에러의 상황을 발생시키고 리셋시키는 기능을 가진 error-boundary가 필요하여 만들게 되었습니다.
라이브러리를 사용하고 싶은 분이미 해당 기능들을 사용하고 싶어하는 분들이 만든 라이브러리가 존재합니다. 참고 부탁드립니다. react-error-boundary
왜 직접 개발하나요?어려운 작업은 아니라고 생각이 되어서 직접 개발을 해보고 추가적인 기능들을 넣기 위해서 직접 개발을 진행 하였습니다!
Error를 공유하기 위한 Context
Error에 대한 상태의 공유와 reset할 수 있는 API를 공유하기 위한 Context를 생성합니다.
공유 될 값들은 다음과 같습니다.
- 에러가 현재 캐치가 되었는지
- 에러가 무엇인지
- reset할 수 있는 메서드
1
import { createContext } from 'react';2
3
export type ErrorControlProviderProps = {4
didCatch: boolean;5
error: any;6
resetErrorBoundary: (...args: any[]) => void;7
};8
9
export const ErrorControlContext = createContext<ErrorControlProviderProps | null>(null);
ErrorBoundary
Error Boundary는 클래스형으로만 생성이 가능하기에 클래스형 문법을 이용해서 제작해주겠습니다.
1
'use client';2
3
import { Component, ErrorInfo, ReactNode, createElement } from 'react';4
import { ErrorControlContext } from '@/lib/errorBoundary/ErrorControlContext';5
6
type ErrorBoundaryState = { didCatch: true; error: any } | { didCatch: false; error: null };7
8
type ErrorBoundaryProps = {9
fallback: ReactNode;10
children: React.ReactNode;11
};12
13
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {14
constructor(props: ErrorBoundaryProps) {15
super(props);16
this.resetErrorBoundary = this.resetErrorBoundary.bind(this);17
this.state = { didCatch: false, error: null };18
}19
20
resetErrorBoundary() {21
const { error } = this.state;22
if (error !== null) {23
this.setState({ didCatch: false, error: null });24
}25
}26
27
static getDerivedStateFromError(error: Error) {28
return { didCatch: true, error };29
}30
31
componentDidCatch(error: Error, errorInfo: ErrorInfo) {32
console.log(error, errorInfo);33
}34
35
render() {36
const { fallback, children } = this.props;37
const { didCatch, error } = this.state;38
39
let childToRender = children;40
41
if (didCatch) {42
if (fallback) {43
childToRender = fallback;44
}45
}46
47
return createElement(48
ErrorControlContext.Provider,49
{ value: { didCatch, error, resetErrorBoundary: this.resetErrorBoundary } },50
childToRender,51
);52
}53
}
생성단계
우선 기본적으로 받을 인자값은 에러 발생시에 보여주어야할 UI를 받기위한 fallback를 받고 렌더링할 요소들을 받을 children이 보입니다.
constructor를 보시면 error의 상태값인 this.state에는 기본값을 지정해주고있고
reset을 할 수 있는 this.resetErrorBoundary의 경우에는 함수에 this를 바인딩한것을 할당하고 있습니다.
왜 this를 바인딩 하나요?JS에서는 this가 동적이기 때문에 호출시에 this값이 변경되어 원했던 동작과는 다르게 동작할 수 있기 때문입니다.
error catch
getDerivedStateFromError
getDerivedStateFromError는 자식 컴포넌트에서 에러가 발생했을 때 호출이 되며 발생한 오류를 기반으로 컴포넌트의 상태값을 업데이트 할 수 있도록 도와줍니다.
오류가 발생하면 this.state의 값을 변경해주고 있습니다.
componentDidCatch
componentDidCatch도 자식 컴포넌트에서 에러가 발생했을 때 실행됩니다. 매개 변수는 발생한 Error와 에러의 추가적인 정보를 포함한 ErrorInfo를 받습니다.
비슷한데 왜 두개를 사용하나요?getDerivedStateFromError의 경우에는 렌더링 이전에 발생하므로 사이드 이펙트가 없이 동작해야하는코드만을 작성합니다
componentDidCatch는 이후 사이드 이펙트를 관리하기 위해 사용됩니다.
render
오류의 상태에 따라서 fallback과 정상적인 UI를 보여주기 위한 로직입니다.
먼저 렌더링 하기 위한 UI가 담긴 부분을 가져옵니다. prop으로 받아온 fallback과 children을 구조분해를 통해 가져옵니다.
에러의 상태에 따라 다르게 보여줘야하기 때문에 에러의 값도 가져와줍니다.
childrenToRender 변수를 생성해주고 default로 children을 할당해줍니다. 이후 error의 상태에 따라서 error상황이라면 fallback를 재할당해줍니다
실제로 반환되어 렌더링되는 return 부분을 보시면 createElement를 사용하고 있습니다.
해당 컴포넌트에서 관리되고 실행하는 상태값들과 함수들을 공유하기 위해서 아까전에 만들었던 Context를 통해서 공유하기 위한 작업입니다.
createElement에 생성할 요소, props, children를 넣어 생성해준 React요소를 반환합니다.
useErrorBoundary
이제는 ErrorBoundary를 hook으로 관리할 수 있도록 hook을 생성해보겠습니다.
hook을 통해서 가져가야할 기능은 다음 두 가지입니다
- 에러를 리셋한다
- 에러를 발생시킨다
1
import { useContext, useMemo, useState } from 'react';2
import { ErrorControlContext } from '@/lib/errorBoundary/ErrorControlContext';3
4
type UseErrorBoundaryState<T> = { error: T; hasError: true } | { error: null; hasError: false };5
6
export function useErrorBoundary<T = any>() {7
const context = useContext(ErrorControlContext);8
const [errorState, setErrorState] = useState<UseErrorBoundaryState<T>>({9
error: null,10
hasError: false,11
});12
13
const memorizedCommand = useMemo(14
() => ({15
resetBoundary: () => {16
context?.resetErrorBoundary();17
setErrorState({ error: null, hasError: false });18
},19
showBoundary: (error: T) => {20
setErrorState({ error, hasError: true });21
},22
}),23
[context],24
);25
26
if (errorState.hasError) {27
throw errorState.error;28
}29
30
return memorizedCommand;31
}
showErrorBoundary
에러 상태를 보여줄 수 있도록 하는 로직을 살펴봅시다
먼저 에러의 값을 관리할 수 있도록 useState를 통해서 상태값을 생성해줍니다.
간단하게 showBoundary함수는 error를 받아서, 받은 error를 useState값을 업데이트 해줍니다.
렌더링이 되기 때문에 밑에 있는 if 조건문을 통해 업데이트한 error를 throw하고 있습니다. throw를 하게되면 만들었던 errorBoundary에서 캐치하여 error의 상태가 되어 fallback를 보여주게됩니다.
resetBoundary
에러의 상태를 초기화하는 로직을 살펴봅시다
reset를 시키기 위해서 만들어 두었던 매서드를 이용해야 하기 때문에 useContext를 통해 context값을 가져옵니다.
가져온 context에 담긴 resetErrorBoundary를 실행하고 useState로 관리되고 있는 상태값을 초기값으로 업데이트 해줍니다.
resetErrorBoundary를 통해서 ErrorBoundary의 상태값은 초기화가 되고 useState로 상태값을 초기화하여 업데이트 되므로 에러의 상태를 초기화 할 수 있습니다.
사용
1
2
function Parent() {3
4
return(5
<ErrorBoundary fallback={<div>에러 가 발생했어요...</div>}>6
<Child>7
</ErrorBoundary>8
)9
};
1
2
function Child() {3
const {showBoundary,resetBoundary} = useErrorBoundary();4
5
const handleRequest = () => {6
try{7
...8
} catch(error){9
showBoundary(error)10
}11
}12
13
return (14
<div>child</div>15
)16
}