Context의 대안인 Redux 사용해보기

학습 목표

  • What is Redux? And Why?
  • Redux Basics & Using Redux with React
  • Redux Toolkit

Understanding Redux: Managing App-Wid State with Redux

What is Redux? And Why?

Redux는 cross-component 또는 app-wide state를 위한 state 관리 시스템이다.

Local State Cross-Component State App-Wide State
하나의 컴포넌트에 속해 있는 State 다수의 컴포넌트에 영향을 미치는 State 전체의 앱에 영향을 미치는 State
useState() 또는 useReducer()로 관리해야 함 prop chains나 drilling이 필요함 prop chains나 drilling이 필요함

물론 Redux 대신 Context API를 사용하여도 되는데 왜 Redux를 사용해야만 할까?

그 이유에는 Context는 2가지 단점을 가지고 있기 때문이다. 먼저 복잡한 앱에선 Context Provider 컴포넌트가 커지고 중첩된다. 다음으로 자주 반복되는 state의 변경의 경우 최적화 되어 있지 않다. 또한 React 팀원이 올린 공식 언급이 있는데 Context는 Redux를 완전히 대체하기 어렵다는 글이었다.

Redux가 작동하는 방식은 먼저 오직 하나의 중앙 데이터 저장소를 가진다. 컴포넌트가 저장소를 구독하면 데이터가 변경될 때마다 저장소가 컴포넌트에게 알려주게 된다. 이 때 데이터를 조작하는 것은 Reducer 함수이다. useReducer()가 아닌 일반적인 개념이다. 이는 저장된 데이터를 변경하는 역할을 한다. 그렇다면 컴포넌트의 트리거 역할은 누가 할까? 바로 Action이 한다. Action은 단순한 JS 객체이며 Dispatch하면 트리거가 된다. 그러면 Reducer 함수가 Action을 읽고 원하는 행동을 한다.

Redux Basics & Using Redux with React

Node.js에서 사용하는 Redux

Redux를 node.js에서 실행해볼 수 있다.

  1. npm init -y으로 초기화 해준다. -y는 답변 스킵.
  2. npm install redux로 설치해준다.
  3. Redux를 불러오고 저장소를 만든다.
const redux = require("redux"); // redux 불러오기

const store = redux.createStore(); // 저장소 만들기
  1. 저장소에 넣을 Reducer 함수를 만든다. 이는 redux 라이브러리를 통해서 호출될 것이고 항상 2개 파라미터(old state + dispatched action)를 받을 것이다. 항상 새로운 상태 객체를 반환해야 한다. 즉, 순수 함수이다.

다시 강조하지만 이전값을 변형하는 것은 좋지 않다. JS는 객체와 배열은 참조형이기 때문이다. 정확한 주소는 스택이 아닌 힙에 저장되기 때문에 버그나 이상이 생길 수 있기 때문이다.

참조 vs 원시 값 설명 더 보기

const counterReducer = (state = { counter: 0 }, action) => {
  if (action.type === "increment") {
    return {
      counter: state.counter + 1,
    };
  }
  if (action.type === "decrement") {
    return {
      counter: state.counter - 1,
    };
  }

  return state;
};

const store = redux.createStore(counterReducer);
  1. 저장소를 구독한다.
const counterSubscriber = () => {
  const latestState = store.getState();
  console.log(latestState);
};

store.subscribe(counterSubscriber);
  1. action을 발송한다. 이는 JS 객체이다.
store.dispatch({ type: "increment" }); // 1 증가
store.dispatch({ type: "decrement" }); // 1 감소
  1. node.js로 실행한다. node [filename].js
// 결과
{ counter: 1 }
{ counter: 0 }

React.js에서 사용하는 Redux

이제 React에서 사용해보자.

  1. npm install redux react-redux redux와 react-redux를 설치한다.
  2. index.js 파일을 만들고 저장소를 생성한다.
import { createStore } from "redux";

const counterReducer = (state = { counter: 0 }, action) => {
  // 생략
};

const store = createStore(counterReducer);

export default store;
  1. 컴포넌트에 공급한다. store를 prop으로 넘겨준다.
import { Provider } from "react-redux";
import store from "./store";

root.render(
  <Provider store={store}>
    <App />
  </Provider>
);
함수형 컴포넌트
  1. react-redux에서 제공하는 커스텀 훅을 사용한다. useStore, useSelector 두가지를 사용할 수 있다. 간단하게 사용할 수 있는 건 useSelector이다. 이는 저장소가 관리하는 State 부분을 자동으로 선택할 수 있기 때문이다.

이는 자동으로 구독을 관리해준다.

import { useSelector } from "react-redux";

const counter = useSelector((state) => state.counter);
// 만약 다수의 state를 가지고 있다면 하나 더 만들어주면 된다.
const show = useSelector((state) => state.showCounter);
  1. action을 dispatch해보자. 이때도 useDispatch 훅을 사용하면 된다.
import { useDispatch } from "react-redux";

const dispatch = useDispatch();

const incrementHandler = () => {
  dispatch({ type: "increment" });
};

return <button onClick={incrementHandler}>increment</button>;
클래스 컴포넌트
  1. 클래스 컴포넌트인 경우엔 connect함수를 사용하면 된다. 두 인자를 받는데 첫 번째 함수는 redux State를 props으로 맵한다. 그러면 컴포넌트에 사용할 수 있다. key는 prop 네임, value는 state의 속성이다.

  2. 두 번째 함수는 dispatch 함수를 props에 저장하는 것이다. key는 이름, value는 다른 함수다.

const mapStateToProps = (state) => {
  return {
    counter: state.counter,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    increment: () => dispatch({ type: "increment" }),
    decrement: () => dispatch({ type: "decrement" }),
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(Counter);

컴포넌트에서 사용법

class Counter extends Component {
  incrementHandler() {
    this.props.increment();
  }
  render() {
    return (
      <div>
        <div className={classes.value}>{this.props.counter}</div>
        <button onClick={this.incrementHandler.bind(this)}>increment</button>
      </div>
    );
  }
}

action에 payload 보내기

만약 payload를 보내고 싶다면 action 객체를 추가해주면 된다.

// store/index.js
if (action.type === "increase") {
  return {
    counter: state.counter + action.amount,
  };
}

// Counter.js
const increaseHandler = () => {
  dispatch({ type: "increase", amount: 10 });
};

Redux Toolkit

사용하다보면 action.type이 많아질수록 오타가 생기면 귀찮아진다. 이에 대한 해결책으론 상수를 만들어서 export해 사용한다던지, reducer를 더 작게 나눈다던지 등등 다양한 방법이 있지만 Redux Toolkit을 사용하면 더 편리하게 사용할 수 있다. 필수는 아니지만 있으면 좋다~

Redux Toolkit 공식 문서

  1. npm install @reduxjs/toolkit 만약 redux를 설치했다면 삭제해야 한다. Toolkit에 이미 포함되어 있기 때문이다.
  2. createSlice, createReducer를 사용할 수 있다. Slice가 한 번에 여러가지를 단축시킬 수 있어 더 강력하다.

앞에서는 이전값을 변경하지 말라고 했지만 toolkit 내부 라이브러리에서 이전값을 변경하는 코드를 알아서 감지하고 새로운 객체로 반환해준다. 그래서 여기서는 신경쓸 필요가 없다.

const counterSlice = createSlice({
  name: "counter", // 식별자가 필요하다. 아무거나 지어도 괜찮다.
  initialState,
  reducers: {
    increment(state) {
      state.counter++;
    },
    increase(state, action) {
      state.counter = state.counter + action.amount;
    },
  },
});
  1. 저장소에 slice를 연결시키려면 일단 redux의 combineReducers를 이용해 합칠 수 있지만, redux를 버리고 toolkit에서 configureStore을 이용하면 간단하게 가능하다.
const store = configureStore({ reducer: counterSlice.reducer });

만약 slice가 여러개라면 이렇게 쓰면 알아서 병합해준다. 이렇게 쓰게 되면 useSelector에서 접근 방식을 추가해야 한다.

// store/index.js
const store = configureStore({
  reducer: { counter: counterSlice.reducer, bla: blaSlice.reducer },
});

// Counter.js
const counter = useSelector((state) => state.counter.counter); // counter 리듀서 안의 counter 값이라는 의미
  1. action을 연결하려면 export 하면 끝이다. 간단! 참고로 slice는 payload라는 이름으로 받기 때문에 payload가 있다면 action의 payload 속성으로 접근할 수 있다.
// store/index.js

// slice 내의 reducers
increase(state, action) {
  state.counter = state.counter + action.payload; // payload 키로 접근
},

export const counterActions = counterSlice.actions;

// Counter.js
import { counterActions } from "../store";

const incrementHandler = () => {
  dispatch(counterActions.increment());
};
const increaseHandler = () => {
  dispatch(counterActions.increase(10));
};

무조건 Redux가 좋은 것은 아니다. 라이브러리를 추가해야하다 보니 앱 크기가 커진다는 단점이 있기에 상황에 맞게 선택하면 된다


강의명

  • Udemy : React 완벽 가이드

카테고리:

업데이트:

댓글남기기