React Query

개발자지망생·2023년 12월 1일
1

React

목록 보기
1/24

1. React Query란?

  • (1) 개념

리액트 쿼리란?

  • (2) 기존 미들웨어의 한계

우리는 다른 서버와의 API 통신과 비동기 데이터 관리를 위해 Redux-thunk, Redux-Saga 등 미들웨어를 채택해서 사용할 수 있어요. 하지만 다음과 같은 문제가 있습니다.

  1. 보일러 플레이트 : 코드량이 너무 많아요.
  2. 규격화 문제 : Redux가 비동기 데이터 관리를 위한 전문 라이브러리가 아님(규격화 문제)
  • (3) 리액트 쿼리의 강점 : 너무 쉽고, 책임에서 자유롭죠
  1. 보일러 플레이트 만들다가 오류날 일이 없어요!
  2. 내가 만든 부분 아니기 때문에 잘못이 일어난들 내 잘못이 아니에요!
  3. 사용방법이 기존 thunk 대비 너무 쉽구요, 직관적이에요.

2. 주요 키워드

  • (1) Query

어떤 데이터에 대한 요청을 의미해요!

axios의 경우 get 요청과 비슷합니다.

const response = await axios.get(’http://localhost:3000/todos’)

  • (2) Mutation

어떤 데이터를 변경하는 거에요.

어떤 데이터라 함은, 데이터 그룹 그 자체를 의미한답니다.

바꾼다는 것은 추가, 수정, 삭제를 의미해요. CRUD 중, CUD에 해당해요.
(Create, Update, Delete)

axios의 경우 post, put, patch, delete 요청과 비슷합니다.

axios.post(’http://localhost:3000/todos’., newTodo); axios.patch(http://localhost:3000/todos/${id}, {isDone: true});

  • (3) Query Invalidation

위에서 보았던 Query를 invalidation. 즉, 무효화 시킨다는 의미입니다.
무효화 시킨다는 것이 무슨 의미일까요?

기존에 가져온 Query는 서버 데이터이기 때문에, 언제든지 변경이 있을 수 있어요. 그렇기 때문에 ‘최신 상태가 아닐 수’ 있습니다. 그런 경우, 기존의 쿼리를 무효화 시킨 후 최신화 시켜야 하겠죠.

이런 과정을 React Query에서는 알아서 해준답니다. 그 유용한 기능이 바로 Query Invalidation이에요.

3. 실습

  • (1) 조회기능 구현 : 기존 Todolist 프로젝트 변경해보기

아래 명령어로 react-query를 설치해주세요.

yarn add react-query

$ npm i @tanstack/react-query
# or
$ pnpm add @tanstack/react-query
# or
$ yarn add @tanstack/react-query

App.jsx

import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from 'react-query'

const queryClient = new QueryClient()

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Router />
    </QueryClientProvider>
  )
}
  • QueryClientProvider : 데이터를 읽어오는 기능(QueryClient)을 애플리케이션 전체에 주입하도록 하는 API

useQuery를 이용하여 조회기능을 구현해볼게요. 먼저 src > api 폴더를 만들어주시고 그 아래에 todos 관련 api를 관리할 파일을 만들겠습니다.

src > api > todos.js

import axios from "axios";

// 모든 todos를 가져오는 api
const getTodos = async () => {
  const response = await axios.get("http://localhost:3000/todos");
  return response;
};

export { getTodos };

TodoList 컴포넌트의 코드도 변경해주겠습니다.

Todolist.jsx

import React from "react";
import { StyledDiv, StyledTodoListHeader, StyledTodoListBox } from "./styles";
import Todo from "../Todo";
import { __getTodosThunk } from "../../modules/todosSlice";

import { getTodos } from "../../../api/todos";
import { useQuery } from "react-query";

/**
 * 컴포넌트 개요 : 메인 > TODOLIST. 할 일의 목록을 가지고 있는 컴포넌트
 * 2022.12.16 : 최초 작성
 *
 * @returns TodoList 컴포넌트
 */
function TodoList({ isActive }) {
  const { isLoading, isError, data } = useQuery({queryKey:["todos"], getTodos});

  if (isLoading) {
    return <p>로딩중입니다....!</p>;
  }

  if (isError) {
    return <p>오류가 발생하였습니다...!</p>;
  }

  return (
    <StyledDiv>
      <StyledTodoListHeader>
        {isActive ? "해야 할 일 ⛱" : "완료한 일 ✅"}
      </StyledTodoListHeader>
      <StyledTodoListBox>
        {data
          .filter((item) => item.isDone === !isActive)
          .map((item) => {
            return <Todo key={item.id} todo={item} isActive={isActive} />;
          })}
      </StyledTodoListBox>
    </StyledDiv>
  );
}

export default TodoList;

| const { isLoading, isError, data } = useQuery("todos", getTodos);

이 부분이 React Query가 가지고 있는 큰 장점이라 할 수 있어요. Thunk를 이용하면 isLoading, isError등을 개발자가 직접 만들어줬어야 했죠. state에서요.

React Query는 서버 데이터를 위한 표준을 이미 제시하고 있기 때문에 개발자들 마다의 특성에 따라 바뀔 염려가 없습니다.

return 문에 도착하기 전에 isLoading 또는 isError에 따라 별도의 처리를 해주기 때문에 대기상태 처리 / 오류 처리에 대한 부분도 아주 쉽게 해결이 되었어요 😎

  • (2) 추가 기능 구현

todo를 추가하는 부분을 함께 구현해볼게요!

src > api > todos.js

import axios from "axios";

// 공통으로 뺐어요(물론 .env를 쓰는게 더 바람직해요)
const SERVER_URI = "http://localhost:4000";

const getTodos = async () => {
  const response = await axios.get(`${SERVER_URI}/todos`);
  return response.data;
};

const addTodo = async (newTodo) => {
  await axios.post(`${SERVER_URI}/todos`, newTodo);
};

export { getTodos, addTodo };

Input.jsx

...
import { addTodo } from "../../../api/todos";
import { QueryClient, useMutation } from "react-query";
...


function Input() {
...
	const queryClient = new QueryClient();
	
	const mutation = useMutation(addTodo, {
	  onSuccess: () => {
	    // Invalidate and refresh
	    // 이렇게 하면, todos라는 이름으로 만들었던 query를
	    // invalidate 할 수 있어요.
	    queryClient.invalidateQueries("todos");
	  },
  });

[invalidate의 과정]

Input.jsx에서 값 입력으로 인해 서버 데이터가 변경됨

→ onSuccess가 일어나면 기존의 Query인 “todos”는 무효화

→ 새로운 데이터를 가져와서 “todos”를 최신화시킴

→ TodoList.jsx를 갱신함

따라서 계속해서 리액트 앱은 최신 상태의 서버 데이터를 유지할 수 있게 되는거에요.

더 나아가기

  • useQuery
    1. useQuery hook의 사용 방법에 대해 다시한번 짚어봅시다
import { useQuery } from 'react-query';
import { fetchTodoList } from '../api/fetchTodoList';

function App() {
	const info = useQuery('todos', fetchTodoList);
}
  1. useQuery의 인자에 대해
    a. 첫 번째 인자 ‘todos’. 우린 이걸 쿼리의 키(Query Keys)라고 부를게요!
    i. refetching 하는 데에 쓰여요.
    ii. 캐싱(caching) 처리를 하는 데에도 쓰이죠.
    iii. 애플리케이션 전체 맥락에서 이 쿼리를 공유하는 방법으로 쓰여요. —> 그 어느 컴포넌트 곳곳에 촥촥 뿌려져 있어도 같은 key면 같은 쿼리 및 데이터 보장!!
  • ‘Query Keys’에 대해 좀 더 자세히 알아볼까요?

    1. QK는 위 예제처럼 한 단어일 수도 있구요. 배열의 형태일 수도 있고, 심지어는 nested 객체일 수도 있어요. Key라는 말이 의미하듯, 모든 Query keys는 Unique해야 함을 잊지 마세요!

      const query1 = useQuery('qk', api); // unique
      const query2 = useQuery('qk2', api); // not unique
      const query3 = useQuery('qk2', api); // not unique
    2. 단어 한 개로 이루어진 Query Keys

      1. 만일 다음과 같은 코드가 있다고 한다면

        useQuery('todos', ...)
      2. 내부적으로는 다음과 같이 해석돼요(배열 형태로 갖고있어요)

        queryKey === ['todos']
    3. 배열 형태의 Query Keys

      1. 정보를 유일하게 식별하기 위해 하나의 단어보다 더 많은 ‘표현’이 필요하다면 문자, 숫자, object 등등 여러가지를 조합한 배열 형태의 key도 사용이 가능하겠죠!

      2. 공식 문서에서는 다음과 같은 예시를 제시하고 있네요.

        // 💥주의! key는 표현이 그렇다는거지, api 로직과는 관련이 없어요!
        
        // ID가 5인 todo 아이템 1개
        useQuery(['todo', 5], ...)
        // queryKey === ['todo', 5]
        
        // ID가 5인 todo 아이템 1개인데, preview 속성은 true야
        useQuery(['todo', 5, { preview: true }], ...)
        // queryKey === ['todo', 5, { preview: true }]
        
        // todolist 전체인데, type은 done이야
        useQuery(['todos', { type: 'done' }], ...)
        // queryKey === ['todos', { type: 'done' }]
    4. 문제! 다음 Query Keys는 Unique 한가요?

      1. 1번 문제

        useQuery(['todos', { status, page }], ...)
        useQuery(['todos', { page, status }], ...)
        // useQuery(['todos', { page, status, other: undefined }], ...)
      2. 2번 문제

        useQuery(['todos', status, page], ...)
        useQuery(['todos', page, status], ...)
        // useQuery(['todos', undefined, page, status], ...)
  • ‘Query Keys’에 대해 좀 더 자세히 알아볼까요?
    b. 두 번째 인자, ‘fetchTodoList’. 우린 이걸 쿼리 함수(Query Functions)라고 불러요.

  1. 쿼리 함수는 promise 객체를 return 합니다.

  2. promise 객체는 반드시 data를 resolve하거나 에러를 내야 해요.

  3. resolve는 정상적으로 통신이 되었음을 의미해요

  4. 원했던 상황이 아닌 경우. 즉, 오류가 발생한 경우에는 그에 맞는 적절한 오류 처리 관련 로직을 삽입해서 처리를 해줘야만 해요. axios, fetch, graphql 중 어떤 방법을 이용하던지 적절한 오류 처리를 통해 사용자가 혼란에 빠지지 않도록 해줘야만 해요.

  5. useQuery의 결과물에 대해
    a. useQuery를 통해 얻은 결과물은 객체(object)에요 객체!

    b. 그 안에는 여러분이 ‘조회’를 요청한 결과에 대한 거의 모든 정보가 들어있고 그 과정에 대한 정보도 다음과 같이 들어있어요.

i. 시~작 하면, isLoading이 true가 돼요.

ii. 조회 결과 오류가 나면 isError가 true가 돼요. isLoading은 false가 되겠죠. error 객체를 통해 좀 더 상세한 오류 내용을 확인할 수 있어요.

iii. 조회 결과 정상이 되면 isSuccess가 true가 돼요. isLoading은 false가 된답니다. data 객체를 통해 좀 더 상세한 조회 결과를 확인할 수 있어요.

  • mutations
    1. 위에서 언급했듯이, query와는 다르게 mutation은 CUD에서 사용돼요.
    2. useMutation 예제를 다시 한번 짚어봅시다.
// [출처] : 공식문서

function App() {
   const mutation = useMutation(newTodo => {
     return axios.post('/todos', newTodo)
   })
 
   return (
     <div>
       {mutation.isLoading ? (
         'Adding todo...'
       ) : (
         <>
           {mutation.isError ? (
             <div>An error occurred: {mutation.error.message}</div>
           ) : null}
 
           {mutation.isSuccess ? <div>Todo added!</div> : null}
 
           <button
             onClick={() => {
               mutation.mutate({ id: new Date(), title: 'Do Laundry' })
             }}
           >
             Create Todo
           </button>
         </>
       )}
     </div>
   )
 }
  1. useMutation은 hook이죠. 함수구요. API에요.

a. mutation.mutate( 인자 )

i. 인자는 반드시 한 개의 변수 또는 객체여야 해요.

ii. mutation.mutate(인자1, 인자2) → 오류

b. 결과를 객체(object 형태로) 갖고 있습니다.

c. 그 결과물 객체는 항상 어느 상태 중 하나에 속해요.
i. isIdle

ii. isLoading

iii. isError
1. error 객체를 항상 품고 있음을 명심!
iv. isSuccess(query에만 있는게 아니에요)
1. data 객체를 항상 품고 있음을 명심

profile
프론트엔드개발자를 목표로 공부중입니다.

0개의 댓글