리덕스 라이브러리 이해하기

리덕스를 사용하면 컴포넌트의 상태 업데이트 관련 로직을 다른 파일로 분리시켜 더욱 효율적으로 관리할 수 있습니다. 또한 컴포넌트끼리 똑같은 상태를 공유해야 할 때도 여러 컴포넌트를 거치지 않고 손쉽게 상태 값을 전달하거나 업데이트 할 수 있습니다.

단순히 전역 상태 관리만 한다면 Context API를 사용하는 것만으로도 충분합니다.
하지만 리덕스를 사용하면 상태를 더욱 체계적으로 관리할 수 있기 때문에 프로젝트의 규모가 클 경우에는 리덕스를 사용하는 편이 좋습니다.

리덕스는 편리한 개발자 도구도 지원하며, 미들웨어라는 기능을 제공하여 비동기 작업을 훨씬 효율적으로 관리할 수 있게 해줍니다.


리덕스의 키워드

리덕스를 사용하면서 접하게 될 키워드의 개념


액션(action)

상태에 변화가 필요하면 액션(action)이란 것이 발생합니다.

이는 하나의 객체로 표현되어 아래와 같은 형식으로 이루어져 있습니다.

1
2
3
{
type: 'TOGGLE_VALUE'
}

액션(action) 객체는 반드시 type 필드를 가지고 있어야 합니다.(이 값을 액션의 이름이라고 생각하면 됩니다.)

그 외의 값들은 나중에 상태 업데이트를 할 때 참고해야 할 값이며, 작성자 마음대로 작성할 수 있습니다.

액션(action) 객체 예시
1
2
3
4
5
6
7
8
9
10
11
12
{
type: 'ADD_TODO',
data: {
id: 1,
text: '리덕스 배우기'
}
}

{
type: 'CHANGE_INPUT'
text: '안녕하세요'
}

액션 생성 함수

액션 생성 함수(action creator)는 말 그대로 액션 객체를 만들어 주는 함수를 말합니다.

액션 생성 함수
1
2
3
4
5
6
7
8
9
10
11
function addTodo(data) {
return {
type: 'ADD_TODO',
data
};
}

const changeInput = text => ({
type: 'CHANGE_INPUT',
text
});

주로 액션 객체를 만들 때 매번 직접 작성하기 귀찮거나, 만드는 과정에서 생기는 실수를 방지하기 위해 함수로 만들어 관리할 때 사용합니다.


리듀서(reducer)

리듀서(reducer)는 변화를 일으키는 함수입니다.
액션을 만들어서 발생시키면 리듀서가 현재 상태와 전달받은 액션 객체를 파라미터로 받아옵니다.
그리고 두 값을 참고하여 새로운 상태를 만들어 반환합니다.

리듀서 코드는 다음과 같은 형태로 이루어져 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const initialState = {
counter: 1
};

function reducer(state = initialState, action) {
switch (action.type) {
case INCREMENT:
return {
counter: state.counter + 1
};
default:
return state;
}
}

스토어(store)

프로젝트에 리덕스를 적용하기 위해 스토어(store)를 만듭니다.

한 개의 프로젝트는 단 하나의 스토어만 가질 수 있습니다.

스토어 안에는 현재 애플리케이션 상태와 리듀서가 들어가 있으며, 그 외에도 몇 가지 중요한 내장 함수를 지닙니다.


디스패치(dispatch)

디스패치(dispatch)는 스토어의 내장 함수 중 하나입니다. (액션을 발생시키는 것 이라고 이해하면 됩니다.)

dispatch(action)과 같은 형태로 액션 객체를 파라미터로 넣어 호출합니다.

디스패치 함수가 호출되면 스토어는 리듀서 함수를 실행시켜 새로운 상태를 만들어 줍니다.


구독(subscribe)

구독(subscribe)도 스토어의 내장 함수 중 하나입니다. subscribe 함수 안에 리스터 함수를 파라미터로 넣어서 호출해 주면, 액션이 디스패치되어 상태가 업데이트될 때마다 리스너 함수가 호출됩니다.

1
2
3
4
5
6
const listener = () => {
console.log('상태가 업데이트 되었습니다.')
}
const unsubscribe = store.subscribe(listener);

unsubscribe(); // 추후 구독을 비활성화할 때 함수를 호출

리액트 없이 쓰는 리덕스

리덕스는 리액트에 종속되는 라이브러리가 아닙니다.
리액트에서 사용하려고 만들어졌지만 실제로 다른 UI 라이브러리/프레임워크와 함께 사용할 수도 있습니다.

바닐라 자바스크립트 환경에서 리덕스를 사용하여 리덕스의 핵심 기능과 작동 원리를 이해해 보겠습니다.

Parcel

Parcel 이라는 도구를 사용하면 쉽고 빠르게 웹 애플리케이션 프로젝트를 구성할 수 있습니다.

설치
yarn global add parcel-bundler

npm install -g parcel-bundler

프로젝트 디렉터리를 생성한 후 package.json 파일을 생성합니다.

package.json 파일 생성
yarn init -y

npm init -y

프로젝트 디렉터리에 index.html 과 index.js 파일을 만들고 다음과 같이 수정 해줍니다.

index.html
1
2
3
4
5
6
<html>
<body>
<div>바닐라 자바스크립트</div>
<script src="./index.js"></script>
</body>
</html>
index.js
1
console.log('hello parcel');

다 작성한 후 parcel index.html 명령어를 실행하면 개발용 서버가 실행됩니다.

다음으로 리덕스 모듈도 설치해줍니다.

yarn add redux
npm i redux


Ui 구성

index.css
1
2
3
4
5
6
7
8
9
10
11
.toggle {
border: 2px solid black;
width: 64px;
height: 64px;
border-radius: 32px;
box-sizing: border-box;
}

.toggle.active {
background: yellow;
}
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<head>
<link rel="stylesheet" type="text/css" 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>
</body>
</html>

DOM 레퍼런스

UI를 관리할 때 별도의 라이브러리를 사용하지 않기 때문에 DOM을 직접 수정해 줘야합니다.

기존 코드는 지우고 다음과 같이 자바스크립트 파일 상단에 수정할 DOM 노드를 가리키는 값을 미리 선언해 줍니다.

index.js
1
2
3
4
5
// DOM 노드 값 설정
const divToggle = document.querySelector('.toggle');
const counter = document.querySelector('h1');
const btnIncrease = document.querySelector('#increase');
const btnDecrease = document.querySelector('#decrease');

액션 타입 정의

프로젝트의 상태에 변화를 일으키는 것을 액션이라고 합니다.

먼저 액션 이름을 정의해 줍니다.
액션 이름은 문자열 형태로, 주로 대문자로 작성하고 액션 이름은 고유해야 합니다.

index.js 액션 이름 정의
1
2
3
4
5
6
7
8
9
10
// DOM 노드 값 설정
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';

액션 생성 함수 정의

다음으로 액션 이름을 사용하여 액션 객체를 만드는 액션 생성 함수를 작성해 줍니다.

액션 객체는 type 값을 반드시 가지고 있어야 하며, 그 외의 값은 상태를 업데이트 할 때 참고할 값을 작성자가 마음대로 작성할 수 있습니다.

index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// DOM 노드 값 설정
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 });

초기값 설정

이 프로젝트에서 사용할 초기값도 설정해 줍니다.

index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// DOM 노드 값 설정
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 initState = {
toggle: false,
counter: 0,
};

리듀서 함수 정의

리듀서 함수는 파라미터로 state 와 action 값을 받아옵니다.

index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

(...)
// DOM 노드 값 설정
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 initState = {
toggle: false,
counter: 0,
};

// 리듀서 함수 정의
function reducer(state = initState, 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;
}
}

스토어 만들기

스토어를 만들 때는 createStore 함수를 사용합니다.

createStore 함수를 사용하기 위해서는 import 구문을 넣어 리덕스에서 해당 함수를 불러와 사용하고

함수의 파라미터에는 리듀서 함수를 넣어 주어야 합니다.

index.js
1
2
3
4
5
import { createStore } from 'redux';

(...)

const store = createStore(reducer);

이제 스토어 생성이 완료되었으니 스토어 내장 함수들을 사용할 수 있습니다.


함수 만들기

상태가 업데이트될 때 마다 호출시킬 render 함수를 만들어 봅니다.
(리액트의 render 함수가 아닙니다.)

index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { createStore } from 'redux';

(...)
// 스토어 만들기
const store = createStore(reducer);

// render 함수 작성
const render = () => {
const state = store.getState(); // 현재 상태를 불러옵니다.
// 토글 처리
if (state.toggle) {
divToggle.classList.add('active');
} else {
divToggle.classList.remove('active');
}
// 카운터
counter.innerText = state.counter;
};

render();

구독(subscribe)

상태가 업데이트될 때마다 호출시킬 render 함수는 만들었습니다.

업데이트 때 마다 호출하는 작업은 스토어 내장 함수 subscribe를 사용하여 처리할 수 있습니다.

subscribe 함수의 파라미터로 render 함수를 전달해 주면 액션이 발생하여 상태가 업데이트될 때마다 render 함수를 호출합니다.

subscribe
1
store.subscribe(render);

디스패치 (액션 발생시키기)

스토어의 내장 함수 dispatch는 액션을 발생시키는 역활을 합니다.

파라미터에 액션 객체를 넣어 주면 됩니다.

다음과 같이 각 DOM 요소에 클릭 이벤트를 설정해 줍니다.

index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import { createStore } from 'redux';

// DOM 노드 값 설정
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 initState = {
toggle: false,
counter: 0,
};

// 리듀서 함수 정의
function reducer(state = initState, 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;
}
}

// 스토어 만들기
const store = createStore(reducer);

// render 함수 작성
const render = () => {
const state = store.getState(); // 현재 상태를 불러옵니다.
// 토글 처리
if (state.toggle) {
divToggle.classList.add('active');
} else {
divToggle.classList.remove('active');
}
// 카운터
counter.innerText = state.counter;
};

render();
// 상태 업데이트시 마다 render 함수 실행
store.subscribe(render);

// dispatch: 액션 발생
divToggle.onclick = () => {
store.dispatch(toggleSwitch());
};
btnIncrease.onclick = () => {
store.dispatch(increase(1));
};
btnDecrease.onclick = () => {
store.dispatch(decrease());
};

리덕스의 세 가지 규칙

리덕스를 프로젝트에서 사용할 때 지켜야 하는 세 가지 규칙

단일 스토어

하나의 어플리케이션 안에는 하나의 스토어가 들어 있습니다.
여러개의 스토어를 사용하는 것이 완전 불가능 한 것은 아니나 권장하지 않습니다.

읽기 전용 상태

리덕스 상태는 읽기 전용입니다. 기존에 리액트에서 불변성을 지켜준 것 처럼 리덕스도 마찬가지 입니다.

기존의 객체는 건드리지 않고 새로운 객체를 생성해 줘야 합니다.

리듀서는 순수한 함수

리듀서는 순수한 함수여야 합니다. 순수한 함수란 다음 조건을 만족합니다.

  • 리듀서 함수는 이전 상태와 액션 객체를 파라미터로 받습니다.

  • 파라미터 외의 값에는 의존해면 안됩니다.

  • 기존값을 건드리지 않고, 변화를 준 새로운 상태 객체를 만들어서 반환합니다.

  • 똑같은 파라미터로 호출된 리듀서 함수는 언제나 같은 값을 반환해야 합니다.