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
542 lines
14 KiB
Markdown
542 lines
14 KiB
Markdown
# 해결 방법 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)
|