Redux | 데이터 종류별로 상탯값 나누기
데이터 종류별로 상탯값 나누기
프로그램 안에서 사용되는 데이터 양이 많아지면 모든 액션을 하나의 파일에 작성하거나 하나의 리듀서 함수로 작성할 수 없음
리덕스에서 제공하는 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 처리 리듀서 함수를 합침
참고도서: [ 실전 리액트 프로그래밍 / 저자_ 이재승 / 출판사_ 프로그래밍 인사이트 ]