티스토리 뷰
최근 회사에서 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가지였다
- 데이터를 캐싱하여 불필요한 데이터 손실을 막는다.
- 데이터 fetching과 에러핸들링, Loading을 한 번에 구현할수 있다.
- 복잡한 비동기 로직 처리를 단 몇 줄의 react-query로직으로 바꿀 수 있다.(가독성)
- 서버의 상태를 쉽게 관리할 수 있다.
- 쉽게 인피니트 스크롤 기능을 구현할 수 있다(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