# 해결 방법 1: dispatchHelper.js ## 📦 개요 **파일**: `src/utils/dispatchHelper.js` **작성일**: 2025-11-05 **커밋**: `9490d72 [251105] feat: dispatchHelper.js` Promise 체인 기반의 dispatch 순차 실행 헬퍼 함수 모음입니다. ## 🎯 핵심 함수 1. `createSequentialDispatch` - 순차적 dispatch 실행 2. `createApiThunkWithChain` - API 후 dispatch 자동 체이닝 3. `withLoadingState` - 로딩 상태 자동 관리 4. `createConditionalDispatch` - 조건부 dispatch 5. `createParallelDispatch` - 병렬 dispatch --- ## 1️⃣ createSequentialDispatch ### 설명 여러 dispatch를 **Promise 체인**을 사용하여 순차적으로 실행합니다. ### 사용법 ```javascript import { createSequentialDispatch } from '../utils/dispatchHelper'; // 기본 사용 dispatch(createSequentialDispatch([ { type: types.SET_LOADING, payload: true }, { type: types.UPDATE_DATA, payload: data }, { type: types.SET_LOADING, payload: false } ])); // thunk와 plain action 혼합 dispatch(createSequentialDispatch([ { type: types.GET_HOME_TERMS, payload: response.data }, { type: types.SET_TERMS_ID_MAP, payload: termsIdMap }, getTermsAgreeYn() // thunk action ])); // 옵션 사용 dispatch(createSequentialDispatch([ fetchUserData(), fetchCartData(), fetchOrderData() ], { delay: 100, // 각 dispatch 간 100ms 지연 stopOnError: true // 에러 발생 시 중단 })); ``` ### Before & After #### Before (setTimeout 방식) ```javascript 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 (createSequentialDispatch) ```javascript const onSuccess = (response) => { dispatch(createSequentialDispatch([ { type: types.GET_HOME_TERMS, payload: response.data }, { type: types.SET_TERMS_ID_MAP, payload: termsIdMap }, getTermsAgreeYn() ])); }; ``` ### 구현 원리 **파일**: `src/utils/dispatchHelper.js:96-129` ```javascript export const createSequentialDispatch = (dispatchActions, options) => (dispatch, getState) => { const config = options || {}; const delay = config.delay || 0; const stopOnError = config.stopOnError !== undefined ? config.stopOnError : false; // Promise 체인으로 순차 실행 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() ); }; ``` **핵심 포인트**: 1. `Array.reduce()`로 Promise 체인 구성 2. 각 action이 완료되면 다음 action 실행 3. thunk가 Promise를 반환하면 대기 4. 에러 처리 옵션 지원 --- ## 2️⃣ createApiThunkWithChain ### 설명 API 호출 후 성공 콜백에서 여러 dispatch를 자동으로 체이닝합니다. TAxios의 onSuccess/onFail 패턴과 완벽하게 호환됩니다. ### 사용법 ```javascript import { createApiThunkWithChain } from '../utils/dispatchHelper'; // 기본 사용 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 }) ] ); // 에러 처리 포함 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 }) ); ``` ### Before & After #### Before ```javascript export const addToCart = (props) => (dispatch, getState) => { const onSuccess = (response) => { dispatch({ type: types.ADD_TO_CART, payload: response.data.data }); dispatch(getMyInfoCartSearch({ mbrNo })); }; const onFail = (error) => { console.error(error); }; TAxios(dispatch, getState, "post", URLS.ADD_TO_CART, {}, props, onSuccess, onFail); }; ``` #### After ```javascript export const addToCart = (props) => 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: response.data.data.mbrNo }) ] ); ``` ### 구현 원리 **파일**: `src/utils/dispatchHelper.js:170-211` ```javascript 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') { const dispatchAction = errorDispatch(error); dispatch(dispatchAction); } else { dispatch(errorDispatch); } } }; // API 호출 실행 return apiCallFactory(dispatch, getState, enhancedOnSuccess, enhancedOnFail); }; ``` **핵심 포인트**: 1. API 호출의 onSuccess/onFail 콜백을 래핑 2. 성공 시 여러 action을 순차 실행 3. response를 각 action에 전달 가능 4. 에러 처리 action 지원 --- ## 3️⃣ withLoadingState ### 설명 API 호출 thunk의 로딩 상태를 자동으로 관리합니다. `changeAppStatus`로 `showLoadingPanel`을 자동 on/off합니다. ### 사용법 ```javascript import { withLoadingState } from '../utils/dispatchHelper'; // 기본 로딩 관리 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 }); }); } ); // 성공/에러 시 추가 dispatch export const fetchUserData = (userId) => withLoadingState( fetchUser(userId), { loadingType: 'spinner', successDispatch: [ fetchCart(userId), fetchOrders(userId) ], errorDispatch: [ (error) => ({ type: types.SHOW_ERROR_MESSAGE, payload: error.message }) ] } ); ``` ### Before & After #### Before ```javascript export const getProductDetail = (props) => (dispatch, getState) => { // 로딩 시작 dispatch(changeAppStatus({ showLoadingPanel: { show: true } })); const onSuccess = (response) => { dispatch({ type: types.GET_PRODUCT_DETAIL, payload: response.data.data }); // 로딩 종료 dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); }; const onFail = (error) => { console.error(error); // 로딩 종료 dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); }; TAxios(dispatch, getState, 'get', URLS.GET_PRODUCT_DETAIL, props, {}, onSuccess, onFail); }; ``` #### After ```javascript 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 }); }); } ); ``` ### 구현 원리 **파일**: `src/utils/dispatchHelper.js:252-302` ```javascript 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; }); } // 동기 실행인 경우 dispatch(changeAppStatus({ showLoadingPanel: { show: false } })); return result; }; ``` **핵심 포인트**: 1. 로딩 시작/종료를 자동 관리 2. Promise 기반 thunk만 지원 3. 성공/실패 시 추가 action 실행 가능 4. 에러 발생 시에도 로딩 상태 복원 --- ## 4️⃣ createConditionalDispatch ### 설명 getState() 결과를 기반으로 조건에 따라 다른 dispatch를 실행합니다. ### 사용법 ```javascript import { createConditionalDispatch } from '../utils/dispatchHelper'; // 단일 action 조건부 실행 dispatch(createConditionalDispatch( (state) => state.common.appStatus.isAlarmEnabled === 'Y', addReservation(reservationData), deleteReservation(showId) )); // 여러 action 배열로 실행 dispatch(createConditionalDispatch( (state) => state.common.appStatus.loginUserData.userNumber, [ fetchUserProfile(), fetchUserCart(), fetchUserOrders() ], [ { type: types.SHOW_LOGIN_REQUIRED_POPUP } ] )); // false 조건 없이 dispatch(createConditionalDispatch( (state) => state.cart.items.length > 0, proceedToCheckout() )); ``` --- ## 5️⃣ createParallelDispatch ### 설명 여러 API 호출을 병렬로 실행하고 모든 결과를 기다립니다. `Promise.all`을 사용합니다. ### 사용법 ```javascript import { createParallelDispatch } from '../utils/dispatchHelper'; // 여러 API를 동시에 호출 dispatch(createParallelDispatch([ fetchUserProfile(), fetchUserCart(), fetchUserOrders() ], { withLoading: true })); ``` --- ## 📊 실제 사용 예제 ### homeActions.js 개선 ```javascript // Before export const getHomeTerms = (props) => (dispatch, getState) => { const onSuccess = (response) => { if (response.data.retCode === 0) { dispatch({ type: types.GET_HOME_TERMS, payload: response.data }); dispatch({ type: types.SET_TERMS_ID_MAP, payload: termsIdMap }); setTimeout(() => { dispatch(getTermsAgreeYn()); }, 0); } }; TAxios(dispatch, getState, "get", URLS.GET_HOME_TERMS, ..., onSuccess, onFail); }; // After export const getHomeTerms = (props) => createApiThunkWithChain( (d, gs, onS, onF) => TAxios(d, gs, "get", URLS.GET_HOME_TERMS, ..., onS, onF), [ { type: types.GET_HOME_TERMS, payload: response.data }, { type: types.SET_TERMS_ID_MAP, payload: termsIdMap }, getTermsAgreeYn() ] ); ``` ### cartActions.js 개선 ```javascript // Before export const addToCart = (props) => (dispatch, getState) => { const onSuccess = (response) => { dispatch({ type: types.ADD_TO_CART, payload: response.data.data }); dispatch(getMyInfoCartSearch({ mbrNo })); }; TAxios(dispatch, getState, "post", URLS.ADD_TO_CART, ..., onSuccess, onFail); }; // After export const addToCart = (props) => 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: response.data.data.mbrNo }) ] ); ``` --- ## ✅ 장점 1. **간결성**: setTimeout 제거로 코드가 깔끔해짐 2. **가독성**: 의도가 명확하게 드러남 3. **재사용성**: 헬퍼 함수를 여러 곳에서 사용 가능 4. **에러 처리**: 옵션으로 에러 처리 전략 선택 가능 5. **호환성**: 기존 코드와 호환 (선택적 사용) ## ⚠️ 주의사항 1. **Promise 기반**: 모든 함수가 Promise를 반환하도록 설계됨 2. **Chrome 68**: async/await 없이 Promise.then() 사용 3. **기존 패턴**: TAxios의 onSuccess/onFail 패턴 유지 --- **다음**: [해결 방법 2: asyncActionUtils.js →](./03-solution-async-utils.md)