관리 메뉴

거니의 velog

(53) 리액트 쿼리와 리코일 5 본문

SpringBoot_React 풀스택 프로젝트

(53) 리액트 쿼리와 리코일 5

Unlimited00 2024. 3. 11. 19:58

5. 리코일(Recoil) 라이브러리

* 리코일 라이브러리는 리덕스나 리덕스 툴킷과 유사하게 애플리케이션 내에 상태를 처리하기 위한 라이브러리이다.

https://recoiljs.org/ko/

 

Recoil

A state management library for React.

recoiljs.org

* 리액트의 생태계가 발전하면서 상태관리를 위한 다양한 라이브러리가 등장했는데 리코일은 2020년에 등장하면서 많은 인기를 끌고 있다.

* 리코일 라이브러리를 이해하기 위해서는 위의 그림에 나오는 Atoms와 useRecoilState()를 이해해야 한다. Atoms는 리코일을 통해서 보관하고 싶은 데이터를 의미한다. 리덕스가 애플리케이션당 하나의 상태를 유지하는 것과 달리, 리코일은 여러 개의 Atoms를 만들고 컴포넌트들은 자신이 원하는 상태를 선별적으로 접근해서 사용하는 방식이다.

* useRecoilState()은 쉽게 말해서 useState()의 확장판이라고 생각하면 된다. useRecoilState()의 파라미터에는 Atoms를 설정하는데 이를 통해서 useState() 처럼 애플리케이션 내 공유되는 데이터를 접근, 변경할 수 있다.


(1) 리코일 설치와 설정

* 리코일 라이브러리는 하단의 CLI를 통해서 쉽게 설치할 수 있다.

$ npm install recoil

* 설치 후에는 package.json을 통해서 확인해 준다(리코일은 현재 아직 1버전이 출시되지 않았다. 때문에 예제에서는 리덕스 툴킷을 우선적으로 설명하였다).

* 리코일을 사용하기 위해서는 애플리케이션 내에 RecoilRoot를 설정해 주어야 한다. 프로젝트 내 App.js나 index.js를 이용할 수 있다. 예제에서는 index.js를 이용한다.

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { Provider } from "react-redux";
import store from "./store";
import { RecoilRoot } from "recoil";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <Provider store={store}>
    <RecoilRoot>
      <App />
    </RecoilRoot>
  </Provider>
);

reportWebVitals();

* 예제에서는 이전에 사용했던 리덕스 툴킷의 설정과 겹치게 되어 있지만, 리코일의 설정이 완료되면 이를 제거할 것이다(물론 현재와 같이 함께 사용할 수도 있다).


(2) 로그인용 Atom

* Atom은 공유하고 싶은 데이터를 atom()을 이용해서 생성하고 컴포넌트에서는 이를 구독한다. Atom으로 유지되는 데이터가 변경되면 이를 구독하는 컴포넌트들은 다시 렌더링이 이루어진다.

* 프로젝트 내 atoms 폴더를 생성하고 signinState.js를 추가한다.

import { atom } from "recoil";

const initState = {
  email: "",
};

const signinState = atom({
  key: "signinState",
  default: initState,
});

export default signinState;

* Atom을 생성할 때는 key 속성과 default 속성을 이용해서 초깃값을 지정한다.


[useRecoilState()]

* 리코일 역시 많은 종류의 훅들을 제공하는데 useRecoilState()는 그 중에서 가장 기본이 되는 훅으로 useState()와 유사하지만 상태 유지의 범위가 애플리케이션 전체라고 생각하면 된다. useRecoilState()가 읽고, 쓰는 용도로 사용한다면 읽기/쓰기를 구분해서 다음과 같은 훅들도 존재한다.

- useRecoilValue() : 읽기 전용으로 사용

- useRecoilState() : 쓰기 전용으로 사용

- useResetRecoilState() : 초기화 용도

* 기존의 로그인과 관련된 처리는 useCustomLogin() 내부에서 리덕스 툴킷을 이용했으므로 이를 리코일로 변경시켜 준다.

* useRecoilState()를 이용해서 상태관리를 변경하고 이를 사용하는 doLogin()의 내부도 수정한다. 로그아웃 처리는 useResetRecoilState()를 이용하도록 하고, 로그인 관련된 정보를 쿠키로 저장할 수 있는 saveAsCookie()를 추가해 준다.

import { Navigate, useNavigate, createSearchParams } from "react-router-dom";
import { useRecoilState, useResetRecoilState } from "recoil";
import signinState from "../atoms/signinState";
import { loginPost } from "../api/memberApi";
import { removeCookie, setCookie } from "../util/cookieUtil";

const useCustomLogin = () => {
  const navigate = useNavigate();
  const [loginState, setLoginState] = useRecoilState(signinState);
  const resetState = useResetRecoilState(signinState);
  const isLogin = loginState.email ? true : false; // 로그인 여부

  // 로그인 함수
  const doLogin = async (loginParam) => {
    const result = await loginPost(loginParam);
    console.log(result);
    saveAsCookie(result);
    return result;
  };
  const saveAsCookie = (data) => {
    setCookie("member", JSON.stringify(data), 1); // 1일
    setLoginState(data);
  };

  // 로그아웃 함수
  const doLogout = () => {
    removeCookie("member");
    resetState();
  };

  // 페이지 이동
  const moveToPath = (path) => {
    navigate({ pathname: path }, { replace: true });
  };

  // 로그인 페이지로 이동
  const moveToLogin = () => {
    navigate({ pathname: "/member/login" }, { replace: true });
  };

  // 로그인 페이지로 이동 컴포넌트
  const moveToLoginReturn = () => {
    return <Navigate replace to="/member/login" />;
  };

  // 토큰에 따른 예외 처리
  const exceptionHandle = (ex) => {
    console.log("Exception------------------------");
    console.log(ex);
    const errorMsg = ex.response.data.error;
    const errorStr = createSearchParams({ error: errorMsg }).toString();
    if (errorMsg === "REQUIRE_LOGIN") {
      alert("로그인 해야만 합니다.");
      navigate({ pathname: "/member/login", search: errorStr });
      return;
    }
    if (ex.response.data.error === "ERROR_ACCESSDENIED") {
      alert("해당 메뉴를 사용할 수 있는 권한이 없습니다.");
      navigate({ pathname: "/member/login", search: errorStr });
      return;
    }
  };

  return {
    loginState,
    isLogin,
    doLogin,
    doLogout,
    saveAsCookie,
    moveToPath,
    moveToLogin,
    moveToLoginReturn,
    exceptionHandle,
  };
};

export default useCustomLogin;

* 상단의 메뉴를 담당하는 components/menus의 BasicMenu 컴포넌트는 리덕스 툴킷을 이용했으므로 이를 useCustomLogin으로 변경해서 리코일을 이용한다.

import React from "react";
import { Link } from "react-router-dom";
import useCustomLogin from "../../hooks/useCustomLogin";

const BasicMenu = () => {
  const { loginState } = useCustomLogin();

  return (
    (...)
  );
};

export default BasicMenu;

* 로그인 처리를 담당하는 LoginComponent는 별도의 코드 수정 없이 기존과 동일하게 로그인 처리가 가능하다.

* 로그아웃은 쿠키를 삭제하는 것은 동일하지만, 리코일의 상태 데이터를 삭제하는 useResetRecoilState()를 이용해서 처리한다.


[카카오 로그인 처리]

* 카카오 로그인의 경우에는 pages/member/KakaoRedirectPage.js 에서 리덕스 툴킷을 이용하고 있으므로 이를 변경한다.

import React, { useEffect } from "react";
import { useSearchParams } from "react-router-dom";
import { getAccessToken, getMemberWithAccessToken } from "../../api/kakaoApi";
import useCustomLogin from "../../hooks/useCustomLogin";

const KakaoRedirectPage = () => {
  const [searchParams] = useSearchParams();
  const authCode = searchParams.get("code");
  const { moveToPath, saveAsCookie } = useCustomLogin();

  useEffect(() => {
    getAccessToken(authCode).then((accessToken) => {
      console.log(accessToken);

      getMemberWithAccessToken(accessToken).then((memberInfo) => {
        console.log("-------------------");
        console.log(memberInfo);

        // 쿠키 저장
        saveAsCookie(memberInfo);

        // 소셜 회원이 아니라면
        if (memberInfo && !memberInfo.social) {
          moveToPath("/");
        } else {
          moveToPath("/member/modify");
        }
      });
    });
  }, [authCode]);

  return (
    <div>
      <div>Kakao Login Redirect</div>
      <div>{authCode}</div>
    </div>
  );
};

export default KakaoRedirectPage;

* 변경된 코드를 이용했을 때 문제가 없는지 카카오 로그인을 확인한다.


[로그인의 새로고침 문제]

* 로그인에 대한 마지막 처리는 새로고침으로 인해서 로그인한 모든 정보가 사라지는 초기화 현상을 처리하는 것으로 Atom의 초기 상태를 쿠키를 이용해서 체크하도록 수정한다.

import { atom } from "recoil";
import { getCookie } from "../util/cookieUtil";

const initState = {
  email: "",
  nickname: "",
  social: false,
  accessToken: "",
  refreshToken: "",
};

const loadMemberCookie = () => {
  //쿠키에서 체크
  const memberInfo = getCookie("member");

  //닉네임 처리
  if (memberInfo && memberInfo.nickname) {
    memberInfo.nickname = decodeURIComponent(memberInfo.nickname);
  }

  return memberInfo;
};

const signinState = atom({
  key: "signinState",
  default: loadMemberCookie() || initState,
});

export default signinState;

새로고침을 해도 유지되어야 한다...