React | React Query

블로그 이사 완료·2023년 1월 6일
0
post-thumbnail

📍 상태(State)

#1. 모던 웹프론트엔드 개발

  • UI/UX의 중요성과 함께 프로덕트 규모가 많이 커지고 프론트엔드에서 수행하는 역할이 늘어났다.
  • 즉,관리하는 상태가 많아지고 상태관리의 필요성이 중요해졌다.

#2. 상태란?

  • 주어진 시간에 대해 시스템을 나타내는 것으로 언제든지 변경될 수 있다.
  • 즉 문자열, 배열, 객체 등의 형태로 응용 프로그램에 저장된 데이터
  • 개발자 입장에선 관리해야하는 데이터들이라고 볼 수 있다.

#2.1. 상태 관리는?

  • 상태를 관리하는 방법에 대한 것 ➡️ 프로덕트가 커짐에 따라 어려움도 커진다.
  • 상태들은 시간에 따라 변화한다.
  • React에선 단방향 바인딩이므로 Props Drilling 이슈도 존재한다.
  • Redux와 MobX 같은 라이브러리를 활용해 해결하기도 한다.

#2.2. Client State & Server State

Client State 와 Server StateKey Point는 데이터의 Ownership이 있는 곳이다.

Client State

Ownership이 Client에 있다.

  • Client에서 소유하며 온전히 제어가능하다.
  • 초기값 설정이나 조작에 제약사항이 없다.
  • 다른 사람들과 공유되지 않으며 Client 내에서 UI/UX 흐름이나 사용자 인터랙션에 따라 변할 수 있다.
  • 항상 Client 내에서 최신 상태로 관리된다.

Server State

Ownership이 Server에 있다.

  • Client에서 제어하거나 소유되지 않는 원격의 공간에서 관리되고 유지된다.
  • Fetching이나 Updating에 비동기 API가 필요하다.
  • 다른 사람들과 공유되는 것으로 사용자가 모르는 사이에 변경될 수 있다.
  • 신경 쓰지 않는다면 잠재적으로 "out of date"가 될 가능성을 지닌다.

📍 React Query

react-query는 리액트 애플리케이션에서 서버 상태 가져오기캐싱동기화 및 업데이트를 보다 쉽게 다룰 수 있도록 도와주며 클라이언트 상태와 서버 상태를 명확히 구분하기 위해서 만들어진 라이브러리이다.

react-query에서 기존 상태 관리 라이브러리(redux, mobX)는 클라이언트 상태 작업에 적합하지만 비동기 또는 서버 상태 작업에는 그다지 좋지 않다고 말하고 있다.

클라이언트 상태(Client State)와 서버 상태(Server State)는 완전히 다르며 클라이언트 상태는 컴포넌트에서 관리하는 각각의 input 값으로 예를 들 수 있고 서버 상태는 database에 저장되어있는 데이터로 예를 들 수 있다.

React Query는 우리에게 친숙한 Hook을 사용하여 React Component 내부에서 자연스럽게 서버(또는 비동기적인 요청이 필요한 Source)의 데이터를 사용할 수 있는 방법을 제안한다.

#1. react-query의 상태

StateDescription
✅ fresh새롭게 추가된 쿼리 & 만료되지 않은 쿼리 ➜ 컴포넌트가 마운트, 업데이트되어도 데이터 재요청 ❌
✅ fetching요청 중인 쿼리
✅ stale만료된 쿼리 ➜ 컴포넌트가 마운트, 업데이트되면 데이터 재요청 ⭕️
✅ inactive비활성화된 쿼리 ➜ 특정 시간이 지나면 가비지 컬렉터에 의해 제거

#2. react-query 사용방법

#2.1. 설치 및 설정

npm i @tanstack/react-query

캐시를 관리하기 위해 QueryClient 인스턴스를 생성한 후 QueryClientProvider를 통해 컴포넌트가 QueryClient 인스턴스에 접근할 수 있도록 App컴포넌트 최상단에 추가한다.

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

root.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <BrowserRouter>
        <Provider store={store}>
          <App />
        </Provider>
      </BrowserRouter>
    </QueryClientProvider>
  </React.StrictMode>,
);

#2.2. react-query의 샘플코드

import axios from 'axios';
import {
  QueryClient,
  QueryClientProvider,
  useMutation,
  useQuery,
  useQueryClient,
} from 'react-query';
// React Query는 내부적으로 queryClient를 사용하여
// 각종 상태를 저장하고, 부가 기능을 제공한다.
const queryClient = new QueryClient();
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Menus />
    </QueryClientProvider>
  );
}
function Menus() {
  const queryClient = useQueryClient();
  // "/menu" API에 Get 요청을 보내 서버의 데이터를 가져온다.
  const { data } = useQuery('getMenu', () =>
    axios.get('/menu').then(({ data }) => data),
  );
  // "/menu" API에 Post 요청을 보내 서버에 데이터를 저장한다.
  const { mutate } = useMutation(
    (suggest) => axios.post('/menu', { suggest }),
    {
      // Post 요청이 성공하면 위 useQuery의 데이터를 초기화한다.
      // 데이터가 초기화되면 useQuery는 서버의 데이터를 다시 불러온다.
      onSuccess: () => queryClient.invalidateQueries('getMenu'),
    },
  );
  return (
    <div>
      <h1> Tomorrow's Lunch Candidates! </h1>
      <ul>
        {data.map((item) => (
          <li key={item.id}> {item.title} </li>
        ))}
      </ul>
      <button
        onClick={() =>
          mutate({
            id: Date.now(),
            title: 'Toowoomba Pasta',
          })
        }
      >
        Suggest Tomorrow's Menu
      </button>
    </div>
  );
}

📍 useQuery(데이터 가져오는 법)

useQuery Hook으로 수행되는 Query 요청은 HTTP METHOD GET 요청과 같이 서버에 저장되어 있는 "상태"를 불러와 CREATE 같은 작업을 할 때 사용한다.

// 가장 기본적인 형태의 React Query useQuery Hook 사용 예시
const { data } = useQuery(
  queryKey,
  fetchFn,
  options,
);
Description
✅ queryKeyQuery 요청에 대한 응답 데이터를 캐싱할 때 사용할 Unique Key (필수)
✅ fetchFnQuery 요청을 수행하기 위한 Promise를 Return 하는 함수 (필수)
✅ optionsuseQuery에서 사용되는 Option 객체 (선택)

❗️중요❗️ 쿼리 키가 다르면 호출하는 API가 같더라도 캐싱을 별도로 관리한다.

data request

function Users() {
  const { isLoading, error, data } = useQuery(
    'userInfo', // 'userInfo'를 Key로 사용하여 데이터 캐싱
    // 다른 컴포넌트에서 'userInfo'를 QueryKey로 사용한 useQuery Hook이 있다면 캐시된 데이터를 우선 사용한다.
    () => axios.get('/users').then(({ data }) => data),
  );
  // FYI, `data === undefined`를 평가하여 로딩 상태를 처리하는것이 더 좋다.
  // React Query는 내부적으로 stale-while-revalidate 캐싱 전략을 사용하고 있기 때문이다.
  if (isLoading) return <div> 로딩중... </div>;
  if (error) return <div> 에러: {error.message} </div>;
  return (
    <div>
      {' '}
      {data?.map(({ id, name }) => (
        <span key={id}> {name} </span>
      ))}{' '}
    </div>
  );
}
function UserInfo({ userId }) {
  const { isLoading, error, data } = useQuery(
    // 'userInfo', userId를 Key로 사용하여 데이터 캐싱
    ['userInfo', userId],
    () => axios.get(`/users/${userId}`)
  );
  if (isLoading) return <div> 로딩중... </div>;
  if (error) return <div> 에러: {error.message} </div>;
  return <div> {...} </div>;
}
Description
✅ data서버 요청에 대한 데이터
✅ isLoading캐시가 없는 상태에서의 데이터 요청 중인 상태 (true / false)
✅ isFetching캐시의 유무 상관없이 데이터 요청 중인 상태 (true / false)
✅ isError서버 요청 실패에 대한 상태 (true / false)
✅ error서버 요청 실패 (object)

✔ 이외에 더 다양한 데이터가 많다.

options

optionsDescription
✅ cacheTime언마운트된 후 어느 시점까지 메모리에 데이터를 저장하여 캐싱할 것인지를 결정
✅ staleTime쿼리가 fresh 상태에서 stale 상태로 전환되는 시간
✅ refetchOnMount컴포넌트 마운트시 새로운 데이터 패칭
✅ refetchOnWindowFocus브라우저 클릭 시 새로운 데이터 패칭
✅ refetchInterval지정한 시간 간격만큼 데이터 패칭
✅ refetchIntervalInBackground브라우저에 포커스가 없어도 refetchInterval에서 지정한 시간 간격만큼 데이터 패칭
✅ enabled컴포넌트가 마운트 되어도 데이터 패칭 ❌
✅ onSuccess데이터 패칭 성공
✅ onError데이터 패칭 실패
✅ select데이터 패칭 성공 시 원하는 데이터 형식으로 변환 가능

#1. cacheTime

언마운트된 후 어느 시점까지 메모리에 데이터를 저장하여 캐싱할 것인지를 결정

  • 기본값 : 30000 -> 5분
// cacheTime을 3초로 설정
const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
  cacheTime: 3000,
});

#2. staleTime

쿼리가 fresh 상태에서 stale 상태로 전환되는 시간

  • 기본값 : 0
  • fresh 상태에서는 컴포넌트가 마운트, 업데이트가 되어도 재요청을 보내지 않으므로 API 요청 횟수를 줄일 수 있다.
  • 보통 쉽게 변하지 않는 컴포넌트에 한해서 staleTime을 지정한다.
// staleTime을 3초로 설정
const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
  staleTime: 3000,
});

#3. refetchOnMount

컴포넌트 마운트시 새로운 데이터 패칭

  • 기본값 : true
  • false로 설정할 경우 마운트시 새로운 데이터를 가지고 오지 않는다.
const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
  refetchOnMount: true, // or false
});

#4. refetchOnWindowFocus

브라우저 클릭 시 새로운 데이터 패칭

  • 기본값 : true
  • flase로 설정할 경우 브라우저가 포커스 되어도 데이터를 가지고 오지 않는다.
const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
  refetchOnWindowFocus: true, // or false
});

#5. refetchInterval

지정한 시간 간격만큼 데이터 패칭

  • 기본값 : 0
  • 브라우저에 포커스가 없을 때 실행되지 않는다.
// 2초 간격으로 데이터 패칭
const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
  refetchInterval: 2000,
});

#6. refetchIntervalInBackground

브라우저에 포커스가 없어도 refetchInterval에서 지정한 시간 간격만큼 데이터 패칭

  • 기본값 : false
// 2초 간격으로 데이터 패칭
const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
  refetchInterval: 2000,
  refetchIntervalInBackground: true,
});

#7. enabled

컴포넌트가 마운트 되어도 데이터 패칭 ❌

  • 기본값 : true
  • useQuery의 반환값 중 refetch를 활용하여 데이터 패칭을 할 수 있다.
const { data, isLoading, refetch } = useQuery('super-heroes', fetchSuperHeroes, {
  enabled: false,
});

return (
  <button onClick={ refetch }>Fetch Button</button>
)

#8. onSuccess

데이터 패칭 성공

const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
  onSuccess: (data) => {
  	console.log('데이터 요청 성공', data)
  }
});

#9. onError

데이터 패칭 실패

const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
  onError: (error) => {
  	console.log('데이터 요청 실패', error)
  }
});

#10. select

데이터 패칭 성공 시 원하는 데이터 형식으로 변환 가능

const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes);

console.log(data.data)
/*
[
  {id: 1, name: 'batman'},
  {id: 2, name: 'superman'},
  {id: 3, name: 'wonder woman'},
]
*/
const { data, isLoading } = useQuery('super-heroes', fetchSuperHeroes, {
  select: (data) => {
    return data.data.map(hero => hero.name)
  }
});

console.log(data) // ['batman', 'superman', 'wonder woman']

병렬 처리

데이터 패칭이 여러개 실행되어야 한다면 useQuery를 병렬로 선언하면 된다.

import { useQuery } from 'react-query';
import axios from 'axios';

const fetchSuperHeroes = () => {
  return axios.get('http://localhost:4000/superheroes');
};
const fetchFriends = () => {
  return axios.get('http://localhost:4000/friends');
};

const ParallelQueries = () => {
  const heroes = useQuery('super-heroes', fetchSuperHeroes);
  const friends = useQuery('freinds', fetchFriends);

  return (
  	<div>
    	{heroes.data?.data.map(hero => (
     	    <div key={hero.id}>{hero.name}</div>
     	)}

    	{friends.data?.data.map(friend => (
     	    <div key={friend.id}>{friend.name}</div>
     	)}
    </div>
  );
};

export default ParallelQueries;

하지만 쿼리의 수가 많아지면 많아질수록 변수를 다 기억해야 하는 단점이 생기고 모든 쿼리에 대한 로딩, 성공, 실패 처리를 다 해줘야 하므로 불편함을 겪을 수 있다. 그럴때는 useQueries를 사용하면 된다.

const results = useQueries([
  {
    queryKey: ["super-hero"],
    queryFn: () => fetchSuperHeroes()
  },
  {
    queryKey: ["freinds"],
    queryFn: () => fetchFriends()
  }
]);

console.log(results) // 아래 이미지 참조

동기적 실행

어느 순간이든 코드가 동기적으로 수행되어야 하는 일이 발생한다. 그럴 때는 위에서 봤던 enabled 속성을 이용하면 된다.

useQuery는 enabled 속성의 값이 true일때 실행된다.

const fetchUserByEmail = (email) => {
  return axios.get(`http://localhost:4000/users/${email}`);
};

const fetchCoursesByChannelId = (channelId) => {
  return axios.get(`http://localhost:4000/channels/${channelId}`);
};

const DependentQueries = ({ email }) => {
  const { data: user } = useQuery(['user', email], () => fetchUserByEmail(email));
  const channelId = user?.data.channelId;

  // 집중❗️ 이중 부정을 통해서 channelId이 true -> useQuery 실행, false -> 실행 X
  useQuery(['courses', channelId], () => fetchCoursesByChannelId(channelId), {
    enabled: !!channelId,
  });
  return <div>DependentQueries</div>;
};

export default DependentQueries;

📍 useMutation(데이터 변경 및 삭제 방법)

useMutation Hook으로 수행되는 Mutation 요청은 HTTP METHOD POST, PUT, DELETE 요청과 같이 서버에 Side Effect를 발생시켜 서버의 상태를 변경시킬 때 사용한다.

  • useMutation Hook의 첫번째 파라미터는 이 Mutation 요청을 수행하기 위한 Promise를 Return 하는 함수이며, useMutation의 return 값 중 mutate(또는 mutateAsync) 함수를 호출하여 서버에 Side Effect를 발생시킬 수 있다.
// 가장 기본적인 형태의 React Query useMutation Hook 사용 예시
const { mutate } = useMutation(
  mutationFn,
  options,
);
Description
✅ mutationFnMutation 요청을 수행하기 위한 Promise를 Return 하는 함수 (필수)
✅ optionsuseMutation에서 사용되는 Option 객체 (선택)
function NotificationSwitch({ value }) {
  // mutate 함수를 호출하여 mutationFn 실행
  const { mutate, isLoading } = useMutation(
    (value) => axios.post(URL, { value }), // mutationFn
  );
  return (
    <Switch
      checked={value}
      disabled={isLoading}
      onChange={(checked) => {
        // mutationFn의 파라미터 'value'로 checked 값 전달
        mutate(checked);
      }}
    />
  );
}
  • useQuery와 같은 반환값을 받으며 mutate 메서드가 추가된다.
  • mutate 메서드를 이용하면 API 요청 함수를 호출하여 요청이 이루어진다.
import { useMutation } from 'react-query';

const AddSuperHero = () => {
  const addSuperHero = (hero) => {
    return axios.post('http://localhost:4000/superheroes', hero);
  };
  
  const { mutate: addHero, isLoading, isError, error } = useMutation(addSuperHero);

  const handleAddHeroClick = () => {
    const hero = { 이름, 성별 };
    addHero(hero);
  };

  if (isLoading) {
    return <h2>Loading...</h2>;
  }

  if (isError) {
    return <h2>{error.message}</h2>;
  }
}

하지만 버튼을 클릭 후 수동적으로 Fetch를 해줘야 화면에 보여진다는 불편함이 있다.

이 문제점을 해결하기 위해서는 쿼리 무효화(Invalidation)를 시켜줘야 한다.이 전에 캐싱된 쿼리를 직접 무효화 시킨 후 데이터를 새로 패칭하도록 해야 한다.

import { useMutation, useQueryClient } from 'react-query';

const AddSuperHero = () => {const queryClient = useQueryClient();
  
  const addSuperHero = (hero) => {
    return axios.post('http://localhost:4000/superheroes', hero);
  };
  
  const { mutate: addHero, isLoading, isError, error } = useMutation(addSuperHero, {
    onSuccess: () => {
    // 캐시가 있는 모든 쿼리 무효화
    ✅ queryClient.invalidateQueries();
    
    // queryKey가 'super-heroes'로 시작하는 모든 쿼리 무효화
    ✅ queryClient.invalidateQueries('super-heroes');
    }
  });

  const handleAddHeroClick = () => {
    const hero = { 이름, 성별 };
    addHero(hero);
  };

  if (isLoading) {
    return <h2>Loading...</h2>;
  }

  if (isError) {
    return <h2>{error.message}</h2>;
  }
}

mutate 함수가 실행되기 전, 성공 여부, 끝과 같이 라이프사이클에 따라 콜백함수를 작성할 수 있다.

useMutation(addSuperHero, {
   onMutate: (variables) => {
     // mutate 함수가 실행되기 전에 실행
     console.log(variables) // addSuperHero에 들어가는 인자
   },
   onSuccess: (data, variables) => {
     // 성공
   },
   onError: (error, variables) => {
     // 에러 발생
   },
   onSettled: (data, error, variables, context) => {
     // 성공 or 실패 상관 없이 실행
   },
 })

📍 React Query를 이용한 비동기 데이터 동기화 기능을 갖춘 Todo List 예제

// Todo.jsx
import useTodosMutation from 'quires/useTodosMutation';
import useTodosQuery from 'quires/useTodosQuery';
import { useForm } from 'react-hook-form';
function Todo() {
  // 서버에서 저장되어 있는 Todo 정보를 사용하기 위한 Custom Hook
  const { data } = useTodosQuery();
  // 서버에 새로운 Todo 정보를 저장하기 위한 Custom Hook
  const { mutate } = useTodosMutation();
  const { register, handleSubmit } = useForm<{
    contents: string;
  }>();
  const onSubmit = handleSubmit((value) => {
    // useTodosMutation의 'mutate' 함수를 사용하여 서버로 데이터를 전송한다.
    mutate(value.contents);
  });
  return (
    <div>
      <header>
        <form onSubmit={onSubmit}>
          <input
            {...register('contents')}
            type="text"
            placeholder="What needs to be done?"
            autoComplete="off"
          />
        </form>
      </header>
      <div>
        <ul>
          {data?.map(({ id, contents }) => (
            <li key={id}> {contents} </li>
          ))}
        </ul>
      </div>
    </div>
  );
}
export default Todo;
// quires/useTodosQuery.js
import axios from 'axios';
import { useQuery } from 'react-query';
import { TodoItem } from 'types/todo';
// useQuery에서 사용할 UniqueKey를 상수로 선언하고 export로 외부에 노출한다.
// 상수로 UniqueKey를 관리할 경우 다른 컴포넌트 (or Custom Hook)에서 쉽게 참조가 가능하다.
export const QUERY_KEY = '/todos';
// useQuery에서 사용할 서버의 상태를 불러오는데 사용할 Promise를 반환하는 함수
const fetcher = () => axios.get<TodoItem[]>('/todos').then(({ data }) => data);
const useTodosQuery = () => {
  return useQuery(QUERY_KEY, fetcher);
};
export default useTodosQuery;
// quires/useTodosMutation.js
import axios from 'axios';
import { useMutation, useQueryClient } from 'react-query';
import { QUERY_KEY as todosQueryKey } from './useTodosQuery';
// useMutation에서 사용할 서버에 Side Effect를 발생시키기 위해 사용할 함수
// 이 함수의 파라미터로는 useMutation의 mutate 함수의 파라미터가 전달된다.
const fetcher = (contents: string) => axios.post('/todos', { contents });
const useTodosMutation = () => {
  // mutation 성공 후 useTodosQuery로 관리되는 서버 상태를 다시 불러오기 위한
  // Cache 초기화를 위해 사용될 queryClient 객체
  const queryClient = useQueryClient();
  return useMutation(fetcher, {
    // mutate 요청이 성공한 후 queryClient.invalidateQueries 함수를 통해
    // useTodosQuery에서 불러온 API Response의 Cache를 초기화
    onSuccess: () => queryClient.invalidateQueries(todosQueryKey),
  });
};
export default useTodosMutation;

참고

카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유

react-query

profile
https://kyledev.tistory.com/

0개의 댓글