본문 바로가기

FrontEnd

Refactoring React(리팩토링 리액트) : Query(서버 데이터 조회)

반응형

리액트 프로젝트 서버 데이터 조회 구현의 모범 사례를 탐구해 볼까요?

react


간단한 서버 조회 구현 with useEffect

가장 흔히 볼 수 있는 구현은 다음과 같습니다.

useEffect를 사용하여 렌더링 시마다 효과를 유발합니다.

import axios from "axios";

export function IssueList() {
  useEffect(() => {
    axios
      .get("https://prolog-api.profy.dev/issue")
      .then((response) => console.log(response));
  // empty dependency array means this runs only once (at least in production)
  }, []);

    ...
}

해당 방법의 문제점

다음 요구사항이 발생할 때마다 복잡도가 추가됩니다.

  • 에러 여부 : (x2)
  • 로딩 여부 : (x2)
  • 페이지네이션 :(xN)
export function IssueList() {
  const [page, setPage] = useState(1);
  const [data, setData] = useState();
  const [error, setError] = useState();
  const [isLoading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    const abortController = new AbortController();
    axios
      .get(`https://prolog-api.profy.dev/issue?page=${page}`, {
        signal: abortController.signal,
      })
      .then((response) => setItems(response.data.items))
      .catch((error) => {
        // this is easily missed
        if (error.message !== "canceled") {
          setError(error);
        }
      })
      .finally(() => setLoading(false));

    return () => abortController.abort();
  }, [page]);

  if (isLoading) {
    return <div>Loading...</div>;
  }
  if (error) {
    return <div>An error occured: {error.message}</div>;
  }
  
  // ...
  
  }

필요에 따라 다음과 같은 요구사항이 추가될 수 있습니다.

  • 에러  시 재시도
  • 프리페치
  • 캐시
  • SWR

의 구조로 해당 요구사항을 처리하는 것은 악몽일 것입니다.


해결 방안 : 비동기 상태관리 라이브러리 사용

직접 데이터 가져오기 코드를 작성하지 않는 것이 좋습니다.
Redux를 사용하고 있다면 아마도 RTK 쿼리를 사용하세요
React만 사용한다면 React Query를 사용하세요.

- Mark Erikson , 리덕스 메인테이너

Apollo, RTK Query, React Query와 같은 비동기 상태관리 라이브러리가 존재합니다.

해당 게시글에서는 React Query를 알아봅니다.

해당 라이브러리는 비동기 데이터와 관련한 수많은 기능들을 제공합니다.

a lot of useful feature

모범 사례 1. 커스텀 훅 사용하기

데이터 가져오기 로직은 흔히 컴포넌트 간에 쉽게 공유됩니다.

각 끝점에 해당하는 데이터 가져오기 함수를 추출합니다.

  • axios, fetch와 같은 비동기 데이터를 가져오는데 사용하는 라이브러리
  • 데이터를 가져와서 비동기 데이터를 만드는 로직을 추상화해 줍니다.
export async function getIssues() {
  const { data } = await axios.get("https://prolog-api.profy.dev/issue");
  return data;
}

각 끝점에 해당하는 query를 각각 별도의 훅으로 노출합니다.

export function useIssues() {
  return useQuery(["issues"], () => getIssues());
}

별도로 컴포넌트 별로 데이터를 커스터마이징 하는게 필요하면, useIssues를 호출해 작업하면 됩니다.

모범 사례 2. 타입스크립트

타입 안정성과 개발자 경험 향상을 위해 타입스크립트를 적용합니다.

import { useQuery, useQueryClient } from "@tanstack/react-query";
import axios from "axios";
import type { IssuePage } from "./types";

async function getIssues(page: number) {
  const { data } = await axios.get(
    `https://prolog-api.profy.dev/issue?page=${page}`
  );
  return data;
}

export function useIssues(page: number) {
  const query = useQuery<IssuePage, Error>(["issues", page], () =>
    getIssues(page)
  );
  return query;
}

모범 사례 3. 페이지네이션 - 로딩바 대신 이전 데이터

페이지네이션 시 로딩 화면 대신 이전 데이터를 보여주는게 좋습니다.

react-query를 사용할 경우 keepPreviousData 설정을 사용합니다.

 

export function useIssues(page) {
  return useQuery(["issues", page], () => getIssues(page), {
    keepPreviousData: true,
  });
}

모범 사례 4. 페이지네이션 - 프리페치

3번을 조금 더 개선합니다.

페이지가 변경될 때마다, 다음 페이지 정보를 미리 가져오도록 하는 것이 더 나을 수 있습니다.

이 경우 prefetchQuery를 사용하며, react-query의 경우 다음과 같이 구현합니다.

import axios from "axios";
import { useQuery, useQueryClient } from "@tanstack/react-query";

export function useIssues(page) {
  const query = useQuery(["issues", page], () => getIssues(page), {
    keepPreviousData: true,
  });

  // Prefetch the next page!
  const queryClient = useQueryClient();
  useEffect(() => {
    if (query.data?.meta.hasNextPage) {
      queryClient.prefetchQuery(["issues", page + 1], () =>
        getIssues(page + 1)
      );
    }
  }, [query.data, page, queryClient]);
  return query;
}

모범 사례 5. 요청 수 줄이기

4번의 경우 동작은 다음과 같습니다.

  • 1페이지에 접근하면 1, 2번 페이지를 위한 데이터를 가져옵니다.
  • 2페이지에 접근하면 2, 3번 페이지를 위한 데이터를 가져옵니다.

왜일까요?
리액트 쿼리는 일단 데이터를 가져오면 stale한 데이터로 취급합니다.

따라서 해당 키에 해당하는 훅이 다시 호출될 때마다, 새로운 데이터로 갱신합니다.

이를 막기 위해선 staleTime을 설정합니다.

리액트 쿼리는 이 시간동안 해당 데이터가 싱싱하니, 서버와 동기화가 필요 없다는 것을 알 수 있습니다.
따라서 같은 훅을 호출하면, 서버 요청 없이 이전 데이터를 사용합니다.

export function useIssues(page) {
  const query = useQuery(["issues", page], () => getIssues(page), {
    keepPreviousData: true,
    // data is considered stale after one minute
    staleTime: 60000,
  });

  ...
}

참고

https://profy.dev/article/react-rest-api

 

REST APIs - How To Connect Your React App Like The Pros (+ Detailed Example)

Data fetching = useEffect? In production apps that quickly gets out of hand. See why and how the pros use libraries for data-driven features with great UX.

profy.dev

https://github.com/alan2207/bulletproof-react

 

GitHub - alan2207/bulletproof-react: 🛡️ ⚛️ A simple, scalable, and powerful architecture for building production ready

🛡️ ⚛️ A simple, scalable, and powerful architecture for building production ready React applications. - GitHub - alan2207/bulletproof-react: 🛡️ ⚛️ A simple, scalable, and powerful architecture for...

github.com

 

반응형