Recoil + React Query

제혁·2022년 12월 23일
19

시작하기 전에

해당 내용은 처음으로 React-Query 와 Recoil을 공부하면서 정리하는 내용으로 틀린 부분이 다수 포함될 수 있을 수 있습니다. 또한 제 개인적 견해를 포함하기 때문에 그냥 공부 기록 정도로 봐 주시면 감사하겠습니다.

Next.js, Typescript, React-Query, Recoil, Next.js API 환경에서 진행됩니다.


Recoil

React Query를 소개하기 전에 Recoil 부터 소개해볼까 합니다. Recoil은 React에서 만든 상태 관리 라이브러리 입니다. 그래서 그런지 사용 방법이 React의 useState와 매우 흡사합니다. 그 말인 즉, React 사용자라면 배우기가 매우 쉽다는 것이겠죠. 많은 내용을 담진 않았기 때문에 예시를 보면서 간단하게 알아보겠습니다.

Atoms

공식 문서에 나온 간단한 예시입니다.

// state.ts
import { atom } from 'recoil';

export const fontSizeState = atom({
	key: 'fontSizeState',
	default: 14,
});

// FontButton.tsx
import { useRecoilState } from "recoil";
import { fontSizeState } from "./state";

function FontButton() {
	const [fontSize, setFontSize] = useRecoilState(fontSizeState);

	return (
		<button onClick={() => setFontSize((size) => size + 1)} style={{ fontSize }}>
			Click to enlarge
		</button>
	);
}

React 사용자라면 한 눈에 알아볼 수 있을 정도입니다. 버튼을 클릭할 때마다 버튼의 글자 크기가 14에서부터 1씩 증가하게 됩니다. 여기까지가 atom의 기본 사용법입니다. 다음으로는 Recoil 의 꽃이라고 불린다는 selector에 대해 알아보겠습니다.

Selectors

다음은 공식 한국 문서에서의 Selector 소개입니다.

Selector는 atom이나 다른 selector를 입력으로 받아들이는 순수 함수입니다.

당연히 저희는 이해가 안됩니다. 바로 예시를 보겠습니다.

// state.ts
import { atom, selector } from 'recoil';

export const fontSizeState = atom({
	key: 'fontSizeState',
	default: 14,
});

export const fontSizeLabelState = selector({
	key: 'fontSizeLabelState',
	get: ({ get }) => {
		const fontSize = get(fontSizeState);
		const unit = 'px';

		return `${fontSize}${unit}`;
	}
})

이렇게 보니 공식 문서의 설명이 이해가 됩니다. 즉, Selector란 get함수를 이용해 다른 Atoms나 Selectors를 선택해 이를 가공, return 해줄 수 있게 해주는 함수라는 것입니다. Selector로 선언된 변수라고 하더라도 Atoms와 동일하게 컴포넌트에서 사용이 가능합니다.

import { useRecoilState, useRecoilValue } from "recoil";
import { fontSizeState, fontSizeLabelState } from "./state";

function FontButton() {
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);
  const fontSizeLabel = useRecoilValue(fontSizeLabelState);

  return (
    <>
      <div>Current font size: ${fontSizeLabel}</div>

      <button onClick={setFontSize(fontSize + 1)} style={{fontSize}}>
        Click to Enlarge
      </button>
    </>
  );
}

Recoil에 대해서는 여기까지만 간단하게 알아보고 이제 React Query로 가보겠습니다.

React Query?

최근 (최근도 아닌 것 같긴 한데) 여러 컨퍼런스나 블로그 내용을 살펴보면 정말 많이 등장하는 내용입니다. if kakao 2021 에서는 카카오 페이 프론트엔드 개발자들이 React Query를 선택한 이유” 라는 주제로 발표가 진행되었고 2021.11.12 우아한 형제들 테크 블로그에도 Store에서 비동기 통신 분리하기 (feat. React Query)” 라는 주제로 블로그에 기재 되었습니다.

React Query를 소개하는 대부분 내용의 서문은 Redux에 대한 비판으로 시작됩니다. 물론 Redux는 다양한 장점이 존재하는 상태 관리 라이브러리 입니다. 여전히 수많은 기업에서 Redux를 사용하고 있으며 당장 취업 공고들만 몇 개 들여다봐도 대부분 Redux 활용이 가능한 인재를 원하고 있습니다.


React Query로의 전환 이유

그럼에도 불구하고 여러 기업들이 Redux의 대체제를 찾는 이유가 뭘까요? 가장 큰 이유는 너무나 장황한 코드입니다. Redux-Saga, Redux-toolkit 등을 같이 사용한다면 코드는 더욱더 길어지게 됩니다. 프로젝트가 진행되고 API가 추가될 수록 API 하나당 생성되는 타입, 액션, Saga 코드 등은 정말 많은 코드를 필요로 했습니다. 아래는 간단한 로그인 예시입니다.

// type.ts
export interface LoginReq {
	email: string;
	password: string;
}
export interface LoginRes {
	success: boolean;
	user: IUser;
	token: string;
}

// api.ts
export const login = async (user: LoginReq) => {
	return await axios.post<LoginRes>("api/login", user);
}

// saga.ts
function* loginApi(action: PayloadAction<LoginReq>) {
  try {
    const { data }: AxiosResponse<LoginRes> = yield call(
      login,
      action.payload
    );

    yield put(userActions.loginSuccess(data));
  } catch (e: any) {
    yield put(
      userActions.loginUserFail({ success: false, msg: "서버에러입니다." })
    );
  }
}
function* watchLogin() {
  yield takeLatest(userActions.loginRequest, loginApi);
}

export default function* saga() {
	yield all([fork(watchLogin)]);
}

// reducer.ts
export interface UserStateType {
  user: IUser;
  isLoading: boolean;
  token: null | string;
	...
}

const initialState: UserStateType = {
  user: { id: "", name: "", email: "" },
  isLoading: false,
  token: null,
	...
};

...
loginUserReq(state, action: PayloadAction<LoginReq>) {
	...
},
loginUserSuc(state, action: PayloadAction<LoginRes>) {
  state.isLoading = false;
  state.user = action.payload.user;
  state.token = action.payload.token;
},
loginUserFail(state, action: PayloadAction<ResponseFail>) {
	...
},
...

...어마어마한 코드 양입니다. 모든 API마다 저런 코드들을 치고 있자니 머리가 아플 수 밖에 없습니다.

그리고 저런 방대한 코드의 이유를 Redux가 그저 상태 관리 라이브러리 이기 때문이라고 생각합니다. 상태를 관리하는 것을 제외한 모든 내용은 추가적인 코드 작성이 필요합니다. 위와 같이 API 호출 및 응답에 대한 State 관리, 심지어 Loading, Success, Fail 상태에 대한 관리까지 모두 직접 코딩해야 합니다.


그럼 React Query는 어떻게 다른데

위에서 주로 Redux의 방대한 코드를 지적했으니 당연히 React Query는 코드가 짧다는 것을 보여주면 됩니다. 바로 예시부터 보겠습니다. (Next.js, Ts, React-Query, Recoil을 한 번에 모두 사용합니다.)

환경 세팅

💡 npx create-next-app@latest —typescript 💡 npm i react-query recoil

_app.tsx

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

const queryClient = new QueryClient();

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

우선 _app.tsx에서 Recoil과 React Query를 사용하겠다고 말을 해줘야 합니다. RecoilRoot로 전체 컴포넌트를 감싼 후, QueryClientProvider로 다시 한 번 컴포넌트를 감싸줍니다. RecoilRoot로 컴포넌트를 감싸면 하위 컴포넌트 내에서 Recoil의 모든 것을 사용할 수 있습니다.

QueryClientProvider 또한 하위 컴포넌트에서 React Query의 기능들을 사용할 수 있게 해줍니다. React Query는 내부적으로 client-queryClient를 이용해 각종 상태를 저장하고, 각종 부가 기능을 제공합니다. QueryClient()에 초기 세팅도 가능하지만 일단은 아무것도 세팅하지 않고 진행하겠습니다.

/api/user.ts

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

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  res.status(200).json({ name: "테스트이름", nickname: "테스트닉네임" });
}

Next.js 는 자체적으로 API 기능을 제공합니다. 자세한 내용을 다루지는 않겠습니다. 그저 “/api/hello” 로 요청을 보내면 { name, nickname } 객체를 응답해줍니다.

state.ts

import { atom, selector } from "recoil";

interface IUser {
  name: string;
  nickname: string;
}

export const userState = atom<IUser>({
  key: "userState",
  default: {
    name: "이름!!",
    nickname: "닉네임!!",
  },
});

전역 상태도 선언해주겠습니다. 간단한 타입도 일단은 state에 선언해 주었습니다. 이렇게 선언해주면 이제 “userState” 라는 키를 이용해 컴포넌트에서 Recoil 사용이 가능합니다.

index.tsx

import { useQuery } from "react-query";
import { useRecoilState } from "recoil";
import { userState } from "../state";

const fetchData = async () => {
  const res = await fetch("/api/hello");

  return res.json();
};

export default function Home() {
  const [user, setUser] = useRecoilState(userState);
  const { data, status } = useQuery("changeUser", fetchData, {
		onSuccess: (data) => setUser(data)
	};

  return (
		<div>
			<div>이름 : {user.name}</div>
			<div>닉네임 : {user.nickname}</div>
		</div>
	);
}

이 상태에서 앱을 실행시키면 아주 잠깐 이름과 닉네임이 “이름!!”, “닉네임!!” 이라고 나왔다가 “테스트이름”, “테스트닉네임”으로 바뀌게 될 것입니다.

useQuery를 보겠습니다. useQuery는 GET Method에 대한 요청을 보냅니다. 즉, “상태”를 불러와 사용할 때 사용합니다. useQuery의 첫 번째 인자는 응답 데이터를 캐시할 때 사용할 Unique Key, 두 번째 인자는 요청 수행을 위한 Promise 를 반환하는 함수, 마지막 인자는 옵션입니다. 옵션을 제외한 나머지 인자는 꼭 넣어야 하는 데이터이기 때문에 빼먹으시면 안됩니다.

useQuery는 Unique Key를 가지고 서버 상태를 로컬에 캐시하고 관리합니다.

그럼 POST, PUT, DELETE와 같은 요청은 무엇을 사용해야 할까요. 바로 useMutation 입니다. useMutation은 2개의 인자를 받습니다. 첫 번째는 Promise를 반환하는 함수, 두 번째는 옵션으로 첫 번째 인자만 꼭 필요한 데이터입니다. 사용법은 useQuery와 동일합니다.


마치며

React Query + Recoil 조합은 세팅부터 시작해 모든 코드를 다 작성했음에도 불구하고 Redux와는 비교도 되지 않게 간단합니다. 물론 내용적 차이가 있었지만 그걸 감안하고라도 훨씬 쉽습니다.

React는 흔히 러닝 커브가 높은 라이브러리 라고 불립니다. 그리고 그 이유는 Redux 였죠. 그만큼 Redux가 어렵다고 생각할 수도 있지만 조금만 비틀어 생각해보면 Redux가 React에 없어선 안 될, React의 기본 Stack이 되어버린건가 라는 생각이 듭니다.

그만큼 여전히 대부분의 기업은 Redux를 선호하고 있습니다. 하지만 React-Query에 대한 관심도도 대기업들을 필두로 높아지고 있습니다. 위에선 정말 간단한 예제만 살펴봤지만 분명 매력적인 기술이고 언젠가 Redux의 대체제로 자리매김 할 수도 있겠다는 생각이 듭니다.


출처

profile
언젠가 성공할 FE 개발자입니다

0개의 댓글