Suspense? ErrorBoundary?
사실 React18에서 완전히 새롭게 도입된 기능은 아닙니다. Suspense와 ErrorBoundary 모두 React16.6에서 처음 등장한 개념이에요. 하지만 그동안은 experimental(실험적) 기능이었고 React18에서 정식 기능으로 편입되었다고 보는 것이 맞습니다.
Suspense
기존에는 lazy loading
을 지원하기 위해서 주로 사용되어 왔습니다. 하지만 이제는 컴포넌트가 API 호출을 하는 동안 fallback으로 보여줄 내용을 지정해줄 수 있게 되었어요! 🎉
사용법은 아래와 같습니다.
import { Suspense } from 'react'
{/* ... */}
<Suspense fallback={<Loading />}>
<ChildComponent />
</Suspense>
ErrorBoundary
이 친구는 UI에서 발생한 에러로 인해서 전체 앱이 뻗는 현상을 방지하기 위해서 만들어졌습니다. 기존에는 API 호출이 실패하는 경우 페이지 전체가 하얗게 변해서 나오지 않는 현상이 있었죠. 하지만 이 친구를 사용하게 되면 하위 tree에서 발생한 에러를 전파 받아서(Suspense와 마찬가지로) fallback UI를 대신 보여줍니다. 하위 렌더 tree를 전부 감싸주지 않아도, 가장 근접한 node에 존재하는 ErrorBoundary까지 delegate됩니다.
React 공식 문서에서 제시해준 방법에 + fallback UI를 포함해서 만들어보았습니다.
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false }
}
// mount | update... render 직전 호출 side effect를 발생시키면 안되므로 다른 작업은 componentDidCatch로 처리
// static getDerivedStateFromProps와 동일하지만 error만 받음
static getDerivedStateFromError(error) {
console.log('error: ', error);
// 다음 렌더링에서 폴백 UI가 보이도록 상태를 업데이트
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.log('error: ', error);
console.log('errorInfo: ', errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
코드를 보시면 class component로 되어있는 걸 보실 수 있을 거예요. 사실 React16.8에서 hooks가 도입된 이후로 class component는 이제 잘 사용되지 않는데, ErrorBoundary는 class component입니다. 이게 불편해 보이는 분들도 있을 수 있겠지만, static getDerivedStateFromError
와 componentDidCatch
lifecycle 때문에 function component로는 구현할 수가 없습니다.
꼭 사용해야 하나요?
사실 이 두 가지 모두 이전 버전의 React에서 전혀 구현할 수 없다든지, 대체가 불가능한 기능인 것은 아닙니다. 우리는 그동안 isLoading
이나 isError
같은 state를 통해서 이와 같은 기능들을 구현해 왔어요.
/* src/Child.tsx */
const [isLoading, setIsLoading] = useState<boolean>(false)
const [isError, setIsError] = useState<boolean>(false)
if (isLoading) return <>Loading... ⏱</>
if (isError) return <>Something went wrong... ❌</>
return (...)
익숙한 코드죠?
하지만 이러한 방식은 명령적
방식으로, 선언적 라이브러리를 표방하고 있는 React와는 패러다임이 맞지 않습니다. 게다가 하나의 컴포넌트가 여러 경우의 return 값을 가지게 되면서 별로 순수하지도 않은 모습을 보여주고 있어요. 그래서 저 두 개의 기능을 모두 사용한다면 코드는 아래와 같이 변화합니다.
/* src/Parent.tsx */
<ErrorBoundary fallback={<Error />}>
<Suspense fallback={<Loading />}>
<Child />
</Suspense>
</ErrorBoundary>
/* src/Child.tsx */
const { data } = useSWR(endpoint, { suspense: true })
return (<>{data.name}</>)
어떤가요? Child component가 매우 순수하게 변했습니다! 만약 data가 fetching중이라면 Suspense에 의해 Loading 화면이 나타나게 될 테고, 그 도중에 Error가 발생한다면 ErrorBoundary에서 받아서 fallback UI를 띄워주겠죠. 굉장히 선언적이면서도 직관적입니다.
단점?
다만 이런 방식을 사용할 경우 suspense mode를 지원하는 라이브러리(e.g. react-query
, swr
등) 사용이 어느 정도는 강제될 것 같습니다. 물론 나만의 suspender를 구현하는 것도 가능하겠죠. 결국 promise를 이용해서 status에 따라 다른 상태를 throw해주기만 하면 될 테니까요. 하지만 여러 가지 예외 case들이 언제나 등장할 수 있고, 그 모든 것을 대응하는 방식을 취하기보다는 swr 등을 통해 단지 { suspense: true }
를 설정하는 편이 여러모로 좋아 보이네요! React 팀에서도 suspense를 지원하는 서드파티 라이브러리를 사용할 것을 권고하고 있습니다.