-
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
구현 기능
① 타임 라인
사용자에게 보여 줄 여러 개의 게시물을 관리
- 각 게시물 데이터를 배열로 관리
- 게시물의 CRUD가 가능해야 함
- 무한 스크롤 기능이 필요하기 때문에 페이지 번호도 관리해야 함
② 친구 목록
- 친구 목록 데이터도 배열로 관리
- 친구 데이터 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) 패턴
액션 타입, 액션 생성자 함수, 리듀서 함수를 각각의 파일로 만들지 않고 하나의 파일에서 관리하는 패턴
→ 대부분의 경우 덕스 패턴으로 리덕스 코드를 작성하는 것이 효율적
덕스 패턴 규칙
- 연관된 액션 타입, 액션 생성자 함수, 리듀서 함수를 하나의 파일로 작성
- 리듀서 함수는 export default 키워드로 내보냄
- 액션 생성자 함수는 export 키워드로 내보냄
- 액션 타입은 접두사와 액션 이름을 조합해서 작명
※ 특정 파일의 코드가 많아지면 하나의 파일을 고집할 필요는 없음
타임라인을 위한 리덕스 코드 작성
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 처리 리듀서 함수를 합침
참고도서: [ 실전 리액트 프로그래밍 / 저자_ 이재승 / 출판사_ 프로그래밍 인사이트 ]
'JavaScript > Redux' 카테고리의 다른 글
Redux | 리덕스 사가를 이용한 비동기 액션 처리 (0) 2020.05.15 Redux | Reselect 패키지로 선택자 함수 만들기 (0) 2020.05.15 Redux | 리액트 상탯값 리덕스로 관리하기 (0) 2020.05.15