ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Redux | 데이터 종류별로 상탯값 나누기
    JavaScript/Redux 2020. 5. 14. 17:33

    데이터 종류별로 상탯값 나누기

    프로그램 안에서 사용되는 데이터 양이 많아지면 모든 액션을 하나의 파일에 작성하거나 하나의 리듀서 함수로 작성할 수 없음
    리덕스에서 제공하는 combineReducer 함수를 이용하면 리듀서 함수를 여러 개로 분리 가능


    실습 프로젝트 생성

    CRA 기반 프로젝트 생성

    $ npx create-react-app redux-test
    
    # src폴더 밑에서 index.js 파일을 제외한 모든 파일 삭제
    # index.js 파일의 내용도 지우고 다음 두 개의 패키지 설치
    
    $ npm i redux immer

    구현 기능

    ① 타임 라인

    사용자에게 보여 줄 여러 개의 게시물을 관리

    1. 각 게시물 데이터를 배열로 관리
    2. 게시물의 CRUD가 가능해야 함
    3. 무한 스크롤 기능이 필요하기 때문에 페이지 번호도 관리해야 함

    ② 친구 목록

    1. 친구 목록 데이터도 배열로 관리
    2. 친구 데이터 CRUD가 가능해야 함

    createReducer 함수 작성

    src 폴더 밑에 common 폴더 생성 후 그 안에 createReducer.js 파일 작성

    import produce from "immer";
    
    export default function createReducer(initialState, handlerMap) {
      return function (state = initialState, action) {
        return produce(state, (draft) => {
          const handler = handlerMap[action.type];
          if (handler) {
            handler(draft, action);
          }
        });
      };
    }

    친구 목록을 위한 리덕스 코드 작성

    src 폴더 밑에 friend 폴더 생성 후 그 안에 state.js 파일 작성

    import createReducer from "../common/createReducer";
    
    // 액션 타입을 상수 변수로 정의
    const ADD = "friend/ADD";
    const REMOVE = "friend/REMOVE";
    const EDIT = "friend/EDIT";
    
    // 액션 생성자 함수 정의, 외부에서 사용해야 함으로 export 사용
    export const addFriend = (friend) => ({ type: ADD, friend });
    export const removeFriend = (friend) => ({ type: REMOVE, friend });
    export const editFriend = (friend) => ({ type: EDIT, friend });
    
    // 초기 상탯값
    const INITIAL_STATE = { friends: [] };
    
    // 친구 데이터 CUD 리듀서
    const reducer = createReducer(INITIAL_STATE, {
      [ADD]: (state, action) => state.friends.push(action.friend),
      [REMOVE]: (state, action) =>
        (state.friends = state.friends.filter(
          (friend) => friend.id !== action.friend.id
        )),
      [EDIT]: (state, action) => {
        const index = state.friends.findIndex(
          (friend) => friend.id === action.friend.id
        );
        if (index >= 0) {
          state.friends[index] = action.friend;
        }
      },
    });
    
    export default reducer;
    • createReducer 함수에서 immer 패키지를 사용했으므로 리듀서 함수에서 간편하게 상탯값 수정 가능

    덕스(ducks) 패턴

    액션 타입, 액션 생성자 함수, 리듀서 함수를 각각의 파일로 만들지 않고 하나의 파일에서 관리하는 패턴
    → 대부분의 경우 덕스 패턴으로 리덕스 코드를 작성하는 것이 효율적


    덕스 패턴 규칙

    1. 연관된 액션 타입, 액션 생성자 함수, 리듀서 함수를 하나의 파일로 작성
    2. 리듀서 함수는 export default 키워드로 내보냄
    3. 액션 생성자 함수는 export 키워드로 내보냄
    4. 액션 타입은 접두사와 액션 이름을 조합해서 작명

    ※ 특정 파일의 코드가 많아지면 하나의 파일을 고집할 필요는 없음


    타임라인을 위한 리덕스 코드 작성

    src 폴더 밑에 timeline 폴더를 만든 뒤 state.js 파일 작성

    import createReducer from "../common/createReducer";
    
    // 액션 타입 상수 변수
    const ADD = "timeline/ADD";
    const REMOVE = "timeline/REMOVE";
    const EDIT = "timeline/EDIT";
    const INCREASE_NEXT_PAGE = "timeline/INCREASE_NEXT_PAGE";
    
    // 액션 생성자 함수
    export const addTimeline = (timeline) => ({ type: ADD, timeline });
    export const removeTimeline = (timeline) => ({ type: REMOVE, timeline });
    export const editTimeline = (timeline) => ({ type: EDIT, timeline });
    export const increaseNextPage = () => ({ type: INCREASE_NEXT_PAGE });
    
    // 초기 상탯값
    const INITIAL_STATE = { timelines: [], nextPage: 0 };
    
    // 리듀서
    const reducer = createReducer(INITIAL_STATE, {
      [ADD]: (state, action) => state.timelines.push(action.timeline),
      [REMOVE]: (state, action) =>
        (state.timelines = state.timelines.filter(
          (timeline) => timeline.id !== action.timeline.id
        )),
      [EDIT]: (state, action) => {
        const index = state.timelines.findIndex(
          (timeline) => timeline.id === action.timeline.id
        );
        if (index >= 0) {
          state.timelines[index] = action.timeline;
        }
      },
      [INCREASE_NEXT_PAGE]: (state, action) => (state.nextPage += 1),
    });
    
    export default reducer;
    • INCREASE_NEXT_PAGE: 타임라인의 끝에 도달했을 때 서버에 요청할 페이지 번호를 관리하는 액션 타입
    • 페이지 번호를 제외하고는 친구 목록 코드와 동일

    ※ friend, timeline 폴더 밑에 각각의 기능 구현을 위한 파일 추가 가능
    각 기능에서 사용되는 리액트 컴포넌트 파일도 해당 폴더 밑에서 작성하면 됨


    여러 리듀서 합치기

    리덕스에서 제공하는 combineReducers 함수를 이용하면 여러 개의 리듀서를 하나로 합칠 수 있음

    src/index.js 파일에 다음 코드 입력

    import { createStore, combineReducers } from "redux";
    
    import timelineReducer, {
      addTimeline,
      removeTimeline,
      editTimeline,
      increaseNextPage,
    } from "./timeline/state";
    
    import friendReducer, {
      addFriend,
      removeFriend,
      editFriend,
    } from "./friend/state";
    
    const reducer = combineReducers({
      timeline: timelineReducer,
      friend: friendReducer,
    });
    
    const store = createStore(reducer);
    store.subscribe(() => {
      const state = store.getState();
      console.log(state);
    });
    
    store.dispatch(addTimeline({ id: 1, desc: "코딩은 즐거워" }));
    store.dispatch(addTimeline({ id: 2, desc: "리덕스 좋아" }));
    store.dispatch(increaseNextPage());
    store.dispatch(editTimeline({ id: 2, desc: "리덕스 너무 좋아" }));
    store.dispatch(removeTimeline({ id: 1, desc: "코딩은 즐거워" }));
    
    store.dispatch(addFriend({ id: 1, name: "아이유" }));
    store.dispatch(addFriend({ id: 2, name: "손나은" }));
    store.dispatch(editFriend({ id: 2, name: "수지" }));
    store.dispatch(removeFriend({ id: 1, name: "아이유" }));
    
    // 최종 상탯값
    const state = {
        timeline: {
            timelines: [{ id: 2, desc: '리덕스 너무 좋아' }],
            nextPage: 1,
        },
        friend: {
            friends: [{ id: 2, name: "수지"}],
        }
    }
    • 친구 목록과 타임라인 모듈에서 액션 생성자 함수와 리듀서 함수를 가져옴
    • combineReducers 함수를 이용해서 두 개의 리듀서를 하나로 합침
      상탯값에는 각각 timeline, friend 라는 이름으로 데이터가 저장됨
    • 합친 reducer로 스토어 생성
    • subscribe 메서드를 이용해서 액션 처리가 끝날 때마다 상탯값을 로그로 출력
    • 타임라인과 친구 목록을 테스트하기 위해 액션 생성
    • npm start 명령어 입력 후 브라우저 개발자 도구에서 로그 확인

    공통 기능 분리하기

    위에서 작성한 타임라인과 친구 목록 코드에는 중복된 코드가 많음

    • 배열과 관련된 액션 타입과 액션 생성자 함수
    • 초기 상탯값을 빈 배열로 정의
    • 배열의 데이터를 추가, 삭제, 수정하는 리듀서 코드

    ※ 중복되는 코드를 별도의 파일로 분리해서 관리 가능


    중복 로직 파일 작성

    common 폴더 밑에 createItemsLogic.js 파일 생성 후 아래 코드 입력

    import createReducer from "./createReducer";
    
    export default function createItemsLogic(name) {
      // 액션 타입
      const ADD = `${name}/ADD`;
      const REMOVE = `${name}/REMOVE`;
      const EDIT = `${name}/EDIT`;
    
      // 액션 생성자 함수
      const add = (item) => ({ type: ADD, item });
      const remove = (item) => ({ type: REMOVE, item });
      const edit = (item) => ({ type: EDIT, item });
    
      // 리듀서
      const reducer = createReducer(
        // 초기 상탯값으로 빈 배열 입력
        { [name]: [] },
        {
          [ADD]: (state, action) => state[name].push(action.item),
          [REMOVE]: (state, action) => {
            const index = state[name].findIndex(
              (item) => item.id === action.item.id
            );
            state[name].splice(index, 1);
          },
          [EDIT]: (state, action) => {
            const index = state[name].findIndex(
              (item) => item.id === action.item.id
            );
            if (index >= 0) {
              state[name][index] = action.item;
            }
          },
        }
      );
    
      return { add, remove, edit, reducer };
    }
    • 관리하고자 하는 데이터 종류의 이름을 매개변수로 받음
    • 입력받은 이름을 이용해서 액션 타입 생성
    • 액션 생성자 함수와 리듀서 함수를 내보냄

    기존 코드 리팩터링하기

    friend/state.js

    친구 목록 상탯값 관련 코드 수정

    import createItemsLogic from "../common/createItemsLogic";
    
    const { add, remove, edit, reducer } = createItemsLogic("friends");
    
    export const addFriend = add;
    export const removeFriend = remove;
    export const editFriend = edit;
    export default reducer;
    • 공통 로직 생성자 함수를 가져옴
    • friends 라는 이름으로 공통 로직 생성
    • 액션 생성자 함수를 원하는 이름으로 바꿔서 내보냄
    • 리듀서 함수를 그대로 내보냄

    mergeReducers 함수 만들기

    리덕스에서 제공하는 combineReducers 함수를 이용하면 상탯값의 깊이가 불필요하게 깊어지기 때문에
    상탯값의 깊이가 깊어지지 않으면서 리듀서를 하나로 합치는 함수 작성

    // combineReducers 사용례
    import { combineReducers } from 'redux';
    // ...
    export default combineReducers({
        common: reducer,
        timelines: timelinesReducer,
    })
    
    // combineReducers 함수를 사용한 상탯값 구조
    const state = {
        timeline: {
            common: {
                nextPage: 0,
            }
    //...
    • 각 리듀서마다 새로운 이름을 부여하면서 객체의 깊이가 깊어짐
    • state의 timeline에 불필요하게 common이라는 이름의 객체가 추가됨

    mergeReducers 함수

    common 폴더 밑에 mergeReducers.js 파일 생성 후 아래 코드 입력

    export default function mergeReducers(reducers) {
      // 리듀서 반환
      return function (state, action) {
        // 초기 상탯값 계산
        if (!state) {
          return reducers.reduce(
            (acc, reducer) => ({ ...acc, ...reducer(state, action) }),
            {}
          );
        } else {
          // 초기화 단계가 아닌 경우
          let nextState = state;
          for (const reducer of reducers) {
            nextState = reducer(nextState, action);
          }
          return nextState;
        }
      };
    }
    • mergeReducers 함수는 리듀서를 반환
    • 초기 상탯값을 계산할 때는 모든 리듀서 함수의 결괏값을 합침
    • 초기화 단계가 아니라면 입력된 모든 리듀서를 호출해서 다음 상탯값 반환

    timeline/state.js

    타임라인 상태값 관련 코드 수정

    import createReducer from "../common/createReducer";
    import createItemsLogic from "../common/createItemsLogic";
    import mergeReducers from "../common/mergeReducers";
    
    // 공통 로직
    const { add, remove, edit, reducer: timelineReducer } = createItemsLogic(
      "timelines"
    );
    
    const INCREASE_NEXT_PAGE = "timeline/INCREASE_NEXT_PAGE";
    
    export const addTimeline = add;
    export const removeTimeline = remove;
    export const editTimeline = edit;
    export const increaseNextPage = () => ({ type: "INCREASE_NEXT_PAGE" });
    
    const INITIAL_STATE = { nextPage: 0 };
    const reducer = createReducer(INITIAL_STATE, {
      [INCREASE_NEXT_PAGE]: (state, action) => (state.nextPage += 1),
    });
    
    const reducers = [reducer, timelineReducer];
    export default mergeReducers(reducers);
    • timelines 라는 이름으로 공통 로직 생성
    • nextPage 상태 관련 코드들은 공통 로직에 포함되지 않았기 때문에 각각 따로 정의
    • mergeReducers 함수를 사용해서 공통 로직의 리듀서 함수와 nextPage 처리 리듀서 함수를 합침

    참고도서: [ 실전 리액트 프로그래밍 / 저자_ 이재승 / 출판사_ 프로그래밍 인사이트 ]

    댓글

Designed by Tistory.