[문서화] Dispatch 비동기 처리 순서 보장 솔루션 문서 작성

dispatch 비동기 처리 순서 보장 문제와 해결 방법을 체계적으로 정리한 문서를 작성했습니다.

## 작성된 문서

1. README.md - 전체 개요 및 목차
2. 01-problem.md - 문제 상황 및 원인 분석
3. 02-solution-dispatch-helper.md - dispatchHelper.js 솔루션
4. 03-solution-async-utils.md - asyncActionUtils.js 솔루션
5. 04-solution-queue-system.md - 큐 기반 패널 액션 시스템
6. 05-usage-patterns.md - 사용 패턴 및 실전 예제

## 주요 내용

### 문제
- Redux-thunk에서 여러 dispatch 순서가 보장되지 않는 문제
- setTimeout(fn, 0) 임시방편의 한계

### 해결 방법
1. **dispatchHelper.js** (2025-11-05)
   - createSequentialDispatch: Promise 체인 기반 순차 실행
   - createApiThunkWithChain: API 후 dispatch 체이닝
   - withLoadingState: 로딩 상태 자동 관리

2. **asyncActionUtils.js** (2025-11-06)
   - 성공 기준 명확화: HTTP 200-299 + retCode 0/'0'
   - reject 없이 resolve만 사용하여 Promise 체인 보장
   - 타임아웃 지원

3. **큐 기반 패널 액션 시스템** (2025-11-06)
   - queuedPanelActions.js: 패널 액션 큐
   - panelQueueMiddleware.js: 자동 큐 처리
   - 비동기 액션 순차 실행

## 관련 커밋
- 9490d72 [251105] feat: dispatchHelper.js
- 5bd2774 [251106] feat: Queued Panel functions
- f9290a1 [251106] fix: Dispatch Queue implementation
This commit is contained in:
Claude
2025-11-10 09:26:40 +00:00
parent 11b6b24266
commit f75860c8dd
6 changed files with 2974 additions and 0 deletions

View File

@@ -0,0 +1,541 @@
# 해결 방법 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)