지금까지 Context API
를 사용하여 전역상태를 관리하는 방법에 대해 알아봤습니다
그런데 왜 많은 개발자들이 Redux
와 같은 상태관리 라이브러리를 사용할까요?
이유는 다양하지만 첫 순위로는 리덕스의 미들웨어 사용이 가지는 장점이 크기 때문입니다
리덕스는 액션과 리듀서 사이에서 미들웨어를 실행시키는데요
이 미들웨어 함수는 일반적으로 다음과 같은 구조를 가지고 있습니다
const middleware = store => next => action => {
// 미들웨어 로직
return next(action);
}
위 코드에서 store
는 Redux 스토어 객체를, next
는 다음 미들웨어 함수를 가리키는 함수를,
action
은 디스패치된 액션 객체를 의미합니다
그리고 미들웨어의 결과물에 따라서 어떤 Reducer
함수를 실행시킬지 결정합니다
노드JS환경에서 리덕스를 써서 간단한 카운터 기능을 만들어보겠습니다
먼저 연습용 디렉토리를 생성합니다
redux_example
redux_setting
*연습용 디렉토리에서는 CommonJS import
문을 사용할 예정
[redux_example]
npm install redux
mkdir src && cd src
mkdir store && cd store
vi index.js
createContext
와 같은 역할)const { createStore } = require("redux")
// import { createStore } from 'redux'
// 첫번째 인자는 reducer 함수입니다 ... 리턴은 함수의 초기값(state)
const store = createStore(()=>{})
console.log(store)
// Function: dispatch, subscribe, getState, replaceReducer, '@@observable'
console.log(store.getState())
// undefined ... 초기값을 지정하는 함수가 아직 정의되지 않았기 때문입니다
// 첫번째 매개변수는 이전 상태값, 두번째 매개변수는 액션
const reducer = (state, action) => {
return {
counter: 0,
}
}
const store = createStore(reducer)
console.log(store.getState())
// {counter: 0}
// dispatch 사용법이 조금 달라졌습니다
store.dispatch({ type: "increment" })
// {counter: 0} { type: "increment" }
디렉토리 구조
|-- src
|--- reducers
|---- index.js
|--- store
|---- index.js
[reducers/index]
const initialState = { counter: 0 }
export const rootReducer = (state, action) => {
switch (action.type) {
case "increment":
return { counter : state.counter + 1 }
case "decrement":
return { counter : state.counter - 1 }
default:
return initialState
}
}
const store = createStore(reducer)
// 상태 바꾸기
store.dispatch({ type: "increment" })
store.dispatch({ type: "increment" })
store.dispatch({ type: "decrement" })
console.log(store.getState())
// { counter : 1 }
아래와 같이 객체 형태로 상태를 전역 상태를 만들어보겠습니다
{
"counter" : 0,
"user" : {
"userid" : "web7722",
"username" : "kim",
},
"board" : [
{
"id": 1,
"subject" : "board 1"
}
]
}
[reducers/index]
// 초기 상태 설정
const initialState = {
counter: 0,
user: {},
board: [],
}
export const rootReducer = (state, action) => {
switch (action.type) {
case "increment":
// 스프레드 연산자를 사용해야 user, board에 관한 상태가 유지됩니다
return { ...state, counter : state.counter + 1 }
case "decrement":
return { ...state, counter : state.counter - 1 }
default:
return initialState
}
}
const store = createStore(reducer)
이제 각각의 상태에 관한 로직을 나누어볼까요
바꿀 디렉토리 구조
|--src
|--reducers
|---user.js
|---counter.js
|---index.js
[counter.js]
const initialState = {
number: 0,
}
exports const counterReducer = (state, action) => {
switch(action.type) {
case "increment":
return { number : state.number + 1 }
case "decrement":
return { number : state.number - 1 }
default:
return initialState
}
}
[user.js]
const ADD = "USER/ADD";
const add = (userid, username) => {
return { type: ADD, payload: { userid, username } };
};
const initialState = {
userid: "",
username: "",
};
const userReducer = (state = initialState, action) => {
switch (action.type) {
case ADD: {
const { userid, username } = action.payload;
return {
...state,
userid,
username,
};
}
default:
return state;
}
};
module.exports = { userReducer, add };
분할된 코드를 합치기 위해 combineReducers
라는 함수를 호출합니다
[index.js]
const { combineReducers } = require("redux")
const { counterReducer, userReducer } = require("./index")
export const rootReducer = combineReducers({
counter: counterReducer,
user: userReducer,
})
// {counter: { number : 0 }, user: { userid: "", username: "" }}
[store]
const { add } = require("../reducers/user")
store.dispatch(add('web7722', 'kim'))
console.log(store.getState())
// {counter: { number : 0 }, user: { userid: "web7722", username: "kim" }}
combineReducers
를 사용하니 스프레드 연산자 없이도 다른 객체의 상태가 출력되네요
add
)를 따로 만들기도 합니다(redux-toolkit
에 대해서도 알아볼 것!)
본격적으로 미들웨어 사용에 대해 알아보겠습니다
redux-thunk는 리덕스 미들웨어의 일종으로 액션 객체 대신에 함수를 디스패치할 수 있게 해줍니다
이 함수는 dispatch
와 getState
를 매개변수로 받아서, 액션 객체를 조작하는데 사용합니다
특히 비동기 작업을 처리하는데 용이하기 때문에 많이 사용합니다
npm install redux-thunk
를 써서 설치할 수 있지만
생각보다 구조가 단순하기 때문에 직접 구현해볼 수도 있습니다
그래서 오늘은 설치 대신 직접 구현해보기로...
const { createStore, applyMiddleware } = require("redux")
const { rootReducer } = require("../reducer")
const { add } = require("../reducers/user")
const createThunkMiddleware = (arguments) => {
// next 함수는 express에서 많이 쓰던 그것과 같은 역할
return (store) => (next) => (action) => {
console.log("hello world")
console.log(type of action, action)
return next(action)
}
}
// 두번째 인자는 미들웨어 실행을 뜻합니다
const store = createStore(rootReducer, applyMiddleware(createThunkMiddleware()))
store.dispatch({ type: "increment" })
// hello world!
// object { type: "incremnet" }
console.log(store.getState())
// { counter: { number: 1 }, user: {..} } ~ next함수가 실행
store.dispatch()
함수가 실행될 때, createThunkMiddleware()
함수에서 정의된 로그("hello world")가 출력됩니다console.log(store.getState())
는 최종적으로 변경된 상태를 출력const { createStore, applyMiddleware } = require("redux")
const { rootReducer } = require("../reducer")
const { add } = require("../reducers/user")
const createThunkMiddleware = (arguments) => {
return (store) => (next) => (action) => {
if (typeof action === 'function') {
console.log("함수라면")
return action()
}
return next(action)
}
}
store.dispatch(()=> {"호출"})
// 함수라면
// 호출
위 예제에서 상태는 바뀌지 않았습니다. 아직 reducer
함수가 호출되지 않았기 때문에...
그런데 dispatch
함수 안에서 다시 한번 dispatch
를 사용하면 어떻게 될까요
store.dispatch(()=> {
store.dispatch({ type: "increment" })
})
console.log(store.getState())
// 함수라면
// { counter: { number: 1 }, user: {..} } ~ next함수가 실행
dispatch
인자가 함수이기 때문에 if문의 로그가 실행 => next
함수로 인해 미들웨어 재실행 =>
dispatch
인자가 객체이기 때문에 if문의 로그 실행 X => 상태 변경!
즉, 상태를 바꾸는 과정에서 중간에 로직을 조작할 수 있는 함수(미들웨어)가 추가된 것을 알 수 있습니다
const api = () => {
// axios...
store.dispatch({ type: "increment" })
}
store.dispatch(api)
console.log(store.getState())
|-- src
|--- api
|--- reducers
|--- store
실제 요청 로직이 담기는 코드는 전부 api 디렉토리 안에 담는 것이 좋습니다
오늘은 여기까지