ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React Query] (6) Suspense와 Error Boundary 적용
    Front-End(Web)/React - 라이브러리들 2022. 11. 3. 04:46
    반응형

    🧐 서론

    지난, React 18의 Concurrency(동시성)을 시리즈로 포스팅하면서 비동기의 선언적 로딩인 Suspense와 에러처리인 Error Boundary 에 대해 알아보았다.

    * Suspense(위, 링크), Error Boundary(아래, 링크)

     

    [React] Concurrency(동시성) - #1. Suspense와 React.lazy

    🧐 서론 전 회사분들과 진행하는 스터디에서 나는 Suspense와 React.lazy를 통한 코드 스플리팅 부분을 맡게 되었다. Suspense에 대해서만 공부하려고 했다가, Error Boundary나 Transition API 등 관련된 기능

    abangpa1ace.tistory.com

     

    [React] Concurrecy(동시성) - #2. Error Boundary

    지난 포스팅에선, Code Splitting(코드 분할) 그리고 이를 React에서 구현하는 Suspense와 React.lazy를 알아보았다. Suspense는 비동기 컴포넌트(React.lazy) 혹은 비동기 데이터 요청에 따른 선언적 Loading UI 노

    abangpa1ace.tistory.com

     

    이러한 로딩과 에러처리가 가장 많이 요구되는 곳 중에 하나는 단연 API 호출 쪽일 것이다.

    그리고, 전에 정리했듯이 캐싱 등 다양한 이점이 있어 React Query가 범용적으로 사용되고 있는 추세이다.

     

    React Query를 통해 API를 호출할 때, Suspense 및 Error Boundary를 어떻게 연계해서 구현할 수 있는지 이번 예제를 학습하면서 한 번 정리할 필요성을 느껴 포스팅을 시작한다.

     


     

    🌺 React Query와 Suspense

     

    - 적용

    React Query 패칭에 대해 Suspense를 적용할 지 여부를 설정하는 방법은 1) 쿼리 설정, 2) 전역 설정 2가지 방법이 있다.

     

    1) 쿼리 설정

    import { useQuery } from '@tanstack/react-query'
    
    function Example() {
      const { data } = useQuery(['example'], fetchExample, {
        suspense: true,
      })
      
      // ...
    }

    Suspense를 적용할 Child Component의 쿼리에 suspense 옵션을 추가하면 된다.

     

    2) 전역 설정

    import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
    
    const queryClient = new QueryClient({
      defaultOptions: {
        queries: {
          suspense: true,
        },
      },
    })
    
    function Root() {
      return (
        <QueryClientProvider client={queryClient}>
          <App />
        </QueryClientProvider>
      )
    }

    React Query 적용을 위해 보통 최상단(<App>)을 <QueryClientProvider> 프로바이더로 랩핑한다.

    여기서 client 설정을 적용할 때, QueryClient 인스턴스의 defaultOptions 설정에 쿼리 suspense를 추가해주면 된다.

     

     

    - 적용예제

    // Child Component
    
    import { useState } from 'react';
    import { useQuery } from '@tanstack/react-query';
    import { fetchTodo } from '@/api/fetchSimple';
    
    export default function RQTodo(): JSX.Element {
      const [id, setId] = useState<number>(1);
      const { data } = useQuery(['simple', id], () => fetchTodo(id), {
        suspense: true,
      });
    
      const refetchById = () => {
        setId(prev => prev + 1);
      };
    
      return (
        <div>
          <p>ID: {data?.id}</p>
          <p>title: {data?.title}</p>
          <p>completed: {data?.completed ? 'done' : 'yet'}</p>
          <button onClick={refetchById}>refetch</button>
        </div>
      );
    }
    // Parent Component
    
    // ...
        <Suspense fallback={<CommonFallback color="bg-lime-400" />}>
          <RQChild />
        </Suspense>

    첫 번째 방법인 쿼리 설정을 적용한 예제이다. Child Component에 suspense 옵션을 추가하고, Parent에서 Suspense로 랩핑만 해주면 된다!

     

    Suspense를 적용하면, 기존에 status, isLoading 등으로 명령적으로 노출한 로딩 UI 방식이 불필요해질 것이다.

     

     

    * 적용하면서 경험한 고난

    (왼쪽) Query Id(state) 변경은 정상, (오른쪽) refetch나 QueryClient 메서드는 비정상

     

    쿼리로 불러온 데이터를 재호출(refetch)하는 경우가 있을 것이다.

    이를 처음에 쿼리에서 반환하는 refetch 함수로 구현하고자 했다. 하지만 Fallback UI가 노출되지 않았다.

    비슷하게, QueryClient 인스턴스의 refetchQueries, invalidateQueries 메서드들로 구현해도 같았다.

    import { useQuery, useQueryClient } from "@tanstack/react-query";
    import { fetchBeer } from "@/api/fetchSimple";
    
    export default function RQChild(): JSX.Element {
      const { data, refetch } = useQuery(["beer"], fetchBeer, {
        suspense: true,
      });
    
      const queryClient = useQueryClient();
      const refetchQueryByClient = async () => await queryClient.refetchQueries({ queryKey: ["beer"], type: "active" });
      const invalidQueryByClient = async () => await queryClient.invalidateQueries(["beer"]);
    
      return (
        <div>
          <p>ID: {data?.id}</p>
          <p>name: {data?.name}</p>
          <p>yeast: {data?.yeast}</p>
          
          <button onClick={() => refetch()}>refetch(q)</button>
          <button onClick={refetchQueryByClient}>refetch(qc)</button>
          <button onClick={invalidQueryByClient}>invalid(qc)</button>
        </div>
      );
    }

    * React Query의 컨셉과 refetch에 대해 잘 정리된 글이 있어 참고하는걸 권장한다. (링크)

     

     

    refetch 등 메서드들에는 아직 Suspense가 정상적으로 적용되지 않기에,

    아래와 같이 Query Key에 state를 넣어서 이 값의 갱신에 따라 쿼리를 refresh 하는 방법을 권장한다.

    // ...
      const [id, setId] = useState<number>(1);
      const { data } = useQuery(['simple', id], () => fetchTodo(id), {
        suspense: true,
      });
    
      const refetchById = () => {
        setId(prev => prev + 1);
      };
    // ...

     


     

    🌺 React Query와 Error Boundary

    먼저, 기본적인 문법에 대해 알아보자.

    // Child Component
    
    import { useQuery } from '@tanstack/react-query';
    import { fetchError } from '@/api/fetchSimple';
    
    export default function RQError(): JSX.Element {
      const { data, isError, error } = useQuery(['query-error'], () => fetchError(true, 'Query Error!'), {
        useErrorBoundary: true,
      });
    
      return <div>RQError</div>;
    }
    // Parent Component
    
    // ...
      <ErrorBoundary FallbackComponent={ErrorFallback}>
        <RQError />
      </ErrorBoundary>
    // Fallback Component
    
    import type { FallbackProps } from 'react-error-boundary';
    
    export default function ErrorFallback({ error, resetErrorBoundary }: FallbackProps): JSX.Element {
      return (
        <div>
          <p>Error: {error || 'Undefined Error'}</p>
          <button onClick={resetErrorBoundary}>reset</button>
        </div>
      );
    }

    먼저, suspense mode와 유사하게 쿼리 옵션으로 설정하는 경우이다.

    useErrorBoundary 옵션을 true로 설정하면, 가장 가까운 Error Boundary로 에러처리를 위임한다. (전역설정도 Suspense와 동일)

     

     

    - Query 재시도 설정

    Error가 발생한 뒤 Error Boundary의 Fallback 컴포넌트가 resetErrorBoundary Props를 받아서 처리하는 경우 쿼리에 대한 재시도 설정을 적용할 수 있다.

     

    1) QueryErrorResetBoundary 컴포넌트

    // Parent Component
    
    // ...
      const { reset } = useQueryErrorResetBoundary();
      
      <QueryErrorResetBoundary>
        {({ reset }) => (
          <ErrorBoundary onReset={reset} FallbackComponent={ErrorFallback}>
            <RQError />
          </ErrorBoundary>
        )}
      </QueryErrorResetBoundary>

     

    <ErrorBoundary>를 다시 <QueryErrorResetBoundary> 컴포넌트로 감싸준다.

    이 컴포넌트는 내부적으로 콜백함수를 반환하며, 여기의 reset 인자를 <ErrorBoundary>의 onReset 에 부여해주면 된다.

     

    이렇게 설정해주면, <QueryErrorResetBoundary> 가 감싸고 있는 모든 <ErrorBoundary>의 쿼리 오류를 재설정한다.

     

     

    2) useQueryErrorResetBoundary 훅

    // Parent Component
    
    // ...
      const { reset } = useQueryErrorResetBoundary();
      
      <ErrorBoundary onReset={reset} FallbackComponent={ErrorFallback}>
        <RQError />
      </ErrorBoundary>

    useQueryErrorResetBoundary() Hooks를 활용해서 간단하게 설정도 가능하다. 훅이 반환하는 reset 함수를 <ErrorBoundary>의 onReset에 설정하면 된다.

     

    이는 가장 근접한 <QueryErrorResetBoundary> 컴포넌트 하위의 모든 쿼리 오류를 재설정한다. (정의된 바운더리가 없다면 전역으로 재설정)

     


     

    📎 출처

    - [React Query + Suspense] TanStack 공식문서 : https://tanstack.com/query/v4/docs/guides/suspense  

    - [React Query + Suspense] 카카오엔터 기술 블로그(노벨님) : https://fe-developers.kakaoent.com/2021/211127-211209-suspense/

     

    - [React Query + Error Boundary] suyeon9456 님의 블로그 : https://velog.io/@suyeon9456/React-Query-Error-Boundary-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0

    - [useQueryErrorResetBoundary Hooks] TanStack 공식문서 : https://tanstack.com/query/v4/docs/reference/useQueryErrorResetBoundary 

    - [QueryERrorResetBoundary Component] TanStack 공식문서 : https://tanstack.com/query/v4/docs/reference/QueryErrorResetBoundary

     

    - [React Query와 Concurrent UI] 카카오페이 기술 블로그(에릭님) : https://tech.kakaopay.com/post/react-query-2/

    반응형
Designed by Tistory.