리덕스는 가장 많이 사용하는 리액트 상태 관리 라이브러리입니다. 리덕스를 사용하면 컴포넌트의 상태 업데이트 관련 로직을 다른 파일로 분리시켜서 더욱 효율적으로 관리할 수 있습니다. 단순한 전역 상태 관리만 한다면 Context API를 사용하는 것만으로도 충분하지만, 체계적으로 상태를 관리할 수 있기 때문에 프로젝트 규모가 클 경우에는 리덕스를 사용하는 편이 좋습니다.
상태에 어떠한 변화가 필요하면 액션이라는 것이 발생하며, 이는 하나의 객체로 표현됩니다.
{
type: 'TOGGLE_VALUE'
}
액션 객체는 type 필드를 반드시 가지고 있어야 합니다. 이 값은 액션의 이름이며, 작성자 마음대로 넣으면 됩니다.
{
type: 'ADD_TODO',
data: {
id: 1,
text: '리덕스 배우기'
}
}
{
type: 'CHANGE_INPUT',
text: '안녕하세요'
}
액션 생성 함수는 액션 객체를 만들어 주는 함수입니다.
function addTodo(data) {
return {
type: 'ADD_TODO',
data
};
}
const changeInput = text => ( {
type: 'CHANGE_INPUT',
text
});
리듀서는 변화를 일으키는 함수입니다. 액션을 만들어서 발생시키면 리듀서가 현재 상태와 전달받은 액션 객체를 파라미터로 받아 옵니다. 그리고 두 값을 참고하여 새로운 상태를 만들어서 반환해 줍니다.
const initialState = {
counter: 1
};
function reducer(state = initialState, action) {
switch(action.type) {
case INCREMENT:
return {
counter: state.counter + 1
}
default:
return state;
}
}
프로젝트에 리덕스를 적용하기 위해 스토어를 만듭니다. 한 개의 프로젝트는 단 하나의 스토어만 가질 수 있으며, 스토어 안에는 현재 애플리케이션 상태와 리듀서가 들어가 있으며 몇 가지 중요한 내장 함수를 지닙니다.
디스패치는 스토어의 내장 함수 중 하나입니다. 디스패치는 '액션을 발생시키는 것' 이라고 이해하면 됩니다. 이 함수는 dispatch(action)과 같은 형태로 액션 객체를 파라미터로 넣어서 호출합니다.
구독도 스토어의 내장 함수 중 하나입니다. subscribe 함수 안에 리스너 함수를 파라미터로 넣어서 호출해서 주면, 이 리스너 함수가 액션이 디스패치되어 상태가 업데이트될 때마다 호출됩니다.
const listener = () => {
console.log('상태가 업데이트됨');
}
const unsubscribe = store.subscribe(listener);
unsubscribe(); // 구독을 비활성화할 때 함수를 호출
위와 같이만 보면 어떻게 사용하는지 이해하기가 힘들기 때문에 예시를 만들어 보겠습니다.
yarn add react-redux or npm install react-redux
Redux에서 중요한 것은 상태를 직접 변경하는 것이 아닌 action을 통해 변경하는 것입니다. Redux에서 중요한 것은 state, action, reducer, store가 있습니다.
이를 활용하여 간단한 숫자 카운터 예제를 만들어 보겠습니다.
addsub.js
const initialState = {
value: 0
}
export default function addsubReducer(state = initialState, action) {
switch (action.type) {
case 'increment': {
return {
...state,
value: state.value + 1
}
}
case 'decrement': {
return {
...state,
value: state.value - 1
}
}
case 'reset': {
return {
...state,
value: 0
}
}
default:
return state
}
}
counting.js
const initialState = {
count: 0
}
export default function countingReducer(state = initialState, action) {
switch (action.type) {
case 'push': {
return {
...state,
count: state.count + 1
}
}
default:
return state
}
}
initialState를 통한 초기 state를 저장, action.type을 보고 reducer를 실행합니다.
...state란 불변성을 유지하기 위해 이전 state를 복사하기 위해서 사용합니다.
reducer.js
import { combineReducers } from 'redux'
import addsubReducer from './reducers/addsub'
import countingReducer from './reducers/counting'
const rootReducer = combineReducers({
value: addsubReducer,
count: countingReducer
})
export default rootReducer
reducer.js에서는 addsub, counting에서의 reducer를 combineReducers를 통해 통합시켜서 export 시켰습니다.
combineRedcuers란 여러개로 분리되어 있는 reducer를 하나로 합쳐서 rootReducer로 만들 수 있는 함수입니다.
reducer까지 정의한 이후엔 store를 만들 수 있으며 store는 createStore를 통해 만들 수 있습니다.
store.js
import { createStore } from 'redux'
import rootReducer from './reducer'
const store = createStore(rootReducer)
export default store
Redux에는 다음과 같은 함수가 있습니다.
getState() : 현재 state를 받아옴, dispatch() : action을 reducer한테 보내서 state를 update시킴, subscribe(): state가 변경되면 callback함수 호출
Redux는 Subscribe()를 지정하여 호출 -> dispatch()를 통해 액션 객체를 파라미터로 넣어서 state를 변화 -> getState()를 통해 state를 가져온 후, 그 값으로 component를 다시 rendering하는 과정을 거치는데, 이는 굉장히 비효율적이기 때문에 Redux Hook을 사용하면 해결할 수 있습니다.
useSelector: Redux의 state관리를 도와주며, React Component에서 Redux의 store내부의 data를 읽을 수 있다. 따로 subscribe를 사용하지 않아도 되는 편리함이 있다.
useDispatch: store.dispatch를 대신하여 사용할 수 있으며, return 값은 store.dispatch() 이다.
** Hook은 JS이므로, store.js에서 store를 가져올 수 없다. 따라서 provider를 활용하여 가져온다.
index.js
import reactDom from 'react-dom'
import React from 'react'
import App from './App'
import { Provider } from 'react-redux'
import store from './store'
reactDom.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
)
provider를 통해 우리가 작성한 store를 App.js에 지정해준다.
App.js
import { useSelector, useDispatch } from "react-redux";
function App() {
const dispatch = useDispatch()
const { value } = useSelector( state => state.value)
const { count } = useSelector( state => state.count)
const addValue = () => {
dispatch({ type: 'increment' })
}
const subValue = () => {
dispatch({ type: 'decrement' })
}
const resetValue = () => {
dispatch({ type: 'reset' })
}
const pushButton = () => {
dispatch({ type: 'push' })
}
return (
<div className="App">
<div>
value: { value }
</div>
<button onClick={addValue}> + </button>
<button onClick={subValue}> - </button>
<button onClick={resetValue}> reset </button>
<div>
count: { count }
</div>
<button onClick={pushButton}> click </button>
</div>
)
}
export default App
useDispatch를 통해 addValue, subValue, resetValue pushButton의 액션을 발생시킵니다. 또한 useSelector을 활용하여 rootReducer의 value와 count에 해당하는 state을 읽을 수 있었습니다.
리덕스는 리액트에 종속되는 라이브러리가 아닙니다. 리액트에서 쓰려고 만들어졌지만 실제로는 다른 UI 라이브러리, 프레임워크와도 사용할 수 있습니다. 예: angular-redux, ember redux, Vue에서도 사용할 수 있지만 Vue는 vuex를 주로 사용합니다. 심지어 바닐라 자바스크립트와도 사용할 수 있습니다.
프로젝트를 구성하기 위해 Parcel이라는 도구를 사용합니다.
index.html
<link rel="stylesheet" href="index.css">
</head>
<body>
<div class="toggle"></div>
<hr />
<h1>0</h1>
<button id="increase">+1</button>
<button id="decrease">-1</button>
<script src="./index.js"></script>
index.css
.toggle {
border: 2px solid black;
width: 64px;
height: 64px;
border-radius: 32px;
box-sizing: border-box;
}
.toggle.active {
background: yellow;
}
index.js
import { createStore } from 'redux'
const divToggle = document.querySelector('.toggle')
const counter = document.querySelector('h1')
const btnIncrease = document.querySelector('#increase')
const btnDecrease = document.querySelector('#decrease')
const TOGGLE_SWITCH = 'TOGGLE_SWITCH';
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
const toggleSwitch = () => ({ type: TOGGLE_SWITCH });
const increase = difference => ({ type: INCREASE, difference });
const decrease = () => ({ type: DECREASE });
const initialState = {
toggle: false,
counter: 0
}
initialState로써 초기값을 설정해주고, toggle의 상태와 counter를 형태로 설정해줍니다.
function reducer(state = initialState, action) {
switch(action.type) {
case TOGGLE_SWITCH:
return {
...state,
toggle: !state.toggle
};
case INCREASE:
return {
...state,
counter: state.counter + action.difference
};
case DECREASE:
return {
...state,
counter: state.counter - 1
}
default:
return state;
}
}
Reducer는 변화를 일으키는 함수로써 state와 action 값을 받아옵니다. state가 undefined로 주어졌을 때는 초기 값인 initialState로 설정됩니다. ...state는 아까도 말했듯이 불변성을 유지하기 위하여 설정하였습니다.
const store = createStore(reducer);
const render = () => {
const state = store.getState();
if(state.toggle) {
divToggle.classList.add('active');
} else {
divToggle.classList.remove('active')
}
counter.innerText = state.counter;
}
render();
store.subscribe(render);
divToggle.onclick = () => {
store.dispatch(toggleSwitch());
};
btnIncrease.onclick = () => {
store.dispatch(increase(1));
}
btnDecrease.onclick = () => {
store.dispatch(decrease())
}
store를 만들기 위하여 createStore 함수를 사용하였고, reducer 함수를 파라미터로 넣어주어야 합니다. 여기서는 subscribe 함수를 통해 store의 상태가 변할 때 마다 render함수가 실행되도록 하였으나, Redux를 활용하면 useSelector를 활용합니다. toggle과 버튼의 액션을 전달해주기 위하여 dispatch를 사용한 모습까지 있는 예제 였습니다.
하나의 애플리케이션 안에는 하나의 스토어가 있습니다. 여러 개를 사용하는 것이 가능하긴 하지만, 상태 관리가 복잡해질 수 있으므로 권장하지 않습니다.
리덕스 상태는 읽기 전용입니다. 리액트는 setState를 사용하여, state를 업데이트할 때도 객체나 배열을 업데이트하는 과정에서 불변성을 지켜주기 위해 spread 연산자를 사용하거나 immer와 같은 불변성 관리 라이브러리를 사용하는 것처럼 리덕스도 기존의 객체는 건드리지 않고 새로운 객체를 생성해줍니다. 불변성을 유지하는 이유는 내부적으로 데이터가 변경되는 것을 감지하기 위해 얕은 비교 검사를 하기 때문입니다.