본문 바로가기
  • 하고 싶은 일을 하자
dev

Tanstack Query 는 필름 시뮬레이션이다 (회사 프로젝트에 TQ 도입기)

by 박빵떡 2024. 8. 2.
반응형

요즘 카메라에 미쳐버린 나, 맨날 카메라 관련 유튜브 영상과 쇼츠를 보고 있다.

후지 필름에 x100v라는 카메라에는 재밌는 이야기가 있다.

 

이번에 새로 나온 x100vi 출고될 때마다 매진이라 사고싶어도 살 수가 없다.

 

x100 시리즈는 2010년 출시를 시작한 카메라로 후지 필름의 색감으로 사진을 찍을 수 있다. 올해 2024년에는 x100vi로 여섯 번째 시리즈가 나왔다.

한국 다나와 기준으로 x100v는 정가가 169만 원인데 현재 274만 원에 판매되고 있다. 해외에서는 327만 원에 팔리기도 한다. 이렇게 비싸게 팔린 이유는 "필름 시뮬레이션"이라는 기능 때문이다.

 

틱톡에서 디지털카메라인데 필름 느낌이 나는 사진을 찍을 수 있는 카메라로 입소문을 탔다. 후지 필름의 실제 필름과 비슷한 색감으로 사진을 찍을 수 있는데 굉장히 감성적이다.

 

원래라면 사진을 찍고, 컴퓨터로 옮기고, 보정을 공부해서 해야 하는데 x100v는 카메라에서 필터를 켜기만 하면 필름 느낌의 감성적인 사진을 찍을 수 있게 되었다. 즉, 사진을 찍는 데만 집중할 수 있도록 이외의 기능은 카메라가 알아서 해준다. 덕분에 굉장히 비싼 디지털 카메라임에도 불구하고 (같은 가격에 훨씬 좋은 스펙의 카메라를 살 수 있다.) 사고 싶어도 항상 매진이라 살 수 없는 카메라가 된 것이다. (나도 너무 사고 싶당..)

 

회사에 도입한 tanstack query도 x100v와 비슷하게 data fetching을 쉽게 제어하는 편의 기능들을 제공한다. 캐싱, 백그라운드 업데이트, 재시도, 로딩 처리 등을 아주 손쉽게 사용할 수 있다. 실제로 사용해 보고 어떤 게 좋았는지 이야기해보겠습니다.

 

로딩에 대한 처리를 쉽게 할 수 있다

아래는 tanstack query를 쓰지 않았을 때의 예시이다.

export default function UserHistory {
  const [history, setHistory] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  
  useEffct(() => {
    (async () => {
      try {
        setLoading(true);
        const response = await axios.get('https://api.example.com/user/history');
        setData(response.data);
      } catch (error: unknown) {
        if (error instanceof Error) {
       	  // sncakbar 에러 표시 
        }
      } finally {
        setLoading(false);
      }
    })();
  }, []);
  
  if (loading) return <div>Loading...</div>;
  
  return (
    <h1>User History</h1>
    <div>{history}</div>
  );
}

 

이런 식으로 isLoading이라는 useState 상태를 선언해 로딩 중을 표시할 수 있다.

그런데 매번 상태를 선언하고, setLoading를 사용하는 코드를 만들어야 한다.

tanstack query를 사용하면 간편히 loading 상태를 관리할 수 있다.

 

// _app.tsx
const MyApp = () => {
  const queryClient = useMemo(() => {
    return new QueryClient({
      queryCache: new QueryCache({
        onError: error => {
          if (error instanceof ResError) {
            // 스낵바 표시
          }
        },
      }),
    });
  }, [dispatch]);
  
  return (
    <QueryClientProvider client={queryClient}>
     {/* 생략 */}
    </QueryClientProvider>
  );
}

 

// UserHistory.tsx
const fetchUserHistory = async () => {
  const response = await axios.get('https://api.example.com/user/history');
  return response.data;
};

export default function UserHistory {
  const [history, setHistory] = useState(null);
  const { data, error, isLoading } = useQuery(['apiData'], fetchApiData);
  
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  
  return (
    <div>
      <h1>User History</h1>
      <div>{history}</div>
    </div>
  );
}

 

이렇게 코드를 만들면 로딩에 대한 처리를 일일이 해주지 않아도 되어 매우 간편하다.

 

 

커스텀 훅을 생성하여 공용으로 쓸 수 있다

api를 여러 페이지에서 호출하게 될 경우 커스텀 훅으로 만드는 방법을 고려할 수 있다.

// useGetUserHistory.ts
const fetchUserHistory = async () => {
  const response = await axios.get('https://api.example.com/user/history');
  return response.data;
};

export const useGetUserHistory = () => {
  return useQuery({
    queryKey: ['user-history'],
    queryFn: () => fetchUserHistory(),
    refetchOnWindowFocus: false,
    refetchOnMount: true,
    refetchOnReconnect: false,
    retry: false,
    staleTime: 0,
  });
};

 

Data Fetching의 다양한 설정을 쉽게 적용할 수 있다.

return useQuery({
  queryKey: ['user-history'],
  queryFn: () => fetchUserHistory(),
  refetchOnWindowFocus: false,
  refetchOnMount: true,
  refetchOnReconnect: true,
  retry: true,
  staleTime: 0,
});

 

위의 코드를 보면 각종 설정을 쉽게 적용하고 있다.

어떤 것인지 알아보자.

 

refetchOnWindowFocus: 웹 브라우저에서 다른 탭에 있다가 돌아왔을 때 refetch 하여 최신 정보를 보여준다.

refetchOnMount: 컴포넌트가 마운트 될 때 refetch 하여 최신 정보를 보여준다.

refetchOnReconnect: 네트워크 연결이 오프라인이었다가 온라인이 될 경우 refetch 하여 최신 정보를 보여준다.

retry: data fetching이 실패했을 때 다시 시도한다. 지하철 와이파이와 같은 네트워크 신호가 약한 곳은 fetching이 한 번만에 성공하지 않을 수 있으니 유용하다.

staleTime: ms 단위로 설정할 수 있다. stale은 '신선하지 않은'이라는 의미로 캐시 된 데이터가 최신 상태가 아니라는 의미로 사용된다. 이 값을 30000으로 설정하면 5분 동안 stale 하지 않은 최신의 데이터라고 간주하여 refetch를 하지 않는다.

 

각종 설정 값들은 어떻게 사용하면 좋을까?

보면 refetchOnWindowFocus가 false이다. 탭으로 돌아왔을 때 refetch 하는 건 프론트엔드 입장에선 굉장히 유용하지만, 서버 입장에서는 api 호출이 많아 오버헤드가 발생할 수 있다. 그래서 false로 사용했다. 이 부분은 팀적으로 어떤 게 더 나은지 판단하여 true로 사용할 수도 있을 것 같다.

 

staleTime은 웬만하면 0, 즉 캐싱하지 않는 것으로 사용했었다. 커머스 서비스의 특성상 언제든지 관리자가 상품에 대한 정보를 바꿀 수 있다. 만약 고객이 옛날 정보를 보고 구매했는데 구매 이후 가격이 바뀐 걸 알아차리고 배송까지 받는다면 강성 CS가 들어올 수 있다. 그래서 캐싱은 사용하지 않았다.

 

반면 개발 중에 staleTime을 사용하면 괜찮은 경우도 있었다. 개발 일정이 급한데 어떤 페이지에서 api를 중복 호출하는 문제가 있었다. 원인을 찾기 어려운데 서버에게는 중복 api를 호출하면 안 좋은 상황. 이때 임시로 staleTime을 10초로 설정했다. 유저가 이 페이지에 오랫동안 체류하지 않을 것이기 때문에 10초면 충분할 것 같았고, staleTime을 설정했기 때문에 api를 중복 호출하는 문제를 해결할 수 있었다. (물론 나중에 원인을 찾아 해결하여 staleTime은 사용하지 않도록 변경했다.)

 

api를 다시 호출하고 싶으면 어떻게 할까?

이렇게 간단하게 invalidateQueries에 key 값으로 호출하면 된다.

queryClient.invalidateQueries([QUERY_KEYS.USER_HISTORY]);

 

api를 실행하는 함수를 실행하는 게 아니라 key값을 invalidate 한다. 즉 캐싱된 데이터를 무효화하고 최신 데이터를 가져온다는 것을 코드를 보면 알 수 있다. tanstack query의 철학이 보인다.

 

Infinite Scroll 도 문제없어요

 

회사의 구독 서비스의 리뷰를 보여주는 페이지가 있다. mui의 masonry(https://mui.com/material-ui/react-masonry/) 형태로 리뷰를 보여준다.

 

잠깐 간단하게 Masonry에 대해 설명하자면, 바둑판 형식으로 배열하되 height가 가변적이고, y축 기준으로 높은 곳부터 배열의 데이터를 쌓아간다. 아래에 데이터 순서의 예시가 있다.

 

리뷰의 개수가 매우 많아 한꺼번에 모두 로드할 수가 없다. 그래서 처음에 n개를 호출하고 infinite scroll로 n개의 마지막 부분에 도달했을 때 다음 n개를 호출하는 방식으로 구현했다. tanstack query에서도 infinite scroll이 구현 가능하다.

 

const { data, fetchNextPage, hasNextPage, isLoading, isFetchingNextPage } = useInfiniteQuery({
  queryKey: ['/subscribe/reviews/getReviews'],
  queryFn: ({ pageParam = 1 }) => fetchUserHistory(pageParam),
  getNextPageParam: (lastPage, allPages) => {
    if (lastPage.length === limit) {
      return allPages.length + 1;
    }
    return undefined; // 다음 페이지가 없음
  },
  refetchOnWindowFocus: false,
  refetchOnMount: false,
  refetchOnReconnect: false,
});

 

lastPage와 allPage를 console.log로 찍어보면 구조를 알 수 있다.

만약 2 페이지까지 넘어갈 경우

1페이지 일 때 [1, 2, 3], 2페이지 일 때 [4, 5, 6]이라면

lastPage: [4,5,6]

allPage: [[1, 2, 3], [4, 5, 6]]

이런 식으로 쌓이는 걸 알 수 있다.

 

data를 console.log로 찍어보면 pages 배열이 [[1, 2, 3], [4, 5, 6]] 이런 식으로 되어있는데

data.pages.flatMap(page => page);

이런 식으로 flatMap을 사용하여 1차원 배열로 만들어서 UI에 표현하면 된다.

 

결론

Tanstack Query를 이용하여 다양한 네트워크 관련 설정, 상태 관리를 통한 캐싱 등의 기능을 쉽게 적용할 수 있고, 많은 IT 회사에서 tanstack query를 사용하고 있다는 점이 매력적이었다. (채용 공고를 보면 tanstack query를 쓴다는 내용이 많더라.) 많은 사람들이 사용하는 만큼 커뮤니티가 잘 형성되어 있어 자료나 예시를 찾기 쉽고, 최신 개발 트렌드를 따라갈 수 있다는 장점이 있었다.

 

Redux Toolkit을 쓰고 있기 때문에 Redux Toolkit Query(https://redux-toolkit.js.org/rtk-query/overview)를 쓰는 방법도 괜찮은 것 같았다. Tanstack Query가 React 18의 Suspense와 궁합이 잘 맞고, 좀 더 사용자가 많은 대세 라이브러리인 것 같아 Tanstack Query를 선택했다.

 

각자 상황에 맞는 라이브러리를 선택하면 좋을 것 같다.

 

반응형

댓글