왜 리덕스를 사용해야할까요?
Context API를 통해서 다양한 상태를 관리를 할 수 있고 가장 최고 레벨에 있는 컴포넌트 안에는 JSX만 남겨 깨끗한 코드로 유지할 수 있지만, 프로젝트 규모가 커짐에 따라 아래와 같은 코드를 마주해야할 수도 있다.
Provider로 감싸진 컴포넌트들의 지옥에 빠져버릴 수도있다는 것.. 그래서 Redux가 필요해지게 된다.!! 물론, 소규모 프로젝트에서는 Context API를 통한 상태관리로도 충분할 수 있다.
● 개념 미리 정리하기
개념을 미리 접하기 전에, 먼저 리덕스의 구조를 좀 보면 이해하기가 쉽다. 그리고 확실히 Redux를 배우기전에 useReducer를 공부해놔야 구조를 더 이해하기가 쉬운 것 같다.
- useReducer를 공부할 때, 초기 설정값과 type을 지정해주고, dispatch를 통해 type을 전달해 type에 맞는 액션이 작용하게끔했다.
- 액션이 작용하면 액션객체를 반환하게 했다.
useReducer에서 작용했던 방식처럼 Redux도 비슷하게 작용한다. 컴포넌트가 바로 저장소(store)에 접근해서 상태를 변동시키면 상태의 불변성에 문제가 생길 수 있기 때문에 여러 단계를 거쳐야한다.
- 컴포넌트가 액션에 dispatch를 통해 액션을 전달한다. 즉, 액션객체를 생성한다.
- 리듀서함수는 현재 상태와 전달받은 액션 객체를 파라미터로 받아오고 상태변화를 일으킨다.
따라서 순서는 아래와 같다.
컴포넌트가 액션 action을 보냄 > action에서 액션 객체 반환 > 리듀서 액션객체 받아서 상태 처리함 > 상태 변환
이제 각각의 구성요소에 대해서 자세히 알아보자.
⚬ 액션
상태에 어떠한 변화가 필요하면 액션이란 것이 발생한다. 이는 하나의 객체로 표현된다. 액션 객체는 다음과 같은 형식으로 이루어져 있다.
{
type : 'TOGGLE_VALUE'
}
액션 객체는 type필드를 반드시 가지고 있어야 한다. (useReducer와의 차이점이다.) 이 값을 액션의 이름이라고 생각하면 된다.
예시 액션은 아래와 같다.
{
type : 'ADD_TODO',
data : {
id : 1,
text : '리덕스 배우기'
}
}
{
type : 'CHANGE_INPUT',
text : '안녕하세요'
}
⚬ 액션 생성함수
매번 정해진 액션을 적어서 넘기는 것은 힘들 것이다. 이를 생성함수로 만들어서 리턴하게끔한다.
function addTodo(data){
return {
type : 'ADD_TODO',
data
};
}
//위는 화살표 함수로도 만들 수 있다.
const addTodo = text => ({ type : 'ADD_TODO', data })
⚬ 리듀서
리듀서는 상태 변화를 일으키는 함수이다. 액션을 만들어서 발생시키려면 리듀서가 현재 상태와 전달받은 액션 객체를 파라미터로 받아온다. 이는 useReducer를 사용하는 방식과 비슷하다. 그리고 두 값을 참고하여 새로운 상태를 만들어서 반환해준다.
const initialState = {
counter : 1,
};
function reducer(state = initialState, action){
switch(action.type){
case INCREMENT :
return {
counter : state.counter + 1
};
default : {
return state;
}
}
}
코드를 따라 치다보면, useReducer와 매우 닮아있다.
⚬ 스토어
프로젝트에 리덕스를 적용하기 위해 스토어를 만든다. 한 개의 프로젝트는 단 하나의 스토어만 가질 수 있다. (*컨택스트는 여러개의 컨택스트를 가질 수 있다.) 스토어 안에는 현재 애플리케이션 상태와 리듀서가 ㄷ르어가 잇으며 그외에도 몇가지 중요한 내장 함수를 지닌다.
⚬ 디스패치
디스패치는 스토어의 내장 함수 중 하나이다. 디스패치는 '엑션을 발생시키는 것'이라고 이해하면 된다.
컴포넌트에서 디스패치를 직접 실행하는 것은 권장되지 않으며, 대신 액션 생성자(action creator) 함수를 사용하여 액션을 만들어 디스패치해야한다. 이렇게하면 코드를 더 재사용 가능하고 테스트하기 쉽게 만들 수 있다.
⚬ 구독
구독도 스토의 내장 함수 중 하나이다. subscribe 함수 안에 리스너 함수를 파라미터로 넣어서 호출해 주면, 이 리스너 함수가 액션이 디스패치되어 상태가 업데이트 될 때마다 호출된다.
const listener = ()=>{
console.log('상태가 업데이트됨');
}
const unsubscribe = store.subscribe(listener);
unsubscribe(); // 추후 구독을 비활성화 할 때 함수를 호출
● 리액트 없이 Parcel로 프로젝트 만들기
index.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="./index.css">
<title>Document</title>
</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.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
};
// 리듀서 함수 정의
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;
}
}
// 스토어 생성
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();
store.subscribe(render);
// 구독하기
const listener = () => {
console.log("update requested");
};
const unsubscribe = store.subscribe(listener);
// 추후에는 subscribe 함수 대신 react-redux 라이브러리를 사용할 예정
// unsubscribe(); // 추후 구독을 비활성화 할 때 함수를 호출
// 액션 발생시키기
divToggle.onclick = () => {
console.log("divToggle click");
store.dispatch(toggleSwitch());
};
btnIncrease.onclick = () => {
console.log("btnIncrease click");
store.dispatch(increase(1));
};
btnDecrease.onclick = () => {
console.log("btnDecrease click");
store.dispatch(decrease());
};
- 액션 객체를 반환하는 함수를 만들어 준다.
- initialState를 만들어준다.
- reducer 함수를 만들어준다.
- createStore를 통해 store를 만들고, reducer함수와 연결해준다.
- render될 때, 어떻게 UI가 변하게 할지 render함수를 만들어준다. (store.getState( ) );
- render함수를 실행해준다.
- 어떤 컴포넌트가 store을 구독하는지 설정한다. (여기서는 어떤 UI?) store.subscribe(render);
- UI에서 어떤 이벤트 발생 시, dispatch하게 될지 설정해준다.
● 리덕스의 세 가지 규칙
⚬ 단일 스토어
하나의 애플리케이션 안에는 하나의 스토어가 들어 있다. 여러가지 스토어를 사용할 수는 있지만 상태 관리가 복잡해질 수 있다.
⚬ 읽기 전용 상태
리덕스 상태는 읽기 전용이다. 기존에 리액트에서 setter함수로 state를 업데이를 할 때도 객체나 배열을 업데이트하는 과정에서 불변성을 지켜주기위해 spread 연산자를 사용하거나 immer와 같은 라이브러리를 사용했었다. 리덕스도 마찬가지로 새로운 객체를 생성해주어야한다. 리덕스도 내부적으로 데이터가 변경되었다고 감지하기 위해서 얕은 비교 검사를 하기 때문이다.
⚬ 리듀서는 순수한 함수
변화를 일으키는 리듀서 함수는 순수한 함수여야한다.
- 리듀서 함수는 이전 상태와 액션 객체를 파라미터로 받는다.
- 파라미터 외의 값에는 의존하면 안된다.
- 이전 상태는 절대로 건드리지 않고, 변화를 준 새로운 상태 객체를 만들어서 반환한다.
- 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과 값을 반환해야 한다.