배우는 순서
Pattern이기 때문에 라이브러리가 있는 것이 아니고 그냥 Pattern입니다.
Ducks Pattern
에 대한 자세한 설명은 github페이지에서 확인할 수 있습니다.
https://github.com/erikras/ducks-modular-redux
한국어로도 번역이 되어 있습니다.
하나의 모듈은...
reducer()
란 이름의 함수를 export default
해야합니다.export
해야합니다.npm-module-or-app/reducer/ACTION_TYPE
형태의 action 타입을 가져야합니다.UPPER_SNAKE_CASE
로 export
할 수 있습니다. 만약, 외부 reducer
가 해당 action들이 발생하는지 계속 기다리거나, 재사용할 수 있는 라이브러리로 퍼블리싱할 경우에 말이죠.재사용가능한 Redux 라이브러리 형태로 공유하는 {actionType, action, reducer}
묶음에도 위 규칙을 추천합니다.
모듈(module)은 reducer 하나를 의미합니다.
reducer에서 사용되는 액션들을 모아서 액션을 따로 파일로 분리하지 않고 모듈안에서 관리하게 됩니다.
이 리듀서들을 모아서 대표되는 reducer.js 를 만들게 되고, 이 reducer를 가지고 create해서 store를 만들게됩니다.
이전과 가장 크게 달리진 점은 module
안에 reducer
, actionType
, action생성함수
가 다 들어있다는 것입니다.
이렇게 작성된 module은 액션의 타입에 namespace같은 것을 달아서 다른 타입들과 섞이지 않도록 하는 작업을 추가적으로 해줍니다.
작업하던 프로젝트를 ducks 패턴으로 바꿔보겠습니다.
redux/modules/filter.js
/todos.js
/users.js
/reducer.js
reducers 폴더안에 있는 파일들을 그대로 새로만든 modules 폴더로 옮겨옵니다. reducers 폴더는 지웁니다.
actions.js 에 정의한 액션타입과 액션 생성 함수들을 그대로 가져와서 modules 안의 해당하는 파일들에 붙여넣습니다.
그럼 action.js 파일은 이제 사용되지 않습니다.
새로 만든 reducer에서도 경로를 바꿔줍니다.
아래는 ducks 패턴으로 바꾼 파일들
// redux/modules/filter.js
// 액션타입과 액션생성함수를 직접 가지고 왔으니 import로 가지고 올 필요가 없어 졌습니다.
// 처음부터 ducks 패턴으로 만들었다면, 애초에 따로 action들을 action.js에 관리하지도 않아서 import 코드를 쓸 일이 없지만요..
// import { SHOW_COMPLETE, SHOW_ALL } from "../actions";
// 액션 타입 정의
// 다른 액션 생성 함수에서 사용될 수도 있으니 export는 유지하겠습니다.
// 액션 타입의 값을 똑같이 쓰지 않고 앞에 리듀서 이름을 붙여줍니다. 그리고 그 리듀서 이름 앞에는 프로젝트의 이름을 붙여줍니다.
export const SHOW_ALL = "redux-start/filter/SHOW_ALL";
export const SHOW_COMPLETE = "redux-start/filter/SHOW_COMPLETE";
// 액션 생성 함수
export function showAll() {
return { type: SHOW_ALL };
}
export function showComplete() {
return { type: SHOW_COMPLETE };
}
// 초기값
const initilaState = "ALL";
// 리듀서
// export default 리듀서의 이름을 reducer라고 하기로 한 규칙대로 reducer로 합니다.
export default function reducer(previousState = initilaState, action) {
if (action.type === SHOW_COMPLETE) {
return "COMPLETE";
}
if (action.type === SHOW_ALL) {
return "ALL";
}
return previousState;
}
// redux/modules/todos.js
// import { ADD_TODO, COMPLETE_TODO } from "../actions";
// 액션 타입 정의
export const ADD_TODO = "redux-start/todos/ADD_TODO";
export const COMPLETE_TODO = "redux-start/todos/COMPLETE_TODO";
// 액션 생성 함수
// {type: ADD_TODO, text: '할 일'}
export function addTodo(text) {
return {
type: ADD_TODO,
text,
};
}
// {type: COMPLETE_TODO, index: 3}
export function completeTodo(index) {
return {
type: COMPLETE_TODO,
index,
};
}
// 초기값
const initilaState = [];
// 리듀서
export default function reducer(previousState = initilaState, action) {
if (action.type === ADD_TODO) {
return [...previousState, { text: action.text, done: false }];
}
if (action.type === COMPLETE_TODO) {
return previousState.map((todo, index) => {
if (index === action.index) {
return { ...todo, done: true };
}
return todo;
});
}
return previousState;
}
// redux/modules/users.js
// import { GET_USERS_FAIL, GET_USERS_START, GET_USERS_SUCCESS } from "../actions";
import axios from "axios";
// 액션 타입 정의
// github api 호출을 시작하는 것을 의미합니다.
export const GET_USERS_START = "redux-start/users/GET_USERS_START"; // 로딩시작
// github api 호출에 대한 응답이 성공적으로 돌아온 경우.
export const GET_USERS_SUCCESS = "redux-start/users/GET_USERS_SUCCESS"; // 로딩 끝내고 데이터를 세팅합니다.
// github api 호출에 대한 응답이 실패한 경우.
export const GET_USERS_FAIL = "redux-start/users/GET_USERS_FAIL"; // 로딩 끝내고 에러를 세팅합니다.
// 액션 생성 함수
export function getUsersStart() {
return {
type: GET_USERS_START,
};
}
export function getUsersSuccess(data) {
return {
type: GET_USERS_SUCCESS,
data,
};
}
export function getUsersFail(error) {
return {
type: GET_USERS_FAIL,
error,
};
}
// 초기값
const initialState = {
loading: false,
data: [],
error: null,
};
// 리듀서
export default function reducer(state = initialState, action) {
if (action.type === GET_USERS_START) {
return {
...state,
loading: true,
error: null,
};
}
if (action.type === GET_USERS_SUCCESS) {
return {
...state,
loading: false,
data: action.data,
};
}
if (action.type === GET_USERS_FAIL) {
return {
...state,
loading: false,
error: action.error,
};
}
return state;
}
// middleware를 사용하는 함수(redux-thunk)
export function getUsersThunk() {
return async (dispatch) => {
try {
dispatch(getUsersStart());
const res = await axios.get("https://api.github.com/users");
dispatch(getUsersSuccess(res.data));
} catch (error) {
dispatch(getUsersFail(error));
}
};
}
// redux/modules/reducer.js
import { combineReducers } from "redux";
import todos from "./todos";
import filter from "./filter";
import users from "./users";
const reducer = combineReducers({
todos,
filter,
users,
});
// 리듀서를 state의 프로퍼티로 지정해서 세팅해주면됩니다.
// 이제 프로퍼티 별로 처리할 각각의 리듀서를 만듭니다.
// todosReducer와 filterReducer로 reducer를 분리한 다음에 이 둘을 combineReducers로 합쳐서
// 최종적으로 combineReducers가 export될 reducer입니다.
export default reducer;
여기까지 하고 실행해보면 아직 에러가 나오고 있습니다.
이유는 이전에는 action.js 에서 액션 생성함수를 가져다가 사용했었지만, 지금 그 위치가 modules/밑의 각각의 파일로 나누어져서 들어갔습니다.
액션생성함수를 주로 사용하는 container 쪽에 가서 경로를 수정하고 실행하면 정상적으로 잘 실행되는 것을 확인할 수 있습니다.
기존에는 actions.js에 action들이 몰려있었고, reducer만 각각 분리된 상태에서 reducer에서 액션타입들을 actions.js에서 받아와서 처리했습니다.
이렇게 하는 것 보다는 module이라고 하는 같은 관심사로 묶게되면 액션타입과 액션 생성함수와 리듀서까지 모여있기 때문에 작업하면서 훨신 수월한 부분이 있다고 합니다.
그래서 요즘에는 실무에서 거의 ducks pattern
을 활용하고 있습니다.