Context API Store 분리, 합쳐서 사용해보기

IT공부중·2020년 11월 10일
1

React

목록 보기
3/10

Context API와 useReducer를 통해 상태관리를 할 수 있다. 이번 프로젝트는 Redux를 사용하지 않고 둘을 사용해서 적용하면서 해보고 사용을 하고 있었는데 처음에는 모든 state들을 하나의 store에 저장 해놓고 사용했다. 그리고 state Context와 dispatch Context를 나누어서 리렌더링이 적게 일어나게 최적화 했다. 만약 dispatch와 state를 같은 Context에 두게 된다면 state가 바뀔 때 state를 쓰지 않고 dispatch만 사용하는 컴포넌트 또한 리렌더링 될 것이다. 하지만 dispatch는 바뀌는 일이 없으므로 dispatch만 사용하는 컴포넌트는 리렌더링 될 필요가 없을 것이다. 이러한 예제가 잘 나와 있는 블로그가 벨로퍼트님 블로그이다 ㅎㅎ

그런데 한 곳에 모든 state들을 관리하니깐 하나의 state만 업데이트해도 다른 state를 사용하는 모든 곳에서 리렌더링이 일어나는 것을 알 수 있었다. 이것은 별로 좋지 않다고 생각했고, Context들을 더 작게 나누어봐야겠다고 판단했다. Redux에서는 combineReducers 같은 함수를 이용해 합칠 수 있었는데 Context 에선 어떻게 할까 하다가 허접하지만 나누고 합쳐보았다...

그 과정을 간단한 예제로 정리하고자 한다.

예제 코드샌드백스


/App.js

import React from "react";
import MainContext from "./context";
import Test from "./Test";
import TestName from "./TestName";
import "./styles.css";

export default function App() {
  return (
    <MainContext>
      <div className="App">
        <Test />
        <TestName />
      </div>
    </MainContext>
  );
}

기본적인 구조를 이렇게 잡을 것이다. Test는 간단하게 숫자를 증가시키는 예제, TestName은 간단하게 input창의 값을 dispatch하는 예제로 만들어 number 나 name이 바뀌었을 때 하나만 리렌더링 되는지 다른 값도 리렌더링 되는지 등을 확인해볼 것이다.

그럼 먼저 context폴더의 index.js에서 여러 컨텍스트들을 중첩해주는 provider를 만들어본다.

context/index.js

export const ContextProvider = (...Provider) => {
  const MainContextProvider = ({ children }) => {
    let temp = children;
    Provider.forEach((Prov) => {
      temp = <Prov>{temp}</Prov>;
    });
    return temp;
  };

  return ({ children }) => (
    <MainContextProvider>{children}</MainContextProvider>
  );
};

const MainContext = ContextProvider(NameContextProvider, NumberContextProvider);

export default MainContext;

ContextProvider는 Provider들을 받아서 모두가 감싸진 Provider로 만들어주는 함수이다. 이거를 App.js에서 감싸주어 App 아래의 모든 컴포넌트들이 Context의 state들을 사용할 수 있게 하는 것이다.

이제 Name과 Number의 Context들을 만들어줘야한다. 그전에 utils에 간단한 createAction 함수를 만들어준다.

context/utils.js

export const createAction = (type) => (data) => ({ type, data });

createAction 함수는 type을 미리 받아서 Action생성 함수를 만들어두는 고차함수이다. type은 클로저로 내부스코프에 저장된다.

context/NumberContext.js

import React, { createContext, useReducer, useContext } from "react";
import { createAction } from "./utils";

// 최적화를 위해 state와 Dispatch Context를 따로 생성
const NumberStateContext = createContext(null);
const NumberDispatchContext = createContext(null);

// ADD_DATA라는 type을 선언하고, createAction util 함수를 사용하여
// addDataAction이라는 액션생성 함수 생성.

const ADD_DATA = "ADD_DATA";
export const addDataAction = createAction(ADD_DATA);


// 리듀서, type이 ADD_DATA일 때 state + 1 해준다.

const numberReducer = (state, action) => {
  switch (action.type) {
    case ADD_DATA:
      return state + 1;
    default:
      throw new Error("Unhandled action");
  }
};


// state와 dispatch를 가지고 오기 쉽게 만든 커스텀 훅이다.
export const useNumberState = () => {
  const state = useContext(NumberStateContext);

  return state;
};

export const useNumberDispatch = () => {
  const dispatch = useContext(NumberDispatchContext);

  return dispatch;
};

// NumberContextProvider로 index.js에서 감싸서
// 사용하기 편하게 만들어볼 것이다...
const NumberContextProvider = ({ children }) => {
  const [state, dispatch] = useReducer(numberReducer, 0);

  return (
    <NumberDispatchContext.Provider value={dispatch}>
      <NumberStateContext.Provider value={state}>
        {children}
      </NumberStateContext.Provider>
    </NumberDispatchContext.Provider>
  );
};
export default NumberContextProvider;

name 또한 마찬가지로 만들어준다.

import React, { createContext, useReducer, useContext } from "react";
import { createAction } from "./utils";

const NameStateContext = createContext(null);
const NameDispatchContext = createContext(null);

const CHANGE_NAME = "CHANGE_NAME";
export const changeNameAction = createAction(CHANGE_NAME);

const nameReducer = (state, action) => {
  switch (action.type) {
    case CHANGE_NAME:
      return { ...state, name: action.data };
    default:
      throw new Error("Unhandled action");
  }
};

export const useNameState = () => {
  const state = useContext(NameStateContext);
  if (!state) throw new Error("ContextProvider not found");
  return state;
};

export const useNameDispatch = () => {
  const dispatch = useContext(NameDispatchContext);
  if (!dispatch) throw new Error("ContextProvider not found");
  return dispatch;
};

const NameContextProvider = ({ children }) => {
  const [state, dispatch] = useReducer(nameReducer, {
    name: ""
  });

  return (
    <NameDispatchContext.Provider value={dispatch}>
      <NameStateContext.Provider value={state}>
        {children}
      </NameStateContext.Provider>
    </NameDispatchContext.Provider>
  );
};

export default NameContextProvider;

이렇게 하면 끝난다. 그런데 useNumberState, useNameState 각각 다르게 사용해야 하는 불편함이 있었다. 그래서 react-redux에 있는 비슷한 함수를 만들어봤다.

/hooks.js

import {
  useNameDispatch,
  useNameState,
  useNumberDispatch,
  useNumberState
} from "./context";

const stateMap = {
   number: useNumberState,
   name: useNameState
};

const dispatchMap = {
   number: useNumberDispatch,
   name: useNameDispatch
 };

export const useSelector = (callback) => {
  return callback(stateMap)();
};


export const useDispatch = (callback) => {
  return callback(dispatchMap)();
};

useSelector와 useDispatch 함수를 만들어서 callback 함수를 받으면 그에 해당하는 state와 dispatch를 사용할 수 있도록 하였다.

/Test.jsx

import React from "react";
import { addDataAction } from "./context";
import { useDispatch, useSelector } from "./hooks";

const Test = () => {
  const number = useSelector((state) => state.number);
  const numberDispatch = useDispatch((dispatch) => dispatch.number);


  const onClick = () => {
    numberDispatch(addDataAction());
  };

  console.log("넘버 테스트");

  return (
    <div>
      {number}
      <button onClick={onClick}>+</button>
    </div>
  );
};

export default Test;

/TestName.jsx

import React, { useState } from "react";
import { changeNameAction } from "./context";
import { useDispatch, useSelector } from "./hooks";

const TestName = () => {
  const [tempName, setTempName] = useState("");
 const nameDispatch = useDispatch((dispatch) => dispatch.name);
  const { name } = useSelector((state) => state.name);

  const onChange = (e) => {
    setTempName(e.target.value);
  };

  const onClick = () => {
    nameDispatch(changeNameAction(tempName));
  };

  console.log("Name 테스트");
  return (
    <div>
      <input onChange={onChange} />
      <button onClick={onClick}>변경</button>
      <div>{name}</div>
    </div>
  );
};

export default TestName;

이렇게 만들어서 사용하니깐 console.log로 테스트 해보았을 때 number가 바뀌면 number만 렌더링 되고 name이 바뀌면 name쪽만 렌더링 되어 console.log가 각각만 찍히는걸 볼 수 있었다. 이 화면은 react-devtools를 사용하면 더 확인하기 쉽지만 코드샌드박스에서 진행해서 console.log로 확인해보았다.


(변경했을 때 하나씩만 console.log가 찍힌 사진)

profile
4년차 프론트엔드 개발자 문건우입니다.

0개의 댓글