위코드에서 공부하며 정리한 내용입니다.
Redux(리덕스)는 자바스크립트 앱을 위한 예측 가능한 상태 컨테이너 입니다. 리덕트는 데이터가 단방향으로 흐르게 설계한 Flux 에서 영감을 받아 만들었습니다. Flux 는 개념적인 디자인 패턴이고, Redux 는 Flux 개념을 자바스크립트로 구현한 것입니다.
애플리케이션의 모든 상태(state)를 하나의 저장소(Storage)안에 하나의 객체 트리로 저장합니다.
store 에 직접 접근해 state 를 수정하지 않으므로 읽기 전용입니다. setState 처럼 Dispatch 함수에 액션객체를 담아서 보내면 state 를 수정할 수 있습니다. 이렇게 상태 업데이트 방식과 시점을 제한해서 동작을 예측할 수 있습니다.
// 무슨일이 벌어지는지 묘사하는 액션객체를 전달하는 dispatch 예시
store.dispatch({
type: 'COMPLETE_TODO',
index: 1,
});
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: 'SHOW_COMPLETED',
});
순수함수는 input 받은대로 output 이 나오는 예측하기 위한 구조로,
리듀셔는 이전 상태값과 액션 객체를 입력 받아 다음 상태를 반환하는 순수 함수 입니다. 이는 테스트 코드를 작성하기도 쉽습니다.
View 에서 이벤트가 발생하면 그 이벤트의 eventHandler의 함수가 호출되면서 Dispatch 함수가 Action 객체를 Store 에 있는 Reduce로 보냅니다. Reduce 에서 Action 객체를 따라 State 를 업데이트하고 새로운 State 를 반환합니다. 반환한 State 는 UI 를 업데이트하면서 한 사이클을 종료합니다.
유저가 사용하는 화면, 버튼, 컴포넌트 등 UI 구성요소를 말합니다.
Action 은 상태 변화에 대한 의도를 표현한 자바스크립트 객체입니다. Action 은 Store 에 담긴 데이터를 변형시키는 유일한 방법이지만 Action 스스로 Reducer로 이동할 수 없습니다. Action 은 store.dispatch 에 인자로 담겨 Reducer에 정보를 제공하는 역할을 합니다. 어떤 동작인지 알려주는 type 프로퍼티만 필수적으로 넣으면 나머지 요소는 사용자에 따라 추가할 수 있습니다.
// action 객체 예시
const ADD_TODO = {
type: 'ADD_TODO', //무슨일이 벌어질지 type으로 묘사
payload: { content: '출근하기', priority: 1 },
};
Action Creator 는 Action 객체를 정해진 틀에 맞게 리턴하는 단순 함수입니다. 규모가 작다면 Action 을 손수 만들 수 있겠지만, 이는 규모가 커질수록 비효율적이고 반복적인 작업이 될 것이므로 Action Creator 를 이요해 반환된 Action 을 Dispatch 에 담아 보냅니다.
// action creator 함수 예시
export const addCart = (item) => { // 액션 "생성 함수"
return {
type: 'ADD_ITEM', // 액션 "객체"
payload: item,
};
};
Dispatcher 는 Action 객체를 Reducer 에 보내는 역할을 합니다. store.dispatch() 형태로 사용하고, Dispatch 메서드는 동기적으로 처리되게 설계되어 있습니다. 비동기 Action 이 필요하다면 event 함수의 dispatch 와 reducer 사이엥서 Middleware 로 비동기 처리합니다.
// Dispatcher 메서드 에시
store.dispatch(addCart(payload));
Reducer 는 이전 상태와 액션을 받아서 새로운 상태를 반환하는 함수입니다. (예측 가능한 순수 함수 형태) Reduce 는 감지된 Action 타입에 따라 이벤트를 처리하는 이벤트 리스너와 비슷합니다.
// Array.prototye.reduce() 의 예시
const cart = [
{ id: 1, name: '청바지', price: 10000, quantity: 2 },
{ id: 2, name: '반바지', price: 10000, quantity: 1 },
{ id: 3, name: '반팔', price: 10000, quantity: 2 },
];
const totalPrice = cart.reduce((acc, cur) => {
return acc + cur.price * cur.quantity;
}, 0);
// output 50000
// reducer 형태
(previousState, action) => newState;
// slice reducer 예시1
// cart.js
// reducer 형태
const INITIAL_STATE = [];
export default function cart(state = INITIAL_STATE, action) {
switch (action.type) {
case 'ADD_ITEM':
return [...state, action.payload]; // 이전 상태에 새로운 item을 추가
case 'DELETE_ITEM':
return state.filter((product) => product.id !== payload.id);
default:
return state; // 해당 사항 없으면 이전 상태를 그대로 리턴
}
}
// slice reducer 예시2
// count.js
const INITIAL_STATE = { number: 0 };
export default function count(state = INITIAL_STATE, action) {
switch (action.type) {
case 'INCREASE':
return { ...state, number: state.number + 1 };
case 'DECREASE':
return { ...state, number: state.number - 1 };
default:
return state;
}
}
// rootReducer
// redux/index.js
import { combineReducers } from 'redux';
import cart from './cart';
import count from './count';
const rootReducer = combineReducers({ cart, count });
export default rootReducer;
관심사에 따른 Reducer 로직들을 모두 하나의 Reducer 에 넣는건 유지보수하기 어렵기 때문에 관심사별로 slice Reducer 를 만들고 redux에서 제공하는 combineReducer 메서드로 slice Reducer 들을 root Reducer 로 만들어 관리합니다.
Slice Reducer - 관심사에 따라 분리된 Reducer. 여러 Slice Reducer 들이 결합되어 Root Reducer 를 구성
Root Reducer - createStore 의 첫 번째 인자로 전달되는 함수. 관심사에 따라 분리되어 있는 Reducer 들을 하나로 묶기
주의할 점
첫번째, Reducer는 불변성을 지켜 업데이트 해야 합니다. state 원본을 직접 수정하면 안됩니다. useState 를 쓰듯 기존 값을 복하사고 새롭게 복사된 값을 덮어쓰는 방식으로 업데이트 합니다.
두번째, Reducer 내부에서 비동기나 여타 순수하지 않은 Promise(), Math.random(), Date.now() 같은 로직을 처리하면 안됩니다.
Store 은 Redux 앱 전체의 상태로 보통 깊게 중첩되어 있는 객체입니다. 모든 state 저장소로 store.getState() 로 접근할 수 있습니다. 항상 JSON 으로 변환할 수 있어야 하므로 JSON 으로 변환할 수 없는 함수나 Promise 들은 제외하는 게 좋습니다.
// store 예시
// store/index.js
import { createStore } from 'redux';
import rootReducer from 'redux/index.js';
const store = createStore(rootReducer);
Middleware 는 액션을 처리하기 전에 실행되는 함수로 비동기 API 호출 등 순수하지 않은 요청을 처리하거나 Redux Store 로 전달되는 Action 등을 로깅하는 장소입니다. Middleware 를 설치하지 않으면 dispatch한 action 은 동기적으로 바로 리듀서로 보내집니다. 대표적인 Middleware 로 redux-logger, redux-thunk, redux-saga 등이 있습니다.
// redux-thunk 예시
export default function thunkMiddleware({ dispatch, getState }) {
return (next) => (action) =>
typeof action === 'function' ? action(dispatch, getState) : next(action);
}
Redux 주요 개념에 대한 예시 코드입니다.
// reducers/index.js
// reducer 예시 : (store, action) => newStore
const reducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
// store생성 예시 - 애플리케이션 전체에서 하나만 존재
const store = createStore(reducer); // getState, subscribe, dispatch
// store조회 예시
const render = () => {
document.querySelector('.app').innerText = store.getState();
};
// view에서 일어난 이벤트에 Action을 실어 dispatch
document.addEventListener('click', () => {
store.dispatch({ type: 'INCREMENT' }); // action
});
// store에 업데이트가 일어났을 때 subscribe에 전달받은 함수를 실행
store.subscribe(render);
createStore Store 의 API 는 getState, subscribe, dispatch 3가지 입니다. getState 는 state 를 조회하고, subscribe 는 변경사항을 구독하고 있다가 상태 일부가 변경될 수 있을 때 마다 호출되고, dispatch 는 Action 을 보내는 메서드이면서 상태 변경을 일으킵니다.(유일한 방법)
// createStore API 예시
// reducer를 받는 store
const createStoreFromScratch = (reducer) => {
let state;
let listeners = [];
// store의 state 를 조회하는 getState
const getState = () => state;
// state의 변화를 구독(감지)하는 subscribe
const subscribe = (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter((l) => l !== listener);
};
};
// reducer에 action을 전달하는 dispatch
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach((listener) => listener());
};
dispatch({});
return { getState, subscribe, dispatch }; // store
};
import { createStore } from "redux";
import "./styles.css";
document.getElementById("root").innerHTML = `
<h1>Welcome Redux!</h1>
<div class="app">
store count: ${0}
</div>
`;
const store = createStore(reducer);
// getState, dispatch, subscribe
store.subscribe(render);
document.addEventListener("click", handleClick);
// dispatcher
function handleClick() {
store.dispatch({ type: "INCREMENT" }); // action
}
// view
function render() {
const target = document.querySelector(".app");
target.innerText = `store count: ${store.getState()}`;
}
// reducer : (store, action) => newStore 인 함수
function reducer(state = 0, action) {
switch (action.type) {
case "INCREMENT":
return state + 1;
case "DECREMENT":
return state - 1;
default:
return state;
}
}