오늘은 리액트 리덕스에 대해 알아보자.
useState를 사용할 경우 컴포넌트 내부에서 state를 만들고, 함수로 state를 바꾼다.
때문에 state는 컴포넌트에 종속되는데 redux는 컴포넌트에 종속되지 않고, 상태관리를 밖에서 한다.
프로젝트 루트 레벨에서 store라는 곳에 state를 저장하고, 모든 컴포넌트는 store에 구독을 하며 state와 그 state를 바꾸는 함수를 전달받게 된다. 함수를 바꿈으로 state가 바뀌면 해당 state를 바라보고 있는 컴포넌트는 모두 재렌더링이 일어난다.
사용중인 state가 자식 -> 자식 -> 자식 컴포넌트에서 사용한다면 props를 계속 내려준다.
또 그 state를 바꾸기 위한 함수를 또 내리게 된다. 이렇게 된다면 나중에 불필요한 렌더링이 일어나기 쉽고, 코딩에서도 실수 하기가 쉬워진다. 혹시나 문제가 생겼을 때 어디서 문제가 생긴건지 찾아내기가 번거로워진다.
하지만 redux에서 store는 프로젝트의 루트 페이지에 위치하고, 해당 store를 구독하는 컴포넌트는 모두 state와 state를 바꾸는 함수를 받을 수 있다.
어느 위치에 있든 상관 없이 상위 컴포넌트를 거치지 않고 한번에 받을 수 있다는 뜻이다.
리덕스는 기본적으로 flux패턴을 따른다.
Action -> Dispatch -> Store -> View
redux의 데이터 흐름은 동일하게 단방향으로 view(컴포넌트)에서 Dispatch(store에서 주는 state를 바꾸는 함수)라는 함수를 통해 action(Dispatch 함수 이름)이 발동되고 reducer에 정의 된 로직에 따라 store의 state가 변하고 그 state를 쓰는 view(컴포넌트)가 변하는 흐름을 따른다.
사용할 패키지를 설치한다.
npm i redux react-redux redux-devtools-extension redux-logger
redux-devtools-extension은 크롬 확장 프로그램에서 redux dev tools를 통해 설치할 수 있고, redux의 데이터 흐름을 알아보기 쉽게 하기 위해 사용한다.
redux-logger는 redux를 통해 바뀔 이전 state, dispatch 실행으로 바뀐 state가 콘솔에 찍혀 쉽게 디버깅 할 수 있도록 돕는 라이브러리다.
reducer는 store에 들어갈 state와 state를 바꿀 함수를 정의하는 곳이다.
기본적으로 순수함수로 코딩하고 불변성을 지켜야한다.
redux는 이전 state와 바뀐 state를 구분하는 방법이 참조값이 바뀌었는지 확인하고, 참조값이 바뀌면, state가 바뀌었다고 redux가 인식하여, 해당 state를 사용하는 컴포넌트에게 재렌더링을 요청하기 때문이다.
때문에 state.test = action.test와 같이 직접적으로 state를 변경하면 참조값이 변하지 않아 redux는 값이 바뀌었다고 인식하지 않고 재렌더링이 일어나지 않는다.
state.test = {...test, action.test}
또는 immer라는 라이브러리를 사용해 쉽게 불변성을 유지한다.
//reducers/index.js
//root reducer
import { combineReducers } from "redux";
import conter from "./conter";
//여러 reducer를 사용하는 경우 reducer를 하나로 묶어주는 메서드.
//store에 저장되는 reducer는 오직 1개다.
const rootReducer = combineReducers({
conter
});
export default rootReducer;
//reducers/counter.js
//reducer가 많아지면 action상수가 중복될 수 있어 action 이름 앞에 파일 이름을 넣는다.
export const INCRESE = "COUNT/INCRESE";
export const increseCount = count => ({ type: INCRESE, cont });
const initalState = {
count: 0
};
const conter = (state = initalState, action) => {
switch (action.type) {
case INCRESE:
return {
...state,
count: state.count + 1
};
//default를 쓰지 않으면 맨 처음 state에 count값이 undefined가 나온다.
default:
return state;
}
};
//index.js
import React from "react";
import ReactDOM from "react-dom";
import { createStore, applyMiddleware, compose } from "redux";
import { Provider } from "react-redux";
import logger from "redux-logger";
import { composeWithDevTools } from "redux-devtools-extension";
import App from "./App";
import rootReducer from "./reducers";
//배포 레벨에서는 리덕시 발동시 찍히는 logger를 사용하지 않는다.
const enhacer =
process.env.NODE_ENV === "production"
? comose(applyMiddleware())
: comoseWithDevTools(applyMiddleware(looger));
//위에서 만든 reducer를 스토어 만들 때 넣어준다.
const store = createStore(rootReducer, enhancer);
ReactDOM.render(
//만든 store를 앱 상위에 넣어준다.
<Provider store={store}>
<App />
</Provider>
document.getElementById('root'),
);
import { useSelector, useDispatch } from "react-redux";
import { increaseCount } from "reducers/count";
//dispatch를 사용하기 위한 준비
const dispatch = useDispatch();
//store에 접근하여 state 가져오기
const { count } = useSelector(state => state.counter);
const increse = () => {
//store에 있는 state 바꾸는 함수 실행
dispatch(increaseCount());
};
const Counter = () => {
return (
<div>
{count}
<button onClick={increse}>증가</button>
</div>
);
};
export default Counter;
위 처럼 store에서 useDispatch, useSelector로 state와 함수를 가져와 필요한 곳에서 호출해주면 된다.