[251105] feat: dispatchHelper.js

🕐 커밋 시간: 2025. 11. 05. 10:26:56

📊 변경 통계:
  • 총 파일: 1개

📁 추가된 파일:
  + com.twin.app.shoptime/src/utils/dispatchHelper.js

🔧 주요 변경 내용:
  • 공통 유틸리티 함수 최적화
This commit is contained in:
2025-11-05 10:26:57 +09:00
parent 4c02bd9c05
commit 9490d72c11

View File

@@ -0,0 +1,444 @@
// src/utils/dispatchHelper.js
// Redux Thunk Sequential Dispatch Helper Functions
//
// ==================== 실제 사용 예시 ====================
//
// 이 Helper 함수들은 프로젝트의 실제 dispatch 패턴을 분석하여 설계되었습니다.
// 다음과 같은 기존 패턴들을 간소화할 수 있습니다:
//
// 1. setTimeout을 이용한 순차 dispatch (homeActions.js)
// Before:
// const onSuccess = (response) => {
// dispatch({ type: types.GET_HOME_TERMS, payload: response.data });
// dispatch({ type: types.SET_TERMS_ID_MAP, payload: termsIdMap });
// setTimeout(() => { dispatch(getTermsAgreeYn()); }, 0);
// };
// After:
// dispatch(createSequentialDispatch([
// { type: types.GET_HOME_TERMS, payload: response.data },
// { type: types.SET_TERMS_ID_MAP, payload: termsIdMap },
// getTermsAgreeYn()
// ]));
//
// 2. API 성공 후 재조회 (cartActions.js)
// Before:
// const onSuccess = (response) => {
// dispatch({ type: types.ADD_TO_CART, payload: response.data.data });
// dispatch(getMyInfoCartSearch({ mbrNo }));
// };
// After:
// createApiThunkWithChain(
// (d, gs, onS, onF) => TAxios(d, gs, 'post', URLS.ADD_TO_CART, {}, props, onS, onF),
// [
// (response) => ({ type: types.ADD_TO_CART, payload: response.data.data }),
// (response) => getMyInfoCartSearch({ mbrNo: props.mbrNo })
// ]
// );
//
// 3. Loading 상태 관리 (mainActions.js)
// Before:
// dispatch(changeAppStatus({ showLoadingPanel: { show: true } }));
// TAxios(..., onSuccess, onFail);
// const onSuccess = () => { ...; dispatch(changeAppStatus({ show: false })); };
// const onFail = () => { dispatch(changeAppStatus({ show: false })); };
// After:
// withLoadingState(
// (dispatch, getState) => TAxiosPromise(dispatch, getState, 'get', URLS.API, props, {})
// .then((response) => dispatch({ type: types.ACTION, payload: response.data.data }))
// );
//
// 상세한 문서: .docs/plan/Promise/Redux-Sequential.md
// ========================================================
import { changeAppStatus } from '../actions/commonActions';
/**
* 여러 dispatch를 순차적으로 실행하는 Helper 함수
*
* @param {Array<Function|Object>} dispatchActions - 실행할 dispatch들의 배열
* - Function: thunk action creator (props) => (dispatch, getState) => {}
* - Object: plain action { type: 'ACTION_TYPE', payload: data }
* @param {Object} options - 옵션 객체
* @param {number} options.delay - 각 dispatch 간 지연 시간 (ms, 기본값: 0)
* @param {boolean} options.stopOnError - 에러 발생 시 중단 여부 (기본값: false)
* @returns {Function} Redux thunk 함수 (dispatch, getState) => Promise
*
* @example
* // 사용 예시 1: 여러 plain action 순차 실행
* dispatch(createSequentialDispatch([
* { type: types.SET_LOADING, payload: true },
* { type: types.UPDATE_DATA, payload: data },
* { type: types.SET_LOADING, payload: false }
* ]));
*
* @example
* // 사용 예시 2: thunk와 plain action 혼합
* dispatch(createSequentialDispatch([
* { type: types.GET_HOME_TERMS, payload: response.data },
* { type: types.SET_TERMS_ID_MAP, payload: termsIdMap },
* getTermsAgreeYn() // thunk action
* ]));
*
* @example
* // 사용 예시 3: delay 옵션 사용
* dispatch(createSequentialDispatch([
* fetchUserData(),
* fetchCartData(),
* fetchOrderData()
* ], { delay: 100, stopOnError: true }));
*/
export const createSequentialDispatch = (dispatchActions, options) =>
(dispatch, getState) => {
const config = options || {};
const delay = config.delay || 0;
const stopOnError = config.stopOnError !== undefined ? config.stopOnError : false;
return dispatchActions.reduce(
(promise, action, index) => {
return promise
.then(() => {
// delay가 설정되어 있고 첫 번째가 아닌 경우 지연
if (delay > 0 && index > 0) {
return new Promise((resolve) => setTimeout(resolve, delay));
}
return Promise.resolve();
})
.then(() => {
// action 실행
const result = dispatch(action);
// Promise인 경우 대기
if (result && typeof result.then === 'function') {
return result;
}
return Promise.resolve(result);
})
.catch((error) => {
console.error('createSequentialDispatch error at index', index, error);
// stopOnError가 true면 에러를 다시 throw
if (stopOnError) {
throw error;
}
// stopOnError가 false면 계속 진행
return Promise.resolve();
});
},
Promise.resolve()
);
};
/**
* API 호출 후 콜백에서 여러 dispatch를 자동으로 체이닝하는 Helper
* TAxios의 onSuccess/onFail 콜백 패턴과 호환
*
* @param {Function} apiCallFactory - API 호출을 생성하는 함수
* (dispatch, getState, onSuccess, onFail) => void
* @param {Array<Function|Object>} successDispatchActions - 성공 시 실행할 dispatch들
* @param {Function|Object|null} errorDispatch - 에러 시 실행할 dispatch (선택)
* @returns {Function} Redux thunk 함수
*
* @example
* // 사용 예시 1: API 호출 후 여러 dispatch 자동 실행
* export const addToCart = (props) =>
* createApiThunkWithChain(
* (dispatch, getState, onSuccess, onFail) => {
* TAxios(dispatch, getState, 'post', URLS.ADD_TO_CART, {}, props, onSuccess, onFail);
* },
* [
* (response) => ({ type: types.ADD_TO_CART, payload: response.data.data }),
* (response) => getMyInfoCartSearch({ mbrNo: response.data.data.mbrNo })
* ]
* );
*
* @example
* // 사용 예시 2: 에러 처리 포함
* export const registerDevice = (params) =>
* createApiThunkWithChain(
* (dispatch, getState, onSuccess, onFail) => {
* TAxios(dispatch, getState, 'post', URLS.REGISTER_DEVICE, {}, params, onSuccess, onFail);
* },
* [
* (response) => ({ type: types.REGISTER_DEVICE, payload: response.data.data }),
* getAuthenticationCode(),
* fetchCurrentUserHomeTerms()
* ],
* (error) => ({ type: types.API_ERROR, payload: error })
* );
*/
export const createApiThunkWithChain = (
apiCallFactory,
successDispatchActions,
errorDispatch
) => (dispatch, getState) => {
const actions = successDispatchActions || [];
const enhancedOnSuccess = (response) => {
// 성공 시 순차적으로 dispatch 실행
actions.forEach((action, index) => {
setTimeout(() => {
if (typeof action === 'function') {
// action이 함수인 경우 (동적 action creator)
// response를 인자로 전달하여 실행
const dispatchAction = action(response);
dispatch(dispatchAction);
} else {
// action이 객체인 경우 (plain action)
dispatch(action);
}
}, 0);
});
};
const enhancedOnFail = (error) => {
console.error('createApiThunkWithChain error:', error);
if (errorDispatch) {
if (typeof errorDispatch === 'function') {
// errorDispatch가 함수인 경우
const dispatchAction = errorDispatch(error);
dispatch(dispatchAction);
} else {
// errorDispatch가 객체인 경우
dispatch(errorDispatch);
}
}
};
// API 호출 실행
return apiCallFactory(dispatch, getState, enhancedOnSuccess, enhancedOnFail);
};
/**
* API 호출 thunk의 Loading 상태를 자동으로 관리하는 Helper
* changeAppStatus로 showLoadingPanel을 자동 on/off
*
* @param {Function} thunk - API 호출 thunk 함수
* @param {Object} options - 옵션 객체
* @param {string} options.loadingType - 로딩 타입 (기본값: 'wait')
* @param {Array} options.successDispatch - 성공 시 추가 dispatch (선택)
* @param {Array} options.errorDispatch - 에러 시 추가 dispatch (선택)
* @returns {Function} Redux thunk 함수
*
* @example
* // 사용 예시 1: 기본 로딩 관리
* export const getProductDetail = (props) =>
* withLoadingState(
* (dispatch, getState) => {
* return TAxiosPromise(dispatch, getState, 'get', URLS.GET_PRODUCT_DETAIL, props, {})
* .then((response) => {
* dispatch({ type: types.GET_PRODUCT_DETAIL, payload: response.data.data });
* });
* }
* );
*
* @example
* // 사용 예시 2: 성공/에러 시 추가 dispatch
* export const fetchUserData = (userId) =>
* withLoadingState(
* fetchUser(userId),
* {
* successDispatch: [
* fetchCart(userId),
* fetchOrders(userId)
* ],
* errorDispatch: [
* (error) => ({ type: types.SHOW_ERROR_MESSAGE, payload: error.message })
* ]
* }
* );
*/
export const withLoadingState = (thunk, options) => (dispatch, getState) => {
const config = options || {};
const loadingType = config.loadingType || 'wait';
const successDispatch = config.successDispatch || [];
const errorDispatch = config.errorDispatch || [];
// 로딩 시작
dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: loadingType } }));
// thunk 실행
const result = dispatch(thunk);
// Promise인 경우 처리
if (result && typeof result.then === 'function') {
return result
.then((res) => {
// 로딩 종료
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
// 성공 시 추가 dispatch 실행
successDispatch.forEach((action) => {
if (typeof action === 'function') {
dispatch(action(res));
} else {
dispatch(action);
}
});
return res;
})
.catch((error) => {
// 로딩 종료
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
// 에러 시 추가 dispatch 실행
errorDispatch.forEach((action) => {
if (typeof action === 'function') {
dispatch(action(error));
} else {
dispatch(action);
}
});
throw error;
});
}
// 동기 실행인 경우 (Promise가 아님)
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
return result;
};
/**
* 조건에 따라 다른 dispatch를 실행하는 Helper
* getState() 결과를 기반으로 분기 처리
*
* @param {Function} condition - 조건을 반환하는 함수 (state) => boolean
* @param {Function|Array|Object} trueDispatch - 조건 true 시 실행할 dispatch
* @param {Function|Array|Object|null} falseDispatch - 조건 false 시 실행할 dispatch (선택)
* @returns {Function} Redux thunk 함수
*
* @example
* // 사용 예시 1: 단일 action 조건부 실행
* dispatch(createConditionalDispatch(
* (state) => state.common.appStatus.isAlarmEnabled === 'Y',
* addReservation(reservationData),
* deleteReservation(showId)
* ));
*
* @example
* // 사용 예시 2: 여러 action 배열로 실행
* dispatch(createConditionalDispatch(
* (state) => state.common.appStatus.loginUserData.userNumber,
* [
* fetchUserProfile(),
* fetchUserCart(),
* fetchUserOrders()
* ],
* [
* { type: types.SHOW_LOGIN_REQUIRED_POPUP }
* ]
* ));
*
* @example
* // 사용 예시 3: false 조건 없이
* dispatch(createConditionalDispatch(
* (state) => state.cart.items.length > 0,
* proceedToCheckout()
* ));
*/
export const createConditionalDispatch = (
condition,
trueDispatch,
falseDispatch
) => (dispatch, getState) => {
const state = getState();
const shouldExecuteTrue = condition(state);
const actionsToDispatch = shouldExecuteTrue ? trueDispatch : falseDispatch;
// dispatch할 것이 없으면 종료
if (!actionsToDispatch) {
return Promise.resolve();
}
// 배열이 아니면 배열로 변환
const actions = Array.isArray(actionsToDispatch)
? actionsToDispatch
: [actionsToDispatch];
// 각 action 실행
actions.forEach((action) => {
if (typeof action === 'function') {
// thunk action
dispatch(action);
} else {
// plain action
dispatch(action);
}
});
return Promise.resolve();
};
/**
* 여러 API 호출을 병렬로 실행하고 모든 결과를 기다리는 Helper
* Promise.all을 사용하여 동시 실행
*
* @param {Array<Function>} thunks - 실행할 thunk 함수들의 배열
* @param {Object} options - 옵션
* @param {boolean} options.withLoading - 로딩 상태 관리 여부 (기본값: false)
* @returns {Function} Redux thunk 함수
*
* @example
* // 사용 예시: 여러 API를 동시에 호출
* dispatch(createParallelDispatch([
* fetchUserProfile(),
* fetchUserCart(),
* fetchUserOrders()
* ], { withLoading: true }));
*/
export const createParallelDispatch = (thunks, options) => (dispatch, getState) => {
const config = options || {};
const withLoading = config.withLoading || false;
if (withLoading) {
dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } }));
}
const promises = thunks.map((thunk) => {
const result = dispatch(thunk);
// Promise가 아닌 경우 Promise로 변환
return Promise.resolve(result);
});
return Promise.all(promises)
.then((results) => {
if (withLoading) {
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
}
return results;
})
.catch((error) => {
if (withLoading) {
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
}
throw error;
});
};
// ==================== 편의 함수들 ====================
/**
* setTimeout을 Promise로 래핑한 delay 함수
* @param {number} ms - 지연 시간 (밀리초)
* @returns {Promise}
*/
export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/**
* dispatch를 Promise로 변환하는 Helper
* @param {Function} thunk - thunk 함수
* @returns {Function} Promise를 반환하는 thunk
*/
export const promisifyDispatch = (thunk) => (dispatch, getState) => {
const result = dispatch(thunk);
if (result && typeof result.then === 'function') {
return result;
}
return Promise.resolve(result);
};