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개의 댓글

관련 채용 정보