React Context API → Redux 리팩토링하기

1

React

목록 보기
1/1

리팩토링 이유

Context API vs Redux

우선 Context API같은 경우 리액트 내부 기능- useContext으로 존재하고 redux외부라이브러리이다.
컨텍스트 api 같은 경우 리덕스에 비해서 중첩되고 복잡해지는 컴포넌트를 가지는 경우 훨씬 사용하기 부적절하다.

그 예시는 아래 코드를 비교해보면 간단히 느낄 수 있다.

// context api를 사용할 때 app의 구조
ReactDOM.render(
  <React.StrictMode>
    <AuthContextProvider>
      <GoalContextProvider>
        <App />
      </GoalContextProvider>
    </AuthContextProvider>
  </React.StrictMode>,
  document.getElementById("root")
);

// redux를 사용했을 때 app의 구조 
ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById("root")
);

현재 프로젝트에서 2개의 전역 상태값을 사용하기 때문에 그렇게 큰 차이는 느껴지지 않을지라도 redux같은 경우 유일한 한 store에 전역 상태값을 관리하기 때문에 하나의 store를 props를 가지는 Provider로 앱을 감싸주면된다.

코드가 단순해져서 더욱 코드의 가독성이 향상된다.

반면에 ContextAPI같은 경우 사용하는 모든 provider를 만들고 감싸주어야 하며 Provider간에도 계층이 존재할수 있기 때문에 props 드릴링에서 문제가 발생할 수 있다.

Redux 기본 개념

Redux-flow

🟢 Store

우선 redux는 위에 언급한대로 유일한 하나의 'store'를 가져야 한다. 이 store에는 앱에 있는 모든 상태값이 담기게 된다.
이 store를 component는 구독하여 해당 상태값을 가져오게 된다.

🟢 Dispatch

각 컴포넌트에서 특정 상태값을 변경하고 싶다는 의사를 Action에게 '전파'한다. 직접적으로 state값을 변경하지 않기 위해서다.

🟢 Reducer

변경요청을 전달받은 액션은 자신 안에 reducer함수를 확인을 하고 해당 reducer 함수를 통해서 store상태값을 업데이트한다.

Redux-toolkit

리덕스 툴킷은 리덕스를 사용하며 불편한 점들을 보완해주는 기능을 제공한다.
가장 우선적으로는 기존에 리덕스같은 경우 아래와 같이 새로운 상태 값을 업데이트하려면 기존에 값을 모두 다시 적어주어 (또는 구조분해할당) 새로운 값으로 덮어씌우는 방식으로 상태 업데이트를 진행을 한다.


const productReducer (state= initialState, action) => {
  	let {type, payload} = action;
  switch(type){
    case "GET_PRODUCT_SUCCESS":
      return {...state, productList : payload.data};
    case "GET_PRODUCT_SUCCESS":
      return {...state, productList : payload.data};
    default :
      return {...state}
  }
}

리덕스 툴킷은 반면 아래와 같이 표현이 가능하다. 여기서 중요한 점은 직접 변경을 하는 게 가능해보이지만 실제로는 리덕스 툴킷 내부적으로 값을 복사하여 새롭게 덮어씌우는 과정이 이루어진다는 것이다.

import { createSlice } from "@reduxjs/toolkit";

const initialState = {}

const productSlice = createSlice ({
  name: "Product,
  initialState,
  reducers: {
  getAllProducts(state.action) {
  	state.productList = action.payload.data
  getSingleproduct(state,action) {
    state.selectedItem = action.payload.data
  }
})
  

참고 : https://www.youtube.com/watch?v=UKnLwVm9suY

Auth

기존 store코드 - 리팩토링 store코드

// auth-context.js --- context API 
import React, { useState } from "react";
const AuthContext = React.createContext({
  token: "",
  isLoggendIn: false,
  login: (token) => {},
  logout: () => {},
});

export const AuthContextProvider = (props) => {
  const initialToken = localStorage.getItem("token");
  const [token, setToken] = useState(initialToken);
  
  const userIsLoggedIn = !!token;
  const loginHandler = (token) => {
    setToken(token);
    localStorage.setItem("token", token);
  };
  const logoutHandler = () => {
    setToken(null);
    localStorage.removeItem("token");
  };

  const contextvalue = {
    token: token,
    isLoggendIn: userIsLoggedIn,
    login: loginHandler,
    logout: logoutHandler,
  };

  return (
    <AuthContext.Provider value={contextvalue}>
      {props.children}
    </AuthContext.Provider>
  );
};

export default AuthContext;

컨텍스트 API로 유저 authentication을 구현했을 때는 위 코드로 표현이 가능하다. 특징으로는 초기 상태값함수가 같이 포함되어 들어간다는 것이다.
그리고 내부적으로 다시 local state값인 token을 설정해서 해당하는 값을 포함한 값인 contextvalue를 다시 props로 보낸다.

// store/index.js
import { configureStore } from "@reduxjs/toolkit";

import authReducer from "./auth";
import goalReducer from "./goal";

const store = configureStore({
  reducer: { auth: authReducer, goal: goalReducer },
});

export default store;


redux에서는 각 액션에 대해서 별도의 파일로 관리가 가능하다. 그리고 각 액션이 담긴 reducer는 하나의 store로 통합하여 관리한다.


// store/auth.js

import { createSlice } from "@reduxjs/toolkit";

const loggedIn = localStorage.getItem("token") ? true : false;
const token = localStorage.getItem("token")
  ? localStorage.getItem("token")
  : "";

const initialAuthState = {
  isAuthenticated: loggedIn,
  token: token,
};

const authSlice = createSlice({
  name: "authentication",
  initialState: initialAuthState,
  reducers: {
    login(state, action) {
      localStorage.setItem("token", action.payload);
      state.isAuthenticated = true;
      state.token = action.payload;
    },
    logout(state) {
      localStorage.removeItem("token");
      state.isAuthenticated = false;
      state.token = "";
    },
  },
});

export const authActions = authSlice.actions;

export default authSlice.reducer;

초기상태값을 설정하는 부분은 비슷하다. 다만 액션을 설정할 때 각각을 함수로 정의하는 것이 아니라 reducer 안에 객체 형태로 바뀔 상태값을 지정해준다. 그리고 함수를 상태값에 넣는 것이 아니라 실제 사용할 주체가 useSelector를 통해서 조회 , usedispatch를 통해서 변경한다.
다음 카테고리에서 좀 더 자세히 알아보자.

상태값 사용 기존 코드 - 리팩토링 코드

이제 상태값을 조회 변경하는 경우 달라지는 코드를 정리해보자.

단순 조회, 단순 변경

// 기존 Context api 
// navabar - conditional rendering

import AuthContext from "../../store/auth-context";
import GoalContext from "../../store/goal-context";

const Navbar = ({ toggle }) => {
  const authCtx = useContext(AuthContext);
  const goalCtx = useContext(GoalContext);
  const isLoggendIn = authCtx.isLoggendIn;
  const logoutHandler = () => {
    authCtx.logout();
    goalCtx.reset();
  };

작성한 context를 가져와서 해당 컨텍스트를 각각 정의해준다.
그리고 해당하는 컨텍스트 안에서 상태값함수를 조회해준다.


import { authActions } from "../../store/auth";
import { goalActions } from "../../store/goal";

const Navbar = ({ toggle }) => {
  const dispatch = useDispatch();
  const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
  const logoutHandler = () => {
    dispatch(authActions.logout());
    dispatch(goalActions.reset());
  };
  const togglehome = () => {
    scroll.scrollToTop();
  };

useSelector : 상태값 조회
useDispatch : 해당하는 상태값 변경

위 두가지 훅을 이용해서 원하는 작업을 할 수 있다.

인자를 받아서 변경

  const getAllGoals = async () => {
    // event.preventDefault();
    const config = {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    };

    await axios
      .get("/goals", config)
      .then((response) => {
        if (response.data.success) {
          dispatch(goalActions.update(response.data.data));
        }
      })// 이렇게 해당 액션에 인자로 payload를 넣어주면 된다.
      .catch((err) => {
        const statusCode = err.message.slice(-3, err.message.length);
        console.log(`${statusCode} ${err.message}`);
      });
  };

dispatch 안에 액션에 인자로 값을 넣어주면 그 값은 action.payload로 store로 전달이 된다.

const goalSlice = createSlice({
  name: "goalList",
  initialState: initialAuthState,
  reducers: {
    update(state, action) {
      state.goals = action.payload;
    },
    reset(state) {
      state.goals = [];
    },
    addItem(state, action) {
      state.goals.push(action.payload);
    },
  },
});

update(state.action){state.goals = action.payload}처럼 지정해주면 인자로 받은 값을 action.payload로 사용할 수 있다.

profile
기록을 통해 한 걸음씩 성장ing!

0개의 댓글