Recoil to Context API (feat. login)

정인우·2022년 12월 2일
1

React Dev.

목록 보기
2/8

본 글은 다음의 순서로 전개됩니다.

  • 개선 이전의 코드
  • 이러한 로그인 처리 방식이 불편한 것은
  • Context API란?
  • Context API의 장단
  • Recoil에서 Context API로의 대체
  • 최종평

개선 이전의 코드

기존의 좋지 못한 코드를 일단 먼저 공유하고자 합니다.

React에서 로그인과 관련된 상태관리를 하고자 다음과 같이 코드를 작성하였습니다. 추가적으로 아래에 등장하는 코드들은 부분적으로 생략되어 있습니다.(너무 길어지기에)

const [loginState, setLoginState] = useRecoilState(LoginState);

  useEffect(() => {
    //header에서 token validate를 매 생성시 체크
    refreshToken()
      .then((res) => {
        setAccessToken(res.data.accessToken);
        setLoginState(true);
      })
      .catch(() => {
        if (getAccessToken().length > 0) {
          deleteToken();
          setLoginState(false);
          window.location.reload();
        } else {
          deleteToken();
          setLoginState(false);
        }
      });
  }, [setLoginState]);

간단한 코드로 token validate check api를 call하고 성공한 경우 해당 결과로 받아오는 최신화된 access token으로 교체를 진행하고 실패한 경우 현재 가지고 있는 토큰이 있을 경우 폐기를 진행하는 방식으로 했고 이 과정에서 login 여부를 global state로 관리하기 위해 recoil를 사용했습니다.

이러한 로그인 처리 방식이 불편한 것은

  1. 일단 위의 코드를 비롯한 로그인과 관련된 코드들이 각자 다른 곳에서 돌아가고 있다는 점입니다. 로그아웃과 관련된 코드는 로그아웃 핸들러에 달려있고, axios instance에서도 로그인과 관련된 token을 관리합니다. 그렇기에 로그인이라는 하나의 관심사에 대한 코드들이 너무 분산되어 유지보수가 용이하지 않다고 생각됩니다.

  2. 이 로그인 상태 관리 하나를 위해서 recoil를 사용하고 있다는 점이다. recoil 역시 unpacked size 2.2MB로 결코 작지 않은 사이즈의 모듈인데 이 모듈을 단지 loginState 하나를 위해서 사용하는 것은 아깝다고 생각했다. React 자체의 Context API를 통해 충분히 대체 가능하다고고 생각했습니다.

실제로 recoil에서 loginState 파일 하나만 관리했습니다. 물론 여러 다른 상태들을 recoil를 통해 관리하면 의존성을 줄일 수 있지 않을까라는 생각을 했지만, 전역 상태 관리 도구의 원래 의미에 맞게 사용하고자 노력했습니다.

LoginState.ts

import { atom } from 'recoil';

const LoginState = atom<boolean>({
  key: 'LoginState',
  default: false,
});

export { LoginState };

아래에서 이를 context API로 전환하고 로그인과 관련해서 여러 부분에 흩어져 있는 코드들을 하나로 모아서 관리할 것입니다.

Context API란?

그래서 Context API가 무엇인가요? 종속성 주입의 방법 중 한 가지입니다.

React 공식 문서에는 Context를 다음과 같이 설명합니다.

Context의 주된 용도는 다양한 레벨에 네스팅된 많은 컴포넌트에게 데이터를 전달하는 것입니다. context를 사용하면 컴포넌트를 재사용하기가 어려워지므로 꼭 필요할 때만 쓰세요.

이러한 제어의 역전(inversion of control) 을 이용하면 넘겨줘야 하는 props의 수는 줄고 최상위 컴포넌트의 제어력은 더 커지기 때문에 더 깔끔한 코드를 쓸 수 있는 경우가 많습니다. 하지만 이러한 역전이 항상 옳은 것은 아닙니다. 복잡한 로직을 상위로 옮기면 이 상위 컴포넌트들은 더 난해해지기 마련이고 하위 컴포넌트들은 필요 이상으로 유연해져야 합니다.

요약해보자면 Context API를 활용하면 다양하게 분포되어 있는 상태를 props thrilling를 하지 않고 한 번에 내려줄 수 있고 이를 기반으로 깔끔한 코드를 쓸 수 있게 됩니다. 하지만 이러한 상태 로직들을 상위로 옮기는 것은 상위 컴포넌트의 로직이 복잡하게 만들고, 하위 컴포넌트가 고려해야할 상위 요소들을 증가시키기에 조심해야 한다고 합니다.

코드로도 기초적 사용에 대해서 간단하게 살펴보겠습니다.

React.createContext

const MyContext = React.createContext(defaultValue);

Context 객체를 만듭니다. Context 객체를 구독하고 있는 컴포넌트를 렌더링할 때 React는 트리 상위에서 가장 가까이 있는 짝이 맞는 Provider로부터 현재값을 읽습니다.

defaultValue 매개변수는 트리 안에서 적절한 Provider를 찾지 못했을 때만 쓰이는 값입니다.

Context.Provider

<MyContext.Provider value={/* 어떤 값 */}>

Context 오브젝트에 포함된 React 컴포넌트인 Provider는 context를 구독하는 컴포넌트들에게 context의 변화를 알리는 역할을 합니다.

Provider 컴포넌트는 value prop을 받아서 이 값을 하위에 있는 컴포넌트에게 전달합니다. 값을 전달받을 수 있는 컴포넌트의 수에 제한은 없습니다. Provider 하위에 또 다른 Provider를 배치하는 것도 가능하며, 이 경우 하위 Provider의 값이 우선시됩니다.

그렇다면 Context API만 쓰는 게 더 좋겠네요! Store 왜 씀?

일단 한 가지를 짚고 가자면, Context API 자체로는 종속성 주입의 형태일 뿐이고 그 안에서 useState 등을 사용하여 상태를 관리합니다. 이 부분은 기존의 React 사용자에게 장점으로 다가올 수 있습니다.

그렇다면 본 주제로 돌아가서 Context API vs Store Library(여기서는 대표적인 전역 상태 관리 도구 Redux로 상정)에 대해 여러 가지로 비교해보겠습니다.

빠른 응답시간

응답 시간 중 주요한 것으로 초기 로드에 소요된 시간이 있습니다. 초기 로드에 소요된 시간은 서버에서 보낸 데이터의 양에 비례하고 네트워크 속도에 반비례합니다. 동일한 두 Application에서 하나는 Context API를 사용하고 하나는 Redux를 사용한다면 Redux의 경우 번들에 Redux 외부 라이브러리를 필요로 하므로 로드에 시간이 더 필요합니다. 하지만 번들링 및 압축을 걸칠 시 이로 인한 차이는 2KB에 불과하므로 유의미한 차이는 없다고 봐도 무방합니다.

개발 용이성

개발 용이성 자체는 정확히는 여러분이 어떤 프로젝트를 진행하는 지에 크게 달려 있습니다.

초심자를 기준으로 한다면 Redux 보다는 Context API가 용이하다고 볼 수 있겠습니다. Redux의 경우 Redux 뿐만 아니라 다양한 미들웨어 등 Redux 자체를 사용하기 위해 다양한 개념들을 공부해야 하고 Context API 경우에는 대개 Context라는 개념을 제외하고는 React의 문법을 그대로 차용하기에 용이합니다.

하지만 관리할 전역 변수가 많아질 경우 여러 번의 Provider, Context 중첩이 일어날 수 있고 이 경우는 Redux가 더 나은 선택이 될 수 있겠습니다.

이 같은 경우는 여러분의 프로젝트와 개발 경험에 좌우되는 것으로 보입니다.

이를 표로 살펴보자면,

그렇다면 이제는 기존의 Store를 Context API로 대체한 코드를 만나봅시다.

Recoil에서 Context API로의 대체

우선, 기존에 로그인을 관리하던 코드입니다.

  1. axios api instance
const instance = axios.create({
  baseURL: baseURL,
  headers: {
    'Access-Control-Allow-Origin': '*',
    withCredentials: true,
  },
});

instance.interceptors.request.use((config) => {
  if (config && config.headers) config.headers.authorization = `${authHeader()}`;
  return config;
});

instance.interceptors.response.use(
  (res: AxiosResponse) => {
    return res;
  },
  //error code toast push
    return Promise.reject(error);
  },
);

export const axiosInstance = instance;

초반에 axios interceptor를 활용한 retry를 axios에서 처리하려고 했으나, 관련해서 에러가 발생하여 해당 기능을 모든 페이지에 들어가는 헤더에 임시로 이관해두었습니다. 기본적인 instance 설정과 error 코드별 toast 알람, auth header를 삽입합니다.

  1. logout 버튼에 달려있던 핸들러
const logoutHandler = () => {
    setVisible((prev) => !prev);
    logout().then(() => {
      deleteToken();
      pushNotification('로그아웃 성공', 'success');
      window.location.replace('/');
    });
  };

헤더의 로그아웃과 관련된 부분에 해당 핸들러가 포함되어 있었고, 길이가 짧고 가독성이 괜찮지만 login과 관련된 부분이 이런 식으로 각 Component들에 분리가 되어 있는 것은 보기 좋지 않고 유지보수에도 좋지 않습니다.

  1. Recoil Login State
import { atom } from 'recoil';

const LoginState = atom<boolean>({
  key: 'LoginState',
  default: false,
});

export { LoginState };

로그인 여부를 전역 상태로 관리하는 코드입니다.


이 두 부분 및 위의 헤더 부분을 수정한 것이 아래의 코드입니다.

  1. context API

export const AuthenticationContext = React.createContext<AuthenticationContextProps>({
  isAuthenticating: false,
  isAuthenticated: false,
  setIsAuthenticating: () => {},
  setIsAuthenticated: () => {},
  login: () => false,
  logout: async () => {},
});

export const AuthenticationContextProvider: React.FC<PropsWithChildren> = React.memo(
  ({ children }: PropsWithChildren) => {
    const [isAuthenticating, setIsAuthenticating] = useState(true);
    const [isAuthenticated, setIsAuthenticated] = useState(false);

    const initialize = useCallback(async () => {
      const { status, data } = await refreshToken();
      if (status === 200) {
        setIsAuthenticated(true);
        setAccessToken(data.accessToken);
      }
      setIsAuthenticating(false);
    }, [setIsAuthenticating, setIsAuthenticated]);

    const login = useCallback((payload: LoginRequest, navigate: NavigateFunction) => {
      //로그인 및 토큰 처리
      }

      return false;
    }, []);

    const logout = useCallback((navigate: NavigateFunction) => {
      //로그아웃 및 토큰 처리
    }, []);

    useEffect(() => {
      initialize();
    }, []);

    return (
      <AuthenticationContext.Provider
        value={{
          isAuthenticated,
          isAuthenticating,
          setIsAuthenticated,
          setIsAuthenticating,
          login,
          logout,
        }}
      >
        {children}
      </AuthenticationContext.Provider>
    );
  },
);

로그인과 관련된 정보를 처리하고 제공하는 AuthenticationContext입니다. 기존에 분리되어 있던 로그인 및 로그아웃 함수 및 로그인 여부 상태를 하나의 통합된 Context에서 제공합니다.

  1. 로그인 상태를 뽑아서 사용하는 방법
  const { isAuthenticated } = useAuthenticationContext();

로그인과 관련된 로직을 context에 집중하여 나머지 부분에서는 해당 값들을 추출하여 사용하도록 수정하였습니다.

  1. logout과 관련된 핸들러
const { logout } = useAuthenticationContext();

context에서 사용할 수 있도록 했습니다.

위와 같이 로그인과 관련된 동작을 하나의 context에 집중시킬 수 있었고, axios interceptor를 적절히 활용하였습니다.

최종평

이 글의 결론이 Recoil과 같은 전역 상태 관리 도구를 모두 Context API로 바꾸자는 것은 아닙니다. 위의 명시된 저의 경우에서Recoil에서 Context API로 대체한, 대체할 수 있었던 이유는 전역적으로 관리하는 상태가 1개 뿐이고, 이 또한 복잡한 로직이지 않기 때문입니다.

여러 개의, 또는 복잡한 둘 중 하나에 속한다면 여전히 Redux, Recoil과 같은 전역 상태 관리 도구가 좋은 선택입니다. 또한 이들이 지원하는 여타 강력한 기능 역시 인상적입니다.

이 글에서 제가 말씀드리고자 하는 것은, 전역적으로 관리될 상태가 적고 간단할 시 선택 가능한 옵션이 있다는 것입니다. 간단한 전역 상태 관리에 Context API를 도입해보시는 게 어떠실까요?

참고

React 공식문서 Context API
Context API vs Redux

profile
Portfolio: https://inwoo-chung.notion.site/c03fa1f3f25a48cb88b6d0888d1af997

0개의 댓글