사실 이 부분에 대해서 제일 먼저 말씀드려야 했을지도 모릅니다. 제가 처음으로 와서 보고 이해했던 내용이고, 지금도 계속 꾸준히 사용하고 있는 내용이니까 말이죠. 하지만 너무 당연하다 생각하고 넘겨버리고 말았네요. 이번 기회에 우리 회사에서 사용되는 Context API가 어떻게 동작하는지 리마인드 하는 느낌으로 설명해 드리려고 합니다.


보기전에 알고계시면 좋을 것들

상태관리

저는 개인적으로 리액트를 하며 가장 중요한 것이 상태라고 생각합니다. react로 대부분의 문제를 해결할 때 상태를 조절하여 해결하는 편입니다.

contextAPI

제공자가 있고(Provider), 소비자가 있으며(Consumer), 제공자 안에 있는 소비자는 제공자가 주는 state를 사용할 수 있다. 라고 추상적으로 알고 계시면 될 것 같습니다. 저는 로그인한 유저의 데이터를 여러 곳에서 제약없이 사용하기 위해 contextAPI 를 사용하고있습니다.

reducer

reducer또한 상태관리를 쉽게 해주기 위해서 고안된 장치로 알고 있습니다. 이번 프로젝트에서 reducer는 본격적으로 사용되지 않았습니다. graphql을 도입해서 컴포넌트 자체에서 받아온 데이터를 사용하는 경우가 많았기 때문이죠. 회사 코드에서는 globalState로 저장되어있는 사용자의 값을 손쉽게 변경할 수 있도록 reducer를 사용하였습니다.

react-router

SPA를 기반으로 만들어진 react는 페이지가 하나입니다. 이것을 여러 페이지로 만들어 주기 위해서 가상으로 URL 마다 다른 컴포넌트를 그려주게끔 보여주어야 하는데, 이것을 쉽게 가능하게 해주는 것이 서드파티 라이브러리리 react-router라고 볼 수 있습니다.

ContextAPI, Hooks

저희 회사 코드의 프론트는 React library를 사용하여 만들어졌으며 Redux, MobX같은 다른 라이브러리를 사용하지 않았습니다. 대신, context API와 Hooks를 적극적으로 사용하여 제작하였습니다. 이번 글에서는, 저희 회사에서 어느 부분에서 어떻게 이런 기능들을 사용하였는지 알아보려고 합니다.

GlobalContextProvider

먼저 회사에 사용된 global 한 state를 만드는 컴포넌트를 가져와서 말씀드리겠습니다. 이것은 위에서 말씀드린 Context의 Provider입니다.

import React, { useReducer, useContext } from "react";
import decodeJwt from "jwt-decode";
import stateReducer from "./stateReducer";

const GlobalState = (() => {
  const token = localStorage.getItem('bearer');

  if(token) {
    const decodedToken = decodeJwt(token);
    decodedToken.bAuthor = !!decodedToken.role

    return {user: decodedToken}
  }

  return {user: {
    id: null,
    bAuthor: false,
  },}
})();


const GlobalContext = React.createContext(GlobalState);

const GlobalStateProvider = ({ children }) => (
  <GlobalContext.Provider value={useReducer(stateReducer, GlobalState)}>
    {children}
  </GlobalContext.Provider>
);

export const useStateValue = () => useContext(GlobalContext);

export default GlobalStateProvider;

코드를 이해 하는 데는 이 페이지를 많이 참고하였으며, "GlobalState"를 만드는 즉시 실행함수를 제외하면 저의 사수가 짜고 간 코드입니다. 위부터 제가 이해한 대로 설명해 드리겠습니다.

  1. globalState 라는 변수에 즉시 실행 함수를 통하여 객체를 만들고 할당할 수 있도록 하였습니다.
    로컬스토리지에 bearer라는 key를 가지고 있는 값이 있다면 그 값을 해체하여 얻는 값을,
    아니라면 그저 null 값을 할당하도록 만들었습니다.

  2. createContext를 사용하여 위에서 만든 객체가 반영된 context를 만들어줍니다.
    createContext는 context를 위한 객체를 만드는 함수입니다.

  3. 이후 GlobalStateProvider 라는 컴포넌트를 만듭니다.
    위에서 만든 context를 사용하여 Provider를 만들고, value는 useRedcuer를 줍니다.(이 부분에 대해서는 나중에 설명해 드리겠습니다)

  4. 이후 useStateValue 라는 함수를 만드는데, 이 함수는 useContext(GlobalContext)를 사용합니다. React공식문서에 에 따르면,

    context 객체(`React.createContext`에서 반환된 값)을 받아 그 context의 현재 값을 반환합니다.
    context의 현재 값은 트리 안에서 이 Hook을 호출하는 컴포넌트에 가장 가까이에 있는
     `<MyContext.Provider>`의 `value` prop에 의해 결정됩니다.

    라고 합니다. 또한,

    만약 여러분이 Hook 보다 context API에 친숙하다면 useContext(MyContext)는 클래스에서의 static contextType = MyContext
    또는 <MyContext.Consumer>와 같다고 보면 됩니다.

    라고 합니다.

위의 공식문서 설명과 provider의 value에 useReducer를 준 부분에 대해서 꼭 기억해 주세요! 나중에 실질적으로 사용할 때, 위의 코드가 어떻게 반영되어 사용되는지 설명해드리겠습니다. 이제 import stateReducer from "./stateReducer";부분에 대해서 알아보겠습니다.

stateReducer

export default (state, action) => {
  switch (action.type) {
    case "loginSuccess":
      return {
        ...state,
        user: {
          ...action.args,
        },
      };

    default:
      return state;
  }
};

이 부분은 reducer입니다. reducer는 action.type을 확인하여 그 요청에 맞게 상태를 변경한 후 반환해 줍니다. 저희 회사에서 사용하는 reducer에는 로그아웃, 로그인, token의 만료 기간이 지난 경우를 체크하여 변경 시켜 주는 부분이 있습니다만, 이번엔 로그인이 성공했을 때 state를 변경시켜주는 부분에 대해서만 말씀드리려고 합니다.

Login

import React from "react";
import { Redirect } from "react-router";
// css-in-js
import { Grid } from "@material-ui/core";
// custom authentication hook
import useAuth from "./useAuth";

const analysisLocation = search => {
  //  location 분석
  // authCode=> 분석된 값
  return [[code],[authCode]]
}

function Login({ location }) {
  const [[, authCode]] = analysisLocation(location.search);
  const authSuccess = useAuth(authCode);

  // TEMP_DEV 마지막 위치 불러오기
  const lastLocation = sessionStorage.getItem("lastLocation");

  return (
    <Grid container justify="center" className={classes.root}>
      {/* 로그인 성공 후 이전 페이지로 이동 */}
      {authSuccess && <Redirect to={lastLocation || "/"} />}
    </Grid>
  );
}

export default Login;

위의 코드는 로그인을 구현한 코드입니다.

현재 저희 회사에서는 깃헙 로그인을 사용하고 있습니다. 짤막하게 설명하자면 깃헙 로그인을 사용하기 위한 주소를 로그인 버튼을 눌렀을 때 연결 되도록 만들고, github에서 주는 callBackUrl을 사용하여 이 컴포넌트를 띄워주도록 하였습니다. 순서대로 정리하면 아래와 같습니다.

  1. 로그인 버튼을 누른다.
  2. github과 연결이 된다,
  3. github이 callBackUrl을 준다.
  4. url에 입력된다.
  5. react-router가 컴포넌트를 실행시켜준다.

github OAuth에 대해 자세한 내용은 나중에 알아보도록 합시다.

다시 넘어가서, Grid와 같이 material-UI에 대한 코드를 빼고 위의 코드가 어떤 동작을 하는지에 대해 말씀드리겠습니다.

  1. analysisLocation를 통해(이 코드는 지금하는 설명과는 전혀 연관이 없는 코드이기에 주석처리하였습니다.) location.search값을 분석합니다.
  2. token 값을 가져와 useAuth코드로 넘겨줍니다.(useAuth에 대해서는 바로 다음에 나옵니다!)
  3. 그리고 authSuccess가 제대로 실행 되어 true가 되는 순간, sessionStorage에 저장해 두었던 이전 주소 값을 가져와 redirect 시켜주는 방식입니다.

sessionStorage에 어떻게 URL을 저장했는지는 나중에 기회가 된다면 말씀드리겠습니다. 자 이제 중요한 useAuth코드를 한번 살펴봅시다. 아 참고로 useAuth는 제 사수분이 만든 custom-hook 입니다.

useAuth

import React from "react";
// global state
import { useStateValue } from "scripts/Components/GlobalState";
import { OAUTH_LOGIN_URI } from "settings/";
// node modules
import decodeJwt from "jwt-decode";

function useAuth(authCode) {
  const [, dispatch] = useStateValue();
  const [authResult, setAuthResult] = React.useState(false);

  React.useEffect(() => {
    if (!authCode) return;

    const fetchInit = {
      method: "GET",
      mode: "cors",
      credentials: "include",
    };

    async function login() {
      let fetchResult = await fetch(`${OAUTH_LOGIN_URI}/callback?code=${authCode}`, fetchInit);
      let serverRes = await fetchResult.json();

      localStorage.setItem("bearer", serverRes.token);

      const decodedToken = decodeJwt(serverRes.token);
      dispatch({ type: "loginSuccess", args: { ...decodedToken } });
      setAuthResult(true);
    }

    login();
  }, [authCode]);

  return authResult;
}

export default useAuth;

이곳에는 useEffect, useState, 위에서 설명해 드렸던 useStateValue가 모두 사용되었습니다. 어떻게 사용되었는지, 위에서부터 차근차근 살펴보겠습니다.

  1. 위에서 만들었던 useStateValue 함수, 로그인할 때 사용하는 주소인 OAUTH_LOGIN_URI, 그리고 토큰을 해체할 decodeJwt 함수를 가져옵니다.

  2. const [, dispatch] = useStateValue();를 사용합니다. 이곳에서 위에서 말씀드린 모든 것들을 설명해 드릴 것입니다.

    context 객체(`React.createContext`에서 반환된 값)을 받아 그 context의 현재 값을 반환합니다.
    context의 현재 값은 트리 안에서 이 Hook을 호출하는 컴포넌트에 가장 가까이에 있는
     `<MyContext.Provider>`의 `value` prop에 의해 결정됩니다.

    위에서 한번 적어놓았던 설명입니다. 이 설명에 따르면 context의 값은 MyContext.Provider의 value prop에 의해 결정된다고 합니다. 그렇게되면, useStateValue() 함수의 반환 값은 윗부분에서 만든 Provider에 제공된 value, useReducer(stateReducer, GlobalState) 가 되는 것이죠. 그리고 useReducer 의 반환 값은 [state, dispatch] 입니다. state는 위에서 만든 GlobalState, dispatch는 stateReducer를 통해 동작하게 됩니다. 이곳에서, 저희는 state는 사용하지 않고 dispatch 만 사용할 것이기 때문에 const [, dispatch] = useStateValue();이런 방식으로 사용하였습니다. 6번에서 dispatch가 어떻게 동작하는지 조금 더 설명해 드리겠습니다.

  3. React.useState(false) 부분입니다. useState는 state와 그 state를 변경하는 함수를 배열을 통해 반환하여 사용합니다. useState함수 인자로 들어가는 값은 state가 최초로 가지는 값을 나타냅니다. 위 코드에서는 'false' 상태로 만들어지고 로그인이 완료되고 GlobalState가 변경된 후, 'true'로 상태가 변경되죠. 그리고 이 컴포넌트는 이 상태를 반환해줍니다.

  4. React.useEffect( 부분입니다. useEffect는 함수 구성 요소에서 sideEffect 를 수행 할 수 있도록 만들어줍니다. 이 부분에서는 2번째 인자로 들어간 [authCode] 가 변경될 때마다 이 내부 함수가 실행될 거야. 라는 의미로 사용되었죠.

  5. 내부의 login함수에서는 OAuth를 사용하는 URL로 fetch 요청을 보내고, 요청받은 값을 받아서 (요청은 토큰을 반환합니다. 이 부분에 대해서는 나중에 꼭 한번 글을 써보도록 하겠습니다.) 로컬스토리지에 저장합니다.

  6. dispatch를 이용해서 state도 변경해 줍니다. 여기서 reducer 쪽 파일로 올라가 보면, action.type"loginSuccess" 인 곳으로 가서, action.args...decodedToken 인 것을 참고해서 state 를 바꾸어 주는 것을 알 수 있죠.

  7. 그 작업이 끝나면 위에서 말했듯 state를 변경해주는 함수를 사용해서 state를 true로 변경 시켜 줍니다.

  8. 이후 변경된 state인 true를 반환합니다. 그리하여, Login 컴포넌트의 authSuccess가 true가 되는 것입니다.

마무리

제가 잘못 이해하고 짠 코드가 있을 수도 있고, 이것보다 훨씬 좋은 방법이 있을 수도 있습니다. 다른 좋은 방법이나 피드백 해주실 부분이 있다거나, 코드 부분에 대한 부족한 설명 때문에 이해가 안되는 부분이 있다면, 꼭 댓글 부탁드립니다. 여러분들에게 나중에 꼭 도움이 되는 글이었으면 좋겠네요.😃
더운 날씨에도 불구하고 긴 글과 코드를 읽어주셔서 감사합니다. 🙇‍♂️ 좋은 하루 되시길 바라겠습니다. 🙋‍♂️
참고: React Router V4 정리, react공식 문서 사이트