-
반응형
앞선 포스팅에서 Suspense와 ErrorBoundary를 이용 했을 때의 이점에 대해서 포스팅 했습니다. 이제는 이
두 컴포넌트를 하나로 합쳐서 더욱 사용하기 쉽게 만드는 방법
에 대한 포스팅입니다.현재는 두 가지 컴포넌트로 감싸야 한다.
function Main() { return ( <ErrorBoundary fallback={<div>에러 발생!</div>}> <Suspense fallback={<div>로딩 중...</div>}> <UserInfo /> </Suspense> </ErrorBoundary> ) }
UserInfo
컴포넌트안에서는 API 패칭이 일어나고 있고 데이터를 가져오는 동안에 랜더링 처리는Suspense
에 위임하고 있고 발생하는 오류(에러)처리는ErrorBoundary
로 위임 하는 모습입니다. 이 방식을 사용할려면 항상 두 가지의 컴포넌트로 감싸 주고 있는 모습을 볼 수 있습니다. 매번 데이터를 가져와서 랜더링 하는 컴포넌트를 사용 할 때마다 이 두 가지로 감싸줘야 한다면 그것도 매우 피곤한 일 입니다. 이는 두 가지 컴포넌트를 하나로 합쳐서 더욱 깔금하게 사용 할 수 있도록 개선되어야 합니다.합치기 전 좀 더 유용한 ErrorBoundary로.
기존 React 공식 홈페이지에 있는 ErrorBoundary는 오류가 난 뒤 다시 오류를
리셋
하는 기능을 제공하고 있지 않습니다(https://ko.reactjs.org/docs/error-boundaries.html). 아래는 공식 문서에서 제공하는ErrorBoundary
입니다.class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // 다음 렌더링에서 폴백 UI가 보이도록 상태를 업데이트 합니다. return { hasError: true }; } componentDidCatch(error, errorInfo) { // 에러 리포팅 서비스에 에러를 기록할 수도 있습니다. logErrorToMyService(error, errorInfo); } render() { if (this.state.hasError) { // 폴백 UI를 커스텀하여 렌더링할 수 있습니다. return <h1>Something went wrong.</h1>; } return this.props.children; } }
내부 구현체를 살펴보면
componentDidCatch
에서 에러 객체와 에러에 대한 정보를 받을 수 있지만 이 에러 상태를 다시 초기 상태로 초기화 하는 기능은 제공하고 있지 않습니다. 즉 한번 에러가 발생하면<h1>Something went wrong.</h1>
가 랜더링이 된 후 다시 처음으로 돌아갈 수 없습니다. 즉 데이터 패칭을 다시 시도 할 수 없습니다. 이는 사용자에게 다시 불러오기와 같은 버튼을 제공 할 수 없게 됩니다.그러므로 공식으로 제공하는 Errorboundary보다 좀 더 확장성있는 버전을 사용하게 됩니다. 그것이 바로
react-error-boundary
(https://www.npmjs.com/package/react-error-boundary)입니다. 이 라이브러리는공식 문서에서 제공된 Errorboundary에서 여러가지 기능들이 추가
되어 있습니다. 해당 라이브러리의 컴포넌트의 Props로 여러가지 옵션을 더 설정 할 수 있습니다.resetKeys
→ 특정 값이 변경 되었을 때 초기화 함(의존성 배열).onReset
→ 리셋이 일어난 후에 바로 실행되므로 후처리가 가능 함.onError
→ 에러가 난 후에 바로 실행되므로 후처리가 가능 함.FallbackProps
→ 리셋 콜백 함수를 넘길 수 있음.
위와 같이 추가적인 기능으로 에러처리를 더욱 유용하게 가능하게 되었습니다.
두 가지 컴포넌트를 합치자: AsyncBoundary
react-error-boundary의
ErrorBoundary
와 React의Suspense
를 합친 구현체인AsyncBoundary
의 모습입니다. 아래에서 어떤 방식으로 구현하였는지 하나하나 설명합니다.import React, { ReactNode, Suspense, SuspenseProps } from "react"; import { ErrorBoundary, ErrorBoundaryPropsWithRender, } from "react-error-boundary"; type ExceptFallbackErrorBoundaryAttributes = Omit< ErrorBoundaryPropsWithRender, "fallbackRender" | "fallback" | "FallbackComponent" >; type AsyncBoundaryProps = { children: ReactNode; ErrorFallback: ErrorBoundaryPropsWithRender["fallbackRender"]; SuspenseFallback: SuspenseProps["fallback"]; } & ExceptFallbackErrorBoundaryAttributes; function AsyncBoundary({ children, ErrorFallback, SuspenseFallback, ...restErrorBoundaryAttributes }: AsyncBoundaryProps) { return ( <ErrorBoundary fallbackRender={ErrorFallback} {...restErrorBoundaryAttributes} > <Suspense fallback={SuspenseFallback}>{children}</Suspense> </ErrorBoundary> ); } export default AsyncBoundary;
ErrorBoundary
와Suspense
로 받아야 할 Props를AsyncBoundary
의 Props로 모두 받게 해야합니다.그 다음AsyncBoundary
의 chidren는Suspense
의 chidren으로 넘겨 줌으로 써 에러와 팬딩 처리를 외부로 위임 할 수 있는 형태로 만들어야 합니다.AsyncBoundary의 Props 설정하기
일단 ErrorBoundary의 Props와 Susense의 Props를 합치기 위해서 ErrorBoundary의 Props를 추출해야 합니다.
type ExceptFallbackErrorBoundaryAttributes = Omit< ErrorBoundaryPropsWithRender, "fallbackRender" | "fallback" | "FallbackComponent" >;
ErrorBoundary
의 fallback에는 에러 발생 시 랜더링 할 컴포넌트가 들어가게 됩니다. 이 컴포넌트는 각 3가지 방식으로 넣어 줄 수 있습니다. 각각의 차이점은 공식문서를 참조하면 됩니다. 우선AsyncBoundary
는fallbackRender
를 사용하게 됩니다.ExceptFallbackErrorBoundaryAttributes
타입에선ErrorBoundary
가 에러 발생 시 랜더링 할 컴포넌트를 넣는 Props( fallback 관련)를 제외하고 나머진 옵션들만 넣기 위해서 Typescript의 유틸리티 타입인Omit
을 이용하여 fallback 관련 타입들은제거
합니다.type AsyncBoundaryProps = { children: ReactNode; ErrorFallback: ErrorBoundaryPropsWithRender["fallbackRender"]; SuspenseFallback: SuspenseProps["fallback"]; } & ExceptFallbackErrorBoundaryAttributes;
이제 최종적으로 AsyncBoundaryProps를 정의해야합니다.
chidren
에는 감싸줄 컴포넌트를 받기 위해서 ReactNode로 정의해줍니다.ErrorFallback
는 에러 발생 시 랜더링 할 컴포넌트를 넣어주기 위해 ErrorBoundaryPropsWithRender 타입에서 fallbackRender 옵션만 가져옵니다.SuspenseFallback
도 마찬가지로 SuspenseProps에서 fallback옵션만 가져옵니다. 그 다음 Errorboundary의 나머지 옵션들을 받기 위해서 위에서 만든ExceptFallbackErrorBoundaryAttributes
를 & 연산자로 확장합니다.function AsyncBoundary({ children, ErrorFallback, SuspenseFallback, ...restErrorBoundaryAttributes }: AsyncBoundaryProps) { return ( <ErrorBoundary fallbackRender={ErrorFallback} {...restErrorBoundaryAttributes} > <Suspense fallback={SuspenseFallback}>{children}</Suspense> </ErrorBoundary> ); } export default AsyncBoundary;
자. 이제
AsyncBoundaryProps
는 Errorboundary의 Props와 Suspense의 Props를 모두 받을수 있게 되었습니다.이제 이제 각각 Props로 받은 값들을 제자리로 넣어줍니다. Errorboundary의 나머지 옵션들은 스프레드 연산자를 이용하여ErrorBoundary
의 Props로 넣어줍니다.AsyncBoundary 사용하기
앞선 포스팅에서 언급 하였듯이 React Suspense 컴포넌트에서 데이터를 가져오는 시점을 컨트롤 할려면 감싸준 컴포넌트가 React-query를 이용하여 suspense 모드이거나 Recoil의 비동기 셀렉터를 사용해야합니다. 이 예제에서는
비동기 셀렉터
를 이용합니다.function RejectTest({ ms }: RejectTestProps) { const result = useTestReject(ms); return <div>{result.success}</div>; } export default RejectTest;
RejectTest
컴포넌트를 이용하여 테스트 하고자 합니다.useTestReject
hook을 이용하여 데이터를 가져오고 성공 유무를 랜더링하는 컴포넌트입니다.비동기 셀렉터의 모습 (useTestReject)
class TestingService { static reject(ms: number) { return new Promise<TestEntity>((_, reject) => { setTimeout( () => reject({ success: false, status: 400, }), ms ); }); } } export default TestingService; const testRejectSelector = selectorFamily<TestEntity, number>({ key: "testRejectState", get: (ms) => async ({ get }) => { const response = await TestingService.reject(ms); return response; }, }); export function useTestReject(ms: number) { return useRecoilValue(testRejectSelector(ms)); }
Recoil의 비동기 셀렉터를 이용하는 모습입니다.
TestingService
는 파라메터로 전달받은 millisec 만큼 기다리다가 reject를 시킵니다. 이 요청을testRejectSelector
에서 비동기 셀렉터로 연결합니다. 마지막으로 만든 셀렉터를useTestReject
라는 hook으로 따로 빼어낸 모습입니다.실패하는 예시를 보며 최종적으로 사용해보기
const [millisecond, setMillisecond] = useState(3000); ... <AsyncBoundary ErrorFallback={(rest) => <ErrorTestNotice {...rest} />} SuspenseFallback={<div>...loading</div>} onReset={(ms) => setMillisecond(ms as number)} > <RejectTest ms={millisecond} /> </AsyncBoundary>;
AsyncBoundary
의 각각의 Props로 값들을 넣어준다음RejectTest
를 감싸줍니다. 이렇게 된다면 당연히 3초 후에ErrorTestNotice
컴포넌트가 랜더링 될 것 입니다.import { FallbackProps } from "react-error-boundary"; type ErrorTestNoticeProps = {} & FallbackProps; function ErrorTestNotice({ ...errorProps }: ErrorTestNoticeProps) { return ( <div role="alert"> <p>Something went wrong:</p> <pre>{errorProps.error.message}</pre> <button onClick={() => errorProps.resetErrorBoundary(Math.random() * 3000)} > Try again </button> </div> ); } export default ErrorTestNotice;
ErrorTestNotice
는 props로FallbackProps
를 받게 됩니다. 이 Props는에러 객체와 에러를 리셋 할 수 있는 콜백 함수
를 받게 됩니다. 최종적으로 버튼을 클릭 할 때마다 millisec가 랜덤으로 배정되며onReset
에 정의 된 대로 밖에 있는 종속 변수인 millisecond가 변경되면서 계속해서 에러 상황을 리셋 할 수 있습니다.오류 상황이 아닐 떈?
오류 상황이 아닐 때는 데이터를 가져오는 동안에는
<div>...loading</div>
가 랜더링 되가 최종적으로는AsyncBoundary
으로 감싼 컴포넌트가 잘 가져온 데이터로 아름답게 랜더링하게 될 것 입니다.Errorboundary와 Suspense를 같이 사용하는 법에 대해 포스팅 했습니다. 다음 포스팅은 에러 핸들링에 대해서 더 자세하게 포스트 하겠습니다.
3탄:
Refs.
https://ko.reactjs.org/docs/error-boundaries.html
https://www.npmjs.com/package/react-error-boundary
https://jbee.io/react/error-declarative-handling-1/
https://react-query.tanstack.com/guides/suspense#_top
https://recoiljs.org/ko/docs/guides/asynchronous-data-queries/
반응형'코딩' 카테고리의 다른 글
React에서 axios로 받은 데이터 클래스로 관리하기 with recoil async selector (0) 2021.10.27 원격 저장소에 잘못 올라간 파일 삭제 (0) 2021.10.23 React Suspense와 ErrorBoundary를 이용한 로딩처리와 에러 핸들링 (0) 2021.10.21 CRA에서 eslint 특정 옵션만 수정하기 (0) 2021.10.16 리액트 절대 경로 사용하기 (0) 2021.10.16