리액트로 작업하면서 Error Boundary를 아주 간단하게 에러를 처리할때 쓰는 기능으로만 알고 사용해본 적이 없었다. 리액트 네이티브로 프로젝트를 하면서 데이터를 fetching하는 과정에서 에러가 발생할 때 사용자에게 fallback으로 설정한 컴포넌트를 보여주면서 refetching을 할 수 있는 버튼을 만들면 좋겠다라는 생각에서 시작하여 Error Boundary를 도입하기로 했다.
공식문서에 따르면, 하위 컴포넌트 트리의 어디에서든 자바스크립트 에러를 기록하며 깨진 컴포넌트 트리 대신 폴백 UI를 보여주는 React 컴포넌트이다.
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;
}
}
공식문서에서 가져온 에러 바운더리 컴포넌트이다. 함수형이 아닌 클래스형 컴포넌트만을 지원하는 것 같다. (현재 버전 18 이상) 이 클래스를 export해서 사용하면 된다.
import ErrorBoundary from '@components/ErrorBoundary'
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
❗️오직 하위에 존재하는 컴포넌트의 에러만을 포착한다. 즉 렌더링하는 과정 중 <MyWidget/>
에서 에러가 발생한다면 이 에러를 잡아내는 것이다.
render() {
if (this.state.hasError) {
// 폴백 UI를 커스텀하여 렌더링할 수 있습니다.
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
에러가 발생했을 때 보여주고 싶은 컴포넌트를 커스텀해서 보여주면 된다.
getDerivedStateFromError
나 componentDidCatch
등을 직접 구현해야하는 Error Boundary를 좀 더 쉽게 사용하기 위해서는 라이브러를 사용한다.
(필자는 리액트 네이티브에 적용했기 때문에 react-native-error-boundary
를 사용했다.)
설치하기
npm i react-native-error-boundary
cd ios
pod install
../
사용하기
import ErrorBoundary from "react-native-error-boundary";
import CustomFallback from "components/fallback/CustomErrorFallback";
(...)
return (
<Layout>
<ErrorBoundary FallbackComponent={CustomFallback}>
<Sample />
</ErrorBoundary>
</Layout>
);
<ErrorBoundary/>
는 아래와 같은 props를 전달한다.
Children
FallbackComponent
onError
<CustomFallback.tsx/>
import React from "react";
import { Text, Button } from "react-native";
import { styled } from "styled-components/native";
export type Props = { error: Error; resetError: () => void };
const CustomErrorFallback = ({ error, resetError }: Props) => {
return (
<Container>
<ErrorMsg>Something happened!</ErrorMsg>
<Text>{error.message}</Text>
<RetryCopy>Please try again.</RetryCopy>
<Button title="Refresh" onPress={resetError} />
</Container>
);
};
<FallbackComponent/>
의 props
error
: 발생한 에러resetError
: error state를 reset한다. 결과
error boundary를 사용하기 전에는 사용자에게 빈 화면만 보여줄 수밖에 없었지만 사용 후, 이렇게 fallback으로 에러 상황을 보여줄 수 있게 되었다.
Suspense는 컴포넌트가 렌더링되기 전에 다른 작업이 먼저 이루어지도록 해주는 컴포넌트이다. Suspense를 사용하면 로딩화면 구성, 컴포넌트 lazy loading등을 사용할 수 있다.
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
HugeComponent
는 lazy를 사용해서 lazy loading이 이루어지는 동안 Suspense 는 fallback을 보여준다.
여기서 lazy component는 Suspense 컴포넌트의 하위에서 렌더링되어야 한다.
(...)
<Layout>
<ErrorBoundary FallbackComponent={CustomFallback}>
<Suspense fallback={<Spinner />}>
<Sample />
</Suspense>
</ErrorBoundary>
</Layout>
위 코드처럼 Error Boundary 하위에 Suspense와 하위 컴포넌트를 배치하면 된다. Suspense는 <Sample/>
컴포넌트를 렌더링을 잠시 멈추고 fallback인 <Spinner/>
를 먼저 보여준다. 이때 <Sample/>
를 렌더링 하는 과정에서 에러가 발생하면 <ErrorBoundary/>
에서 에러를 캐치하여 fallback을 보여준다.
const useNotesListQuery = (user: TUser) => {
const filter = useRecoilValue(notesFilterState);
const {
isLoading,
error,
refetch,
data: notesState,
} = useQuery(["notes"], () => getNotes(user), {
select: (data) => data.filter((note) => note.folder === filter),
// Error Boundary 사용을 위한 옵션
// suspense 옵션이 true인 경우에는 기본값이 true로 설정된다.
useErrorBoundary: true,
// 데이터 불러오기를 위한 Suspense를 활성화하는 옵션
// useQuery Hook을 "Suspense를 지원하는 특별한 객체"로 사용
suspense: true,
});
return { isLoading, error, refetch, notesState };
};
커스텀 훅으로 만든 useQuery를 가져왔다. suspense
를 true로 설정해야 <Suspense/>
가 잘 작동한다.
(...)
return (
<Layout>
<ErrorBoundary FallbackComponent={CustomFallback}>
<Suspense fallback={<NoteSkeleton />}>
<NoteSection
user={currentUser}
handleScroll={handleScroll}
moveToNote={moveToNote}
/>
</Suspense>
</ErrorBoundary>
</Layout>
);
따로 만들어뒀던 커스텀 Skeleton 컴포넌트를 Suspense의 폴백으로 사용하여 로딩이 이루어지는 과정 중에 Skeleton UI를 노출시켰다. 이로써 사용자는 데이터를 로딩하고 있는 중이라는 것을 인지하게 된다.
(skeleton UI 만드는데도 꽤 애먹었다.. 리액트 네이티브에서는 css의 애니메이션인 keyframe을 사용하지 못해서 RN에서 제공하는 Animated를 사용해야해서 많이 헤맸다. 😭 이 부분은 까먹지 않기 위해 따로 작성할 예정이다.)
고맙습니다 정리가 너무 깔끔하네요