React.js 리덕스

강정우·2023년 1월 26일
0

react.js

목록 보기
31/45
post-thumbnail

context의 단점


우리는 단순히 한개의 컴포넌트에서만 이루어지는 Local State, 또는 Property로 data를 주고받는 Cross-Compoents State, 또는 App 전체에 영향을 주는 유형검사 또는 context를 사용했다.

  • 리덕스도 마찬가지로 같은 일을 한다. 그럼 왜? redux가 별도로 존재할까?
  • Context 훅에는 "잠재적인" 단점이 몇가지가 있다. 여기서 잠재적인 표현을 쓴 이유는 우리가 웹앱을 설계하면서 영향을 받지 않을 수도 있기 때문이다.
    그것은 바로 기업 수준의 프로젝트를 할 때 발생한다.

  • 즉 이렇게 되면 한가지의 context로 묶을 수도 있긴 한데 그럼 인증, 테마, 모달, 사용자 입력 등 매우 많은 것들을 관리하고 이렇게 되면 유지보수가 힘들어진다.

  • 또한 context는 리액트 개발자 피셜 모달, 사용자 입력 같이 매우 빈번하게 변경이 이루어지는 곳에는 사용하지 않을 것을 권장하고 있다.

  • 즉 요약하자면
  1. setup과 관리가 굉장히 복잡하고 유지보수가 힘들어진다.
  2. 퍼포먼스 측면으로 봐도 수정이 빈번하게 일어나는 곳에는 context가 별로 좋지 못하다
    라는 이유때문에 redux를 사용해야한다는 것이다.

리덕스란?

  • cross component 또는 "app wide state"를 위한 state 매니징 시스템이다.
  • 하지만 react에서만 쓰이는 것은 절대 아니다. 바닐라 JS에서도 사용 가능하다.
  1. redux는 하나의 central Data(state) Store를 갖고있다. 즉, cross component든, app wide state든 모두 하나의 저장소에 저장되어있다는 뜻이다. 그리고 컴포넌트는 그 저장소에 접근할 수 있다.
  2. 만약 컴포넌트가 변경되었다면 그에 맞춰 대응하고 UI를 업데이트하길 요청한다.
    이 컴포넌트를 위해 우리 중앙 저장소에 대한 "구독"을 설정한다. 컴포넌트가 저장소를 구독하고 데이터가 변경될 때마다 저장소가 컴포넌트에 알려주게 되면 컴포넌트는 필요한 데이터를 받게 된다.
  • 이때 컴포넌트는 절대로 저장된 데이터를 직접 조작하지 않는다. 그럼 어캄? => Reducer를 사용한다. 이는 useReducer 훅과는 전혀 다른 concept이다.
  1. 컴포넌트에서 어떠한 action을 trigger하면 Rudecer함수로 Forward하게 되고 이 작업을 Reducer가 대신 하게 된다. => 항상 new state snapshot을 return한다.
  2. data store에 기존 state를 업뎃하고 이는 구독중인 component가 알게되고 이를 업뎃한다.

reducer function

  • 리듀서 함수는 일반 js함수와 같이 매개변수를 받고 값을 return하는데 이때 항상 2개의 값(state, action)을 받고 Object(사실 어떠한 값도 가능)를 (=> 얘가 state가 되는 거) 반환한다.
  • 그 뜻은 새로운 객체가 생성되는 것이 아닌 기존의 객체를 덮어 쓴다는 것이다. 왜? state니까

예제 코드 (without react.js)

const redux = require("redux");

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);

const counterSubscriber = () => {
  const latestState = store.getState();
  console.log(latestState);
};

store.subscribe(counterSubscriber);

store.dispatch({type:"increment"});
store.dispatch({type:"decrement"});

### result
{ counter : 1 }
{ counter : 0 }

예제 코드 (with react.js)

import redux from "redux";    // => redux 전체 가져오기
import {createStore} from "redux";    // => redux library의 특정 부분 가져오기

const initialState = { counter: 0, showCounter: true };

const counterReducer = (state = initialState, action) => {
  if (action.type === "increment") {
    return { counter: state.counter + 1, showCounter: state.showCounter };
  }
  if (action.type === "increase") {
    return {
      counter: state.counter + action.value,
      showCounter: state.showCounter,
    };
  }
  if (action.type === "decrement") {
    return { counter: state.counter - 1, showCounter: state.showCounter };
  }
  if (action.type === "toggle") {
    return { counter: state.counter, showCounter: !state.showCounter };
  }
  return state;
};

const store = createStore(counterReducer);

export default store;
  • store 폴더에 위와 같이 생성을 하였다면 이제 이 리덕스 코드를 사용할 곳을 찾아야한다.
    앞서 언급했듯 리덕스는 단 1개의 store를 갖고있고 이를 딱 한 번 사용한다면 당연 최상위 root에서 사용하는 것이 맞기에 index.js에서 루트 컴포넌트인 <App/>을 감싸주어야한다.
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";

import "./index.css";
import App from "./App";
import store from "./store/reduxLogix";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);
  • 이제 해당 컴포넌트에서 사용해 보도록 하자 이때 당연 "react-redux"로 부터 import를 받아와서 사용해야하고
    이때 2가지의 선택권이 있다.
  1. useStore
  2. useSelector : 저장소가 관리하는 상태 부분을 우리가 자동으로 선택할 수 있다.
  3. connect : 만약 class component를 사용중이라면 connect를 wrapper로써 사용할 수 있다.
  • 둘다 redux 팀이 만든 custom hook이며 저장소에 직접 access할 수 있다. 하디만 useSelector가 사용이 편리함으로 2번을 사용하자.

useSelector

  • useSelector는 매개변수로 함수를 받는다. 이때 이 함순는 매개변수로 redux가 관리하는 state를 받는다.
    그리고 우리가 추출하려는 state를 return할 것이다.
  • 사용법
useSelector(state=>state.counter);
  • 또한 redux에서 이 컴포넌트를 위해 store에 자동으로 subscribe를 설정한다.
    그래서 store에서 data가 변경될 때마다 해당 컴포넌트가 자동으로 업뎃되고 최신 state를 받게 된다.

예제 코드

import classes from "./Counter.module.css";
import { useSelector, useDispatch } from "react-redux";

const Counter = () => {
  const dispatch = useDispatch();
  const counter = useSelector((state) => state.counter);
  const show = useSelector((state) => state.showCounter);

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

  const increaseHandler = () => {
    dispatch({ type: "increase", value: 5 });
  };

  const decrementHandler = () => {
    dispatch({ type: "decrement" });
  };

  const toggleCounterHandler = () => {
    dispatch({ type: "toggle" });
  };

  return (
    <main className={classes.counter}>
      <h1>Redux Counter</h1>
      {show && <div className={classes.value}>{counter}</div>}
      <button onClick={increaseHandler}>더하기 +5</button>
      <button onClick={incrementHandler}>더하기 +1</button>
      <button onClick={decrementHandler}>빼기 -1</button>
      <button onClick={toggleCounterHandler}>Toggle Counter</button>
    </main>
  );
};

export default Counter;

Redux 사용시 주의할 점

  • 앞서 언급했듯 redux store의 reducer함수는 객체(state)를 반환한다. 이때 새로운 객체가 아닌 기존의 객체를 덮어씌운다고 했는데 그래서 많은 사람들이 많은 사람들이 redux store에서
const counterReducer = (state = initialState, action) => {
  if (action.type === "increment") {
    return { counter: state.counter + 1, showCounter: state.showCounter };
  }
  return state;
};
  • 위 처럼 사용해야하는데
const counterReducer = (state = initialState, action) => {
  if (action.type === "increment") {
    state.counter++;
     return state;
(또는)return { counter: state.counter, showCounter: state.showCounter };
  }
  return state;
};
  • 이러한 실수를 해버린다. 이렇게 되면 가장 큰 문제가 작동은 하는데 문제는 JS에서 객체와 array가 reference값이기에 기존의 state를 변형시켜버린다. 이는 useMemo를 사용할 때도 같은 맥락이었다.
    만약 위와같이 사용해버리면
  1. 이로 인해 버그인 예측 불가능한 동작이 발생할 수 있고
  2. 프로그램을 디버깅하는 것도 어려워 질 수 있다.
  3. 만약 버그가 발생하지 않더라도 state가 동기화되지 않는 더 큰 애플리케이션에서 예기치 않은 부작용이 생길 수 있다.
  4. 갑자기 UI가 더이상 state를 정확히 반영하지않을 수 있다.

즉, 정말 중요한 것은 절대 기존의 state 형식을 변경해선 안 된다. 따라서 항상 새로운 객체가 배열을 만들어야한다.

참조(class based component)

import {Component} from "react";
import {connect} from "react-redux";

class Counter extends Components{
  
  incrementHandler(){
    this.props.increment();
  }
  decrementHandler(){
    this.props.decrement();
  }
  toggleCounterHandler(){}
  
  render(){
      return (
        <main className={classes.counter}>
          <h1>Redux Counter</h1>
          <div className={classes.value}>{this.props.counter}</div>
          <button onClick={this.incrementHandler.bind(this)}>더하기 +1</button>
          <button onClick={this.decrementHandler.bind(this)}>빼기 -1</button>
          <button onClick={this.toggleCounterHandler}>Toggle Counter</button>
        </main>
		);
	};
}

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);
profile
智(지)! 德(덕)! 體(체)!

0개의 댓글