JS 앱의 상태 관리가 복잡해진만큼 버그를 최소화 하고, 잘 만들어진 사용자 경험(UX)을 제공하는 데 있어 상태를 효과적으로 관리할 필요성이 생겨났다.
여기서 말하는 상태란? 서버 응답, 캐시 데이터, 로컬 상태(아직 서버에 저장되지 않은 데이터) 등을 의미한다. 뿐만 아니라 활성화 된 라우트, 선택된 탭 핸들, 로딩 표시 여부, 페이지네이션 컨트롤 등 다양한 UI View 상태도 해당된다.이러한 복잡성은 오늘 날 앱 제작에 있어 가장 어려운 부분 중 하나이다.
참고 사이트: https://redux.js.org/understanding/thinking-in-redux/motivation
앱의 규모가 작은 경우 React만으로 상태 관리하는 방법을 사용하는 것이 더 효과적일 수 있다.
애플리케이션 상태는 모두 한 곳에서 관리된다. (동기화 필요 X, 디버깅 용이)
상태는 불변(읽기 전용) 데이터이며 오직 액션을 전달해 디스패치하는 것만이 상태를 업데이트 할 수 있다.(예측 가능)
리듀서(순수 함수)를 사용해 상태를 다음 상태로 업데이트 한다.(순수함울 보장)
Redux는 Store, State, Reducer, Action, Subscription 등을 제공해 효율적으로 상태를 관리한다.
index.html
<!DOCTYPE html>
<html lang="ko-KR">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Redux 아키텍처</title>
<link rel="stylesheet" href="./css/main.css" />
<script type="module" src="./js/main.js"></script>
</head>
<body>
<pre></pre>
<div class="buttonGroup">
<button type="button" class="button" onclick="moveLeft(-5)">move Left</button>
<button type="button" class="button" onclick="moveRight(5)">move Right</button>
<button type="button" class="button" onclick="randomBallColor()">Random Change Ball Color</button>
</div>
<div class="circle"></div>
</body>
</html>
js/main.js
// css variable get/set
// import like redux lib.
import { cssVars } from '../lib/cssvar.js';
import { createStore } from '../lib/likeRedux.js';
// state 매개변수는 초깃값을 전달 받아 설정
const initialState = {
color: cssVars('--color'),
x: 50, // %,
y: 50,
};
// 리듀서 (순수)함수: 상태 업데이트
// 매개변수: state, action
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'MOVE_DOWM':
case 'MOVE_UP':
return {
...state,
y: state.y + action.payload,
};
case 'MOVE_RIGHT':
case 'MOVE_LEFT':
return {
...state,
x: state.x + action.payload,
};
case 'CHANGE_RANDOM_BALL_COLOR':
return {
...state,
color: action.payload,
};
default:
return state;
}
};
// 스토어 생성하기 (<- 리듀서 함수)
const store = createStore(reducer);
// 스토어를 구독할 함수
function render() {
const state = store.getState();
}
// 스토어의 구독 메서드를 사용해 구독할 함수를 구독 설정
store.subscribe(render);
// 액션 (정보 객체 : 무엇을 수행해서 상태를 업데이트 할 것인가?)
// 스토어를 통해 사용자가 요구하는 액션을 디스패치 하기
// store.dispatch({
// type: 'MOVE_UP',
// });
// store.dispatch({
// type: 'MOVE_DOWM',
// });
// store.dispatch({
// type: 'MOVE_LEFT',
// });
// store.dispatch({
// type: 'MOVE_RIGHT',
// });
// 컨트롤 할 DOM 요소노드
const ball = document.querySelector('.circle');
const output = document.querySelector('pre');
// 구독할 함수 작성
function printState() {
const state = store.getState();
output.textContent = JSON.stringify(state, null, 2);
}
printState();
function moveBall() {
// 상태 가져오기
const { x, y } = store.getState();
ball.style.cssText = `left: ${x}%; top: ${y}%`;
}
function updateBallColor() {
// 상태 가져오기
const { color } = store.getState();
ball.style.background = color;
}
function changeRootNodeCssVar() {
const { color } = store.getState();
cssVars('--color', color);
}
// 함수에서 스토어 상태 업데이트를 구독
store.subscribe(moveBall);
store.subscribe(updateBallColor);
store.subscribe(changeRootNodeCssVar);
store.subscribe(printState);
// moveLeft, moveRight 함수 작성
window.moveLeft = function moveLeft(disX) {
// 상태 업데이트 요청
store.dispatch({ type: 'MOVE_LEFT', payload: disX });
};
window.moveRight = function moveRight(disX) {
// 상태 업데이트 요청
store.dispatch({ type: 'MOVE_RIGHT', payload: disX });
};
window.randomBallColor = function randomBallColor() {
//RGB -> Hexcode
let red = Math.ceil(Math.random() * 255).toString(16);
let green = Math.ceil(Math.random() * 255).toString(16);
let blue = Math.ceil(Math.random() * 255).toString(16);
let colorValue = `#${red}${green}${blue}`;
// 새로운 상태로 업데이트 요청
store.dispatch({
type: 'CHANGE_RANDOM_BALL_COLOR',
payload: colorValue,
});
};
lib/cssvar.js
// 루트 요소
const rootNode = document.documentElement;
// 글로벌 css 변수 가져오기
function getCssVar(varName) {
return window, getComputedStyle(rootNode, null).getPropertyValue(varName);
}
// 글로벌 css 변수 설정하기
function setCssVar(varName, value) {
rootNode.style.setProperty(varName, value);
}
// 가져오거나, 설정하는 유틸리티 내보내기
export const cssVars = (varName, value) => {
if (!value) {
return getCssVar(varName);
} else {
setCssVar(varName, value);
}
};
lib/likeRedux.js
export const createStore = reducer => {
if (typeof reducer !== 'function') {
throw new Error('createStore 함수는 reducer 함수를 전달 받아야 한다.');
}
// 외부에서 접근이 불가능한 상태
let state = reducer(undefined, {});
// 외부에서 스토어를 구독하는 함수의 집합 관리
let listeners = [];
// 외부에서 상태를 가져오고자 할 때 사용하는 멧더ㅡ
const getState = () => state;
// 외부에서 사용자가 액션을 받아, 리듀서 함수에 전달한다.
const dispatch = action => {
// 리듀서 함수를 실행해 새로운 상태를 반환
state = reducer(state, action);
// 스토어의 상태를 구독중인 리스너 실행
listeners.forEach(listener => listener?.());
};
// 외부의 함수가 스토어의 상태 업데이트를 구독
// 구독 = 상태 업데이트 감지되면, 함수 실행
const subscribe = addListener => {
// 구독
listeners.push(addListener);
// 구독 해지 (cleanup)
return () => {
listeners = listeners.filter(listener => listener !== addListener);
};
};
// 스토어 객체 반환
return {
getState,
dispatch,
subscribe,
};
};
패키지 설치
yarn add redux
yarn add react-redux
redux devtool 설치
리듀서에 아래의 코드 추가
참고사이트: https://github.com/zalmoxisus/redux-devtools-extension#11-basic-store
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
또는 패키지를 설치
yarn add -D redux-devtools-extension
이후
store/index.js
import { composeWithDevTools } from 'redux-devtools-extension';
...
// 스토어 생성
export const store = createStore(rootReducer, composeWithDevTools());
constate 라이브러리의 경우 하나의 상태를 관리할 때 유용하게 쓰이지만 여러개를 관리하는 경우 provider를 여러개 만들어야하기 때문에 Redux와 같은 라이브러리들을 사용하게 된다.