웹페이지에서는 데이터 요청 상태에 따라 UI는 다른 상태를 보여주어야 한다.
데이터가 패칭 중 일 때 로딩 UI, 에러가 났을 때 에러 UI 등이 있다.
위 상황을 처리하기 위해 다양한 방법을 사용할 수 있다.
프로젝트에 적용하면서 유지보수성 및 가독성이 좋은 방법이 뭐가 있을까 고민하던 중
Suspense와 ErrorBoundary를 활용한 선언적 데이터 패칭 처리 방법을 채택하게 되었고, 블로그로 정리하고자 한다.
전통적인 데이터 패칭 처리
@tanstack/react-query
에서는 useQuery 값 내 isLoading
, isError
상태값을 제공해 준다.
아래는 useQuery를 사용해 standard 한 방법으로 비동기 요청으로 데이터를 받아와 사용자에게 전달하는 OrderList 페이지다.
import {useQuery} from '@tanstack/react-query'
function OrderListPage() {
const {data, isLoading, isError} = useQuery({
queryKey: ['getUserOrders'],
queryFn: getOrderList
})
if (isLoading) return <OrderListContentSkeleton />
if (isError) return <ErrorFallback />
return (
<Layout title="주문목록">
{data?.length > 0 ? (
<VStack>
{data.map((order: any) => (
<OrderCard key={order.order_id} order={order} />
))}
</VStack>
) : (
<VStack align="center" spacing={20}>
<Icon source={ShoppingIcon} />
<Text color="---" align="center">
최근 주문한 내역이 없습니다.
</Text>
</VStack>
)}
</Layout>
)
}
export default OrderListPage
queryFn이 반환하는 데이터가 들어오기 전까지 isLoading, isError 상태 값에 따라 반환되는 컴포넌트를 분기 처리하여.
로딩중일 때는 <OrderListContentSkeleton/>
을, 에러 상태일 때는 <ErrorFallback/>
를 반환한다.
선언적 데이터 패칭
선언적 데이터 패칭은 React의 주요 개념인 선언형 프로그래밍에 기반한다.
선언형 (from.https://ko.legacy.reactjs.org)
React는 상호작용이 많은 UI를 만들 때 생기는 어려움을 줄여줍니다. 애플리케이션의 각 상태에 대한 간단한 뷰만 설계하세요. 그럼 React는 데이터가 변경됨에 따라 적절한 컴포넌트만 효율적으로 갱신하고 렌더링 합니다. 선언형 뷰는 코드를 예측 가능하고 디버그 하기 쉽게 만들어 줍니다.
Suspense
React v18부터 Suspense를 사용하면 자식이 로딩을 완료할 때까지 fallback을 표시할 수 있게 되었다.
ErrorBoundary
ErrorBoundary는 하위 컴포넌트 트리에서 발생하는 에러를 catch 하여 대체 UI를 보여주거나 에러리포팅의 작업을 수행한다.
우선 선언형에 맞춰 컴포넌트의 역할 분리를 해보자
import {Suspense} from 'react'
import {ErrorBoundary} from 'react-error-boundary'
function OrderListPage() {
return (
<Layout title="주문목록">
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<OrderListContentSkeleton />}>
<OrderListContent />
</Suspense>
</ErrorBoundary>
</Layout>
)
}
export default OrderListPage
OrderListPage
에서 콘텐츠 로직을 분리해 주었다.
따라서,OrderListPage
에서는 Layout 및 선언적 데이터 페칭의 역할만 수행하게 된다.
이를 통해 OrderListContent
에서는 로딩 중, 에러 상태는 신경 쓰지 않으며 정상적으로 패칭 된 OrderContent를 보여주는 컴포넌트의 역할을 수행하게 된다.
import {SearchIcon, ShoppingIcon} from '@channel.io/bezier-icons'
import {Icon, Text, TextField, VStack} from '@channel.io/bezier-react'
import {useSuspenseQuery} from '@tanstack/react-query'
function OrderListContent() {
const {data} = useSuspenseQuery({
queryKey: ['getUserOrders'],
queryFn: getOrderList
})
return (
<>
{data?.length > 0 ? (
<VStack>
{data.map((order: any) => (
<OrderCard key={order.order_id} order={order} />
))}
</VStack>
) : (
<VStack align="center" spacing={20}>
<Icon source={ShoppingIcon} />
<Text color="---" align="center">
최근 30일 이내 주문한 내역이 없습니다.
</Text>
</VStack>
)}
</>
)
}
export default OrderListContent
기존 useQuery에서 Suspense를 감싸 사용하기 위해서는 useSuspenseQuery로 교체해주어야 한다.
useSuspenseQuery는 @tanstack/react-query v5 이상에서 사용할 수 있다.
주의사항
Suspense가 감싸고 있는 하나의 컴포넌트에서 2개 이상의 API 요청이 발생하게 되면 네트워크 병목이 발생한다.
(내 경우 1번의 API 요청을 하고 있어 해당 이슈가 발생하지 않았지만, 함께 정리해 보자)
Errorboundary가 error를 catch 하는 것처럼, Suspense는 Promise를 catch 한다.
따라서 API가 Promise를 던지면 Suspense가 이를 catch 하여
1. pending 상태일 경우 Loading Fallback을 return
2. settled(fulfilled, rejected) 면 children을 return
하는 형태이다.
이 과정에서 만약 Suspense 내부에서 여러 개의 API 요청이 발생하면 네트워크 병목(network waterfall)이 발생할 수 있다.
Network Waterfall 현상
네트워크 워터폴(Network Waterfall)은 API 요청이 순차적으로 발생하는 문제를 말한다.
- 예시) API 1 요청 후 응답을 받고 -> API 2 요청 후 응답을 받고 ->... 이런 식으로 연속적으로 요청이 이어진다.
Suspense는 하나의 API 요청이 해결되기 전까지 다음 작업을 진행하지 않기 때문에, 하나의 요청이 끝난 후에 다음 요청이 발생하는 순차적인 작업 흐름이 만들어진다.
따라서 Suspense 내부에서 여러 개의 API 요청이 있을 경우, 각 요청이 병렬적으로 실행되지 않고 순차적으로 실행되면서 네트워크 병목이 발생한다.
해결 방법
이 문제를 해결하기 위해서는 Tanstack에서 제공하는 useSuspenseQueries를 사용하면 된다.
useSuspenseQueries는 여러 API 요청을 병렬적으로 처리할 수 있도록 도와주며,
이를 통해 네트워크 병목을 방지하여 성능을 최적화할 수 있다.
'Front-end > React' 카테고리의 다른 글
[React] React.lazy와 Suspence (0) | 2024.03.21 |
---|---|
[React] reactDevTools로 리렌더링 파악하여 최소화하기 #React.memo #useMemo #useCallback (0) | 2024.02.14 |
[Next13] next/image 속성으로 이미지 최적화 적용하기 (0) | 2023.10.03 |
[Next] dayjs UTC시간 -> 현지시간 적용 (0) | 2023.07.21 |
[React] 타이머 구현하기 (0) | 2023.05.07 |