Redux를 React의 Context 또는 Hook으로 변경하기

학습 목표

Redux에 문제가 있는 것은 아니지만 대체하게 되면 라이브러리에 의존할 필요가 없기 때문에 흥미로운 과정일 것이다. 번들 크기를 줄이고 싶을 수도 있고 그냥 사용해보고 싶어서 사용해볼 수 있다.

Replacing Redux with Context + Hooks: A Totally Optional Of Reducing Your Dependencies

Context로 대체하기

이 방법은 좋지 않다. 전에도 말했지만 고빈도 변경의 경우엔 성능 보장을 해주지 않으므로 사용하는걸 권장하지 않는다.

사용방법은 이미 정리했으니 패스하고 context를 만들 때 state는 변수로, action은 함수로 각각 따로 만들어주면 된다.

export const ProductsContext = React.createContext({
  products: [], // state
  toggleFav: (id) => {}, // action function
});

Custom Hooks로 대체하기

먼저 store.js 이름(커스텀이기에 상관 없지만)으로 파일을 생성하고 전역 변수를 생성한다. 우리는 데이터를 모두 공유하는 식으로 사용할 것이다.

// /store/store.js
let globalState = {};
let listeners = [];
let actions = {};

State Slice를 만들고 Slices을 관리해보자.

먼저 globalStateactions 변수에 초깃값을 저장해주는 함수를 만들어준다.

// /store/store.js
/**
 * actions, states 초깃값을 설정한다.
 * @param {Object} userActions 유저가 입력한 actions 객체
 * @param {Object} initialState 유저가 입력한 초기 states 객체
 */
export const initStore = (userActions, initialState) => {
  if (initialState) {
    // initialState이 있으면 globalState 객체에 추가해준다.
    globalState = { ...globalState, ...initialState };
  }
  // userActions을 actions에 추가해준다.
  actions = { ...actions, ...userActions };
};

Slice 만들기 위해 새로운 파일을 생성하고 state와 action을 작성한다. 이 때 action은 key로 Redux에선 action.type라고 불리는 걸 설정하고 value로는 action 함수를 설정하면 된다. action 함수는 현재 state와 payload를 인자로 받는다.

// /store/products-store.js
import { initStore } from "./store";

const configureStore = () => {
  // key: action type
  // value: action function
  const actions = {
    ACTION_TYPE: (curState, payload) => {},
  };
  const initialStates = { products: 0 };

  // actions 객체와 state 객체를 넘겨준다.
  initStore(actions, initialStates);
};

export default configureStore;

참고로 다른 slice를 만들고 싶다면 다른 파일을 생성하기만 하면 된다. 그리고 공급해주기만 하면 된다. 공급은 인덱스에서 호출만 하면 된다.

// index.js
import configureProductsStore from "./store/products-store"; // slice
import configureCounterStore from "./store/counter-store"; // slice

configureProductsStore();
configureCounterStore();

이제 컴포넌트와 상호 작용할 수 있게 훅을 만들어 보자. Redux를 잠시 떠올려보면 저장된 데이터를 읽을 땐 useSelector()를 사용했고 actions을 dispatch할 땐 useDispatch()를 사용했다. 우리는 합쳐서 useStore()라는 이름으로 만들어보자.

// /store/store.js
import { useState, useEffect } from "react";

export const useStore = (shouldListen = true) => {
  // 전역으로 설정한 state로 상태를 초기화해주고 setState만 들고 온다.
  const setState = useState(globalState)[1];

  // 넘겨줄 dispatch 함수를 생성한다.
  const dispatch = (actionType, payload) => {
    // actions에서 넘겨준 Type으로 함수를 찾고 그 안에 state와 만약 payload가 있다면 payload를 넘겨준 뒤 반환 값을 새 변수에 할당한다.
    const newState = actions[actionType](globalState, payload);
    // 받은 상태를 globalState에 넣어준다.
    globalState = { ...globalState, ...newState };

    // listeners에서 setState 요소를 꺼내서 State를 업데이트 한다.
    for (const listener of listeners) {
      listener(globalState);
    }
  };

  // shouldListen이 바뀔 때마다 실행된다.
  // dispatch인 경우 업데이트하지 않도록 하기 위해 shouldListen 변수를 받아온다.
  useEffect(() => {
    if (shouldListen) {
      // 만약 데이터를 불러오기라면 setState를 넣어준다.
      listeners.push(setState);
    }

    return () => {
      if (shouldListen) {
        // 설정한 setState가 아닌 경우 전부 필터링해주고 저장해준다.
        listeners = listeners.filter((li) => li !== setState);
      }
    };
  }, [setState, shouldListen]);

  // globalState: useSelector를 객체로 넘겨줬다고 생각하면 된다.
  // dispatch: useDispatch
  return [globalState, dispatch];
};

이제 커스텀 훅을 사용하기만 하면 끝이다.

import { useStore } from "../../store/store";

const [state, dispatch] = useStore();

return (
  <div>
    <p>Counter: {state.counter}</p>
    <button onClick={() => dispatch("ADD", 1)}>Add 1</button>
  </div>
);

라이브러리 소스를 보고 구현해보는 것도 좋다.


강의명

  • Udemy : React 완벽 가이드

카테고리:

업데이트:

댓글남기기