OLD/React

[React] React Redux_01 (Redux 기본)

joseph0926 2023. 3. 5. 10:03

앞선 글에서 Context를 살펴보았고,

Context를 활용하면 상태관리가 엄청 효율적으로 가능해지고, 전역적으로 관리 가능하다는 것을 알아봤다

 

하지만 몇몇 단점들도 존재했다.

그리고 마지막에 이러한 단점을 해결해주는 대안이 존재한다고 언급했었는데,

그것이 지금 알아볼 Redux다.

 

이 글에서 알아볼 것

  • Redux란?
  • Redux 작동방식 (이론)
  • Redux 작동방식 (코드)
  • Redux 특징 및 한계

 

1. Redux란?

  • 리덕스는 크로스컴포넌트state or 앱 와이드state에서 상태를 관리하도록 도와주는 시스템이다.
  • 여기서 말하는 크로스컴포넌트 state, 앱 와이드 state란 뭘까?
    1. 로컬 state
      • 데이터가 변경되어서 싱글 컴포넌트에 영향을 미치는 상태
      • 주로 useState를 사용하고, 좀 복잡하면 sueReducer 사용함
      • ex) 토글 버튼..
    2. 크로스 컴포넌트 state
      • 데이터가 변경되어서 다수의 컴포넌트에 영향을 미치는 상태
      • useState, useReducer 사용하고 props 체인을 활용해 관리함
      • ex) 모달..
    3. 앱 와이드 state
      • 데이터가 변경되어서 모든 컴포넌트에 영향을 미치는 상태
      • useState, useReducer 사용하고 props 체인을 활용해 관리함
      • ex) 사용자 인증 (로그인..)
  • 2번, 3번을 처리하는데 물론 props 체인(드릴링)을 사용하여 해결할 수 있지만, 힘들다는 것을 알고있고, 때문에 앞서 Context로 처리하는 방법을 알아봤었다..
  • 이러한 Context처럼 2번, 3번 경우에 활용되는 기술이 Redux다.

 

2. Redux 작동방식 (이론)

  • 리덕스를 사용하게 되면, 리덕스가 제공해주는 하나의 데이터(State) 저장소를 갖게된다
    • 이 저장소의 모든 상태를 저장한다
      • 근데, 그러면 관리가 어렵지 않을까?
      • 아니다,, 왜냐하면, 우리가 직접 관리하지 않고 리덕스에서 관리하기떄문이다.
  • 저장소에 저장되어있는 상태를 여러 컴포넌트에서 사용할수있다
    • 컴포넌트가 저장소를 구독하고, 만약 저장되어있는 상태가 변경되면 저장소가 구독되어있는 컴포넌트에게 알려준다
    • 이렇게 변경된 상태를 컴포넌트는 반영한다
  • 저장소에 저장되어있는 상태를 바꿀수있다.
    • 단, 절대로 저장된 데이터를 컴포넌트가 직접 조작하지는 않는다
      • 이것에 대해서는 뒤에 Redux 한계 부분에서 자세히 알아보겠다
    • 대신 Reducer 함수를 사용한다 (useReducer랑 상관없음)
      • 이 함수는 저장소의 데이터를 변경(업데이트)하는 것을 담당하는 함수다
      • 이전 상태와 디스패치된 액션을 인풋값으로 받고 / 새로운 상태를 아웃풋으로 내놓는다
      • 단, 이 함수는 순수함수여야한다
  • 정리하자면, 컴포넌트에서의 작업이 데이터 변경을 트리거하게되면 Reducer 함수가 그걸 인식하고 저장된 데이터를 조작한다는것인데,,
  • 그러면, 컴포넌트와 Reducer함수는 어떻게 연결되어있을까?
    • 액션을 이용한다.
    • 컴포넌트가 어떠한 액션을 디스패치한다
    • 이 액션을 받은 Reducer 함수는 원하는 작업을 인지하고 수행한다

이렇게 동작하는 것이 Redux다..

 

글로 잘 정리한다고 해봤지만, 아무래도 시각적으로 확인하는 것이 더 이해가 빠르다

발퀄인 이유는 그림판으로 그렸다...ㅜㅜ

아까 글로만으로 볼때보단 훨씬 이해가 잘가지만, 역시 코드를 통해 확인하는 것이 제일일것같다.

 

 

3. Redux 작동방식 (코드)

  1. npm install redux react-redux
  2. 리덕스 저장소 생성
    1. import { createStore } from "redux";
    2. const store = createStore();
    3. 리덕스 리듀서 함수 생성
    4. const store = createStore(counterReducer);
    5. export default store;

여기까지 코드

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

const store = createStore(counterReducer);
export default store;
  • state = {counter: 0} → 초기값
  • return state; → 타입 다 맞지않을때 반환값

3. 저장소를 리액트앱에 제공하기

  1. index.js에서 import {Provider} from "react-redux”
  2. 그후 App을 Provider로 감싸줌
  3. Provider 프로퍼티 store에 우리가 내보낸 store 넣어줌

여기까지 코드

import { Provider } from "react-redux";
import store from "./store";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

 

4. 리덕스 데이터 컴포넌트에서 사용하기

  1. 리덕스 데이터를 사용할 컴포넌트에서, import {useSelector} from "react-redux”
  2. useSelector 는 자동으로 구독을 설정한다
  3. useSelector 함수는 함수를 인자로 받는다
    • 이 함수의 함수는 state를 인자로 받고, 이로인해 우리가 리덕스에 저장된 state에 접근 가능하다
  4. const counter = useSelector((state) => {return state.counter});

 

5. 컴포넌트에서 리덕스 데이터 변경하기

  1. 컴포넌트에서 액션을 디스패치한다
    1. 컴포넌트에서 import { useDispatch } from "react-redux";
    2. const dispatchFn = useDispatch();
      1. useDispatch는 어떤 인자도 받지 않고, 디스패치 함수를 반환한다
    3. 이 dispatchFn은 리덕에 액션을 보내준다
      1. dispatchFn({type: “스토어에서지정한식별자”})

여기까지 코드

import { useSelector, useDispatch } from "react-redux";

const Counter = () => {
  const dispatchFn = useDispatch();
  
  const counter = useSelector((state) => {
    return state.counter.counter;
  });
  
  const addHandler = () => {
    dispatchFn(counterAction.add());
  };
}

 

6. 컴포넌트에서 저장소로 액션과 함께 페이로드도 보내기

  1. 액션을 보내는 컴포넌트에서 추가로 보낼 payload를 설정한다
  2. 리덕스 스토어에서는 그 payload를 action.payload로 사용가능하다

여기까지 코드

// store.js
const add5Handler = () => {
    dispatchFn({ type: "ADD5", payload: 5 });
  };

// Counter.js
if (action.type === "ADD5") {
    return {
      counter: state.counter + action.payload,
    };
  }

 

이렇게 진행된다...

 

4. Redux 특징 및 한계

  • 특징
    • 앞서 말했듯이 리듀서 함수는 오래된 스냅샷을 받아, 최신 스냅샷(객체)를 반환한다
    • 여기서 중요한점은, 최신 객체가 기존 객체와 합쳐지는것이 아닌, 덮어쓴다
      • 따라서, 덮어쓸때는 의도한 경우가 아니라면, 기존 객체의 모든 프로퍼티를 포함시켜야한다..(값을 바꾸지 않을 프로퍼티는 state.프로퍼티..로 지정하면된다)
        • 그러면 들수있는 의문은 객체로 반환하지말고, 특정 프로퍼티에 직접 접근해서 그 값을 바꾸고, 그 프로퍼티만 리턴하면 안되는걸까?
if (action.type === "ADD") {
    return {
      counter: state.counter + 1,
      showCounter: state.showCounter,
    };
    
    /* state.counter++;
    return state */
    
  }
  • 그러니까, 주석 처리한 코드처럼 처리하면 안되는걸까?
  • 작동은 하나, 절대 반드시 해서는 안되는 짓이다. 왜일까?

 

  • 한계
    • 리덕스에는 상태 변경 불가성 문제가 존재한다
      • 무슨말이냐면, 절대 기존의 state를 변경해서는 안된다
    • 따라서, 특징에서 살펴본것처럼 덮어쓰는 방식으로 동작하고, 그렇게 설계해야하는것이다
      • 근데 이게 관리할게 적을때는 지키는것이 어렵지 않지만, 코드가 복잡해지면 (중첩등이 존재하면) 이 규칙을 지키기 힘들다

 

 

나는 여기까지 이해하고 느꼈던 것이 하나 존재했었다.

 

"Context에 비해 뭐가 나아진거지?.."

 

Context의 단점중인 하나인 잦은 변경에 대한 성능문제를 해결해주는것은 알겠으나,, 그건 공부할때는 눈에 직접 보이지 않는다,, 즉, 전혀 체감되는것이 없었다.

또한, Context의 다른 단점인 관리할것이 많아지면 복잡해지고 점점 커진다,, 라는것도 조금 나아졌지만 어차피 마찬가지 아닌가?

게다가 뒤에서 서술하겠지만 Redux에도 당연히 단점? 한계?도 존재한다

 

이러한 의문에 대한 해결은 다음글에서 살펴보자