First written: 23-01-11
Uploaded: 23-01-12
Last modified: 23-03-01
Redux는 Context API와 비슷한 역할을 한다. 즉, 전역적으로 접근이 가능한 상태를 관리한다. 다만 Redux가 Context API에 비해서 오랫동안 쓰여왔고, 프로젝트의 규모가 큰 경우 체계적인 state 관리가 가능하다는 이점이 있다(코드의 유지보수 및 효율성 측면). 거기에 Chrome Extension으로 Redux DevTools
가 있어, 개발에 용이하다. Middleware를 사용하여 비동기 작업을 보다 편리하게 관리할 수 있는 것도 장점이다.
Redux는 React를 위해서 만들어졌지만, Angular나 Ember, Vue와 함께 사용이 가능하다고 한다(물론 Vue에서는 Vuex가 더 많이 쓰인다). 심지어는 Vanilla Javascript와도 호환이 가능하다. Intro로 Vanilla Javascript와 HTML만 사용해 Redux 맛을 보고, Redux 기초 내용을 다져보자. 이렇게 정리하는 편이 차후에 길고 복잡한 React Redux 코드를 이해하는 데에 더 도움이 되는 것 같다.
$ npm init -y
$ npm i redux
(index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Redux + Vanilla Javascript</title>
</head>
<body>
<p>0</p>
<button id="increase">+2</button>
<button id="decrease">-3</button>
<script src="./index.js" type="module"></script>
</body>
</html>
(index.js)
import { createStore } from 'redux';
const counter = document.querySelector('p');
const btnIncrease = document.querySelector('#increase');
const btnDecrease = document.querySelector('#decrease');
// action part
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
const increase = dif => ({ type: INCREASE, dif });
const decrease = dif => ({ type: DECREASE, dif });
// reducer part
const initialState = {
counter: 0
};
const reducer = (state = initialState, action) => {
switch(action.type){
case INCREASE:
return {
...state,
counter: state.counter + action.dif
};
case DECREASE:
return {
...state,
counter: state.counter - action.dif
};
default:
return state;
}
}
// store part
const store = createStore(reducer);
const render = () => {
const state = store.getState();
counter.innerText = state.counter;
}
render();
store.subscribe(render);
btnIncrease.onclick = () => {
store.dispatch(increase(2));
}
btnDecrease.onclick = () => {
store.dispatch(decrease(3));
}
이번 절의 제목을 'Redux is 3-3'라고 잡은 이유는 Redux가 세 가지 구성 요소로 이루어져 있고, 세 가지 규칙을 따르기 때문이다. 위의 코드를 바탕으로 먼저 구성 요소 셋을 살펴보고 곧바로 이어서 세 가지 규칙에 대해서도 알아보자.
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
const increase = dif => ({ type: INCREASE, dif });
const decrease = dif => ({ type: DECREASE, dif });
Action은 state에 변화가 필요할 때 그에 관한 정보를 담아 전달하는 객체라고 생각하면 된다. 이 action에는 꼭 type field가 명시되어 있어야 한다. Action을 전달받은 reducer function이 action 객체의 type field를 참고하여 어떤 state를 변경할지를 결정하기 때문이다.
type 이외에 전달된 dif는 state를 변경할 때 참고할 값이라고 생각하면 좋다.
const initialState = {
counter: 0
};
const reducer = (state = initialState, action) => {
switch(action.type){
case INCREASE:
return {
...state,
counter: state.counter + action.dif
};
case DECREASE:
return {
...state,
counter: state.counter - action.dif
};
default:
return state;
}
}
첫 번째 parameter로 현재(혹은 초기) state를 입력받고, 두 번째 parameter로 action 객체가 dispatch 함수를 통해 전달될 경우 해당 객체 내의 type field를 살펴본 뒤 switch 문의 특정 case에 해당하면 state에 변화를 일으킨다.
코드를 보면 spread operator를 사용하고 있는데, 이는 Redux의 state가 read-only라는 첫 번째 규칙(사실 규칙에 순서가 있는지는 정확하지 않다)을 만족해야 하기 때문이다. state를 update하고자 한다면 원래 있었던 기존의 객체를 수정해서는 안 되기에(불변성 유지) spread operator를 사용했다. 이는 Redux가 변화 감지를 위해 shallow equality(얕은 비교)를 수행하기 때문이다.
또한 Reducer는 순수한 함수여야 한다는 두 번째 규칙도 예시 코드에 나와있다. 순수한 함수는 다음과 같은 조건들을 만족하는 함수를 뜻한다.
import { createStore } from 'redux';
const store = createStore(reducer);
const render = () => {
const state = store.getState();
counter.innerText = state.counter;
}
render();
store.subscribe(render);
이전의 Action과 Reducer가 얼개를 짰다면, Store는 해당 얼개를 현실화하는 역할이다. Store 안에는 현재의 state와 reducer가 담겨있다. 그리고 dispatch, subscribe, getState 등과 같이 Redux의 원활한 작동을 돕는 메서드도 가지고 있다.
우선 const store = createStore(reducer);
로 Store를 생성한다. 하나의 application은 하나의 store만 가져야 하기에, 보통은 하나의 store만 사용한다(이것이 세 번째 규칙이다). 여러 store를 쓰는 게 불가능하진 않지만 복잡성 증가로 인해 권장되지는 않는다.
그 뒤에 render 함수를 정의하는데, 이 함수는 state가 update될 때마다 호출된다. 이 함수는 이미 render되어 있는 html의 UI를 state에 따라 변경하는 역할을 맡는다. 여기서는 counter.innerText = state.counter;
한 줄만 작성했으나, 경우에 따라 class를 더하거나 빼주기도 하는 등의 UI manipulation을 진행할 수 있다.
물론 거기까지만 적는다고 state(정확히는 Store)가 update될 때마다 render 함수가 호출되진 않는다. Store의 subscribe 메서드가 필요하다. store.subscribe(render);
와 같이 호출하면, parameter로 전달된 함수는 Store가 update될 때마다 호출된다. 이 함수는 React를 사용하게 되면 사용할 일이 없는데, react-redux
가 해당 작업을 대신하기 때문이지, React에서 이 작업이 이루어지지 않는다는 뜻은 아니다.
이제 버튼을 클릭하면 dispatch 메서드를 통해 store의 state를 변경할 수 있도록 event listener를 달아주자. 혹시 이 함수가 render 바깥에 있는 이유를 알겠는가? 그렇다. render는 어디까지나 store가 변할 때 호출될 필요가 있는 코드를 모아두면 되는 것이지, 그와 상관 없는 코드는 굳이 재호출될 필요가 없으므로, 아래의 코드는 render 함수 바깥에 적어주게 된다.
btnIncrease.onclick = () => {
store.dispatch(increase(2));
}
btnDecrease.onclick = () => {
store.dispatch(decrease(3));
}
해당 코드를 parcel 및 parcel-bundler와 같은 프로젝트 web-app bundler를 이용해 실행하면 HTML, CSS, Vanilla Javascript만 사용한 프로젝트에서도 마치 redux와 같은 상태관리 tool을 이용해 state를 변경하는 것과 같은 효과를 얻을 수 있다.
Redux is 3-3
"Redux is 3-3". 세 가지 구성요소와 세 가지 규칙이 무엇인지만 쓰고 글을 마치자. 만일 해당 구성요소나 규칙을 보고 머리속으로 redux가 구조화되지 않는다면, 다시 위의 내용을 차근차근 따라가보자.
(참고로 내 스스로 정리하기 위해서 "Redux is 3-3"이라는 말을 했을 뿐, 보통은 세 가지 규칙에 대해서만 이야기하긴 한다)
참고 서적 :
김민준, 2019, 길벗, 『리액트를 다루는 기술, 개정판』