티스토리 뷰

 

최근 회사에서 Props로 관리하던 코드를 react-query를 도입하면서 느꼈던 react-query의 장점과 간단한 사용법을 공유하기 위해 글을 작성하게 되었다.

 

 

왜 React-query를 도입하게 되었나?

 

서론에서 말했다시피 우리 회사에선 따로 상태 관리를 하지 않고 props를 이용하여 데이터를 관리하였다.(+약간의 contextAPI)

그러다 보니 여러 컴포넌트에서 prop drilling 이 발생하였고, 서비스가 커지면 커질수록 코드는 많아지고 유지보수는 더욱 힘들어졌다.

사실 contextAPI를 사용해도 되지만(이미 로그인 부분에서 사용하고 있다) contextAPI는 상위 컴포넌트의 데이터가 업데이트되면 하위 컨포넌트가 re-rendering 되어버려 렌더링 최적화에 좋지 않다.

 

다음으로 고려했던 라이브러리는 redux였는데 도입하지 않았던 가장 큰 이유는 러닝 커브 때문이다. 개인 프로젝트에서 두어 번 써본 수준이어서 실무에 바로 적용시키기 어렵다고 판단했다. 또 많은 보일러 플레이트 코드를 요구하기 때문에 많은 코드가 늘어난다는 것도 부담이었다.

 

그래서 선택한 라이브러리가 바로 React-query다. 사실 react-query도 개인 프로젝트에서 한번 써본 게 다였지만 강력한 기능을 제공하면서도 리덕스에 비해 쉬운 러닝 커브를 가지고 있어, 프로젝트할 때 굉장히 만족했었고 도입을 결정했던 거 같다.

 

 

우선 React-query가 어떤 녀석인지 알아보자.

 

React-query?

많은 기존 프로젝트에서 상태관리를 위해 Redux를 도입하여 사용한다. redux는 데이터를 중앙 관리하여 상태를 컴포넌트에서 관리하지 않아도 되게 되어 관심사 분리를 할 수 있는 장점이 있습니다. 하지만 redux는 동기적으로 작동하기 때문에 비동기 로직을 처리하기 위해선 미들웨어(Redux-saga, Redux-thunk)를 사용해야 하는데, 프로젝트가 커질수록 미들웨어의 덩치가 커져 redux의 단점이 부각된다.

 

react-query는 서버의 상태를 관리하는 라이브러리로, 리액트 애플리케이션에서 데이터를 가져오고, 캐시하고, 업데이트를 쉽게 할 수 있도록 도와준다.

 

다른 장점들도 많겠지만 내가 실무에서 사용하며 느낀 React-query의 장점은 크게 아래 5가지였다

  1. 데이터를 캐싱하여 불필요한 데이터 손실을 막는다.
  2. 데이터 fetching과 에러핸들링, Loading을 한 번에 구현할수 있다.
  3. 복잡한 비동기 로직 처리를 단 몇 줄의 react-query로직으로 바꿀 수 있다.(가독성)
  4. 서버의 상태를 쉽게 관리할 수 있다.
  5. 쉽게 인피니트 스크롤 기능을 구현할 수 있다(InfiniteQuery)

 

React-query 기본개념

 

- QueryClientProvider

React-query를 사용하기 위해선 최상단 컴포넌트를 QeuryClientProvider로 감싸 인스턴스를 생성하고 QueryClient를 Props로 넘겨준다.

import type { AppProps } from "next/app";
import { QueryClientProvider, QueryClient } from "react-query";

const queryClient = new QueryClient();

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <QueryClientProvider client={queryClient}>
      <Component {...pageProps} />
    </QueryClientProvider>
  );
}

export default MyApp;

 

- QueryKey

React-query 훅을 사용할 때 첫 번째 인자에 위치한다. 이 키를 기반으로 데이터 캐싱을 하는데 키가 달라지면 데이터를 새로 fetching 한다. 키가 한 가지일 때는 그냥 넣어도 되지만(React-query가 알아서 배열 안에 넣어준다.) 키를 여러 개로 조합해서 작성할 때는 배열에 넣어서 작성한다.

 

기본적으로 아래와 같은 형식으로 작성한다.

const query = useQuery("key", fn);

const query2 = useQuery(["key", "key2"], fn);

 

 

- useQuery

서버의 상태를 조회할 때 사용하는 hook으로 대표적으로 data, isLoading 같은 값을 리턴한다.(여기서 제공하는 리턴 값이 굉장히 많다. 공식문서를 참고하자). 조회의 로직에서만 쓰이며 서버의 상태를 변화시켜야 할 땐 useMutation을 사용하면 된다.

useQuery의 기본 형태는 아래와 같다.

const { data, isLoading } = useQuery(queryKey, fetcherFn)

 

- useMutaiton

서버의 상태를 생성, 업데이트, 삭제할 때 사용하며 mutate, isLoading 등을 리턴한다.

기본 형태는 아래와 같이 사용한다

const { mutate, isLoading } = useMutation(fetcherFn, option?);

mutate(variables, {
  onMutate
  onError,
  onSettled,
  onSuccess,
});

 

  • onMutate(variable)
    • mutation이 실행되기 전에 실행된다.
    • mutation이 받는 동일한 변수를 받는다.
  • onError(err, variable)
    • mutation hook이 error를 만났을 때 실행된다.
  • onSettled(data, error, variable)
    • mutaition 이 성공하던 실패하던 상관없이 실행된다.
  • onSuccess(data, variable)
    • mutation 이 성공할 때 실행된다.

React-query 활용 예제

Next.js를 활용한 간단한 유저를 등록하는 로직을 만들어 보자.

npx create-react-app my-app

index.tsx

import type { NextPage } from "next";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { addUser, getUser } from "../apis/api";
import useInput from "../hooks/useInput";

// useQuery의 리턴타입 지정
interface IQueryResult {
  ok: boolean;
  user?: {
    name: string;
    email: string;
  };
}

const Home: NextPage = () => {
  const queryClient = useQueryClient();
  // handlename을 onChange에 넣어 인풋값을 바꾸는 간단한 커스텀hook
  const [name, handleName] = useInput("");
  const [email, handleEmail] = useInput("");

// ["profile"] 이라는 키로 useQuery사용
// fetcher함수는 분리하여 관리
  const { data, isLoading } = useQuery<IQueryResult>(["profile"], getUser);

// isLoading 변수명 중복생김 변수명 변경
  const { mutate, isLoading: mutateLoading } = useMutation(
    (data: { name: string; email: string }) => addUser(data)
  );

  const handleRegister = () => {
    if (isLoading) return;
    if (!email) {
    // email작성이 안되어있으면 안넘어감
      return;
    }
    if (!name) {
    // name작성이 안되어있으면 안넘어감
      return;
    }
    // 유효성 검사로직 통과시
    mutate(
      { email, name },
      {
        onSuccess: (data) => {
          // mutation 성공시 실행
          console.log(data);
          // 성공시 ["profile"] key로 저장된 캐시를 refetching
          queryClient.invalidateQueries(["profile"]);
        },
        onError: (err) => {
          console.error(err);
        },
      }
    );
  };

  return (
    <>
      <h2>
        {data?.ok ? data.user?.name + "'s Profile" : "프로필을 추가하세요"}
      </h2>
      {data && (
        <div>
          <div>name: {data.user?.name}</div>
          <div>email: {data.user?.email}</div>
        </div>
      )}
      <div>
        <div>
          <input
            placeholder="name"
            onChange={handleName}
            type="text"
            value={name}
          />
        </div>
        <div>
          <input
            placeholder="email"
            onChange={handleEmail}
            type="email"
            value={email}
          />
        </div>
      </div>
      <button onClick={handleClick} type="button">
        {mutateLoading ? "Loading..." : data ? "프로필수정" : "프로필등록"}
      </button>
    </>
  );
};

export default Home;
  • useQuery를 사용하여 ["profile"]키로 서버의 상태를 받아와 화면에 보여준다. (유저가 없으면 추가하라는 문구를 보여준다.)
  • input을 작성하고 버튼을 클릭시 useMutation의 mutate가 트리거 되어 서버로 put 요청을 보냄
  • put요청이 성공하면 queryClient.invalidateQueries(["profile"])를 이용하여 ["profile"]키로된 데이터를 refetching하여 화면에 업데이트 한다.

 

관심사를 분리한 fetcher 함수는 apis/api.ts 에서 관리한다.

api.ts

import axios from "axios";

export const getUser = () => axios.get("/api/users").then((res) => res.data);

export const addUser = (data: { name: string; email: string }) =>
  axios.post("/api/users", { ...data }).then((res) => res.data);

 

백엔드는 넥스트에서 제공하는 서버리스 api를 활용한다.

GET /api/users으로 요청이 오면 저장된 유저 정보를 리턴한다.

PUT /api/users으로 요청이 오면 유저 정보를 수정하고 성공 여부를 리턴한다.

import type { NextApiRequest, NextApiResponse } from "next";

interface Data {
  ok: boolean;
  user?: {
    name: string;
    email: string;
  };
}

// db 대용
const user = {
  name: "",
  email: "",
};

export default function handler(
  req: NextApiRequest,
  // 응답값에 들어가는 json의 타입을 제네릭으로 정의함
  res: NextApiResponse<Data>
) {
  if (req.method === "GET") {
    return user.name
      // 유저정보가 있으면 ok가 true
      ? res.status(200).json({ ok: true, user })
      : res.status(200).json({ ok: false, user });
  }
  if (req.method === "POST") {
    const { name, email } = req.body;
    user.email = email;
    user.name = name;
    return res.status(200).json({ ok: true });
  }
}

 

결과화면

 

 

'Front-end > React.js' 카테고리의 다른 글

React프로젝트 브라우저 캐시문제  (0) 2022.06.24
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday