리덕스는 상태 관리 라이브러리로 컴포넌트의 상태 업데이트 관련 로직을 다른 파일로 분리시켜 효율적으로 관리할 수 있다. 리덕스 라이브러리는 전역 상태를 관리할 때 효과적이다. 리액트 v16.3이 릴리즈되면서 Context API가 개선되기 전에는 주로 리덕스를 사용해 전역 상태를 관리했다고 한다.
단순히 전역 상태만 관리한다면 Context API를 사용하는 것만으로도 충분하지만 리덕스를 사용하면 상태를 더욱 체계적으로 관리할 수 있다. 코드의 유지 보수성도 높여 주고 작업 효율도 극대화 시킬 수 있다. 미들웨어라는 기능을 제공하여 비동기 작업을 훨씬 효율적으로 관리할 수 있게 해 주기도 한다.
useReducer에서 정리한 핵심 키워드에 이어 두 가지의 추가적인 키워드를 알아보자.
스토어(store)는 현재 애플리케이션 상태와 리듀서가 들어가 있으며, 그 외에도 몇 가지 중요한 내장 함수를 지닌다. 한 개의 프로젝트는 단 하나의 스토어만 가질 수 있다. (context 개념인가?)
구독(subscribe)는 스토어의 내장 함수 중 하나로 subscribe 함수 안에 리스너 함수를 파라미터로 넣어서 호출하면, 이 리스너 함수가 액션이 디스패치되어 상태가 업데이트될 때마다 호출된다.
const listener = () => {
console.log('상태 업데이트!!!');
};
const unsubscribe = store.subscribe(listener);
unsubscribe(); // 추후 구독을 비활성화할 때 이 함수를 호출한다.
액션이 dispatch 되면(액션이 발생하면) → reducer가 액션에 따라 새로운 상태를 반환하고(상태 업데이트) → 상태 업데이트가 일어나면 subscribe 함수로 등록한 리스너 함수가 실행된다!!!
리덕스는 리액트에 종속되는 라이브러리가 아니다. 이번에는 바닐라 자바스크립트환경에서 리덕스를 사용하여 리덕스의 핵심 기능과 작동 원리를 이해해보자!
🍟 책에서 소개하는 방식인 createStore로 스토어를 생성하는 함수는 deprecated되었다. 책의 흐름을 따라가긴 하지만 공식 문서를 참고하여 권장하는 방식으로 사용해본다.🍟
/* parcel-bundler 설치 */
$ yarn global add parcel-bundler
// or npm install -g parcel-bundler
/* 프로젝트 디렉터리 생성 후 package.json 파일 생성 */
$ mkdir vanilla-redux
$ cd vanilla-redux
$ yarn init -y
/* 리덕스 모듈 설치 */
$ yarn add redux
$ yarn add @reduxjs/toolkit
/* index.css */
.toggle {
border: 2px solid black;
width: 64px;
height: 64px;
border-radius: 32px;
box-sizing: border-box;
}
.toggle.active {
background: yellow;
}
/* index.html */
<html>
<head>
<link rel="stylesheet" type="text/css" href="index.css" />
</head>
<body>
<div class="toggle"></div>
<hr />
<h1>0</h1>
<button id="increase">+10</button>
<button id="decrease">-1</button>
<script src="./index.js"></script>
</body>
</html>
터미널에서 parcel index.html 을 실행하면 기본적으로 1234포트가 열린다.

원을 클릭하면 배경이 노란색으로 변하고 각 버튼을 클릭할 때마다 카운트가 10증가, 1감소 하도록 만들어보자!
import { configureStore, createSlice } from '@reduxjs/toolkit';
/* DOM 갸져오기 */
const divToggle = document.querySelector('.toggle');
const count = document.querySelector('h1');
const btnIncrease = document.querySelector('#increase');
const btnDecrease = document.querySelector('#decrease');
/* creaseSlice 🚀 */
const counterSlice = createSlice({
name: 'counter',
initialState: { toggle: false, count: 0 },
reducers: { // action creator
switchToggle: (state) => {
state.toggle = !state.toggle;
// return { ...state, toggle: !state.toggle };
},
increase: (state, action) => {
state.count += action.payload;
// return { ...state, count: state.count + action.payload };
},
decrease: (state) => {
state.count -= 1;
// return { ...state, count: state.count - 1 };
}
}
});
/* configureStore 🚀 */
const store = configureStore({
reducer: counterSlice.reducer
});
/* render 함수 */
const render = () => {
const state = store.getState();
state.toggle ? divToggle.classList.add('active') : divToggle.classList.remove('active');
count.innerText = state.count;
};
render();
/* subscribe */
store.subscribe(render);
/* button에 클릭 이벤트 리스너 등록 */
divToggle.onclick = () => store.dispatch(counterSlice.actions.switchToggle());
btnIncrease.onclick = () => store.dispatch(counterSlice.actions.increase(10));
btnDecrease.onclick = () => store.dispatch(counterSlice.actions.decrease());

위 코드에서 🚀 표시를 한 createSlice의 주석 처리한 부분을 보자. 나는 리듀서가 상태 업데이트를 할 때 불변성을 위해 새로운 상태를 만들어 반환해야 한다고 생각했기 때문에
return { ...state, toggle: !state.toggle };
처음에 이런식으로 작성했었다. 그런데 공식 문서를 보니 이런 내용이 있었다!

대충 Redux Tollkit의 createSlice및 createReducer API는 내부적으로 불변성을 유지한 업데이트 로직을 수행한다는 것이다!! 호오..
공식 문서에 나와있는 예제 코드는 다음과 같다.
// A
const store = configureStore({
reducer: {
counter: counterSlice.reducer
}
});
그런데 내가 실수로 이렇게 작성했다.
// B
const store = configureStore({
reducer: counterSlice.reducer
});
// A
const counterSlice = createSlice({
...
reducers: {
switchToggle: (state) => {
return { ...state, toggle: !state.counter.toggle };
},
...
}
});
...
const render = () => {
const state = store.getState();
state.counter.toggle ?
divToggle.classList.add('active') :
divToggle.classList.remove('active');
count.innerText = state.counter.count;
};
...
// B
const counterSlice = createSlice({
...
reducers: {
switchToggle: (state) => {
return { ...state, toggle: !state.toggle };
},
...
}
});
...
const render =() => {
const state = store.getState();
state.toggle ?
divToggle.classList.add('active') :
divToggle.classList.remove('active');
count.innerText = state.count;
};
...
이렇게 공식문서에 나와있는 대로(A 처럼) state에 counter라는 객체가 있고 그 객체가 toggle, count 상태를 가지고 있는 형태이다.
나는 간단한 사용법을 알아보기 위해 한 파일에 모두 작성했지만 아마 보통은 파일을 분리해서 작성하겠지!! 어쨌든 공식문서를 잘 참고하자.
Quick Start | Redux