리덕스는 리액트 생태계에서 가장 많이 사용하는 상태 관리 라이브러리이다. 리덕스를 사용하면 컴포넌트들의 상태 업데이트 관련 로직을 다른 파일들로 분리시켜 더욱 효율적으로 관리할 수 있으며 전역 상태 관리도 쉽게 할 수 있다.
이전에 배운 Context API
를 사용해도 전역 상태 관리를 할 수 있고 상태 관리 로직을 분리할 수 있다. 특히, Context API
와 useReducer
는 리덕스를 사용하는 것과 개발 방식이 매우 유사하다. (리덕스에서도 리듀서와 액션이라는 개념을 사용)
단순히 전역 상태 관리만 한다면 Context API
사용으로도 충분하다. 하지만 리덕스를 사용하면 상태 관리를 더욱 체계적으로 할 수 있기 때문에 프로젝트 규모가 클 경우 리덕스를 사용하는 편이 좋다.
🧺 Store
스토어는 상태가 관리되는 오직 하나의 공간이다.
컴포넌트와는 별개로 스토어라는 공간이 있어서 스토어 안에 앱에서 필요한 상태를 담는다.
컴포넌트에서 상태 정보가 필요할 때 스토어에 접근한다.
🧾 Action
액션은 앱에서 스토어에 운반할 데이터를 말한다. (주문서)
액션은 자바스크립트 객체 형식으로 되어 있다.
🤵 Reducer
액션을 스토어에 바로 전달하는 것이 아니다.
액션을 리듀서에 전달해야 한다.
리듀서가 주문을 보고 스토어의 상태를 업데이트하는 것이다.
액션을 리듀서에 전달하기 위해서는 dispatch()
메소드를 사용해야 한다.
- 액션 객체가
dispatch()
메소드에 전달된다.dispatch(action)
를 통해 Reducer를 호출한다.- Reducer는 새로운 Store를 생성한다.
상태에 어떠한 변화가 필요하면 액션을 발생시킨다. 액션은 하나의 객체로 표현되는데, 액션 객체는 다음과 같은 형식으로 이뤄져있다. 액션 객체는 type
필드를 필수적으로 가져야 하고 그 외 값들은 개발자 마음대로 넣어줄 수 있다.
{
type: "TOGGLE_VALUE"
}
{
type: "ADD_TODO",
data: {
id: 0,
text: "리덕스 배우기"
}
}
액션 생성 함수는 액션 객체를 만드는 함수다. 어떤 변화를 일으켜야 할 때마다 액션 객체를 만들어야 하는데 매번 액션 객체를 직접 작성하는 것은 번거롭기 때문에 이를 함수로 만들어 관리한다. 즉, 액션 생성 함수를 만들어 사용하는 이유는 나중에 컴포넌트에서 더욱 쉽게 액션을 발생시키기 위함이다.
// 액션 객체
{
type: "CHANGE_INPUT",
text: "안녕하세요"
}
// 액션 생성 함수
export const changeInput = text => ({
type: "CHANGE_INPUT",
text
});
리듀서는 변화를 일으키는 함수다. 액션을 발생시키면 리듀서는 현재 상태와 전달받은 액션 객체를 파라미터로 받아 새로운 상태를 만들어 반환한다.
function reducer(state, action) {
// 상태 업데이트 로직
return alteredState;
}
이 리듀서는 useReducer
를 사용할때 작성하는 리듀서와 똑같은 형태를 가지고 있다.
만약 카운터를 위한 리듀서를 작성한다면 다음과 같이 작성할 수 있다.
const counter = (state, action) => {
switch (action.type) {
case 'INCREASE':
return state + 1;
case 'DECREASE':
return state - 1;
default:
return state;
}
}
스토어는 상태가 관리되는 오직 하나의 공간이다.
컴포넌트와는 별개로 스토어라는 공간이 있어서 스토어 안에 앱에서 필요한 상태를 담는다.
컴포넌트에서 상태 정보가 필요할 때 스토어에 접근한다.
리덕스에서는 한 애플리케이션 (프로젝트) 당 하나의 스토어를 만들게 된다. 스토어 안에는 현재의 애플리케이션 상태와 리듀서가 들어있고, 그 외에 몇 가지 내장 함수들이 있다.
내장함수
디스패치 : 액션을 리듀서에게 전달하기 위한 것이라고 이해하면 된다. dispatch(action)
형태로 파라미터에 액션 객체를 넣어 호출한다.
디스패치가 호출되면 스토어는 리듀서 함수를 실행시켜 새로운 상태를 만들어 준다.
구독 : subscribe
함수에 특정 함수 (리스너 함수)를 전달해주면, 액션이 디스패치되어 상태가 업데이트될 때마다 전달해준 함수가 호출된다.
리덕스는 리액트에 종속되는 라이브러리가 아니다. 리액트에서 사용하려고 만들어졌지만 다른 라이브러리, 프레임워크 or 바닐라 자바스크립트와도 사용할 수 있다. 바닐라 자바스크립트 환경에서 리덕스를 사용해 리덕스의 기능을 파악해보자.
Parcel을 사용하면 쉽고 빠르게 웹 애플리케이션 프로젝트를 구성할 수 있다.
$ npm install -g parcel-bundler // 설치 $ mkdir vanilla-redux // 디렉토리 생성 $ cd vanilla-redux // 디렉토리로 이동 $ yarn init -y // package.json 파일 생성
vscode에서 vanilla-redux 디렉토리를 열어 index.html
, index,css
, index.js
파일을 만든다.
<!-- index.html -->
<html>
<head>
<link href="index.css" rel="stylesheet" type="text/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>
/* index.css */
.toggle {
border: 2px solid black;
width: 64px;
height: 64px;
border-radius: 32px;
box-sizing: border-box;
}
.toggle.active {
background: yellow;
}
// index.js
const divToggle = document.querySelector('.toggle')
const counter = document.querySelector('h1')
const btnIncrease = document.querySelector('#increase')
const btnDecrease = document.querySelector('#Decrease')
작성 후 다음 명령어를 실행하면 개발용 서버가 실행된다. 이후 리덕스 모듈을 설치한다.
$ parcel index.html // 개발 서버 실행 Server running at http://localhost:1234 -> 개발서버 주소 $ yarn add redux // 리덕스 모듈 설치
액션 : 프로젝트 상태에 변화를 일으키는 것, 객체 형태, type 필수
액션 이름은 주로 문자열, 대문자로 작성하며 고유해야 한다.
액션 생성 함수 : 액션 객체를 만든다.
리듀서 : 액션을 발생시키면 리듀서는 현재 상태와 전달받은 액션 객체를 파라미터로 받아 새로운 상태를 만들어 반환
// index.js
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 = (diffrence) => ({ type: INCREASE, diffrence })
const decrease = () => ({ type: DECREASE })
// 초기상태 설정
const initialState = {
toggle: false,
counter: 0
}
// 리듀서 함수 정의
const reducer = ( state = initialState, action) => {
switch(action.type){
case TOGGLE_SWITCH:
return {
...state,
toggle: !state.toggle
}
case INCREASE:
return {
...state,
counter: state.counter + action.diffrence
}
case DECREASE:
return {
...state,
counter: state.counter -1
}
default:
return state
}
}
리듀서 함수가 맨 처음 호출될 때는 state 값이 undefined 이므로 initialState를 기본값으로 설정하기 위해 함수의 파라미터 쪽에 기본값이 설정되어 있다. 리듀서에서는 ...
연산자를 통해 상태의 불변성을 유지하면서 데이터의 변화를 일으킨다.
스토어를 만들 때는 createStore
함수를 사용한다. 이 함수는 redux에서 불러와야 하며 함수의 파라미터에는 리듀서 함수를 넣는다.
render 함수 (리스너 함수)는 상태가 업데이트 될 때마다 호출된다.
// index.js
import {createStore} from 'redux'
(...)
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()
// 상태가 업데이트될 때마다 render 함수 호출
store.subscribe(render)
액션을 발생시키는 것을 디스패치라고 한다. 디스패치 할 때는 스토어의 내장 함수 dispatch를 사용한다. 파라미터는 액션 객체를 넣어주면 된다.
// index.js
( ... )
// 액션 발생
divToggle.onclick = () => {
store.dispatch(toggleSwitch())
}
btnIncrease.onclick = () => {
store.dispatch(increase(1))
}
btnDecrease.onclick = () => {
store.dispatch(decrease())
}
상태 변화
최종 코드
// 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 = (diffrence) => ({ type: INCREASE, diffrence })
const decrease = () => ({ type: DECREASE })
// 초기상태 설정
const initialState = {
toggle: false,
counter: 0
}
// 리듀서 함수 정의
const reducer = ( state=initialState, action) => {
switch(action.type){
case TOGGLE_SWITCH:
return {
...state,
toggle: !state.toggle
}
case INCREASE:
return {
...state,
counter: state.counter + action.diffrence
}
case DECREASE:
return {
...state,
counter: state.counter -1
}
default:
return 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())
}
리덕스를 프로젝트에서 사용하게 될 때 알아두고, 꼭 지켜야 할 3가지 규칙이 있다.
Single source of truth
동일한 데이터는 항상 같은 곳에서 가지고 온다.
즉, 스토어라는 하나뿐인 데이터 공간이 있다는 의미이다.
하나의 애플리케이션에선 단 한 개의 스토어를 만들어 사용한다.
State is read-only
리액트에서는 setState
메소드를 활용해야만 상태 변경이 가능하다.
리덕스에서도 액션이라는 객체를 통해서만 상태를 변경할 수 있다.
기존 리액트에서 setState
를 사용해 state를 업데이트할 때 객체나 배열의 불변성을 지키기 위해 ...
연산자를 사용했다. 리덕스도 마찬가지이다. 기존 객체는 건드리지 않고 새로운 객체를 생성해 상태를 업데이트 하는 방식으로 해주면 나중에 개발자 도구를 통해 뒤로 돌릴 수도, 다시 앞으로 돌릴 수도 있다.
리덕스에서 불변성을 유지해야 하는 이유는 내부적으로 데이터가 변경되는 것을 감지하기 위해 얕은 비교 검사를 하기 때문이다. 이를 통해 객체의 변화를 감지할 때 객체의 깊숙한 안쪽까지 비교하는 것이 아니라 겉핥기 식으로 비교하여 좋은 성능을 유지할 수 있다.
Changes are made with pure functions
변경은 순수 함수로만 가능하다.
변화를 일으키는 함수, 리듀서는 순수 함수
순수 함수는 다음 조건을 만족한다.
1) 리듀서 함수는 이전 상태와 액션 객체를 파라미터로 받는다.
2) 파라미터 외의 값에 의존하면 안된다.
3) 이전 상태는 절대로 건들이지 않고, 변화를 준 새로운 상태 객체를 반환한다.
4) 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과값을 반환해야 한다.
예를 들어 DATE 함수로 현재 시간을 가져오거나, 네트워크 요청을 한다면 파라미터가 같아도 다른 결과를 만들어낼 수 있기 때문에 리듀서 함수 바깥에서 처리해줘야 한다.