문제 상황
현재 redux 모듈 디렉토리 구조입니다.
일단 보자마자 article 안에 왜 articles과 editor가 또 있는거지?.. 싶고요 ㅎㅎ
그리고 domain별로 디렉토리가 나누어져있지만 auth, user 디렉토리 안에는 ducks 패턴으로 작성된 파일이 있고, 나머지는 Type, Action, Reducer, Saga 파일이 전부 나누어져있는 짬뽕 구조입니다. (해맑게 웃는다)
이 외에도 파일 구조 상 정형화 되지 않은 부분들이 많아서 이를 좀 정리 해볼까 합니다.
정리 좀 해볼까요 ..
1. Redux 상태 사용, 기능별로? 데이터별로?
현재 프로젝트에서는 글을 조회하여서 article 데이터를 받아오는 로직이 서로 다른 기능에서 중복 되고 있는 상황입니다. 즉, 같은 데이터 구조의 데이터를 게속해서 받아오는데, 이것을 기능별로 구분해서 가져오는 것이 맞는가?에 대해 고민이 되었습니다.
이 사항에 대해 고민해본 결과, 같은 데이터 구조를 가져온다고 하나의 상태를 두고 해당 상태를 갱신하는 것은 옳지 않다는 생각이 들었습니다.
여러 개의 기능이 동시에 수행되는 경우도 있을텐데, 하나의 상태에 여러 기능에서 불러오는 데이터를 저장하는 것은 말이 안 되네요 ?.. (되나)
되는 상황도 있겠지만, 기능이 어떻게 확장이 될 지 모르는 상황에서 하나의 데이터를 하나의 모듈 속 상태에만 저장해놓는다면 결국 추후에 수정을 해야할 상황이 있을 것 같습니다.
기능 별로 모듈을 나누고, 각 기능에서 필요한 데이터를 요청하여 응답 받은 데이터를 개별로 모두 저장하도록 합니다.
2. API 요청 파일 분리
API 요청을 묶어놓은 파일은, 백엔드로의 endpoint에 따라 묶어 관리하도록 합니다.
어떤 상황일지는 모르겠지만, 훗날 endpoint path가 변경이 될 수도 있습니다.
API 요청을 기능 별로, 기능에서 필요한 api끼리 묶어놓게 되면 하나의 API 요청 파일 안에 여러 개의 endpoint path를 가진 요청들이 존재하게 될 수 있습니다.
이 경우, 수정된 end point path가 사용된 api를 찾아 개발자(인 제가) 직접 모든 파일을 이곳저곳 확인해야 하는 일이 생깁니다 .... (생각만 해도 귀찮습니다....) 그러다 휴먼 에러에 의해 놓친 코드가 생길 가능성도 배제하지 못 합니다.
services 디렉토리의 최종 파일 구조입니다.
특정 path로의 요청을 변경하려면 어떤 파일을 확인하여야 할 지 명확합니다! 만일 백엔드 개발자(인 저)로부터 '/api/member/~~'로 들어온 요청에서 ~~를 변경해서 보내주셔야 해요~ 라고 전달이 온다면 바로 memberAPI.js를 확인하면 됩니다.
3. 전체적인 redux 모듈 패턴은? ducks? domain별?
기능 별로 나눈 후 Action, Reducer, Saga 파일을 따로 작성하여 관리합니다.
우선 현재 상황에서 모듈 패턴을 정형화하면서 ducks를 도입하는 것은 적절치 않다고 생각했습니다.
아래는 ducks 패턴이 적용되어 있는 상태의 auth.js 코드입니다.
import { createAction, handleActions } from 'redux-actions';
import { produce } from 'immer';
import createRequestSaga, { createRequestActionTypes } from '../../services/createRequestSaga';
import * as authAPI from '../../services/api/authAPI';
import { takeLatest } from 'redux-saga/effects';
const INITIALIZE_FORM = 'auth/INITIALIZE_FORM';
const CHANGE_FIELD = 'auth/CHANGE_FIELD';
const [REGISTER, REGISTER_SUCCESS, REGISTER_FAILURE] = createRequestActionTypes('auth/REGISTER');
const [LOGIN, LOGIN_SUCCESS, LOGIN_FAILURE] = createRequestActionTypes('auth/LOGIN');
const [GET_USER_INFO, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAILURE] =
createRequestActionTypes('auth/GET_USER_INFO');
const [REGISTER_USER_INFO, REGISTER_USER_INFO_SUCCESS, REGISTER_USER_INFO_FAILURE] =
createRequestActionTypes('auth/REGISTER_USER_INFO');
// 비밀번호 수정
export const [UPDATE_PASSWORD, UPDATE_PASSWORD_SUCCESS, UPDATE_PASSWORD_FAILURE] =
createRequestActionTypes('auth/UPDATE_PASSWORD');
export const initializeForm = createAction(INITIALIZE_FORM, (form) => form);
export const changeField = createAction(CHANGE_FIELD, ({ form, key, value }) => ({
form, // (register/login)
key, // field name
value, // value
}));
export const register = createAction(REGISTER, ({ email, nickname, password }) => ({
email,
nickname,
password,
}));
export const login = createAction(LOGIN, ({ email, password }) => ({
email,
password,
}));
export const getUserInfo = createAction(GET_USER_INFO);
export const registerUserInfo = createAction(
REGISTER_USER_INFO,
({ email, nickname, password, authProvider }) => ({ email, nickname, password, authProvider }),
);
export const updatePassword = createAction(UPDATE_PASSWORD, ({ currentPassword, newPassword }) => ({
currentPassword,
newPassword,
}));
// ============= redux - saga ================ //
const registerSaga = createRequestSaga(REGISTER, authAPI.register);
const loginSaga = createRequestSaga(LOGIN, authAPI.login);
const getUserInfoSaga = createRequestSaga(GET_USER_INFO, authAPI.getUserInfo);
const registerUserInfoSaga = createRequestSaga(REGISTER_USER_INFO, authAPI.registerUserInfo);
const updatePasswordSaga = createRequestSaga(UPDATE_PASSWORD, authAPI.updatePassword);
export function* authSaga() {
yield takeLatest(REGISTER, registerSaga);
yield takeLatest(LOGIN, loginSaga);
yield takeLatest(GET_USER_INFO, getUserInfoSaga);
yield takeLatest(REGISTER_USER_INFO, registerUserInfoSaga);
yield takeLatest(UPDATE_PASSWORD, updatePasswordSaga);
}
const initialState = {
register: {
email: '',
nickname: '',
password: '',
passwordConfirm: '',
},
login: {
email: '',
password: '',
},
userInfo: {
email: '',
nickname: '',
password: '',
authProvider: '',
},
password: {
message: null,
},
auth: null,
authError: null,
};
const auth = handleActions(
{
[CHANGE_FIELD]: (state, { payload: { form, key, value } }) =>
produce(state, (draft) => {
draft[form][key] = value;
}),
[INITIALIZE_FORM]: (state, { payload: form }) => ({
...state,
[form]: initialState[form],
authError: null,
auth: null,
}),
[REGISTER_SUCCESS]: (state, { payload: auth }) => ({
...state,
authError: null,
auth,
}),
[REGISTER_FAILURE]: (state, { payload: error }) => ({
...state,
authError: error,
}),
[LOGIN_SUCCESS]: (state, { payload: auth }) => ({
...state,
authError: null,
auth,
}),
[LOGIN_FAILURE]: (state, { payload: error }) => ({
...state,
authError: error,
}),
[GET_USER_INFO_SUCCESS]: (state, { payload: userInfo }) => ({
...state,
authError: null,
...userInfo,
}),
[GET_USER_INFO_FAILURE]: (state, { payload: error }) => ({
...state,
authError: error,
}),
[REGISTER_USER_INFO_SUCCESS]: (state, { payload: auth }) => ({
...state,
authError: null,
auth,
}),
[REGISTER_USER_INFO_FAILURE]: (state, { payload: error }) => ({
...state,
authError: error,
}),
[UPDATE_PASSWORD_SUCCESS]: (state, { payload: password }) => ({
...state,
password,
}),
[UPDATE_PASSWORD_FAILURE]: (state, { payload: error }) => ({
...state,
authError: error,
}),
},
initialState,
);
export default auth;
액션 타입이 총 7개인데, 벌써 상당히 깁니다 .. 직관적으로 어디부터가 type인지, action인지, saga 코드인지 확실히 눈에 들어오지도 않습니다. 따라서 프로젝트 기능별로 디렉토리를 나누고, 각각의 코드 파일을 따로 작성하여 Import 하는 식으로 코드를 작성해나가기로 결정하였습니다.
다만, __Types 파일은 제거하고, __Actions.js 파일에 코드를 옮기기로 결정하였습니다.
이유는 두 파일은 연관성이 매우 높고 Action을 작성할 때 매번 다른 파일을 열어 다시 확인하기가 번거로웠기 때문입니다. Type 선언을 Action.js 파일에 합병함으로써 발생하는 side effect도 없었기에 병합을 결정하였습니다!
최종적으로 변경된 파일 구조입니다.
모두 같은 구조를 가지고 있어 마음이 따뜻해집니다 .. =^=
'Project' 카테고리의 다른 글
SpringBoot + QueryDSL 적용 살펴보기 (2) | 2024.11.25 |
---|---|
[BAW] Express + PostgreSQL 트랜잭션 처리 도입기 (1) | 2024.11.09 |
[Express + React] OAuth2 로그인 구현 : Kakao 로그인에 닉네임이 반드시 필요하다면? (2) | 2024.10.21 |