본문 바로가기

Computer Programming/React

카카오 개발자들의 React Query 선택 이유 | react-query 사용법

1. React Query란?

1) 기존 미들웨어의 문제점?

이전까지 API 통신을 리덕스와 사용하기 위해서는 리덕스 툴킷의 Redux-thunk, 또는 Redux-saga라는 라이브러리를 채택해서 사용했다. 하지만 이 두가지는 리덕스의 라이브러리이고, 리덕스는 전역상태관리 전용이기때문에 다수의 API 통신에 사용하기에는 어려움이 있었다.

 

다수의 컴포넌트에서 동일한 API를 호출하거나, 특정 컴포넌트에서의 API응답이 다른 컴포넌트의 API 응답에 영향을 미치는 경우 등 사용자의 시나리오에 대응하기가 적합하지 않았다.

 

코드량이 너무 많고 복잡할 뿐더러 리덕스가 비동기 데이터 관리를 전문적으로 해주지 않아 이러한 라이브러리를 사용할 때 규격화가 잘 안되어 있어 오류를 발생시키기도 했다.

 

redux toolkit 의 등장으로 코드가 조금은 줄어들긴 했으나... 여전히 엄청난 양의 보일러플레이트가  .......

 

 

이러한 문제점을 보완하기위해 리액트 쿼리가 등장하게 되었다. 

 

 

 

 

 

 

 

 

https://tech.kakaopay.com/post/react-query-1/

 

카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유 | 카카오페이 기술 블로그

카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유에 대해 설명합니다. 이 글은 연작 중 1편에 해당합니다. 1편: 카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유, 2편: React Que

tech.kakaopay.com

 

 

 

 

2) React Query의 장점

 

리액트 쿼리는 리액트 앱에서 서버의 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트 하는 작업을 도와주는 라이브러리이다. 이는 사용방법이 thunk 대비 매우 간결하며 코드가 직관적이며  클라이언트 상태를 분리해서 관리할 수 있기 때문에 효율적이다.

 

 

기존의 리덕스를 사용할 때는 컴포넌트에서 원하는데 스토어의 데이터를 가져오기 위해 useEffect에서 비동기 함수처리를 한 뒤 미들웨어를 통해 dispatch 하는 방식이었다.

 

하지만 react-query를 사용한다면 이렇게 useQuery 단 한줄로 바꿀 수 있다 (useEffect 안 써도 됨) 👻

export default function App() {
  const { data } = useQuery(["data"], getServerData);

  return <div>{JSON.stringify(data)}</div>;

 

즉 useEffect를 사용하지 않기 때문에 Side effect를 제거할 수 있다는 장점과 전체적인 흐름을 어렵게 만들지 않는다는 장점이 있다...!!!!

 

 

또한 비동기 처리들을 동기적으로 처리하는 것 처럼 구현할 수 있다. 만약 어떤 비동기 처리 이후의 조건이 처리되어야만 그 다음 비동기 처리를 수행한다. 라는 조건이 있을 때 enabled 속성을 추가하면 간편하다.

 

 

const { data: data1 } = useQuery(["data1"], getServerData);
const { data: data2 } = useQuery(["data2", data1], getServerData, {
  enabled: !!data1
});

 

 

 

 

 

2. react-query 사용방법

yarn add react-query

 

react-query 라이브러리를 설치한 다음 App.js 에서 clientQuery를 생성하고 설정해준다.

 

import Router from "./shared/Router";
import { QueryClient, QueryClientProvider } from "react-query";

const queryClient = new QueryClient();

const App = () => {
  return (
    <QueryClientProvider client={queryClient}>
      <Router />
    </QueryClientProvider>
  );
};

export default App;

이제 프로젝트 전체에서 queryClient를 사용할 수 있다.

 

 

 

1) 조회 READ

 

api를 관리하는 api 폴더에서 todos.js를 생성한다.

 

import axios from "axios";

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

 

그리고나서 데이터 조회가 필요한 TodoList에서 useQuery를 사용한다. (두번째 인자에 getTodos 전달)

 

function TodoList({ isActive }) {
  const { isLoading, isError, data } = useQuery("todos", getTodos);

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

  if (isError) {
    return <h1>오류 발생</h1>;
  }
  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>
  );
}

 

useQuery에서 data뿐만 아니라 isLoading, isError 또한 연산해주기 때문에 매우 편리하다.

 

 

 

갖고있던 todos 데이터가 잘 보인다. 👻

 

 

 

2) 추가 (CREATE)

 

api.js에 add 비동기함수를 추가한다.

 

 

// axios 요청이 들어가는 모든 모듈

import axios from "axios";

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


//데이터 추가
const addTodos = async (newTodo) => {
  return await axios.post(`${process.env.REACT_APP_SERVER_URL}/todos`, newTodo);
};

export { getTodos, addTodos };

addTodos를 정의하고, 이제 post 요청을 날릴 이벤트핸들러를 가지고 있는 Input.jsx로 가보자

 

 

handleSubmitButtonClick 함수인데 이 안의 유효성검사 부분 코드가 배울점이 많아서 일단 다 가져와봤다...

덕분에 중복내용 어떻게 검사하는지 알게 되었다.

const handleSubmitButtonClick = (event) => {
    event.preventDefault();

    // "01" : 필수 입력값 검증 실패 안내
    if (!title || !contents) {
      return getErrorMsg("01", { title, contents });
    }

    // 이미 존재하는 todo 항목이면 오류
    const validationArr = todos.filter(
      (item) => item.title === title && item.contents === contents,
    );

    // "02" : 내용 중복 안내
    if (validationArr.length > 0) {
      return getErrorMsg("02", { title, contents });
    }

    const newTodo = {
      title,
      contents,
      isDone: false,
      id: uuidv4(),
    };

    mutation.mutate(newTodo);

    setTitle("");
    setContents("");
  };

 

아무튼 모든 유효성검사를 마치면 mutation.mutate(newTodo)를 통해 데이터가 추가된다.

 

새로운 데이터로 화면을 갱신하기 위해 이전의 query를 invalidate 하기 위해 

 

const mutation = useMutation(addTodos, {
    onSuccess: () => {
      queryClient.invalidateQueries("todos"); // 화면 갱신
      // const { isLoading, isError, data } = useQuery("todos", getTodos);
      // 이 코드에서 query key를 "todos"로 줬었음
    },
  }); //첫번째 파라미터는 함수, 두번째는 객체 (성공 or 실패)

위의 코드를 추가한다. 

이때 query key를 잘 기억해뒀다가 invalidate 할 때 인자로 해당 query key를 전달해주면 된다.

 

  const { data: todos } = useQuery("todos", getTodos);

조회 코드에서의 key도 마찬가지이다.

 

 

*useQuery의 인자

   1) Query Keys 

        - refetching할 때 쓰임 ( =>queryClient.invalidateQueries("QUERY_KEY");

        - 캐싱처리할 때 쓰임

        - 애플리케이션 전체 맥락에서 이 쿼리를 공유하는 방법으로 쓰임 <- 어느 컴포넌트에 뿌려져있어도 같은 key면 같은 데이터 사용

        - 배열 형태로도 사용 가능 (문자, 숫자, object를 조합하여)

        - 어쨌든 unique 해야함

 

 

// ID가 5dls todo item 1개인데, preview 속성은 true임
useQuery(['todo',5, { preview: true }], ... )

// todolist 전체인데, type은 done임
useQuery(['todos', { type: 'done' }, ...])

이렇게 원하는 쿼리 키를 직관적으로 작성할수도 있다.

 

 

2) Query Functions

      - 두번째 인자로 promise 객체를 리턴하는 비동기 함수를 넣음

      - 반드시 data를 resolve하거나 reject 해야함

      - useQuery 가 반환하는 것은 isLoading, isError, data 가 담긴 객체이다. 각 객체의 키를 잘 핸들해야함

 

 

 

* mutations

- CUD에서 사용된다.

- 첫번째 인자로 비동기 함수가 들어옴

 

const mutation = useMutation(addTodos, {
    onSuccess: () => {
      queryClient.invalidateQueries("todos"); // 화면 갱신
      // const { isLoading, isError, data } = useQuery("todos", getTodos);
      // 이 코드에서 key를 "todos"로 줬었음
    },
  }); //첫번째 파라미터는 함수, 두번째는 객체 (성공 or 실패)

query key 통일시키기!

 

 

const newTodo = {
      title,
      contents,
      isDone: false,
      id: uuidv4(),
    };

mutation.mutate(newTodo);

 

 

 


Reference :

https://lasbe.tistory.com/163