Redux로 상태관리하기 - Redux Advanced (2) - 1

lbr·2022년 8월 16일
0

배우는 순서

  1. Ducks Pattern
  2. react-router-dom 과 redux 함께 쓰기
  3. redux-saga
  4. redux-actions

Ducks Pattern

Pattern이기 때문에 라이브러리가 있는 것이 아니고 그냥 Pattern입니다.

Ducks Pattern 에 대한 자세한 설명은 github페이지에서 확인할 수 있습니다.
https://github.com/erikras/ducks-modular-redux

한국어로도 번역이 되어 있습니다.

규칙

하나의 모듈은...

  1. 항상 reducer()란 이름의 함수를 export default 해야합니다.
  2. 항상 모듈의 action 생성자들을 함수형태로 export 해야합니다.
  3. 항상 npm-module-or-app/reducer/ACTION_TYPE 형태의 action 타입을 가져야합니다.
  4. 어쩌면 action 타입들을 UPPER_SNAKE_CASEexport 할 수 있습니다. 만약, 외부 reducer가 해당 action들이 발생하는지 계속 기다리거나, 재사용할 수 있는 라이브러리로 퍼블리싱할 경우에 말이죠.

재사용가능한 Redux 라이브러리 형태로 공유하는 {actionType, action, reducer} 묶음에도 위 규칙을 추천합니다.

구조

모듈(module)은 reducer 하나를 의미합니다.
reducer에서 사용되는 액션들을 모아서 액션을 따로 파일로 분리하지 않고 모듈안에서 관리하게 됩니다.
이 리듀서들을 모아서 대표되는 reducer.js 를 만들게 되고, 이 reducer를 가지고 create해서 store를 만들게됩니다.

이전과 가장 크게 달리진 점은 module 안에 reducer, actionType, action생성함수 가 다 들어있다는 것입니다.
이렇게 작성된 module은 액션의 타입에 namespace같은 것을 달아서 다른 타입들과 섞이지 않도록 하는 작업을 추가적으로 해줍니다.

작업하던 프로젝트를 ducks 패턴으로 바꿔보겠습니다.

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을 활용하고 있습니다.

0개의 댓글