React vol.9 (Redux)
개요
React JS 를 공부하면서 기본을 정리해두고자 작성
Redux?
Redux에 대해 설명하기 전에 우선 알아야 하는게 있다.
SPA로 웹앱을 제작할 때 절대 빼 놓을수 없는 요소가 "상태"이다. 이러한 상태에도 스코프에 따라 분류하면
- Local State: useState, useReducer 같은 친구들을 이용해 현 컴포넌트 안에서만 쓰여지는 상태
- Crros-Component State: useState, useReducer로 상태를 만들지만, 그걸 props chains prop drilling으로 여러 컴포넌트에서 쓰여지는 상태
- App-Wide State: context-api같은 친구로 앱의 최상단에서 props chains prop drilling으로 전역으로 쓰여지는 상태
여기서 App-Wide State의 상태관리 문제해결을 위해 쓸 수 있는 라이브러리가 Redux이다.
(Redux는 React의 상태관리를 위한 라이브러리가 아니다 즉, Redux는 상태관리 문제해결을 원하는 어떤 곳에서든 사용될 수 있다.)
Redux vs Context API
근데 위에서 적어 놓은 것 처럼 이미 App-Wide State의 상태관리를 react의 내장 Context API 로 관리해 줄 수 있는데 왜 Redux가 왜 쓰여지고 있을까? 물론 이유가 있다고 생각해 찾아본 결과를 말하자면,
- 앱의 규모가 커지면 커질수록 전역상태로 관리해야 하는 항목들도 비대해진다. 그런 상황에서 Context로 전역상태를 관리하는 것은 점점 더 복잡해진다.
- Performance 관련으로도 이슈가 있다. Context API는 자주 바뀌는 상태에 대해서 최적화가 되어 있지 않다.
이러한 이유가 있기 때문에 React를 배운다면 Redux로 당연히 배워야 한다.
Redux의 핵심 구성요소
- State: store의 상태(데이터)
- Mutates: store의 상태를 바꾸는 동작
- Reducer: Mutates를 실제로 실행하는 친구
- Action: Reducer에게 특정한 상태를 바꿔달라고 의뢰하는 동작
- Dispatch: Action을 실제로 실행하는 친구
- Subsciption: State의 상태가 바뀌면 자동으로 평가되는 친구
코드로 간단하게 풀어보자면
npm i redux
const redux = require("redux");
// Reducer
const counterReducer = (state = { counter: 0 }, action) => {
// counter를 1 올리는 Mutate를 할게
if (action.type === "INCREMENT") {
return {
counter: state.counter + 1,
};
}
return state;
};
const store = redux.createStore(counterReducer);
// Subscription 등록
const counterSubscriber = () => {
const lastState = store.getState();
console.log("Subscribe Trigger!", lastState);
};
store.subscribe(counterSubscriber);
console.log(store.getState());
// type: INCREMENT라는 Action을 날리는 dispatch
store.dispatch({ type: "INCREMENT" });
실행 결과
React에서 Redux를 사용하는 경우에는 쉽게 사용할 수 있게 react-redux라는 라이브러리가 존재한다.
npm i redux react-redux
src/store/index.js
import { createStore } from "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 = createStore(counterReducer);
export default store;
src/index.js
import React from "react";
import ReactDOM from "react-dom";
import store from "./store/index.js";
import { Provider } from "react-redux";
import "./index.css";
import App from "./App";
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
src/components/Counter.js
import { useSelector, useDispatch } from "react-redux";
import classes from "./Counter.module.css";
const Counter = () => {
const dispatch = useDispatch();
const counter = useSelector((state) => state.counter); // 자동으로 subscription 등록
const toggleCounterHandler = () => {};
const incrementHandler = () => {
dispatch({ type: "INCREMENT" });
};
const decrementHandler = () => {
dispatch({ type: "DECREMENT" });
};
return (
<main className={classes.counter}>
<h1>Redux Counter</h1>
<div className={classes.value}>{counter}</div>
<div>
<button onClick={incrementHandler}>Increment</button>
<button onClick={decrementHandler}>Decrement</button>
</div>
<button onClick={toggleCounterHandler}>Toggle Counter</button>
</main>
);
};
export default Counter;
useDispatch: dispatch를 날리기 위해 쓰는친구
useSelector: store에서 데이터를 취득하기 위한 친구 (자동 subscription 등록 내부적으로 store의 상태가 변하면 useSelector에 넘긴 콜백함수가 자동으로 실행되서 리렌더링 되는 구조?를 갖고 있다고 추측된다.)