React Query 사용기

    반응형

     


    안녕하세요!
    오늘은 제가 실무에서 쓰고 있는 react-query에 대해 글을 써볼까 합니다
    지금 실무에서 쓰고는 있지만 늘 같은 형태로만 쓰고 있고,
    아무것도 모른 상태에서 다른 동료가 작성한 코드를 토대로만 공부(?) 했기 때문에
    모자른 부분이 굉장히 많아서... 공부 겸 내용 정리를 해보려고 합니다
    블로그 글 중에 react-query에 대해 잘 정리해주신 분이 계셔서 링크 남깁니다
    https://maxkim-j.github.io/posts/react-query-preview

     

    React-Query 살펴보기

    React의 Server State 관리 라이브러리 React Query를 살펴봅니다.

    maxkim-j.github.io

     

     

     

     


     

     

     

    1. react-query와 redux 차이점

    사실 저는 redux만 알고 있던 상태에서 지금 회사에 들어간 후 쭉 react-query만 사용하게 되었습니다
    저는 reacut-query가 redux와 같은 기능을 하는 라이브러리라고 생각했는데
    생각보다 결이... 다르다고 해야하나? 그래서 둘의 차이점에 대해 검색해보게 되었습니다

    • react-query는 서버 상태를 다룬다
    • react-query는 전역 상태관리 라이브러리가 아니라 서버와 클라이언트 간의 비동기 작업을 쉽게 해주는 라이브러리
    • react-query는 데이터 캐싱을 아주 쉽게 해결해준다
    • redux에서는 action으로 조작해야했던 isLoading의 상태를 react-query에서는 직접 선아하고 조작할 필요가 없다
    • redux, mobx 등은 클라이언트 상태를 다룬다
      (redux-thunk, redux-sagea : 비동기 작업을 수행 / 데이터를 store에 저장한 뒤 컴포넌트에서 사용)

    만약 react-query를 도입했는데, 서버 데이터와 관계없이 클라이언트에서 전역적으로 다뤄하는 데이터가 있다면?
    Ex. 테마 다크 모드 등....?
    context나 전역 상태관리 라이브러리를 따로 사용해야한다
    (그래서  react-query + recoil 같은 조합을 많이 사용하는걸까?)
    (react-query로 클라이언트 상태관리에 관한 블로그 글 👉 https://blog.hyunmin.dev/23)

    저는 초반에 react-query와 redux가 대조되는 개념이라고 생각을 해서인지...
    무조건 둘 중 하나만 써야한다! 라고 생각을 했는데요
    실제 실무에서는 두 라이브러리를 섞어서 쓰는 회사도 많은가봅니다
    (https://ridicorp.com/story/how-to-use-redux-in-ridi/)

     

     

    2. 클라이언트 상태? 서버 상태?

    Server State ≠ Client State 둘은 완전 다릅니다!

    • Client-State : 세션간 지속적이지 않는 데이터, 동기적, 클라이언트가 소유, 항상 최신 데이터로 업데이트
      (렌더링에 반영) Ex. 리액트 컴포넌트의 state, 동기적으로 저장되는 redux store의 데이터
    • Server-State : 세션간 지속되는 데이터, 비동기적, 세션을 진행하는 클라이언트만 소유하는게 아니고 공유되는 데이터도 존재하며 여러 클라이언트에 의해 수정될 수 있음.
      클라이언트는 서버 데이터의 스냅샷만을 사용하기 때문에 클라이언트에서 보이는 서버 데이터는 항상 최신임을 보장할 수 없음
      Ex. 백엔드 DB에 저장되어있는 데이터

     

     

    3. react-query의 장점

    • 캐싱, 리패칭을 알아서 해줌 (최신 데이터를 참조할 수 있게 알아서 보장)
      • 캐싱이란? → 자주 사용하는 데이터의 복사본을 저장한다 (앱 처리 속도를 높여줌)
      • 리패칭이란 → 데이터를 다시 가져옴
      • 불필요한 네트워크 통신을 줄일 수 있다!
        (그러나 이 부분은 옵션을 어떻게 해주냐에 따라 다를 것 같다. 개인적으로 나는 아직 react-query 다루는게 미숙해서인지 리패칭이 필요한 순간이 아님에도 불구하고 다른 컴포넌트가 리렌더링될때 같이 리패칭 되는게 있음)
    • Context API를 제공
    • useQuery를 통해 만들어진 query는 고유한 key로 구분되어 여러개의 query를 컴포넌트 곳곳에다가 흩뿌려 놓아도 key만 같으면 동일한 query에 접근할 수 있다
      (saga처럼 비동기 관련 성공, 실패 액션 하나하나를 모두 선언할 필요가 없음)
    • 선언적으로 프로그래밍이 가능하다 => 확실히 redux보다 알아보기 더 편하다고 생각합니다
      (react-query에 대해  잘 몰라도 무슨 기능을 하는지 단번에 이해가 가능한 편!)

     

     

     

    4. 사용법

    $ yarn add react-query

    최상위 루트 파일에서 Context Provider로 컴포넌트를 감싸고 queryClient를 내려보내준다

    // root 파일
    import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
    
    // 해당 부분을 console로 찍어보면 캐싱된 내용들을 확인할 수 있음
    const queryClient = new QueryClient();
    
    export default function App() {
    	return (
    		<QueryClientProvider client={queryClient}>
    			...
    		</QueryClientProvider>
    	);
    }
    • useQuery가 반환하는 객체의 프로퍼티는 4개이며, 어떤 상태인지 확인이 가능하다
      • fresh : 새롭게 추가된 쿼리 인스턴스
        • active 상태의 시작, 기본 staleTime이 0이기 때문에 아무것도 설정을 안해주면 호출이 끝나고 바로 stale 상태로 변한다(캐싱 기능을 활용할 수 없음)
          stale란? → 최신화가 필요한 데이터라는 의미 (오래된 데이터)
        • staleTime을 늘려줄 경우 fresh한 상태가 유지되는데, 이때는 쿼리가 다시 마운트(렌더링)되도 패칭이 발생하지 않고 기존의 fresh한 값을 반환한다 (네트워크 통신 낭비 방지)
      • fetching : 요청을 수행하는 중인 쿼리
      • stale : 인스턴스가 존재하지만 이미 패칭이 완료된 쿼리
        • 특정 쿼리가 stale된 상태에서 같은 쿼리 마운트를 시도한다면 캐싱된 데이터를 반환하면서 리패칭을 시도한다
        • 데이터를 받아오는 즉시 stale 하다고 판단하며 캐싱 데이터와 무관하게 계속해서 fetching을 수행한다
      • inactive : active 인스턴스가 하나도 없는 쿼리
        • inactive된 이후에도 cacheTime 동안 캐시된 데이터가 유지된다 → cacheTime이 지나면 GC(가비지 컬렉션) 된다 (기본값은 5분 뒤에 메모리에서 삭제)

    위 상태들은 react-query devtool로 확인이 가능합니다! (별도 설치)

     

    const {status, data, error, isFetching, isPreviousData } = useQuery(
    	['projects', page],
    	() => fetchProjects(page),
    	[keepPreviousData: true, staleTime: 5000}
    );
    
    // 실무 코드
    // 'faqList' 유니크 key 이름
    // courseId -> fetchCourseFAQList 함수 인자
    // fetchCourseFAQList는 통신을 통해 서버에서 받아온 response data (콜백함수 영역)
    const { data: faqListResponse }: any = useQuery(['faqList', courseId, fetchCourseFAQList]);
    • useQuery hook의 인자로 2개가 필요하다
      1. 쿼리의 유니크한 key → 한번 fresh가 되었다면 계속 추적이 가능하다.
        리패칭, 캐싱, 공유 등을 할때마다 참조되는 값이다
        주로 배열을 사용하고, 배열의 요소로 쿼리의 이름을 나타내는 문자열과 프로미스를 리턴하는 함수의 인자로 쓰이는 값을 넣는다
      2. 프로미스를 리턴하는 함수
      3. 3번째 인자는 옵션이다
    • useQuery의 반환 값
      • 객체
      • 요청의 상태를 나타내는 몇가지 프로퍼티
      • 요청의 결과나 에러 값을 갖는 프로퍼티
        • isLoading : 로딩중
        • isError : 에러
        • isSuccess : 요청 성공
        • isIdle : 쿼리 data가 하나도 없고 비었을때 {enabled : false} 상태로 쿼리가 호출되었을때 이 상태로 시작된다
        • status
        • error, data, isFetching
    • [keepPreviousData: true, staleTime: 5000} 의미
      • 주요 쿼리 옵션이며 이 구간에 옵션 설정이 가능함 (다양한 옵션이 있으니 공식 문서 참조)

     

    쿼리 요청 함수에서 queryKey에 접근할 수 있다

    function fetchTodoList({ queryKey }) {
    	const [_key, { status, page }] = queryKey;
    	return new Prmise();
    }
    
    // 실무 코드
    const fetchData = useCallback(async ({ querykey }) => {
    	const [_key, {pageIndex, pageSize }] = queryKey;
    	const page = pageIndex + 1;
    	const response = await fetchCourseList(page, 50);
    	return response.data;
    }, []);

     

    useQueries : 쿼리 여러개가 동시에 수행해야 하는데, 렌더링이 거듭되는 사이사이에 계속 쿼리가 수행되어야 한다면 쿼리를 수행하는 로직이 hook 룰에 위배될 수도 있다. 그때 사용하는 hook이다

    function App({ users }) {
    	const userQueries = useQueries(
    		users.map(user => {
    			return {
    				querykey: ['user', user.id],
    				queryFn: () => fetchUserById(user.id),
    			}
    		})
    	)
    }

     

    useMutations : useQuery와는 다르게 create, update, delete할때 사용되며 Server-State에 사이드 이펙트를 일으키는 경우에 사용한다
    사이드 이펙트란? 👉 컴포넌트가 화면에 렌더링된 이후에 비동기로 처리되어야 하는 부수적인 효과들
                                             Ex. 외부 API 호출 후, 일단 화면에 렌더링 먼저 해주고 실제 데이터는 비동기로 가져오는 것

    • useMutation으로 mutation 객체를 정의하고, mutate 메서드를 사용하면 요청 함수를 호출해 요청이 보내진다
    • useMutation이 반환하는 객체 프로퍼티로 제공되는 상태값은 useQuery와 동일하다
    • mutation.reset : 현재의 error와 data를 모두 지울 수 있다
    • 두번째 인자로 콜백 객체를 넘겨줘서 라이프사이클 인터셉터 로직을 짤 수도 있다
    // mutation이 성공하면 해당 데이터를 리패칭
    const mutation = useMutation(addTodo, {
    	onSuccess: () => {
    		queryClient.invalidateQueries('todos');
    		queryClient.invalidateQueries('reminders')
    	},
    });
    • invalidateQueries는 query key를 날려버릴때 사용하면 유용하다
      • 대부분 post 요청이나 delete요청에서 query key를 변화시키기 위해 강제 리프레쉬 기능으로 사용된다
    // 서버에서 받은 값이 갱신된 새로운 데이터일 경우
    const mutation = useMutation(editTodo, {
    	onSuccess: data => queryClient.setQueryData(['todo', { id: 5 }], data),
    });
    
    mutation.mutate({
    	id: 5,
    	name: 'Do the laundry'
    });
    
    const { status, data, error } = useQuery(['todo', { id: 5 }], fetchTodoById);

     

     

     

     

    5. Refetching이 일어나는 경우

    이 부분을 잘~ 고려해서 옵션을 설정해주면 불필요한 통신을 낭비할 수 있을 것 같다

    1. 런타임에 stale인 특정 쿼리 인스턴스(객체)가 다시 만들어졌을때
    2. 페이지를 이동했다가 왔을때
    3. window가 다시 포커스가 되었을때 (옵션 설정 가능)
    4. 네트워크가 다시 연결되었을 때 (옵션 설정 가능)
    5. refetch interval(리패칭 간격)이 있을때
      • 요청 실패한 쿼리는 기본으로 3번 더 백그라운드단에서 요청하며, retry, retryDelay 옵션으로 간격과 횟수를 커스텀 가능하다

     

     

     

    6. 마무리

    사실 저도 늘 쓰던 형태만 쓰고 그 형태에서 벗어난 적이 없기에 뭐라 더 설명할 방법이 없네요
    나중에 좀 더 프로젝트를 고도화 하면서 다양한 옵션, hook을 써볼 것이고 
    그러면서 따라오는 시행착오 내용들을 추가할 예정입니다

    반응형

    댓글