Merge pull request #4 from optrader8/claude/fix-async-dispatch-011CUyxLZSVSd5LLnhfSiJgk
[251110] fix: dispatch async
This commit is contained in:
210
.docs/dispatch-async/01-problem.md
Normal file
210
.docs/dispatch-async/01-problem.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# 문제 상황: Dispatch 비동기 순서 미보장
|
||||
|
||||
## 🔴 핵심 문제
|
||||
|
||||
Redux-thunk는 비동기 액션을 지원하지만, **여러 개의 dispatch를 순차적으로 호출할 때 실행 순서가 보장되지 않습니다.**
|
||||
|
||||
## 📝 기존 코드의 문제점
|
||||
|
||||
### 예제 1: homeActions.js
|
||||
|
||||
**파일**: `src/actions/homeActions.js`
|
||||
|
||||
```javascript
|
||||
export const getHomeTerms = (props) => (dispatch, getState) => {
|
||||
const onSuccess = (response) => {
|
||||
if (response.data.retCode === 0) {
|
||||
// 첫 번째 dispatch
|
||||
dispatch({
|
||||
type: types.GET_HOME_TERMS,
|
||||
payload: response.data,
|
||||
});
|
||||
|
||||
// 두 번째 dispatch
|
||||
dispatch({
|
||||
type: types.SET_TERMS_ID_MAP,
|
||||
payload: termsIdMap,
|
||||
});
|
||||
|
||||
// ⚠️ 문제: setTimeout으로 순서 보장 시도
|
||||
setTimeout(() => {
|
||||
dispatch(getTermsAgreeYn());
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
TAxios(dispatch, getState, "get", URLS.GET_HOME_TERMS, ..., onSuccess, onFail);
|
||||
};
|
||||
```
|
||||
|
||||
**문제점**:
|
||||
1. `setTimeout(fn, 0)`은 임시방편일 뿐, 명확한 해결책이 아님
|
||||
2. 코드 가독성이 떨어짐
|
||||
3. 타이밍 이슈로 인한 버그 가능성
|
||||
4. 유지보수가 어려움
|
||||
|
||||
### 예제 2: cartActions.js
|
||||
|
||||
**파일**: `src/actions/cartActions.js`
|
||||
|
||||
```javascript
|
||||
export const addToCart = (props) => (dispatch, getState) => {
|
||||
const onSuccess = (response) => {
|
||||
// 첫 번째 dispatch: 카트에 추가
|
||||
dispatch({
|
||||
type: types.ADD_TO_CART,
|
||||
payload: response.data.data,
|
||||
});
|
||||
|
||||
// 두 번째 dispatch: 카트 정보 재조회
|
||||
// ⚠️ 문제: 순서가 보장되지 않음
|
||||
dispatch(getMyInfoCartSearch({ mbrNo }));
|
||||
};
|
||||
|
||||
TAxios(dispatch, getState, "post", URLS.ADD_TO_CART, ..., onSuccess, onFail);
|
||||
};
|
||||
```
|
||||
|
||||
**문제점**:
|
||||
1. `getMyInfoCartSearch`가 `ADD_TO_CART`보다 먼저 실행될 수 있음
|
||||
2. 카트 정보가 업데이트되기 전에 재조회가 실행될 수 있음
|
||||
3. 순서가 보장되지 않아 UI에 잘못된 데이터가 표시될 수 있음
|
||||
|
||||
## 🤔 왜 순서가 보장되지 않을까?
|
||||
|
||||
### Redux-thunk의 동작 방식
|
||||
|
||||
```javascript
|
||||
// Redux-thunk는 이렇게 동작합니다
|
||||
function dispatch(action) {
|
||||
if (typeof action === 'function') {
|
||||
// thunk action인 경우
|
||||
return action(dispatch, getState);
|
||||
} else {
|
||||
// plain action인 경우
|
||||
return next(action);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 문제 시나리오
|
||||
|
||||
```javascript
|
||||
// 이렇게 작성하면
|
||||
dispatch({ type: 'ACTION_1' }); // Plain action - 즉시 실행
|
||||
dispatch(asyncAction()); // Thunk - 비동기 실행
|
||||
dispatch({ type: 'ACTION_2' }); // Plain action - 즉시 실행
|
||||
|
||||
// 실제 실행 순서는
|
||||
// 1. ACTION_1 (동기)
|
||||
// 2. ACTION_2 (동기)
|
||||
// 3. asyncAction의 내부 dispatch들 (비동기)
|
||||
|
||||
// 즉, asyncAction이 완료되기 전에 ACTION_2가 실행됩니다!
|
||||
```
|
||||
|
||||
## 🎯 해결해야 할 과제
|
||||
|
||||
1. **순서 보장**: 여러 dispatch가 의도한 순서대로 실행되도록
|
||||
2. **에러 처리**: 중간에 에러가 발생해도 체인이 끊기지 않도록
|
||||
3. **가독성**: 코드가 직관적이고 유지보수하기 쉽도록
|
||||
4. **재사용성**: 여러 곳에서 쉽게 사용할 수 있도록
|
||||
5. **호환성**: 기존 코드와 호환되도록
|
||||
|
||||
## 📊 실제 발생 가능한 버그
|
||||
|
||||
### 시나리오 1: 카트 추가 후 조회
|
||||
|
||||
```javascript
|
||||
// 의도한 순서
|
||||
1. ADD_TO_CART dispatch
|
||||
2. 상태 업데이트
|
||||
3. getMyInfoCartSearch dispatch
|
||||
4. 최신 카트 정보 조회
|
||||
|
||||
// 실제 실행 순서 (문제)
|
||||
1. ADD_TO_CART dispatch
|
||||
2. getMyInfoCartSearch dispatch (너무 빨리 실행!)
|
||||
3. 이전 카트 정보 조회 (아직 상태 업데이트 안됨)
|
||||
4. 상태 업데이트
|
||||
→ 결과: UI에 이전 데이터가 표시됨
|
||||
```
|
||||
|
||||
### 시나리오 2: 패널 열고 닫기
|
||||
|
||||
```javascript
|
||||
// 의도한 순서
|
||||
1. PUSH_PANEL (검색 패널 열기)
|
||||
2. UPDATE_PANEL (검색 결과 표시)
|
||||
3. POP_PANEL (이전 패널 닫기)
|
||||
|
||||
// 실제 실행 순서 (문제)
|
||||
1. PUSH_PANEL
|
||||
2. POP_PANEL (너무 빨리 실행!)
|
||||
3. UPDATE_PANEL (이미 닫힌 패널을 업데이트)
|
||||
→ 결과: 패널이 제대로 표시되지 않음
|
||||
```
|
||||
|
||||
## 🔧 기존 해결 방법과 한계
|
||||
|
||||
### 방법 1: setTimeout 사용
|
||||
|
||||
```javascript
|
||||
dispatch(action1());
|
||||
setTimeout(() => {
|
||||
dispatch(action2());
|
||||
}, 0);
|
||||
```
|
||||
|
||||
**한계**:
|
||||
- 명확한 순서 보장 없음
|
||||
- 타이밍에 의존적
|
||||
- 코드 가독성 저하
|
||||
- 유지보수 어려움
|
||||
|
||||
### 방법 2: 콜백 중첩
|
||||
|
||||
```javascript
|
||||
const action1 = (callback) => (dispatch, getState) => {
|
||||
dispatch({ type: 'ACTION_1' });
|
||||
if (callback) callback();
|
||||
};
|
||||
|
||||
dispatch(action1(() => {
|
||||
dispatch(action2(() => {
|
||||
dispatch(action3());
|
||||
}));
|
||||
}));
|
||||
```
|
||||
|
||||
**한계**:
|
||||
- 콜백 지옥
|
||||
- 에러 처리 복잡
|
||||
- 코드 가독성 최악
|
||||
|
||||
### 방법 3: async/await
|
||||
|
||||
```javascript
|
||||
export const complexAction = () => async (dispatch, getState) => {
|
||||
await dispatch(action1());
|
||||
await dispatch(action2());
|
||||
await dispatch(action3());
|
||||
};
|
||||
```
|
||||
|
||||
**한계**:
|
||||
- Chrome 68 호환성 문제 (프로젝트 요구사항)
|
||||
- 모든 action이 Promise를 반환해야 함
|
||||
- 기존 코드 대량 수정 필요
|
||||
|
||||
## 🎯 다음 단계
|
||||
|
||||
이제 이러한 문제들을 해결하기 위한 3가지 솔루션을 살펴보겠습니다:
|
||||
|
||||
1. [dispatchHelper.js](./02-solution-dispatch-helper.md) - Promise 체인 기반 헬퍼 함수
|
||||
2. [asyncActionUtils.js](./03-solution-async-utils.md) - Promise 기반 비동기 처리 유틸리티
|
||||
3. [큐 기반 패널 액션 시스템](./04-solution-queue-system.md) - 미들웨어 기반 큐 시스템
|
||||
|
||||
---
|
||||
|
||||
**다음**: [해결 방법 1: dispatchHelper.js →](./02-solution-dispatch-helper.md)
|
||||
541
.docs/dispatch-async/02-solution-dispatch-helper.md
Normal file
541
.docs/dispatch-async/02-solution-dispatch-helper.md
Normal 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)
|
||||
711
.docs/dispatch-async/03-solution-async-utils.md
Normal file
711
.docs/dispatch-async/03-solution-async-utils.md
Normal file
@@ -0,0 +1,711 @@
|
||||
# 해결 방법 2: asyncActionUtils.js
|
||||
|
||||
## 📦 개요
|
||||
|
||||
**파일**: `src/utils/asyncActionUtils.js`
|
||||
**작성일**: 2025-11-06
|
||||
**커밋**: `f9290a1 [251106] fix: Dispatch Queue implementation`
|
||||
|
||||
Promise 기반의 비동기 액션 처리와 **상세한 성공/실패 기준**을 제공합니다.
|
||||
|
||||
## 🎯 핵심 개념
|
||||
|
||||
### 프로젝트 특화 성공 기준
|
||||
|
||||
이 프로젝트에서 API 호출 성공은 **2가지 조건**을 모두 만족해야 합니다:
|
||||
|
||||
1. ✅ **HTTP 상태 코드**: 200-299 범위
|
||||
2. ✅ **retCode**: 0 또는 '0'
|
||||
|
||||
```javascript
|
||||
// HTTP 200이지만 retCode가 1인 경우
|
||||
{
|
||||
status: 200, // ✅ HTTP는 성공
|
||||
data: {
|
||||
retCode: 1, // ❌ retCode는 실패
|
||||
message: "권한이 없습니다"
|
||||
}
|
||||
}
|
||||
// → 이것은 실패입니다!
|
||||
```
|
||||
|
||||
### Promise 체인이 끊기지 않는 설계
|
||||
|
||||
**핵심 원칙**: 모든 비동기 작업은 **reject 없이 resolve만 사용**합니다.
|
||||
|
||||
```javascript
|
||||
// ❌ 일반적인 방식 (Promise 체인이 끊김)
|
||||
return new Promise((resolve, reject) => {
|
||||
if (error) {
|
||||
reject(error); // 체인이 끊김!
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ 이 프로젝트의 방식 (체인 유지)
|
||||
return new Promise((resolve) => {
|
||||
if (error) {
|
||||
resolve({
|
||||
success: false,
|
||||
error: { code: 'ERROR', message: '에러 발생' }
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 핵심 함수
|
||||
|
||||
1. `isApiSuccess` - API 성공 여부 판단
|
||||
2. `fetchApi` - Promise 기반 fetch 래퍼
|
||||
3. `tAxiosToPromise` - TAxios를 Promise로 변환
|
||||
4. `wrapAsyncAction` - 비동기 액션을 Promise로 래핑
|
||||
5. `withTimeout` - 타임아웃 지원
|
||||
6. `executeParallelAsyncActions` - 병렬 실행
|
||||
|
||||
---
|
||||
|
||||
## 1️⃣ isApiSuccess
|
||||
|
||||
### 설명
|
||||
|
||||
API 응답이 성공인지 판단하는 **프로젝트 표준 함수**입니다.
|
||||
|
||||
### 구현
|
||||
|
||||
**파일**: `src/utils/asyncActionUtils.js:21-34`
|
||||
|
||||
```javascript
|
||||
export const isApiSuccess = (response, responseData) => {
|
||||
// 1️⃣ HTTP 상태 코드 확인 (200-299 성공 범위)
|
||||
if (!response.ok || response.status < 200 || response.status >= 300) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2️⃣ retCode 확인 - 0 또는 '0'이어야 성공
|
||||
if (responseData && responseData.retCode !== undefined) {
|
||||
return responseData.retCode === 0 || responseData.retCode === '0';
|
||||
}
|
||||
|
||||
// retCode가 없는 경우 HTTP 상태 코드만으로 판단
|
||||
return response.ok;
|
||||
};
|
||||
```
|
||||
|
||||
### 사용 예제
|
||||
|
||||
```javascript
|
||||
// 성공 케이스
|
||||
isApiSuccess(
|
||||
{ ok: true, status: 200 },
|
||||
{ retCode: 0, data: { ... } }
|
||||
); // → true
|
||||
|
||||
isApiSuccess(
|
||||
{ ok: true, status: 200 },
|
||||
{ retCode: '0', data: { ... } }
|
||||
); // → true
|
||||
|
||||
// 실패 케이스
|
||||
isApiSuccess(
|
||||
{ ok: true, status: 200 },
|
||||
{ retCode: 1, message: "권한 없음" }
|
||||
); // → false (retCode가 0이 아님)
|
||||
|
||||
isApiSuccess(
|
||||
{ ok: false, status: 500 },
|
||||
{ retCode: 0, data: { ... } }
|
||||
); // → false (HTTP 상태 코드가 500)
|
||||
|
||||
isApiSuccess(
|
||||
{ ok: false, status: 404 },
|
||||
{ retCode: 0 }
|
||||
); // → false (404 에러)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2️⃣ fetchApi
|
||||
|
||||
### 설명
|
||||
|
||||
**표준 fetch API를 Promise로 래핑**하여 프로젝트 성공 기준에 맞춰 처리합니다.
|
||||
|
||||
### 핵심 특징
|
||||
|
||||
- ✅ 항상 `resolve` 사용 (reject 없음)
|
||||
- ✅ HTTP 상태 + retCode 모두 확인
|
||||
- ✅ JSON 파싱 에러도 처리
|
||||
- ✅ 네트워크 에러도 처리
|
||||
- ✅ 상세한 로깅
|
||||
|
||||
### 구현
|
||||
|
||||
**파일**: `src/utils/asyncActionUtils.js:57-123`
|
||||
|
||||
```javascript
|
||||
export const fetchApi = (url, options = {}) => {
|
||||
console.log('[asyncActionUtils] 🌐 FETCH_API_START', { url, method: options.method || 'GET' });
|
||||
|
||||
return new Promise((resolve) => { // ⚠️ 항상 resolve만 사용!
|
||||
fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
},
|
||||
...options
|
||||
})
|
||||
.then(response => {
|
||||
// JSON 파싱
|
||||
return response.json()
|
||||
.then(responseData => {
|
||||
console.log('[asyncActionUtils] 📊 API_RESPONSE', {
|
||||
status: response.status,
|
||||
ok: response.ok,
|
||||
retCode: responseData.retCode,
|
||||
success: isApiSuccess(response, responseData)
|
||||
});
|
||||
|
||||
// ✅ 성공/실패 여부와 관계없이 항상 resolve
|
||||
resolve({
|
||||
response,
|
||||
data: responseData,
|
||||
success: isApiSuccess(response, responseData),
|
||||
error: !isApiSuccess(response, responseData) ? {
|
||||
code: responseData.retCode || response.status,
|
||||
message: responseData.message || getApiErrorMessage(responseData.retCode || response.status),
|
||||
httpStatus: response.status
|
||||
} : null
|
||||
});
|
||||
})
|
||||
.catch(parseError => {
|
||||
console.error('[asyncActionUtils] ❌ JSON_PARSE_ERROR', parseError);
|
||||
|
||||
// ✅ JSON 파싱 실패도 resolve로 처리
|
||||
resolve({
|
||||
response,
|
||||
data: null,
|
||||
success: false,
|
||||
error: {
|
||||
code: 'PARSE_ERROR',
|
||||
message: '응답 데이터 파싱에 실패했습니다',
|
||||
originalError: parseError
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('[asyncActionUtils] 💥 FETCH_ERROR', error);
|
||||
|
||||
// ✅ 네트워크 에러 등도 resolve로 처리
|
||||
resolve({
|
||||
response: null,
|
||||
data: null,
|
||||
success: false,
|
||||
error: {
|
||||
code: 'NETWORK_ERROR',
|
||||
message: error.message || '네트워크 오류가 발생했습니다',
|
||||
originalError: error
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### 사용 예제
|
||||
|
||||
```javascript
|
||||
import { fetchApi } from '../utils/asyncActionUtils';
|
||||
|
||||
// 기본 사용
|
||||
const result = await fetchApi('/api/products/123', {
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log('성공:', result.data);
|
||||
// HTTP 200-299 + retCode 0/'0'
|
||||
} else {
|
||||
console.error('실패:', result.error);
|
||||
// error.code, error.message 사용 가능
|
||||
}
|
||||
|
||||
// POST 요청
|
||||
const result = await fetchApi('/api/cart', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ productId: 123 })
|
||||
});
|
||||
|
||||
// 헤더 추가
|
||||
const result = await fetchApi('/api/user', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer token123'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 반환 구조
|
||||
|
||||
```javascript
|
||||
// 성공 시
|
||||
{
|
||||
response: Response, // fetch Response 객체
|
||||
data: { ... }, // 파싱된 JSON 데이터
|
||||
success: true, // 성공 플래그
|
||||
error: null // 에러 없음
|
||||
}
|
||||
|
||||
// 실패 시 (HTTP 에러)
|
||||
{
|
||||
response: Response,
|
||||
data: { retCode: 1, message: "권한 없음" },
|
||||
success: false,
|
||||
error: {
|
||||
code: 1,
|
||||
message: "권한 없음",
|
||||
httpStatus: 200
|
||||
}
|
||||
}
|
||||
|
||||
// 실패 시 (네트워크 에러)
|
||||
{
|
||||
response: null,
|
||||
data: null,
|
||||
success: false,
|
||||
error: {
|
||||
code: 'NETWORK_ERROR',
|
||||
message: '네트워크 오류가 발생했습니다',
|
||||
originalError: Error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3️⃣ tAxiosToPromise
|
||||
|
||||
### 설명
|
||||
|
||||
프로젝트에서 사용하는 **TAxios를 Promise로 변환**합니다.
|
||||
|
||||
### 구현
|
||||
|
||||
**파일**: `src/utils/asyncActionUtils.js:138-204`
|
||||
|
||||
```javascript
|
||||
export const tAxiosToPromise = (
|
||||
TAxios,
|
||||
dispatch,
|
||||
getState,
|
||||
method,
|
||||
baseUrl,
|
||||
urlParams,
|
||||
params,
|
||||
options = {}
|
||||
) => {
|
||||
return new Promise((resolve) => {
|
||||
console.log('[asyncActionUtils] 🔄 TAXIOS_TO_PROMISE_START', { method, baseUrl });
|
||||
|
||||
const enhancedOnSuccess = (response) => {
|
||||
console.log('[asyncActionUtils] ✅ TAXIOS_SUCCESS', { retCode: response?.data?.retCode });
|
||||
|
||||
// TAxios 성공 콜백도 성공 기준 적용
|
||||
const isSuccess = response?.data && (
|
||||
response.data.retCode === 0 ||
|
||||
response.data.retCode === '0'
|
||||
);
|
||||
|
||||
resolve({
|
||||
response,
|
||||
data: response.data,
|
||||
success: isSuccess,
|
||||
error: !isSuccess ? {
|
||||
code: response.data?.retCode || 'UNKNOWN_ERROR',
|
||||
message: response.data?.message || getApiErrorMessage(response.data?.retCode || 'UNKNOWN_ERROR')
|
||||
} : null
|
||||
});
|
||||
};
|
||||
|
||||
const enhancedOnFail = (error) => {
|
||||
console.error('[asyncActionUtils] ❌ TAXIOS_FAIL', error);
|
||||
|
||||
resolve({ // ⚠️ reject가 아닌 resolve
|
||||
response: null,
|
||||
data: null,
|
||||
success: false,
|
||||
error: {
|
||||
code: error.retCode || 'TAXIOS_ERROR',
|
||||
message: error.message || 'API 호출에 실패했습니다',
|
||||
originalError: error
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
TAxios(
|
||||
dispatch,
|
||||
getState,
|
||||
method,
|
||||
baseUrl,
|
||||
urlParams,
|
||||
params,
|
||||
enhancedOnSuccess,
|
||||
enhancedOnFail,
|
||||
options.noTokenRefresh || false,
|
||||
options.responseType
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[asyncActionUtils] 💥 TAXIOS_EXECUTION_ERROR', error);
|
||||
|
||||
resolve({
|
||||
response: null,
|
||||
data: null,
|
||||
success: false,
|
||||
error: {
|
||||
code: 'EXECUTION_ERROR',
|
||||
message: 'API 호출 실행 중 오류가 발생했습니다',
|
||||
originalError: error
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### 사용 예제
|
||||
|
||||
```javascript
|
||||
import { tAxiosToPromise } from '../utils/asyncActionUtils';
|
||||
import { TAxios } from '../utils/TAxios';
|
||||
|
||||
export const getProductDetail = (productId) => async (dispatch, getState) => {
|
||||
const result = await tAxiosToPromise(
|
||||
TAxios,
|
||||
dispatch,
|
||||
getState,
|
||||
'get',
|
||||
URLS.GET_PRODUCT_DETAIL,
|
||||
{},
|
||||
{ productId },
|
||||
{}
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
dispatch({
|
||||
type: types.GET_PRODUCT_DETAIL,
|
||||
payload: result.data.data
|
||||
});
|
||||
} else {
|
||||
console.error('상품 조회 실패:', result.error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4️⃣ wrapAsyncAction
|
||||
|
||||
### 설명
|
||||
|
||||
비동기 액션 함수를 Promise로 래핑하여 **표준화된 결과 구조**를 반환합니다.
|
||||
|
||||
### 구현
|
||||
|
||||
**파일**: `src/utils/asyncActionUtils.js:215-270`
|
||||
|
||||
```javascript
|
||||
export const wrapAsyncAction = (asyncAction, context = {}) => {
|
||||
return new Promise((resolve) => {
|
||||
const { dispatch, getState } = context;
|
||||
|
||||
console.log('[asyncActionUtils] 🎯 WRAP_ASYNC_ACTION_START');
|
||||
|
||||
// 성공 콜백 - 항상 resolve 호출
|
||||
const onSuccess = (result) => {
|
||||
console.log('[asyncActionUtils] ✅ WRAP_ASYNC_SUCCESS', result);
|
||||
|
||||
resolve({
|
||||
response: result.response || result,
|
||||
data: result.data || result,
|
||||
success: true,
|
||||
error: null
|
||||
});
|
||||
};
|
||||
|
||||
// 실패 콜백 - 항상 resolve 호출 (reject 하지 않음)
|
||||
const onFail = (error) => {
|
||||
console.error('[asyncActionUtils] ❌ WRAP_ASYNC_FAIL', error);
|
||||
|
||||
resolve({
|
||||
response: null,
|
||||
data: null,
|
||||
success: false,
|
||||
error: {
|
||||
code: error.retCode || error.code || 'ASYNC_ACTION_ERROR',
|
||||
message: error.message || error.errorMessage || '비동기 작업에 실패했습니다',
|
||||
originalError: error
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
// 비동기 액션 실행
|
||||
const result = asyncAction(dispatch, getState, onSuccess, onFail);
|
||||
|
||||
// Promise를 반환하는 경우도 처리
|
||||
if (result && typeof result.then === 'function') {
|
||||
result
|
||||
.then(onSuccess)
|
||||
.catch(onFail);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[asyncActionUtils] 💥 WRAP_ASYNC_EXECUTION_ERROR', error);
|
||||
onFail(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### 사용 예제
|
||||
|
||||
```javascript
|
||||
import { wrapAsyncAction } from '../utils/asyncActionUtils';
|
||||
|
||||
// 비동기 액션 정의
|
||||
const myAsyncAction = (dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(dispatch, getState, 'get', URL, {}, {}, onSuccess, onFail);
|
||||
};
|
||||
|
||||
// Promise로 래핑하여 사용
|
||||
const result = await wrapAsyncAction(myAsyncAction, { dispatch, getState });
|
||||
|
||||
if (result.success) {
|
||||
console.log('성공:', result.data);
|
||||
} else {
|
||||
console.error('실패:', result.error.message);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5️⃣ withTimeout
|
||||
|
||||
### 설명
|
||||
|
||||
Promise에 **타임아웃**을 적용합니다.
|
||||
|
||||
### 구현
|
||||
|
||||
**파일**: `src/utils/asyncActionUtils.js:354-373`
|
||||
|
||||
```javascript
|
||||
export const withTimeout = (
|
||||
promise,
|
||||
timeoutMs,
|
||||
timeoutMessage = '작업 시간이 초과되었습니다'
|
||||
) => {
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
console.error('[asyncActionUtils] ⏰ PROMISE_TIMEOUT', { timeoutMs });
|
||||
resolve({
|
||||
response: null,
|
||||
data: null,
|
||||
success: false,
|
||||
error: {
|
||||
code: 'TIMEOUT',
|
||||
message: timeoutMessage,
|
||||
timeout: timeoutMs
|
||||
}
|
||||
});
|
||||
}, timeoutMs);
|
||||
})
|
||||
]);
|
||||
};
|
||||
```
|
||||
|
||||
### 사용 예제
|
||||
|
||||
```javascript
|
||||
import { withTimeout, fetchApi } from '../utils/asyncActionUtils';
|
||||
|
||||
// 5초 타임아웃
|
||||
const result = await withTimeout(
|
||||
fetchApi('/api/slow-endpoint'),
|
||||
5000,
|
||||
'요청이 시간초과 되었습니다'
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log('성공:', result.data);
|
||||
} else if (result.error.code === 'TIMEOUT') {
|
||||
console.error('타임아웃 발생');
|
||||
} else {
|
||||
console.error('기타 에러:', result.error);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6️⃣ executeParallelAsyncActions
|
||||
|
||||
### 설명
|
||||
|
||||
여러 비동기 액션을 **병렬로 실행**하고 모든 결과를 기다립니다.
|
||||
|
||||
### 구현
|
||||
|
||||
**파일**: `src/utils/asyncActionUtils.js:279-299`
|
||||
|
||||
```javascript
|
||||
export const executeParallelAsyncActions = (asyncActions, context = {}) => {
|
||||
console.log('[asyncActionUtils] 🚀 EXECUTE_PARALLEL_START', { count: asyncActions.length });
|
||||
|
||||
const promises = asyncActions.map(action =>
|
||||
wrapAsyncAction(action, context)
|
||||
);
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(results => {
|
||||
console.log('[asyncActionUtils] ✅ EXECUTE_PARALLEL_SUCCESS', {
|
||||
successCount: results.filter(r => r.success).length,
|
||||
failCount: results.filter(r => !r.success).length
|
||||
});
|
||||
return results;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('[asyncActionUtils] ❌ EXECUTE_PARALLEL_ERROR', error);
|
||||
return [];
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### 사용 예제
|
||||
|
||||
```javascript
|
||||
import { executeParallelAsyncActions } from '../utils/asyncActionUtils';
|
||||
|
||||
// 3개의 API를 동시에 호출
|
||||
const results = await executeParallelAsyncActions([
|
||||
(dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(dispatch, getState, 'get', URL1, {}, {}, onSuccess, onFail);
|
||||
},
|
||||
(dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(dispatch, getState, 'get', URL2, {}, {}, onSuccess, onFail);
|
||||
},
|
||||
(dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(dispatch, getState, 'get', URL3, {}, {}, onSuccess, onFail);
|
||||
}
|
||||
], { dispatch, getState });
|
||||
|
||||
// 결과 처리
|
||||
results.forEach((result, index) => {
|
||||
if (result.success) {
|
||||
console.log(`API ${index + 1} 성공:`, result.data);
|
||||
} else {
|
||||
console.error(`API ${index + 1} 실패:`, result.error);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 실제 사용 시나리오
|
||||
|
||||
### 시나리오 1: API 호출 후 후속 처리
|
||||
|
||||
```javascript
|
||||
import { tAxiosToPromise } from '../utils/asyncActionUtils';
|
||||
|
||||
export const addToCartAndRefresh = (productId) => async (dispatch, getState) => {
|
||||
// 1. 카트에 추가
|
||||
const addResult = await tAxiosToPromise(
|
||||
TAxios,
|
||||
dispatch,
|
||||
getState,
|
||||
'post',
|
||||
URLS.ADD_TO_CART,
|
||||
{},
|
||||
{ productId },
|
||||
{}
|
||||
);
|
||||
|
||||
if (addResult.success) {
|
||||
// 2. 카트 추가 성공 시 카트 정보 재조회
|
||||
dispatch({ type: types.ADD_TO_CART, payload: addResult.data.data });
|
||||
|
||||
const cartResult = await tAxiosToPromise(
|
||||
TAxios,
|
||||
dispatch,
|
||||
getState,
|
||||
'get',
|
||||
URLS.GET_CART,
|
||||
{},
|
||||
{ mbrNo: addResult.data.data.mbrNo },
|
||||
{}
|
||||
);
|
||||
|
||||
if (cartResult.success) {
|
||||
dispatch({ type: types.GET_CART, payload: cartResult.data.data });
|
||||
}
|
||||
} else {
|
||||
console.error('카트 추가 실패:', addResult.error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 시나리오 2: 타임아웃이 있는 API 호출
|
||||
|
||||
```javascript
|
||||
import { tAxiosToPromise, withTimeout } from '../utils/asyncActionUtils';
|
||||
|
||||
export const getLargeData = () => async (dispatch, getState) => {
|
||||
const result = await withTimeout(
|
||||
tAxiosToPromise(
|
||||
TAxios,
|
||||
dispatch,
|
||||
getState,
|
||||
'get',
|
||||
URLS.GET_LARGE_DATA,
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
),
|
||||
10000, // 10초 타임아웃
|
||||
'데이터 조회 시간이 초과되었습니다'
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
dispatch({ type: types.GET_LARGE_DATA, payload: result.data.data });
|
||||
} else if (result.error.code === 'TIMEOUT') {
|
||||
// 타임아웃 처리
|
||||
dispatch({ type: types.SHOW_TIMEOUT_MESSAGE });
|
||||
} else {
|
||||
// 기타 에러 처리
|
||||
console.error('조회 실패:', result.error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 장점
|
||||
|
||||
1. **성공 기준 명확화**: HTTP + retCode 모두 확인
|
||||
2. **체인 보장**: reject 없이 resolve만 사용하여 Promise 체인 유지
|
||||
3. **상세한 로깅**: 모든 단계에서 로그 출력
|
||||
4. **타임아웃 지원**: 응답 없는 API 처리 가능
|
||||
5. **에러 처리**: 모든 에러를 표준 구조로 반환
|
||||
|
||||
## ⚠️ 주의사항
|
||||
|
||||
1. **Chrome 68 호환**: async/await 사용 가능하지만 주의 필요
|
||||
2. **항상 resolve**: reject 사용하지 않음
|
||||
3. **success 플래그**: 반드시 `result.success` 확인 필요
|
||||
|
||||
---
|
||||
|
||||
**다음**: [해결 방법 3: 큐 기반 패널 액션 시스템 →](./04-solution-queue-system.md)
|
||||
644
.docs/dispatch-async/04-solution-queue-system.md
Normal file
644
.docs/dispatch-async/04-solution-queue-system.md
Normal file
@@ -0,0 +1,644 @@
|
||||
# 해결 방법 3: 큐 기반 패널 액션 시스템
|
||||
|
||||
## 📦 개요
|
||||
|
||||
**관련 파일**:
|
||||
- `src/actions/queuedPanelActions.js`
|
||||
- `src/middleware/panelQueueMiddleware.js`
|
||||
- `src/reducers/panelReducer.js`
|
||||
- `src/store/store.js` (미들웨어 등록 필요)
|
||||
|
||||
**작성일**: 2025-11-06
|
||||
**커밋**:
|
||||
- `5bd2774 [251106] feat: Queued Panel functions`
|
||||
- `f9290a1 [251106] fix: Dispatch Queue implementation`
|
||||
|
||||
미들웨어 기반의 **액션 큐 처리 시스템**으로, 패널 액션들을 순차적으로 실행합니다.
|
||||
|
||||
## ⚠️ 사전 요구사항
|
||||
|
||||
큐 시스템을 사용하려면 **반드시** store에 panelQueueMiddleware를 등록해야 합니다.
|
||||
|
||||
**파일**: `src/store/store.js`
|
||||
|
||||
```javascript
|
||||
import panelQueueMiddleware from '../middleware/panelQueueMiddleware';
|
||||
|
||||
export const store = createStore(
|
||||
rootReducer,
|
||||
applyMiddleware(thunk, panelHistoryMiddleware, autoCloseMiddleware, panelQueueMiddleware)
|
||||
);
|
||||
```
|
||||
|
||||
미들웨어를 등록하지 않으면 큐에 액션이 추가되어도 자동으로 처리되지 않습니다!
|
||||
|
||||
## 🎯 핵심 개념
|
||||
|
||||
### 왜 큐 시스템이 필요한가?
|
||||
|
||||
패널 관련 액션들은 특히 순서가 중요합니다:
|
||||
|
||||
```javascript
|
||||
// 문제 상황
|
||||
dispatch(pushPanel({ name: 'SEARCH' })); // 검색 패널 열기
|
||||
dispatch(updatePanel({ results: [...] })); // 검색 결과 업데이트
|
||||
dispatch(popPanel('LOADING')); // 로딩 패널 닫기
|
||||
|
||||
// 실제 실행 순서 (문제!)
|
||||
// → popPanel이 먼저 실행될 수 있음
|
||||
// → updatePanel이 pushPanel보다 먼저 실행될 수 있음
|
||||
```
|
||||
|
||||
### 큐 시스템의 동작 방식
|
||||
|
||||
```
|
||||
[큐에 추가] → [미들웨어 감지] → [순차 처리] → [완료]
|
||||
↓ ↓ ↓ ↓
|
||||
ENQUEUE 자동 감지 시작 PROCESS_QUEUE 다음 액션
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 주요 컴포넌트
|
||||
|
||||
### 1. queuedPanelActions.js
|
||||
|
||||
패널 액션을 큐에 추가하는 액션 크리에이터들
|
||||
|
||||
### 2. panelQueueMiddleware.js
|
||||
|
||||
큐에 액션이 추가되면 자동으로 처리를 시작하는 미들웨어
|
||||
|
||||
### 3. panelReducer.js
|
||||
|
||||
큐 상태를 관리하는 리듀서
|
||||
|
||||
---
|
||||
|
||||
## 📋 기본 패널 액션
|
||||
|
||||
### 1. pushPanelQueued
|
||||
|
||||
패널을 큐에 추가하여 순차적으로 열기
|
||||
|
||||
```javascript
|
||||
import { pushPanelQueued } from '../actions/queuedPanelActions';
|
||||
|
||||
// 기본 사용
|
||||
dispatch(pushPanelQueued(
|
||||
{ name: panel_names.SEARCH_PANEL },
|
||||
false // duplicatable
|
||||
));
|
||||
|
||||
// 중복 허용
|
||||
dispatch(pushPanelQueued(
|
||||
{ name: panel_names.PRODUCT_DETAIL, productId: 123 },
|
||||
true // 중복 허용
|
||||
));
|
||||
```
|
||||
|
||||
### 2. popPanelQueued
|
||||
|
||||
패널을 큐를 통해 제거
|
||||
|
||||
```javascript
|
||||
import { popPanelQueued } from '../actions/queuedPanelActions';
|
||||
|
||||
// 마지막 패널 제거
|
||||
dispatch(popPanelQueued());
|
||||
|
||||
// 특정 패널 제거
|
||||
dispatch(popPanelQueued(panel_names.SEARCH_PANEL));
|
||||
```
|
||||
|
||||
### 3. updatePanelQueued
|
||||
|
||||
패널 정보를 큐를 통해 업데이트
|
||||
|
||||
```javascript
|
||||
import { updatePanelQueued } from '../actions/queuedPanelActions';
|
||||
|
||||
dispatch(updatePanelQueued({
|
||||
name: panel_names.SEARCH_PANEL,
|
||||
panelInfo: {
|
||||
results: [...],
|
||||
totalCount: 100
|
||||
}
|
||||
}));
|
||||
```
|
||||
|
||||
### 4. resetPanelsQueued
|
||||
|
||||
모든 패널을 초기화
|
||||
|
||||
```javascript
|
||||
import { resetPanelsQueued } from '../actions/queuedPanelActions';
|
||||
|
||||
// 빈 패널로 초기화
|
||||
dispatch(resetPanelsQueued());
|
||||
|
||||
// 특정 패널들로 초기화
|
||||
dispatch(resetPanelsQueued([
|
||||
{ name: panel_names.HOME }
|
||||
]));
|
||||
```
|
||||
|
||||
### 5. enqueueMultiplePanelActions
|
||||
|
||||
여러 패널 액션을 한 번에 큐에 추가
|
||||
|
||||
```javascript
|
||||
import { enqueueMultiplePanelActions, pushPanelQueued, updatePanelQueued, popPanelQueued }
|
||||
from '../actions/queuedPanelActions';
|
||||
|
||||
dispatch(enqueueMultiplePanelActions([
|
||||
pushPanelQueued({ name: panel_names.SEARCH_PANEL }),
|
||||
updatePanelQueued({ name: panel_names.SEARCH_PANEL, panelInfo: { query: 'test' } }),
|
||||
popPanelQueued(panel_names.LOADING_PANEL)
|
||||
]));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 비동기 패널 액션
|
||||
|
||||
### 1. enqueueAsyncPanelAction
|
||||
|
||||
비동기 작업(API 호출 등)을 큐에 추가하여 순차 실행
|
||||
|
||||
**파일**: `src/actions/queuedPanelActions.js:173-199`
|
||||
|
||||
```javascript
|
||||
import { enqueueAsyncPanelAction } from '../actions/queuedPanelActions';
|
||||
|
||||
dispatch(enqueueAsyncPanelAction({
|
||||
id: 'search_products_123', // 고유 ID
|
||||
|
||||
// 비동기 액션 (TAxios 등)
|
||||
asyncAction: (dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(
|
||||
dispatch,
|
||||
getState,
|
||||
'post',
|
||||
URLS.SEARCH_PRODUCTS,
|
||||
{},
|
||||
{ keyword: 'test' },
|
||||
onSuccess,
|
||||
onFail
|
||||
);
|
||||
},
|
||||
|
||||
// 성공 콜백
|
||||
onSuccess: (response) => {
|
||||
console.log('검색 성공:', response);
|
||||
dispatch(pushPanelQueued({
|
||||
name: panel_names.SEARCH_RESULT,
|
||||
results: response.data.results
|
||||
}));
|
||||
},
|
||||
|
||||
// 실패 콜백
|
||||
onFail: (error) => {
|
||||
console.error('검색 실패:', error);
|
||||
dispatch(pushPanelQueued({
|
||||
name: panel_names.ERROR,
|
||||
message: error.message
|
||||
}));
|
||||
},
|
||||
|
||||
// 완료 콜백 (성공/실패 모두 호출)
|
||||
onFinish: (isSuccess, result) => {
|
||||
console.log('검색 완료:', isSuccess ? '성공' : '실패');
|
||||
},
|
||||
|
||||
// 타임아웃 (ms)
|
||||
timeout: 10000 // 10초
|
||||
}));
|
||||
```
|
||||
|
||||
### 동작 흐름
|
||||
|
||||
```
|
||||
1. enqueueAsyncPanelAction 호출
|
||||
↓
|
||||
2. ENQUEUE_ASYNC_PANEL_ACTION dispatch
|
||||
↓
|
||||
3. executeAsyncAction 자동 실행
|
||||
↓
|
||||
4. wrapAsyncAction으로 Promise 래핑
|
||||
↓
|
||||
5. withTimeout으로 타임아웃 적용
|
||||
↓
|
||||
6. 결과에 따라 onSuccess 또는 onFail 호출
|
||||
↓
|
||||
7. COMPLETE_ASYNC_PANEL_ACTION 또는 FAIL_ASYNC_PANEL_ACTION dispatch
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 API 호출 후 패널 액션
|
||||
|
||||
### createApiWithPanelActions
|
||||
|
||||
API 호출 후 여러 패널 액션을 자동으로 실행
|
||||
|
||||
**파일**: `src/actions/queuedPanelActions.js:355-394`
|
||||
|
||||
```javascript
|
||||
import { createApiWithPanelActions } from '../actions/queuedPanelActions';
|
||||
|
||||
dispatch(createApiWithPanelActions({
|
||||
// API 호출
|
||||
apiCall: (dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(
|
||||
dispatch,
|
||||
getState,
|
||||
'post',
|
||||
URLS.SEARCH_PRODUCTS,
|
||||
{},
|
||||
{ keyword: 'laptop' },
|
||||
onSuccess,
|
||||
onFail
|
||||
);
|
||||
},
|
||||
|
||||
// API 성공 후 실행할 패널 액션들
|
||||
panelActions: [
|
||||
// Plain action
|
||||
{ type: 'PUSH_PANEL', payload: { name: panel_names.SEARCH_PANEL } },
|
||||
|
||||
// Dynamic action (response 사용)
|
||||
(response) => updatePanelQueued({
|
||||
name: panel_names.SEARCH_PANEL,
|
||||
panelInfo: {
|
||||
results: response.data.results,
|
||||
totalCount: response.data.totalCount
|
||||
}
|
||||
}),
|
||||
|
||||
// 또 다른 패널 액션
|
||||
popPanelQueued(panel_names.LOADING_PANEL)
|
||||
],
|
||||
|
||||
// API 성공 콜백
|
||||
onApiSuccess: (response) => {
|
||||
console.log('API 성공:', response.data.totalCount, '개 검색됨');
|
||||
},
|
||||
|
||||
// API 실패 콜백
|
||||
onApiFail: (error) => {
|
||||
console.error('API 실패:', error);
|
||||
dispatch(pushPanelQueued({
|
||||
name: panel_names.ERROR,
|
||||
message: '검색에 실패했습니다'
|
||||
}));
|
||||
}
|
||||
}));
|
||||
```
|
||||
|
||||
### 사용 예제: 상품 검색
|
||||
|
||||
```javascript
|
||||
export const searchProducts = (keyword) =>
|
||||
createApiWithPanelActions({
|
||||
apiCall: (dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(
|
||||
dispatch,
|
||||
getState,
|
||||
'post',
|
||||
URLS.SEARCH_PRODUCTS,
|
||||
{},
|
||||
{ keyword },
|
||||
onSuccess,
|
||||
onFail
|
||||
);
|
||||
},
|
||||
panelActions: [
|
||||
// 1. 로딩 패널 닫기
|
||||
popPanelQueued(panel_names.LOADING_PANEL),
|
||||
|
||||
// 2. 검색 결과 패널 열기
|
||||
(response) => pushPanelQueued({
|
||||
name: panel_names.SEARCH_RESULT,
|
||||
results: response.data.results
|
||||
}),
|
||||
|
||||
// 3. 검색 히스토리 업데이트
|
||||
(response) => updatePanelQueued({
|
||||
name: panel_names.SEARCH_HISTORY,
|
||||
panelInfo: { lastSearch: keyword }
|
||||
})
|
||||
],
|
||||
onApiSuccess: (response) => {
|
||||
console.log(`${response.data.totalCount}개의 상품을 찾았습니다`);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 비동기 액션 시퀀스
|
||||
|
||||
### createAsyncPanelSequence
|
||||
|
||||
여러 비동기 액션을 **순차적으로** 실행
|
||||
|
||||
**파일**: `src/actions/queuedPanelActions.js:401-445`
|
||||
|
||||
```javascript
|
||||
import { createAsyncPanelSequence } from '../actions/queuedPanelActions';
|
||||
|
||||
dispatch(createAsyncPanelSequence([
|
||||
// 첫 번째 비동기 액션
|
||||
{
|
||||
asyncAction: (dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(dispatch, getState, 'get', URLS.GET_USER_INFO, {}, {}, onSuccess, onFail);
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
console.log('사용자 정보 조회 성공');
|
||||
dispatch(pushPanelQueued({
|
||||
name: panel_names.USER_INFO,
|
||||
userInfo: response.data.data
|
||||
}));
|
||||
},
|
||||
onFail: (error) => {
|
||||
console.error('사용자 정보 조회 실패:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 두 번째 비동기 액션 (첫 번째 완료 후 실행)
|
||||
{
|
||||
asyncAction: (dispatch, getState, onSuccess, onFail) => {
|
||||
const userInfo = getState().user.info;
|
||||
TAxios(
|
||||
dispatch,
|
||||
getState,
|
||||
'get',
|
||||
URLS.GET_CART,
|
||||
{},
|
||||
{ mbrNo: userInfo.mbrNo },
|
||||
onSuccess,
|
||||
onFail
|
||||
);
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
console.log('카트 정보 조회 성공');
|
||||
dispatch(updatePanelQueued({
|
||||
name: panel_names.USER_INFO,
|
||||
panelInfo: { cartCount: response.data.data.length }
|
||||
}));
|
||||
},
|
||||
onFail: (error) => {
|
||||
console.error('카트 정보 조회 실패:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 세 번째 비동기 액션 (두 번째 완료 후 실행)
|
||||
{
|
||||
asyncAction: (dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(dispatch, getState, 'get', URLS.GET_ORDERS, {}, {}, onSuccess, onFail);
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
console.log('주문 정보 조회 성공');
|
||||
dispatch(pushPanelQueued({
|
||||
name: panel_names.ORDER_LIST,
|
||||
orders: response.data.data
|
||||
}));
|
||||
},
|
||||
onFail: (error) => {
|
||||
console.error('주문 정보 조회 실패:', error);
|
||||
// 실패 시 시퀀스 중단
|
||||
}
|
||||
}
|
||||
]));
|
||||
```
|
||||
|
||||
### 동작 흐름
|
||||
|
||||
```
|
||||
Action 1 실행 → 성공? → Action 2 실행 → 성공? → Action 3 실행
|
||||
↓ ↓
|
||||
실패 시 실패 시
|
||||
중단 중단
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 미들웨어: panelQueueMiddleware
|
||||
|
||||
### 동작 원리
|
||||
|
||||
**파일**: `src/middleware/panelQueueMiddleware.js`
|
||||
|
||||
```javascript
|
||||
const panelQueueMiddleware = (store) => (next) => (action) => {
|
||||
const result = next(action);
|
||||
|
||||
// 큐에 액션이 추가되면 자동으로 처리 시작
|
||||
if (action.type === types.ENQUEUE_PANEL_ACTION) {
|
||||
console.log('[panelQueueMiddleware] 🚀 ACTION_ENQUEUED', {
|
||||
action: action.payload.action,
|
||||
queueId: action.payload.id,
|
||||
});
|
||||
|
||||
// setTimeout을 사용하여 현재 액션이 완전히 처리된 후에 큐 처리 시작
|
||||
setTimeout(() => {
|
||||
const currentState = store.getState();
|
||||
|
||||
if (currentState.panels) {
|
||||
// 이미 처리 중이 아니고 큐에 액션이 있으면 처리 시작
|
||||
if (!currentState.panels.isProcessingQueue &&
|
||||
currentState.panels.panelActionQueue.length > 0) {
|
||||
console.log('[panelQueueMiddleware] ⚡ STARTING_QUEUE_PROCESS');
|
||||
store.dispatch({ type: types.PROCESS_PANEL_QUEUE });
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// 큐 처리가 완료되고 남은 큐가 있으면 계속 처리
|
||||
if (action.type === types.PROCESS_PANEL_QUEUE) {
|
||||
setTimeout(() => {
|
||||
const currentState = store.getState();
|
||||
|
||||
if (currentState.panels) {
|
||||
// 처리 중이 아니고 큐에 남은 액션이 있으면 계속 처리
|
||||
if (!currentState.panels.isProcessingQueue &&
|
||||
currentState.panels.panelActionQueue.length > 0) {
|
||||
console.log('[panelQueueMiddleware] 🔄 CONTINUING_QUEUE_PROCESS');
|
||||
store.dispatch({ type: types.PROCESS_PANEL_QUEUE });
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
```
|
||||
|
||||
### 주요 특징
|
||||
|
||||
1. ✅ **자동 시작**: 큐에 액션 추가 시 자동으로 처리 시작
|
||||
2. ✅ **연속 처리**: 한 액션 완료 후 자동으로 다음 액션 처리
|
||||
3. ✅ **중복 방지**: 이미 처리 중이면 새로 시작하지 않음
|
||||
4. ✅ **로깅**: 모든 단계에서 로그 출력
|
||||
|
||||
---
|
||||
|
||||
## 📊 리듀서 상태 구조
|
||||
|
||||
### panelReducer.js의 큐 관련 상태
|
||||
|
||||
```javascript
|
||||
{
|
||||
panels: [], // 실제 패널 스택
|
||||
lastPanelAction: 'push', // 마지막 액션 타입
|
||||
|
||||
// 큐 관련 상태
|
||||
panelActionQueue: [ // 처리 대기 중인 큐
|
||||
{
|
||||
id: 'queue_item_1_1699999999999',
|
||||
action: 'PUSH_PANEL',
|
||||
panel: { name: 'SEARCH_PANEL' },
|
||||
duplicatable: false,
|
||||
timestamp: 1699999999999
|
||||
},
|
||||
// ...
|
||||
],
|
||||
|
||||
isProcessingQueue: false, // 큐 처리 중 여부
|
||||
queueError: null, // 큐 처리 에러
|
||||
|
||||
queueStats: { // 큐 통계
|
||||
totalProcessed: 0, // 총 처리된 액션 수
|
||||
failedCount: 0, // 실패한 액션 수
|
||||
averageProcessingTime: 0 // 평균 처리 시간 (ms)
|
||||
},
|
||||
|
||||
// 비동기 액션 상태
|
||||
asyncActions: { // 실행 중인 비동기 액션들
|
||||
'async_action_1': {
|
||||
id: 'async_action_1',
|
||||
status: 'pending', // 'pending' | 'success' | 'failed'
|
||||
timestamp: 1699999999999
|
||||
}
|
||||
},
|
||||
|
||||
completedAsyncActions: [ // 완료된 액션 ID들
|
||||
'async_action_1',
|
||||
'async_action_2'
|
||||
],
|
||||
|
||||
failedAsyncActions: [ // 실패한 액션 ID들
|
||||
'async_action_3'
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 실제 사용 시나리오
|
||||
|
||||
### 시나리오 1: 검색 플로우
|
||||
|
||||
```javascript
|
||||
export const performSearch = (keyword) => (dispatch) => {
|
||||
// 1. 로딩 패널 열기
|
||||
dispatch(pushPanelQueued({ name: panel_names.LOADING }));
|
||||
|
||||
// 2. 검색 API 호출 후 결과 표시
|
||||
dispatch(createApiWithPanelActions({
|
||||
apiCall: (dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(dispatch, getState, 'post', URLS.SEARCH, {}, { keyword }, onSuccess, onFail);
|
||||
},
|
||||
panelActions: [
|
||||
popPanelQueued(panel_names.LOADING),
|
||||
(response) => pushPanelQueued({
|
||||
name: panel_names.SEARCH_RESULT,
|
||||
results: response.data.results
|
||||
})
|
||||
]
|
||||
}));
|
||||
};
|
||||
```
|
||||
|
||||
### 시나리오 2: 다단계 결제 프로세스
|
||||
|
||||
```javascript
|
||||
export const processCheckout = (orderInfo) =>
|
||||
createAsyncPanelSequence([
|
||||
// 1단계: 주문 검증
|
||||
{
|
||||
asyncAction: (dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(dispatch, getState, 'post', URLS.VALIDATE_ORDER, {}, orderInfo, onSuccess, onFail);
|
||||
},
|
||||
onSuccess: () => {
|
||||
dispatch(updatePanelQueued({
|
||||
name: panel_names.CHECKOUT,
|
||||
panelInfo: { step: 1, status: 'validated' }
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
// 2단계: 결제 처리
|
||||
{
|
||||
asyncAction: (dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(dispatch, getState, 'post', URLS.PROCESS_PAYMENT, {}, orderInfo, onSuccess, onFail);
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
dispatch(updatePanelQueued({
|
||||
name: panel_names.CHECKOUT,
|
||||
panelInfo: { step: 2, paymentId: response.data.data.paymentId }
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
// 3단계: 주문 확정
|
||||
{
|
||||
asyncAction: (dispatch, getState, onSuccess, onFail) => {
|
||||
const state = getState();
|
||||
const paymentId = state.panels.panels.find(p => p.name === panel_names.CHECKOUT)
|
||||
.panelInfo.paymentId;
|
||||
|
||||
TAxios(
|
||||
dispatch,
|
||||
getState,
|
||||
'post',
|
||||
URLS.CONFIRM_ORDER,
|
||||
{},
|
||||
{ ...orderInfo, paymentId },
|
||||
onSuccess,
|
||||
onFail
|
||||
);
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
dispatch(popPanelQueued(panel_names.CHECKOUT));
|
||||
dispatch(pushPanelQueued({
|
||||
name: panel_names.ORDER_COMPLETE,
|
||||
orderId: response.data.data.orderId
|
||||
}));
|
||||
}
|
||||
}
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 장점
|
||||
|
||||
1. **완벽한 순서 보장**: 큐 시스템으로 100% 순서 보장
|
||||
2. **자동 처리**: 미들웨어가 자동으로 큐 처리
|
||||
3. **비동기 지원**: API 호출 등 비동기 작업 완벽 지원
|
||||
4. **타임아웃**: 응답 없는 작업 자동 처리
|
||||
5. **에러 복구**: 에러 발생 시에도 다음 액션 계속 처리
|
||||
6. **통계**: 큐 처리 통계 자동 수집
|
||||
|
||||
## ⚠️ 주의사항
|
||||
|
||||
1. **미들웨어 등록**: store에 panelQueueMiddleware 등록 필요
|
||||
2. **리듀서 확장**: panelReducer에 큐 관련 상태 추가 필요
|
||||
3. **기존 코드**: 기존 pushPanel 등과 병행 사용 가능
|
||||
|
||||
---
|
||||
|
||||
**다음**: [사용 패턴 및 예제 →](./05-usage-patterns.md)
|
||||
804
.docs/dispatch-async/05-usage-patterns.md
Normal file
804
.docs/dispatch-async/05-usage-patterns.md
Normal file
@@ -0,0 +1,804 @@
|
||||
# 사용 패턴 및 예제
|
||||
|
||||
## 📋 목차
|
||||
|
||||
1. [어떤 솔루션을 선택할까?](#어떤-솔루션을-선택할까)
|
||||
2. [공통 패턴](#공통-패턴)
|
||||
3. [실전 예제](#실전-예제)
|
||||
4. [마이그레이션 가이드](#마이그레이션-가이드)
|
||||
5. [Best Practices](#best-practices)
|
||||
|
||||
---
|
||||
|
||||
## 어떤 솔루션을 선택할까?
|
||||
|
||||
### 의사결정 플로우차트
|
||||
|
||||
```
|
||||
패널 관련 액션인가?
|
||||
├─ YES → 큐 기반 패널 액션 시스템 사용
|
||||
│ (queuedPanelActions.js)
|
||||
│
|
||||
└─ NO → API 호출이 포함되어 있는가?
|
||||
├─ YES → API 패턴은?
|
||||
│ ├─ API 후 여러 dispatch 필요 → createApiThunkWithChain
|
||||
│ ├─ 로딩 상태 관리 필요 → withLoadingState
|
||||
│ └─ Promise 기반 처리 필요 → asyncActionUtils
|
||||
│
|
||||
└─ NO → 순차적 dispatch만 필요
|
||||
→ createSequentialDispatch
|
||||
```
|
||||
|
||||
### 솔루션 비교표
|
||||
|
||||
| 상황 | 추천 솔루션 | 파일 |
|
||||
|------|------------|------|
|
||||
| 패널 열기/닫기/업데이트 | `pushPanelQueued`, `popPanelQueued` | queuedPanelActions.js |
|
||||
| API 호출 후 패널 업데이트 | `createApiWithPanelActions` | queuedPanelActions.js |
|
||||
| 여러 API 순차 호출 | `createAsyncPanelSequence` | queuedPanelActions.js |
|
||||
| API 후 여러 dispatch | `createApiThunkWithChain` | dispatchHelper.js |
|
||||
| 로딩 상태 자동 관리 | `withLoadingState` | dispatchHelper.js |
|
||||
| 단순 순차 dispatch | `createSequentialDispatch` | dispatchHelper.js |
|
||||
| Promise 기반 API 호출 | `fetchApi`, `tAxiosToPromise` | asyncActionUtils.js |
|
||||
|
||||
---
|
||||
|
||||
## 공통 패턴
|
||||
|
||||
### 패턴 1: API 후 State 업데이트
|
||||
|
||||
#### Before
|
||||
```javascript
|
||||
export const getProductDetail = (productId) => (dispatch, getState) => {
|
||||
const onSuccess = (response) => {
|
||||
dispatch({ type: types.GET_PRODUCT_DETAIL, payload: response.data.data });
|
||||
dispatch(getRelatedProducts(productId));
|
||||
};
|
||||
|
||||
TAxios(dispatch, getState, 'get', URLS.GET_PRODUCT, {}, { productId }, onSuccess, onFail);
|
||||
};
|
||||
```
|
||||
|
||||
#### After (dispatchHelper)
|
||||
```javascript
|
||||
export const getProductDetail = (productId) =>
|
||||
createApiThunkWithChain(
|
||||
(d, gs, onS, onF) => TAxios(d, gs, 'get', URLS.GET_PRODUCT, {}, { productId }, onS, onF),
|
||||
[
|
||||
(response) => ({ type: types.GET_PRODUCT_DETAIL, payload: response.data.data }),
|
||||
getRelatedProducts(productId)
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
#### After (asyncActionUtils - Chrome 68+)
|
||||
```javascript
|
||||
export const getProductDetail = (productId) => async (dispatch, getState) => {
|
||||
const result = await tAxiosToPromise(
|
||||
TAxios, dispatch, getState, 'get', URLS.GET_PRODUCT, {}, { productId }
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
dispatch({ type: types.GET_PRODUCT_DETAIL, payload: result.data.data });
|
||||
dispatch(getRelatedProducts(productId));
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 패턴 2: 로딩 상태 관리
|
||||
|
||||
#### Before
|
||||
```javascript
|
||||
export const fetchUserData = (userId) => (dispatch, getState) => {
|
||||
dispatch(changeAppStatus({ showLoadingPanel: { show: true } }));
|
||||
|
||||
const onSuccess = (response) => {
|
||||
dispatch({ type: types.GET_USER_DATA, payload: response.data.data });
|
||||
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
|
||||
};
|
||||
|
||||
const onFail = (error) => {
|
||||
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
|
||||
};
|
||||
|
||||
TAxios(dispatch, getState, 'get', URLS.GET_USER, {}, { userId }, onSuccess, onFail);
|
||||
};
|
||||
```
|
||||
|
||||
#### After
|
||||
```javascript
|
||||
export const fetchUserData = (userId) =>
|
||||
withLoadingState(
|
||||
(dispatch, getState) => {
|
||||
return TAxiosPromise(dispatch, getState, 'get', URLS.GET_USER, {}, { userId })
|
||||
.then((response) => {
|
||||
dispatch({ type: types.GET_USER_DATA, payload: response.data.data });
|
||||
});
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 패턴 3: 패널 순차 열기
|
||||
|
||||
#### Before
|
||||
```javascript
|
||||
dispatch(pushPanel({ name: panel_names.SEARCH }));
|
||||
setTimeout(() => {
|
||||
dispatch(updatePanel({ results: [...] }));
|
||||
setTimeout(() => {
|
||||
dispatch(popPanel(panel_names.LOADING));
|
||||
}, 0);
|
||||
}, 0);
|
||||
```
|
||||
|
||||
#### After
|
||||
```javascript
|
||||
dispatch(enqueueMultiplePanelActions([
|
||||
pushPanelQueued({ name: panel_names.SEARCH }),
|
||||
updatePanelQueued({ results: [...] }),
|
||||
popPanelQueued(panel_names.LOADING)
|
||||
]));
|
||||
```
|
||||
|
||||
### 패턴 4: 조건부 dispatch
|
||||
|
||||
#### Before
|
||||
```javascript
|
||||
export const checkAndFetch = () => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
if (state.user.isLoggedIn) {
|
||||
dispatch(fetchUserProfile());
|
||||
dispatch(fetchUserCart());
|
||||
} else {
|
||||
dispatch({ type: types.SHOW_LOGIN_POPUP });
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### After
|
||||
```javascript
|
||||
export const checkAndFetch = () =>
|
||||
createConditionalDispatch(
|
||||
(state) => state.user.isLoggedIn,
|
||||
[
|
||||
fetchUserProfile(),
|
||||
fetchUserCart()
|
||||
],
|
||||
[
|
||||
{ type: types.SHOW_LOGIN_POPUP }
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 실전 예제
|
||||
|
||||
### 예제 1: 검색 기능
|
||||
|
||||
```javascript
|
||||
// src/actions/searchActions.js
|
||||
import { createApiWithPanelActions, pushPanelQueued, popPanelQueued, updatePanelQueued }
|
||||
from './queuedPanelActions';
|
||||
import { panel_names } from '../constants/panelNames';
|
||||
import { URLS } from '../constants/urls';
|
||||
|
||||
export const performSearch = (keyword) => (dispatch) => {
|
||||
// 1. 로딩 패널 열기
|
||||
dispatch(pushPanelQueued({ name: panel_names.LOADING }));
|
||||
|
||||
// 2. 검색 API 호출 후 결과 처리
|
||||
dispatch(createApiWithPanelActions({
|
||||
apiCall: (dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(
|
||||
dispatch,
|
||||
getState,
|
||||
'post',
|
||||
URLS.SEARCH_PRODUCTS,
|
||||
{},
|
||||
{ keyword, page: 1, size: 20 },
|
||||
onSuccess,
|
||||
onFail
|
||||
);
|
||||
},
|
||||
|
||||
panelActions: [
|
||||
// 1) 로딩 패널 닫기
|
||||
popPanelQueued(panel_names.LOADING),
|
||||
|
||||
// 2) 검색 결과 패널 열기
|
||||
(response) => pushPanelQueued({
|
||||
name: panel_names.SEARCH_RESULT,
|
||||
results: response.data.results,
|
||||
totalCount: response.data.totalCount,
|
||||
keyword
|
||||
}),
|
||||
|
||||
// 3) 검색 히스토리 업데이트
|
||||
(response) => updatePanelQueued({
|
||||
name: panel_names.SEARCH_HISTORY,
|
||||
panelInfo: {
|
||||
lastSearch: keyword,
|
||||
resultCount: response.data.totalCount
|
||||
}
|
||||
})
|
||||
],
|
||||
|
||||
onApiSuccess: (response) => {
|
||||
console.log(`"${keyword}" 검색 완료: ${response.data.totalCount}개`);
|
||||
},
|
||||
|
||||
onApiFail: (error) => {
|
||||
console.error('검색 실패:', error);
|
||||
dispatch(popPanelQueued(panel_names.LOADING));
|
||||
dispatch(pushPanelQueued({
|
||||
name: panel_names.ERROR,
|
||||
message: '검색에 실패했습니다'
|
||||
}));
|
||||
}
|
||||
}));
|
||||
};
|
||||
```
|
||||
|
||||
### 예제 2: 장바구니 추가
|
||||
|
||||
```javascript
|
||||
// src/actions/cartActions.js
|
||||
import { createApiThunkWithChain } from '../utils/dispatchHelper';
|
||||
import { types } from './actionTypes';
|
||||
import { URLS } from '../constants/urls';
|
||||
|
||||
export const addToCart = (productId, quantity) =>
|
||||
createApiThunkWithChain(
|
||||
// API 호출
|
||||
(dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(
|
||||
dispatch,
|
||||
getState,
|
||||
'post',
|
||||
URLS.ADD_TO_CART,
|
||||
{},
|
||||
{ productId, quantity },
|
||||
onSuccess,
|
||||
onFail
|
||||
);
|
||||
},
|
||||
|
||||
// 성공 시 순차 dispatch
|
||||
[
|
||||
// 1) 장바구니 추가 액션
|
||||
(response) => ({
|
||||
type: types.ADD_TO_CART,
|
||||
payload: response.data.data
|
||||
}),
|
||||
|
||||
// 2) 장바구니 개수 업데이트
|
||||
(response) => ({
|
||||
type: types.UPDATE_CART_COUNT,
|
||||
payload: response.data.data.cartCount
|
||||
}),
|
||||
|
||||
// 3) 장바구니 정보 재조회
|
||||
(response) => getMyCartInfo({ mbrNo: response.data.data.mbrNo }),
|
||||
|
||||
// 4) 성공 메시지 표시
|
||||
() => ({
|
||||
type: types.SHOW_TOAST,
|
||||
payload: { message: '장바구니에 담았습니다' }
|
||||
})
|
||||
],
|
||||
|
||||
// 실패 시 dispatch
|
||||
(error) => ({
|
||||
type: types.SHOW_ERROR,
|
||||
payload: { message: error.message || '장바구니 담기에 실패했습니다' }
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
### 예제 3: 로그인 플로우
|
||||
|
||||
```javascript
|
||||
// src/actions/authActions.js
|
||||
import { createAsyncPanelSequence } from './queuedPanelActions';
|
||||
import { withLoadingState } from '../utils/dispatchHelper';
|
||||
import { panel_names } from '../constants/panelNames';
|
||||
import { types } from './actionTypes';
|
||||
import { URLS } from '../constants/urls';
|
||||
|
||||
export const performLogin = (userId, password) =>
|
||||
withLoadingState(
|
||||
createAsyncPanelSequence([
|
||||
// 1단계: 로그인 API 호출
|
||||
{
|
||||
asyncAction: (dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(
|
||||
dispatch,
|
||||
getState,
|
||||
'post',
|
||||
URLS.LOGIN,
|
||||
{},
|
||||
{ userId, password },
|
||||
onSuccess,
|
||||
onFail
|
||||
);
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
// 로그인 성공 - 토큰 저장
|
||||
dispatch({
|
||||
type: types.LOGIN_SUCCESS,
|
||||
payload: {
|
||||
token: response.data.data.token,
|
||||
userInfo: response.data.data.userInfo
|
||||
}
|
||||
});
|
||||
},
|
||||
onFail: (error) => {
|
||||
dispatch({
|
||||
type: types.SHOW_ERROR,
|
||||
payload: { message: '로그인에 실패했습니다' }
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 2단계: 사용자 정보 조회
|
||||
{
|
||||
asyncAction: (dispatch, getState, onSuccess, onFail) => {
|
||||
const state = getState();
|
||||
const mbrNo = state.auth.userInfo.mbrNo;
|
||||
|
||||
TAxios(
|
||||
dispatch,
|
||||
getState,
|
||||
'get',
|
||||
URLS.GET_USER_INFO,
|
||||
{},
|
||||
{ mbrNo },
|
||||
onSuccess,
|
||||
onFail
|
||||
);
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
dispatch({
|
||||
type: types.GET_USER_INFO,
|
||||
payload: response.data.data
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 3단계: 장바구니 정보 조회
|
||||
{
|
||||
asyncAction: (dispatch, getState, onSuccess, onFail) => {
|
||||
const state = getState();
|
||||
const mbrNo = state.auth.userInfo.mbrNo;
|
||||
|
||||
TAxios(
|
||||
dispatch,
|
||||
getState,
|
||||
'get',
|
||||
URLS.GET_CART,
|
||||
{},
|
||||
{ mbrNo },
|
||||
onSuccess,
|
||||
onFail
|
||||
);
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
dispatch({
|
||||
type: types.GET_CART_INFO,
|
||||
payload: response.data.data
|
||||
});
|
||||
|
||||
// 로그인 완료 패널로 이동
|
||||
dispatch(pushPanelQueued({
|
||||
name: panel_names.LOGIN_COMPLETE
|
||||
}));
|
||||
}
|
||||
}
|
||||
]),
|
||||
{ loadingType: 'wait' }
|
||||
);
|
||||
```
|
||||
|
||||
### 예제 4: 다단계 폼 제출
|
||||
|
||||
```javascript
|
||||
// src/actions/formActions.js
|
||||
import { createAsyncPanelSequence } from './queuedPanelActions';
|
||||
import { tAxiosToPromise } from '../utils/asyncActionUtils';
|
||||
import { types } from './actionTypes';
|
||||
import { URLS } from '../constants/urls';
|
||||
|
||||
export const submitMultiStepForm = (formData) =>
|
||||
createAsyncPanelSequence([
|
||||
// Step 1: 입력 검증
|
||||
{
|
||||
asyncAction: (dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(
|
||||
dispatch,
|
||||
getState,
|
||||
'post',
|
||||
URLS.VALIDATE_FORM,
|
||||
{},
|
||||
formData,
|
||||
onSuccess,
|
||||
onFail
|
||||
);
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
dispatch({
|
||||
type: types.UPDATE_FORM_STEP,
|
||||
payload: { step: 1, status: 'validated' }
|
||||
});
|
||||
dispatch(updatePanelQueued({
|
||||
name: panel_names.FORM_PANEL,
|
||||
panelInfo: { step: 1, validated: true }
|
||||
}));
|
||||
},
|
||||
onFail: (error) => {
|
||||
dispatch({
|
||||
type: types.SHOW_VALIDATION_ERROR,
|
||||
payload: { errors: error.data?.errors || [] }
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Step 2: 중복 체크
|
||||
{
|
||||
asyncAction: (dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(
|
||||
dispatch,
|
||||
getState,
|
||||
'post',
|
||||
URLS.CHECK_DUPLICATE,
|
||||
{},
|
||||
{ email: formData.email },
|
||||
onSuccess,
|
||||
onFail
|
||||
);
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
dispatch({
|
||||
type: types.UPDATE_FORM_STEP,
|
||||
payload: { step: 2, status: 'checked' }
|
||||
});
|
||||
dispatch(updatePanelQueued({
|
||||
name: panel_names.FORM_PANEL,
|
||||
panelInfo: { step: 2, duplicate: false }
|
||||
}));
|
||||
},
|
||||
onFail: (error) => {
|
||||
dispatch({
|
||||
type: types.SHOW_ERROR,
|
||||
payload: { message: '이미 사용 중인 이메일입니다' }
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Step 3: 최종 제출
|
||||
{
|
||||
asyncAction: (dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(
|
||||
dispatch,
|
||||
getState,
|
||||
'post',
|
||||
URLS.SUBMIT_FORM,
|
||||
{},
|
||||
formData,
|
||||
onSuccess,
|
||||
onFail
|
||||
);
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
dispatch({
|
||||
type: types.SUBMIT_FORM_SUCCESS,
|
||||
payload: response.data.data
|
||||
});
|
||||
|
||||
// 성공 패널로 이동
|
||||
dispatch(popPanelQueued(panel_names.FORM_PANEL));
|
||||
dispatch(pushPanelQueued({
|
||||
name: panel_names.SUCCESS_PANEL,
|
||||
message: '가입이 완료되었습니다'
|
||||
}));
|
||||
},
|
||||
onFail: (error) => {
|
||||
dispatch({
|
||||
type: types.SUBMIT_FORM_FAIL,
|
||||
payload: { error: error.message }
|
||||
});
|
||||
}
|
||||
}
|
||||
]);
|
||||
```
|
||||
|
||||
### 예제 5: 병렬 데이터 로딩
|
||||
|
||||
```javascript
|
||||
// src/actions/dashboardActions.js
|
||||
import { createParallelDispatch } from '../utils/dispatchHelper';
|
||||
import { executeParallelAsyncActions } from '../utils/asyncActionUtils';
|
||||
import { types } from './actionTypes';
|
||||
import { URLS } from '../constants/urls';
|
||||
|
||||
// 방법 1: dispatchHelper 사용
|
||||
export const loadDashboardData = () =>
|
||||
createParallelDispatch([
|
||||
fetchUserProfile(),
|
||||
fetchRecentOrders(),
|
||||
fetchRecommendations(),
|
||||
fetchNotifications()
|
||||
], { withLoading: true });
|
||||
|
||||
// 방법 2: asyncActionUtils 사용
|
||||
export const loadDashboardDataAsync = () => async (dispatch, getState) => {
|
||||
dispatch(changeAppStatus({ showLoadingPanel: { show: true } }));
|
||||
|
||||
const results = await executeParallelAsyncActions([
|
||||
// 1. 사용자 프로필
|
||||
(dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(dispatch, getState, 'get', URLS.GET_PROFILE, {}, {}, onSuccess, onFail);
|
||||
},
|
||||
|
||||
// 2. 최근 주문
|
||||
(dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(dispatch, getState, 'get', URLS.GET_RECENT_ORDERS, {}, {}, onSuccess, onFail);
|
||||
},
|
||||
|
||||
// 3. 추천 상품
|
||||
(dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(dispatch, getState, 'get', URLS.GET_RECOMMENDATIONS, {}, {}, onSuccess, onFail);
|
||||
},
|
||||
|
||||
// 4. 알림
|
||||
(dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(dispatch, getState, 'get', URLS.GET_NOTIFICATIONS, {}, {}, onSuccess, onFail);
|
||||
}
|
||||
], { dispatch, getState });
|
||||
|
||||
// 각 결과 처리
|
||||
const [profileResult, ordersResult, recoResult, notiResult] = results;
|
||||
|
||||
if (profileResult.success) {
|
||||
dispatch({ type: types.GET_PROFILE, payload: profileResult.data.data });
|
||||
}
|
||||
|
||||
if (ordersResult.success) {
|
||||
dispatch({ type: types.GET_RECENT_ORDERS, payload: ordersResult.data.data });
|
||||
}
|
||||
|
||||
if (recoResult.success) {
|
||||
dispatch({ type: types.GET_RECOMMENDATIONS, payload: recoResult.data.data });
|
||||
}
|
||||
|
||||
if (notiResult.success) {
|
||||
dispatch({ type: types.GET_NOTIFICATIONS, payload: notiResult.data.data });
|
||||
}
|
||||
|
||||
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 마이그레이션 가이드
|
||||
|
||||
### Step 1: 파일 import 변경
|
||||
|
||||
```javascript
|
||||
// Before
|
||||
import { pushPanel, popPanel, updatePanel } from '../actions/panelActions';
|
||||
|
||||
// After
|
||||
import { pushPanelQueued, popPanelQueued, updatePanelQueued }
|
||||
from '../actions/queuedPanelActions';
|
||||
import { createApiThunkWithChain, withLoadingState }
|
||||
from '../utils/dispatchHelper';
|
||||
```
|
||||
|
||||
### Step 2: 기존 코드 점진적 마이그레이션
|
||||
|
||||
```javascript
|
||||
// 1단계: 기존 코드 유지
|
||||
dispatch(pushPanel({ name: panel_names.SEARCH }));
|
||||
|
||||
// 2단계: 큐 버전으로 변경
|
||||
dispatch(pushPanelQueued({ name: panel_names.SEARCH }));
|
||||
|
||||
// 3단계: 여러 액션을 묶어서 처리
|
||||
dispatch(enqueueMultiplePanelActions([
|
||||
pushPanelQueued({ name: panel_names.SEARCH }),
|
||||
updatePanelQueued({ results: [...] })
|
||||
]));
|
||||
```
|
||||
|
||||
### Step 3: setTimeout 패턴 제거
|
||||
|
||||
```javascript
|
||||
// Before
|
||||
dispatch(action1());
|
||||
setTimeout(() => {
|
||||
dispatch(action2());
|
||||
setTimeout(() => {
|
||||
dispatch(action3());
|
||||
}, 0);
|
||||
}, 0);
|
||||
|
||||
// After
|
||||
dispatch(createSequentialDispatch([
|
||||
action1(),
|
||||
action2(),
|
||||
action3()
|
||||
]));
|
||||
```
|
||||
|
||||
### Step 4: API 패턴 개선
|
||||
|
||||
```javascript
|
||||
// Before
|
||||
const onSuccess = (response) => {
|
||||
dispatch({ type: types.ACTION_1, payload: response.data });
|
||||
dispatch(action2());
|
||||
dispatch(action3());
|
||||
};
|
||||
|
||||
TAxios(dispatch, getState, 'post', URL, {}, params, onSuccess, onFail);
|
||||
|
||||
// After
|
||||
dispatch(createApiThunkWithChain(
|
||||
(d, gs, onS, onF) => TAxios(d, gs, 'post', URL, {}, params, onS, onF),
|
||||
[
|
||||
(response) => ({ type: types.ACTION_1, payload: response.data }),
|
||||
action2(),
|
||||
action3()
|
||||
]
|
||||
));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. 명확한 에러 처리
|
||||
|
||||
```javascript
|
||||
// ✅ Good
|
||||
dispatch(createApiWithPanelActions({
|
||||
apiCall: (d, gs, onS, onF) => TAxios(d, gs, 'post', URL, {}, params, onS, onF),
|
||||
panelActions: [...],
|
||||
onApiSuccess: (response) => {
|
||||
console.log('API 성공:', response);
|
||||
},
|
||||
onApiFail: (error) => {
|
||||
console.error('API 실패:', error);
|
||||
dispatch(pushPanelQueued({
|
||||
name: panel_names.ERROR,
|
||||
message: error.message || '작업에 실패했습니다'
|
||||
}));
|
||||
}
|
||||
}));
|
||||
|
||||
// ❌ Bad - 에러 처리 없음
|
||||
dispatch(createApiWithPanelActions({
|
||||
apiCall: (d, gs, onS, onF) => TAxios(d, gs, 'post', URL, {}, params, onS, onF),
|
||||
panelActions: [...]
|
||||
}));
|
||||
```
|
||||
|
||||
### 2. 타임아웃 설정
|
||||
|
||||
```javascript
|
||||
// ✅ Good
|
||||
dispatch(enqueueAsyncPanelAction({
|
||||
asyncAction: (d, gs, onS, onF) => {
|
||||
TAxios(d, gs, 'post', URL, {}, params, onS, onF);
|
||||
},
|
||||
timeout: 10000, // 10초
|
||||
onFail: (error) => {
|
||||
if (error.code === 'TIMEOUT') {
|
||||
console.error('요청 시간 초과');
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// ❌ Bad - 타임아웃 없음 (무한 대기 가능)
|
||||
dispatch(enqueueAsyncPanelAction({
|
||||
asyncAction: (d, gs, onS, onF) => {
|
||||
TAxios(d, gs, 'post', URL, {}, params, onS, onF);
|
||||
}
|
||||
}));
|
||||
```
|
||||
|
||||
### 3. 로깅 활용
|
||||
|
||||
```javascript
|
||||
// ✅ Good - 상세한 로깅
|
||||
console.log('[SearchAction] 🔍 검색 시작:', keyword);
|
||||
|
||||
dispatch(createApiWithPanelActions({
|
||||
apiCall: (d, gs, onS, onF) => {
|
||||
TAxios(d, gs, 'post', URLS.SEARCH, {}, { keyword }, onS, onF);
|
||||
},
|
||||
onApiSuccess: (response) => {
|
||||
console.log('[SearchAction] ✅ 검색 성공:', response.data.totalCount, '개');
|
||||
},
|
||||
onApiFail: (error) => {
|
||||
console.error('[SearchAction] ❌ 검색 실패:', error);
|
||||
}
|
||||
}));
|
||||
```
|
||||
|
||||
### 4. 상태 검증
|
||||
|
||||
```javascript
|
||||
// ✅ Good - 상태 검증 후 실행
|
||||
export const performAction = () =>
|
||||
createConditionalDispatch(
|
||||
(state) => state.user.isLoggedIn && state.cart.items.length > 0,
|
||||
[proceedToCheckout()],
|
||||
[{ type: types.SHOW_LOGIN_POPUP }]
|
||||
);
|
||||
|
||||
// ❌ Bad - 검증 없이 바로 실행
|
||||
export const performAction = () => proceedToCheckout();
|
||||
```
|
||||
|
||||
### 5. 재사용 가능한 액션
|
||||
|
||||
```javascript
|
||||
// ✅ Good - 재사용 가능
|
||||
export const fetchDataWithLoading = (url, actionType) =>
|
||||
withLoadingState(
|
||||
(dispatch, getState) => {
|
||||
return TAxiosPromise(dispatch, getState, 'get', url, {}, {})
|
||||
.then((response) => {
|
||||
dispatch({ type: actionType, payload: response.data.data });
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// 사용
|
||||
dispatch(fetchDataWithLoading(URLS.GET_USER, types.GET_USER));
|
||||
dispatch(fetchDataWithLoading(URLS.GET_CART, types.GET_CART));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 체크리스트
|
||||
|
||||
### 초기 설정 확인사항
|
||||
|
||||
- [ ] **panelQueueMiddleware가 store.js에 등록되어 있는가?** (큐 시스템 사용 시 필수!)
|
||||
```javascript
|
||||
// src/store/store.js
|
||||
import panelQueueMiddleware from '../middleware/panelQueueMiddleware';
|
||||
|
||||
export const store = createStore(
|
||||
rootReducer,
|
||||
applyMiddleware(thunk, panelHistoryMiddleware, autoCloseMiddleware, panelQueueMiddleware)
|
||||
);
|
||||
```
|
||||
- [ ] TAxiosPromise가 import되어 있는가? (withLoadingState 사용 시)
|
||||
|
||||
### 기능 구현 전 확인사항
|
||||
|
||||
- [ ] 패널 관련 액션인가? → 큐 시스템 사용
|
||||
- [ ] API 호출이 포함되어 있는가? → createApiThunkWithChain 또는 createApiWithPanelActions
|
||||
- [ ] 로딩 상태 관리가 필요한가? → withLoadingState
|
||||
- [ ] 순차 실행이 필요한가? → createSequentialDispatch 또는 createAsyncPanelSequence
|
||||
- [ ] 타임아웃이 필요한가? → withTimeout 또는 timeout 옵션 설정
|
||||
|
||||
### 코드 리뷰 체크리스트
|
||||
|
||||
- [ ] setTimeout 사용 여부 확인
|
||||
- [ ] 에러 처리가 적절한가?
|
||||
- [ ] 로깅이 충분한가?
|
||||
- [ ] 타임아웃이 설정되어 있는가?
|
||||
- [ ] 상태 검증이 필요한가?
|
||||
- [ ] 재사용 가능한 구조인가?
|
||||
|
||||
---
|
||||
|
||||
**이전**: [← 해결 방법 3: 큐 기반 패널 액션 시스템](./04-solution-queue-system.md)
|
||||
**처음으로**: [← README](./README.md)
|
||||
396
.docs/dispatch-async/06-setup-guide.md
Normal file
396
.docs/dispatch-async/06-setup-guide.md
Normal file
@@ -0,0 +1,396 @@
|
||||
# 설정 가이드
|
||||
|
||||
## 📋 목차
|
||||
|
||||
1. [초기 설정](#초기-설정)
|
||||
2. [파일 구조 확인](#파일-구조-확인)
|
||||
3. [설정 순서](#설정-순서)
|
||||
4. [검증 방법](#검증-방법)
|
||||
5. [트러블슈팅](#트러블슈팅)
|
||||
|
||||
---
|
||||
|
||||
## 초기 설정
|
||||
|
||||
### 1️⃣ 필수: panelQueueMiddleware 등록
|
||||
|
||||
큐 기반 패널 액션 시스템을 사용하려면 **반드시** Redux store에 미들웨어를 등록해야 합니다.
|
||||
|
||||
#### 파일 위치
|
||||
`com.twin.app.shoptime/src/store/store.js`
|
||||
|
||||
#### 수정 전
|
||||
```javascript
|
||||
import { applyMiddleware, combineReducers, createStore } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import { autoCloseMiddleware } from '../middleware/autoCloseMiddleware';
|
||||
import { panelHistoryMiddleware } from '../middleware/panelHistoryMiddleware';
|
||||
// panelQueueMiddleware import 없음!
|
||||
|
||||
// ... reducers ...
|
||||
|
||||
export const store = createStore(
|
||||
rootReducer,
|
||||
applyMiddleware(thunk, panelHistoryMiddleware, autoCloseMiddleware)
|
||||
// panelQueueMiddleware 등록 없음!
|
||||
);
|
||||
```
|
||||
|
||||
#### 수정 후
|
||||
```javascript
|
||||
import { applyMiddleware, combineReducers, createStore } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import { autoCloseMiddleware } from '../middleware/autoCloseMiddleware';
|
||||
import { panelHistoryMiddleware } from '../middleware/panelHistoryMiddleware';
|
||||
import panelQueueMiddleware from '../middleware/panelQueueMiddleware'; // ← 추가
|
||||
|
||||
// ... reducers ...
|
||||
|
||||
export const store = createStore(
|
||||
rootReducer,
|
||||
applyMiddleware(
|
||||
thunk,
|
||||
panelHistoryMiddleware,
|
||||
autoCloseMiddleware,
|
||||
panelQueueMiddleware // ← 추가 (맨 마지막 위치)
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
### 2️⃣ 미들웨어 등록 순서
|
||||
|
||||
미들웨어 등록 순서는 다음과 같습니다:
|
||||
|
||||
```javascript
|
||||
applyMiddleware(
|
||||
thunk, // 1. Redux-thunk (비동기 액션 지원)
|
||||
panelHistoryMiddleware, // 2. 패널 히스토리 관리
|
||||
autoCloseMiddleware, // 3. 자동 닫기 처리
|
||||
panelQueueMiddleware // 4. 패널 큐 처리 (맨 마지막)
|
||||
)
|
||||
```
|
||||
|
||||
**중요**: `panelQueueMiddleware`는 **맨 마지막**에 위치해야 합니다!
|
||||
- 다른 미들웨어들이 먼저 액션을 처리한 후
|
||||
- 큐 미들웨어가 큐 관련 액션을 감지하고 처리합니다
|
||||
|
||||
---
|
||||
|
||||
## 파일 구조 확인
|
||||
|
||||
### 필수 파일들이 모두 존재하는지 확인
|
||||
|
||||
```bash
|
||||
# 프로젝트 루트에서 실행
|
||||
ls -la com.twin.app.shoptime/src/middleware/panelQueueMiddleware.js
|
||||
ls -la com.twin.app.shoptime/src/actions/queuedPanelActions.js
|
||||
ls -la com.twin.app.shoptime/src/utils/dispatchHelper.js
|
||||
ls -la com.twin.app.shoptime/src/utils/asyncActionUtils.js
|
||||
ls -la com.twin.app.shoptime/src/reducers/panelReducer.js
|
||||
```
|
||||
|
||||
### 예상 출력
|
||||
```
|
||||
-rw-r--r-- 1 user user 2063 Nov 10 06:32 .../panelQueueMiddleware.js
|
||||
-rw-r--r-- 1 user user 13845 Nov 06 10:15 .../queuedPanelActions.js
|
||||
-rw-r--r-- 1 user user 12345 Nov 05 14:20 .../dispatchHelper.js
|
||||
-rw-r--r-- 1 user user 10876 Nov 06 10:30 .../asyncActionUtils.js
|
||||
-rw-r--r-- 1 user user 25432 Nov 06 11:00 .../panelReducer.js
|
||||
```
|
||||
|
||||
### 파일이 없다면?
|
||||
|
||||
```bash
|
||||
# 최신 코드를 pull 받으세요
|
||||
git fetch origin
|
||||
git pull origin <branch-name>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 설정 순서
|
||||
|
||||
### Step 1: 미들웨어 import 추가
|
||||
|
||||
**파일**: `src/store/store.js`
|
||||
|
||||
```javascript
|
||||
import panelQueueMiddleware from '../middleware/panelQueueMiddleware';
|
||||
```
|
||||
|
||||
### Step 2: applyMiddleware에 추가
|
||||
|
||||
```javascript
|
||||
export const store = createStore(
|
||||
rootReducer,
|
||||
applyMiddleware(
|
||||
thunk,
|
||||
panelHistoryMiddleware,
|
||||
autoCloseMiddleware,
|
||||
panelQueueMiddleware // ← 추가
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
### Step 3: 저장 및 빌드
|
||||
|
||||
```bash
|
||||
# 파일 저장 후
|
||||
npm run build
|
||||
# 또는 개발 서버 재시작
|
||||
npm start
|
||||
```
|
||||
|
||||
### Step 4: 브라우저 콘솔 확인
|
||||
|
||||
브라우저 개발자 도구(F12)를 열고 다음과 같은 로그가 보이는지 확인:
|
||||
|
||||
```
|
||||
[panelQueueMiddleware] 🚀 ACTION_ENQUEUED
|
||||
[panelQueueMiddleware] ⚡ STARTING_QUEUE_PROCESS
|
||||
[panelReducer] 🟡 PROCESS_PANEL_QUEUE
|
||||
[panelReducer] 🟡 PROCESSING_QUEUE_ITEM
|
||||
[panelReducer] ✅ QUEUE_ITEM_PROCESSED
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 검증 방법
|
||||
|
||||
### 방법 1: 콘솔 로그 확인
|
||||
|
||||
큐 시스템을 사용하는 액션을 dispatch하면 다음과 같은 로그가 출력됩니다:
|
||||
|
||||
```javascript
|
||||
import { pushPanelQueued } from '../actions/queuedPanelActions';
|
||||
import { panel_names } from '../utils/Config';
|
||||
|
||||
dispatch(pushPanelQueued({ name: panel_names.SEARCH_PANEL }));
|
||||
```
|
||||
|
||||
**예상 로그**:
|
||||
```
|
||||
[panelQueueMiddleware] 🚀 ACTION_ENQUEUED { action: 'PUSH_PANEL', queueId: 'queue_item_1_1699999999999' }
|
||||
[panelQueueMiddleware] ⚡ STARTING_QUEUE_PROCESS
|
||||
[panelReducer] 🟡 PROCESS_PANEL_QUEUE { isProcessing: false, queueLength: 1 }
|
||||
[panelReducer] 🟡 PROCESSING_QUEUE_ITEM { action: 'PUSH_PANEL', queueId: 'queue_item_1_1699999999999', remainingQueueLength: 0 }
|
||||
[panelReducer] 🔵 PUSH_PANEL START { newPanelName: 'SEARCH_PANEL', currentPanels: [...], duplicatable: false }
|
||||
[panelReducer] 🔵 PUSH_PANEL END { resultPanels: [...], lastAction: 'push' }
|
||||
[panelReducer] ✅ QUEUE_ITEM_PROCESSED { action: 'PUSH_PANEL', queueId: 'queue_item_1_1699999999999', processingTime: 2 }
|
||||
```
|
||||
|
||||
### 방법 2: Redux DevTools 확인
|
||||
|
||||
Redux DevTools를 사용하여 액션 흐름을 확인:
|
||||
|
||||
1. Chrome 확장 프로그램: Redux DevTools 설치
|
||||
2. 개발자 도구에서 "Redux" 탭 선택
|
||||
3. 다음 액션들이 순서대로 dispatch되는지 확인:
|
||||
- `ENQUEUE_PANEL_ACTION`
|
||||
- `PROCESS_PANEL_QUEUE`
|
||||
- `PUSH_PANEL` (또는 다른 패널 액션)
|
||||
|
||||
### 방법 3: State 확인
|
||||
|
||||
Redux state를 확인하여 큐 관련 상태가 정상적으로 업데이트되는지 확인:
|
||||
|
||||
```javascript
|
||||
// 콘솔에서 실행
|
||||
store.getState().panels
|
||||
```
|
||||
|
||||
**예상 출력**:
|
||||
```javascript
|
||||
{
|
||||
panels: [...], // 실제 패널들
|
||||
lastPanelAction: 'push',
|
||||
|
||||
// 큐 관련 상태
|
||||
panelActionQueue: [], // 처리 대기 중인 큐 (처리 후 비어있음)
|
||||
isProcessingQueue: false,
|
||||
queueError: null,
|
||||
queueStats: {
|
||||
totalProcessed: 1,
|
||||
failedCount: 0,
|
||||
averageProcessingTime: 2.5
|
||||
},
|
||||
|
||||
// 비동기 액션 관련
|
||||
asyncActions: {},
|
||||
completedAsyncActions: [],
|
||||
failedAsyncActions: []
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 트러블슈팅
|
||||
|
||||
### 문제 1: 큐가 처리되지 않음
|
||||
|
||||
#### 증상
|
||||
```javascript
|
||||
dispatch(pushPanelQueued({ name: panel_names.SEARCH_PANEL }));
|
||||
// 아무 일도 일어나지 않음
|
||||
// 로그도 출력되지 않음
|
||||
```
|
||||
|
||||
#### 원인
|
||||
panelQueueMiddleware가 등록되지 않음
|
||||
|
||||
#### 해결 방법
|
||||
1. `store.js` 파일 확인:
|
||||
```javascript
|
||||
// import가 있는지 확인
|
||||
import panelQueueMiddleware from '../middleware/panelQueueMiddleware';
|
||||
|
||||
// applyMiddleware에 추가되어 있는지 확인
|
||||
applyMiddleware(..., panelQueueMiddleware)
|
||||
```
|
||||
|
||||
2. 파일 저장 후 앱 재시작
|
||||
3. 브라우저 캐시 삭제 (Ctrl+Shift+R 또는 Cmd+Shift+R)
|
||||
|
||||
### 문제 2: 미들웨어 파일을 찾을 수 없음
|
||||
|
||||
#### 증상
|
||||
```
|
||||
Error: Cannot find module '../middleware/panelQueueMiddleware'
|
||||
```
|
||||
|
||||
#### 원인
|
||||
파일이 존재하지 않거나 경로가 잘못됨
|
||||
|
||||
#### 해결 방법
|
||||
1. 파일 존재 확인:
|
||||
```bash
|
||||
ls -la com.twin.app.shoptime/src/middleware/panelQueueMiddleware.js
|
||||
```
|
||||
|
||||
2. 파일이 없다면 최신 코드 pull:
|
||||
```bash
|
||||
git fetch origin
|
||||
git pull origin main
|
||||
```
|
||||
|
||||
3. 여전히 없다면 커밋 확인:
|
||||
```bash
|
||||
git log --oneline --grep="panelQueueMiddleware"
|
||||
# 5bd2774 [251106] feat: Queued Panel functions
|
||||
```
|
||||
|
||||
### 문제 3: 로그는 보이는데 패널이 열리지 않음
|
||||
|
||||
#### 증상
|
||||
```
|
||||
[panelQueueMiddleware] 🚀 ACTION_ENQUEUED
|
||||
[panelReducer] 🟡 PROCESS_PANEL_QUEUE
|
||||
[panelReducer] ✅ QUEUE_ITEM_PROCESSED
|
||||
// 하지만 패널이 화면에 표시되지 않음
|
||||
```
|
||||
|
||||
#### 원인
|
||||
UI 렌더링 문제 (Redux는 정상 작동)
|
||||
|
||||
#### 해결 방법
|
||||
1. Redux state 확인:
|
||||
```javascript
|
||||
console.log(store.getState().panels.panels);
|
||||
// 패널이 배열에 추가되었는지 확인
|
||||
```
|
||||
|
||||
2. 패널 컴포넌트 렌더링 로직 확인
|
||||
3. React DevTools로 컴포넌트 트리 확인
|
||||
|
||||
### 문제 4: 타입 에러
|
||||
|
||||
#### 증상
|
||||
```
|
||||
Error: Cannot read property 'type' of undefined
|
||||
ReferenceError: types is not defined
|
||||
```
|
||||
|
||||
#### 원인
|
||||
actionTypes.js에 필요한 타입이 정의되지 않음
|
||||
|
||||
#### 해결 방법
|
||||
1. `src/actions/actionTypes.js` 확인:
|
||||
```javascript
|
||||
export const types = {
|
||||
// ... 기존 타입들 ...
|
||||
|
||||
// 큐 관련 타입들
|
||||
ENQUEUE_PANEL_ACTION: 'ENQUEUE_PANEL_ACTION',
|
||||
PROCESS_PANEL_QUEUE: 'PROCESS_PANEL_QUEUE',
|
||||
CLEAR_PANEL_QUEUE: 'CLEAR_PANEL_QUEUE',
|
||||
SET_QUEUE_PROCESSING: 'SET_QUEUE_PROCESSING',
|
||||
|
||||
// 비동기 액션 타입들
|
||||
ENQUEUE_ASYNC_PANEL_ACTION: 'ENQUEUE_ASYNC_PANEL_ACTION',
|
||||
COMPLETE_ASYNC_PANEL_ACTION: 'COMPLETE_ASYNC_PANEL_ACTION',
|
||||
FAIL_ASYNC_PANEL_ACTION: 'FAIL_ASYNC_PANEL_ACTION',
|
||||
};
|
||||
```
|
||||
|
||||
2. 없다면 추가 후 저장
|
||||
|
||||
### 문제 5: 순서가 여전히 보장되지 않음
|
||||
|
||||
#### 증상
|
||||
```javascript
|
||||
dispatch(pushPanelQueued({ name: 'PANEL_1' }));
|
||||
dispatch(pushPanelQueued({ name: 'PANEL_2' }));
|
||||
// PANEL_2가 먼저 열림
|
||||
```
|
||||
|
||||
#### 원인
|
||||
일반 `pushPanel`과 `pushPanelQueued`를 혼용
|
||||
|
||||
#### 해결 방법
|
||||
순서를 보장하려면 **모두** queued 버전 사용:
|
||||
```javascript
|
||||
// ❌ 잘못된 사용
|
||||
dispatch(pushPanel({ name: 'PANEL_1' })); // 일반 버전
|
||||
dispatch(pushPanelQueued({ name: 'PANEL_2' })); // 큐 버전
|
||||
|
||||
// ✅ 올바른 사용
|
||||
dispatch(pushPanelQueued({ name: 'PANEL_1' }));
|
||||
dispatch(pushPanelQueued({ name: 'PANEL_2' }));
|
||||
|
||||
// 또는
|
||||
dispatch(enqueueMultiplePanelActions([
|
||||
pushPanelQueued({ name: 'PANEL_1' }),
|
||||
pushPanelQueued({ name: 'PANEL_2' })
|
||||
]));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 빠른 체크리스트
|
||||
|
||||
설정이 완료되었는지 빠르게 확인:
|
||||
|
||||
- [ ] `src/store/store.js`에 `import panelQueueMiddleware` 추가됨
|
||||
- [ ] `applyMiddleware`에 `panelQueueMiddleware` 추가됨 (맨 마지막 위치)
|
||||
- [ ] 파일 저장 및 앱 재시작
|
||||
- [ ] 브라우저 콘솔에서 큐 관련 로그 확인
|
||||
- [ ] Redux DevTools에서 액션 흐름 확인
|
||||
- [ ] Redux state에서 큐 관련 상태 확인
|
||||
|
||||
모든 항목이 체크되었다면 설정 완료! 🎉
|
||||
|
||||
---
|
||||
|
||||
## 참고 자료
|
||||
|
||||
- [README.md](./README.md) - 전체 개요
|
||||
- [04-solution-queue-system.md](./04-solution-queue-system.md) - 큐 시스템 상세 설명
|
||||
- [05-usage-patterns.md](./05-usage-patterns.md) - 사용 패턴 및 예제
|
||||
- [07-changelog.md](./07-changelog.md) - 변경 이력
|
||||
|
||||
---
|
||||
|
||||
**작성일**: 2025-11-10
|
||||
**최종 수정일**: 2025-11-10
|
||||
314
.docs/dispatch-async/07-changelog.md
Normal file
314
.docs/dispatch-async/07-changelog.md
Normal file
@@ -0,0 +1,314 @@
|
||||
# 변경 이력 (Changelog)
|
||||
|
||||
## [2025-11-10] - 미들웨어 등록 및 문서 개선
|
||||
|
||||
### 🔧 수정 (Fixed)
|
||||
|
||||
#### store.js - panelQueueMiddleware 등록
|
||||
**커밋**: `c12cc91 [수정] panelQueueMiddleware 등록 및 문서 업데이트`
|
||||
|
||||
**문제**:
|
||||
- panelQueueMiddleware가 store.js에 등록되어 있지 않았음
|
||||
- 큐 시스템이 작동하지 않는 치명적인 문제
|
||||
- `ENQUEUE_PANEL_ACTION` dispatch 시 자동으로 `PROCESS_PANEL_QUEUE`가 실행되지 않음
|
||||
|
||||
**해결**:
|
||||
```javascript
|
||||
// src/store/store.js
|
||||
import panelQueueMiddleware from '../middleware/panelQueueMiddleware';
|
||||
|
||||
export const store = createStore(
|
||||
rootReducer,
|
||||
applyMiddleware(thunk, panelHistoryMiddleware, autoCloseMiddleware, panelQueueMiddleware)
|
||||
);
|
||||
```
|
||||
|
||||
**영향**:
|
||||
- ✅ 큐 기반 패널 액션 시스템이 정상 작동
|
||||
- ✅ 패널 액션 순서 보장
|
||||
- ✅ 비동기 패널 액션 자동 처리
|
||||
|
||||
### 📝 문서 (Documentation)
|
||||
|
||||
#### README.md
|
||||
- "설치 및 설정" 섹션 추가
|
||||
- panelQueueMiddleware 등록 필수 사항 강조
|
||||
- 등록하지 않으면 큐 시스템이 작동하지 않는다는 경고 추가
|
||||
|
||||
#### 04-solution-queue-system.md
|
||||
- "사전 요구사항" 섹션 추가
|
||||
- 미들웨어 등록 코드 예제 포함
|
||||
- `src/store/store.js`를 관련 파일에 추가
|
||||
|
||||
#### 05-usage-patterns.md
|
||||
- "초기 설정 확인사항" 체크리스트 추가
|
||||
- panelQueueMiddleware 등록 여부를 최우선 확인 항목으로 배치
|
||||
|
||||
---
|
||||
|
||||
## [2025-11-10] - 초기 문서 작성
|
||||
|
||||
### ✨ 추가 (Added)
|
||||
|
||||
#### 문서 작성
|
||||
**커밋**: `f75860c [문서화] Dispatch 비동기 처리 순서 보장 솔루션 문서 작성`
|
||||
|
||||
dispatch 비동기 처리 순서 보장 문제와 해결 방법을 체계적으로 정리한 문서 세트:
|
||||
|
||||
1. **README.md**
|
||||
- 전체 개요 및 목차
|
||||
- 주요 솔루션 요약
|
||||
- 관련 파일 목록
|
||||
- 커밋 히스토리
|
||||
|
||||
2. **01-problem.md**
|
||||
- 문제 상황 및 원인 분석
|
||||
- Redux-thunk에서 dispatch 순서가 보장되지 않는 이유
|
||||
- 실제 발생 가능한 버그 시나리오
|
||||
- 기존 해결 방법의 한계
|
||||
|
||||
3. **02-solution-dispatch-helper.md**
|
||||
- dispatchHelper.js 솔루션 설명
|
||||
- 5가지 헬퍼 함수 상세 설명:
|
||||
- `createSequentialDispatch`
|
||||
- `createApiThunkWithChain`
|
||||
- `withLoadingState`
|
||||
- `createConditionalDispatch`
|
||||
- `createParallelDispatch`
|
||||
- Before/After 코드 비교
|
||||
- 실제 사용 예제
|
||||
|
||||
4. **03-solution-async-utils.md**
|
||||
- asyncActionUtils.js 솔루션 설명
|
||||
- API 성공 기준 명확화 (HTTP 200-299 + retCode 0/'0')
|
||||
- Promise 체인 보장 방법 (reject 없이 resolve만 사용)
|
||||
- 주요 함수 설명:
|
||||
- `isApiSuccess`
|
||||
- `fetchApi`
|
||||
- `tAxiosToPromise`
|
||||
- `wrapAsyncAction`
|
||||
- `withTimeout`
|
||||
- `executeParallelAsyncActions`
|
||||
|
||||
5. **04-solution-queue-system.md**
|
||||
- 큐 기반 패널 액션 시스템 설명
|
||||
- 기본 패널 액션 (pushPanelQueued, popPanelQueued 등)
|
||||
- 비동기 패널 액션 (enqueueAsyncPanelAction)
|
||||
- API 호출 후 패널 액션 (createApiWithPanelActions)
|
||||
- 비동기 액션 시퀀스 (createAsyncPanelSequence)
|
||||
- panelQueueMiddleware 동작 원리
|
||||
- 리듀서 상태 구조
|
||||
|
||||
6. **05-usage-patterns.md**
|
||||
- 솔루션 선택 가이드 (의사결정 플로우차트)
|
||||
- 솔루션 비교표
|
||||
- 공통 패턴 Before/After 비교
|
||||
- 실전 예제 5가지:
|
||||
- 검색 기능
|
||||
- 장바구니 추가
|
||||
- 로그인 플로우
|
||||
- 다단계 폼 제출
|
||||
- 병렬 데이터 로딩
|
||||
- 마이그레이션 가이드
|
||||
- Best Practices
|
||||
- 체크리스트
|
||||
|
||||
**문서 통계**:
|
||||
- 총 6개 마크다운 파일
|
||||
- 약 3,000줄
|
||||
- 50개 이상의 코드 예제
|
||||
- Before/After 비교 20개 이상
|
||||
|
||||
---
|
||||
|
||||
## [2025-11-06] - 큐 시스템 구현
|
||||
|
||||
### ✨ 추가 (Added)
|
||||
|
||||
#### Dispatch Queue Implementation
|
||||
**커밋**: `f9290a1 [251106] fix: Dispatch Queue implementation`
|
||||
|
||||
- `asyncActionUtils.js` 추가
|
||||
- Promise 기반 비동기 액션 처리
|
||||
- API 성공 기준 명확화
|
||||
- 타임아웃 지원
|
||||
|
||||
- `queuedPanelActions.js` 확장
|
||||
- 비동기 패널 액션 지원
|
||||
- API 호출 후 패널 액션 자동 실행
|
||||
- 비동기 액션 시퀀스
|
||||
|
||||
- `panelReducer.js` 확장
|
||||
- 큐 상태 관리
|
||||
- 비동기 액션 상태 추적
|
||||
- 큐 처리 통계
|
||||
|
||||
#### Queued Panel Functions
|
||||
**커밋**: `5bd2774 [251106] feat: Queued Panel functions`
|
||||
|
||||
- `queuedPanelActions.js` 초기 구현
|
||||
- 기본 큐 액션 (pushPanelQueued, popPanelQueued 등)
|
||||
- 여러 액션 일괄 큐 추가
|
||||
- 패널 시퀀스 생성
|
||||
|
||||
- `panelQueueMiddleware.js` 추가
|
||||
- 큐 액션 자동 감지
|
||||
- 순차 처리 자동 시작
|
||||
- 연속 처리 지원
|
||||
|
||||
- `panelReducer.js` 큐 기능 추가
|
||||
- 큐 상태 관리
|
||||
- 큐 처리 로직
|
||||
- 큐 통계 수집
|
||||
|
||||
---
|
||||
|
||||
## [2025-11-05] - dispatch 헬퍼 함수
|
||||
|
||||
### ✨ 추가 (Added)
|
||||
|
||||
#### dispatchHelper.js
|
||||
**커밋**: `9490d72 [251105] feat: dispatchHelper.js`
|
||||
|
||||
Promise 체인 기반의 dispatch 순차 실행 헬퍼 함수 모음:
|
||||
|
||||
- `createSequentialDispatch`
|
||||
- 여러 dispatch를 순차적으로 실행
|
||||
- Promise 체인으로 순서 보장
|
||||
- delay 옵션 지원
|
||||
- stopOnError 옵션 지원
|
||||
|
||||
- `createApiThunkWithChain`
|
||||
- API 호출 후 dispatch 자동 체이닝
|
||||
- TAxios onSuccess/onFail 패턴 호환
|
||||
- response를 각 action에 전달
|
||||
- 에러 처리 action 지원
|
||||
|
||||
- `withLoadingState`
|
||||
- 로딩 상태 자동 관리
|
||||
- changeAppStatus 자동 on/off
|
||||
- 성공/에러 시 추가 dispatch 지원
|
||||
- loadingType 옵션
|
||||
|
||||
- `createConditionalDispatch`
|
||||
- 조건에 따라 다른 dispatch 실행
|
||||
- getState() 결과 기반 분기
|
||||
- 배열 또는 단일 action 지원
|
||||
|
||||
- `createParallelDispatch`
|
||||
- 여러 API를 병렬로 실행
|
||||
- Promise.all 사용
|
||||
- 로딩 상태 관리 옵션
|
||||
|
||||
---
|
||||
|
||||
## 관련 커밋 전체 목록
|
||||
|
||||
```bash
|
||||
c12cc91 [수정] panelQueueMiddleware 등록 및 문서 업데이트
|
||||
f75860c [문서화] Dispatch 비동기 처리 순서 보장 솔루션 문서 작성
|
||||
f9290a1 [251106] fix: Dispatch Queue implementation
|
||||
5bd2774 [251106] feat: Queued Panel functions
|
||||
9490d72 [251105] feat: dispatchHelper.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 마이그레이션 가이드
|
||||
|
||||
### 기존 코드에서 새 솔루션으로 전환
|
||||
|
||||
#### 1단계: setTimeout 패턴 제거
|
||||
|
||||
```javascript
|
||||
// Before
|
||||
dispatch(action1());
|
||||
setTimeout(() => {
|
||||
dispatch(action2());
|
||||
}, 0);
|
||||
|
||||
// After
|
||||
dispatch(createSequentialDispatch([action1(), action2()]));
|
||||
```
|
||||
|
||||
#### 2단계: API 패턴 개선
|
||||
|
||||
```javascript
|
||||
// Before
|
||||
const onSuccess = (response) => {
|
||||
dispatch({ type: types.ACTION_1, payload: response.data });
|
||||
dispatch(action2());
|
||||
};
|
||||
TAxios(..., onSuccess, onFail);
|
||||
|
||||
// After
|
||||
dispatch(createApiThunkWithChain(
|
||||
(d, gs, onS, onF) => TAxios(d, gs, ..., onS, onF),
|
||||
[
|
||||
(response) => ({ type: types.ACTION_1, payload: response.data }),
|
||||
action2()
|
||||
]
|
||||
));
|
||||
```
|
||||
|
||||
#### 3단계: 패널 액션을 큐 버전으로 전환
|
||||
|
||||
```javascript
|
||||
// Before
|
||||
dispatch(pushPanel({ name: panel_names.SEARCH }));
|
||||
|
||||
// After
|
||||
dispatch(pushPanelQueued({ name: panel_names.SEARCH }));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
### 없음
|
||||
|
||||
모든 새로운 기능은 기존 코드와 완전히 호환됩니다:
|
||||
- 기존 `pushPanel`, `popPanel` 등은 그대로 동작
|
||||
- 새로운 큐 버전은 선택적으로 사용 가능
|
||||
- 점진적 마이그레이션 가능
|
||||
|
||||
---
|
||||
|
||||
## 알려진 이슈
|
||||
|
||||
### 해결됨
|
||||
|
||||
1. **panelQueueMiddleware 미등록 문제** (2025-11-10 해결)
|
||||
- 문제: 큐 시스템이 작동하지 않음
|
||||
- 해결: store.js에 미들웨어 등록
|
||||
|
||||
### 현재 이슈
|
||||
|
||||
없음
|
||||
|
||||
---
|
||||
|
||||
## 향후 계획
|
||||
|
||||
### 예정된 개선사항
|
||||
|
||||
1. **성능 최적화**
|
||||
- 큐 처리 성능 모니터링
|
||||
- 대량 액션 처리 최적화
|
||||
|
||||
2. **에러 처리 강화**
|
||||
- 더 상세한 에러 메시지
|
||||
- 에러 복구 전략
|
||||
|
||||
3. **개발자 도구**
|
||||
- 큐 상태 시각화
|
||||
- 디버깅 도구
|
||||
|
||||
4. **테스트 코드**
|
||||
- 단위 테스트 추가
|
||||
- 통합 테스트 추가
|
||||
|
||||
---
|
||||
|
||||
**작성일**: 2025-11-10
|
||||
**최종 수정일**: 2025-11-10
|
||||
606
.docs/dispatch-async/08-troubleshooting.md
Normal file
606
.docs/dispatch-async/08-troubleshooting.md
Normal file
@@ -0,0 +1,606 @@
|
||||
# 트러블슈팅 가이드
|
||||
|
||||
## 📋 목차
|
||||
|
||||
1. [일반적인 문제](#일반적인-문제)
|
||||
2. [큐 시스템 문제](#큐-시스템-문제)
|
||||
3. [API 호출 문제](#api-호출-문제)
|
||||
4. [성능 문제](#성능-문제)
|
||||
5. [디버깅 팁](#디버깅-팁)
|
||||
|
||||
---
|
||||
|
||||
## 일반적인 문제
|
||||
|
||||
### 문제 1: dispatch 순서가 여전히 보장되지 않음
|
||||
|
||||
#### 증상
|
||||
```javascript
|
||||
dispatch(action1());
|
||||
dispatch(action2());
|
||||
dispatch(action3());
|
||||
// 실행 순서: action2 → action3 → action1
|
||||
```
|
||||
|
||||
#### 가능한 원인
|
||||
|
||||
1. **일반 dispatch와 큐 dispatch 혼용**
|
||||
```javascript
|
||||
// ❌ 잘못된 사용
|
||||
dispatch(pushPanel({ name: 'PANEL_1' })); // 일반
|
||||
dispatch(pushPanelQueued({ name: 'PANEL_2' })); // 큐
|
||||
```
|
||||
|
||||
2. **async/await 없이 비동기 처리**
|
||||
```javascript
|
||||
// ❌ 잘못된 사용
|
||||
fetchData(); // Promise를 기다리지 않음
|
||||
dispatch(action());
|
||||
```
|
||||
|
||||
3. **헬퍼 함수를 사용하지 않음**
|
||||
```javascript
|
||||
// ❌ 잘못된 사용
|
||||
dispatch(asyncAction1());
|
||||
dispatch(asyncAction2()); // asyncAction1이 완료되기 전에 실행
|
||||
```
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
**방법 1: 큐 시스템 사용** (패널 액션인 경우)
|
||||
```javascript
|
||||
// ✅ 올바른 사용
|
||||
dispatch(enqueueMultiplePanelActions([
|
||||
pushPanelQueued({ name: 'PANEL_1' }),
|
||||
pushPanelQueued({ name: 'PANEL_2' }),
|
||||
pushPanelQueued({ name: 'PANEL_3' })
|
||||
]));
|
||||
```
|
||||
|
||||
**방법 2: createSequentialDispatch 사용**
|
||||
```javascript
|
||||
// ✅ 올바른 사용
|
||||
dispatch(createSequentialDispatch([
|
||||
action1(),
|
||||
action2(),
|
||||
action3()
|
||||
]));
|
||||
```
|
||||
|
||||
**방법 3: async/await 사용** (Chrome 68+)
|
||||
```javascript
|
||||
// ✅ 올바른 사용
|
||||
export const myAction = () => async (dispatch, getState) => {
|
||||
await dispatch(action1());
|
||||
await dispatch(action2());
|
||||
await dispatch(action3());
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 문제 2: "Cannot find module" 에러
|
||||
|
||||
#### 증상
|
||||
```
|
||||
Error: Cannot find module '../utils/dispatchHelper'
|
||||
Error: Cannot find module '../middleware/panelQueueMiddleware'
|
||||
```
|
||||
|
||||
#### 원인
|
||||
- 파일이 존재하지 않음
|
||||
- import 경로가 잘못됨
|
||||
- 빌드가 필요함
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
**Step 1: 파일 존재 확인**
|
||||
```bash
|
||||
# 프로젝트 루트에서 실행
|
||||
ls -la com.twin.app.shoptime/src/utils/dispatchHelper.js
|
||||
ls -la com.twin.app.shoptime/src/middleware/panelQueueMiddleware.js
|
||||
ls -la com.twin.app.shoptime/src/utils/asyncActionUtils.js
|
||||
```
|
||||
|
||||
**Step 2: 최신 코드 pull**
|
||||
```bash
|
||||
git fetch origin
|
||||
git pull origin <branch-name>
|
||||
```
|
||||
|
||||
**Step 3: node_modules 재설치**
|
||||
```bash
|
||||
cd com.twin.app.shoptime
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
```
|
||||
|
||||
**Step 4: 빌드 재실행**
|
||||
```bash
|
||||
npm run build
|
||||
# 또는
|
||||
npm start
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 문제 3: 타입 에러 (types is not defined)
|
||||
|
||||
#### 증상
|
||||
```
|
||||
ReferenceError: types is not defined
|
||||
TypeError: Cannot read property 'ENQUEUE_PANEL_ACTION' of undefined
|
||||
```
|
||||
|
||||
#### 원인
|
||||
actionTypes.js에 필요한 타입이 정의되지 않음
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
**Step 1: actionTypes.js 확인**
|
||||
```javascript
|
||||
// src/actions/actionTypes.js
|
||||
export const types = {
|
||||
// ... 기존 타입들 ...
|
||||
|
||||
// 큐 관련 타입들 (필수!)
|
||||
ENQUEUE_PANEL_ACTION: 'ENQUEUE_PANEL_ACTION',
|
||||
PROCESS_PANEL_QUEUE: 'PROCESS_PANEL_QUEUE',
|
||||
CLEAR_PANEL_QUEUE: 'CLEAR_PANEL_QUEUE',
|
||||
SET_QUEUE_PROCESSING: 'SET_QUEUE_PROCESSING',
|
||||
|
||||
// 비동기 액션 타입들 (필수!)
|
||||
ENQUEUE_ASYNC_PANEL_ACTION: 'ENQUEUE_ASYNC_PANEL_ACTION',
|
||||
COMPLETE_ASYNC_PANEL_ACTION: 'COMPLETE_ASYNC_PANEL_ACTION',
|
||||
FAIL_ASYNC_PANEL_ACTION: 'FAIL_ASYNC_PANEL_ACTION',
|
||||
};
|
||||
```
|
||||
|
||||
**Step 2: import 확인**
|
||||
```javascript
|
||||
import { types } from '../actions/actionTypes';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 큐 시스템 문제
|
||||
|
||||
### 문제 4: 큐가 처리되지 않음
|
||||
|
||||
#### 증상
|
||||
```javascript
|
||||
dispatch(pushPanelQueued({ name: panel_names.SEARCH_PANEL }));
|
||||
// 아무 일도 일어나지 않음
|
||||
// 콘솔 로그도 없음
|
||||
```
|
||||
|
||||
#### 원인
|
||||
**panelQueueMiddleware가 등록되지 않음** (가장 흔한 문제!)
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
**Step 1: store.js 확인**
|
||||
```javascript
|
||||
// src/store/store.js
|
||||
import panelQueueMiddleware from '../middleware/panelQueueMiddleware';
|
||||
|
||||
export const store = createStore(
|
||||
rootReducer,
|
||||
applyMiddleware(
|
||||
thunk,
|
||||
panelHistoryMiddleware,
|
||||
autoCloseMiddleware,
|
||||
panelQueueMiddleware // ← 이것이 있는지 확인!
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
**Step 2: import 경로 확인**
|
||||
```javascript
|
||||
// ✅ 올바른 import
|
||||
import panelQueueMiddleware from '../middleware/panelQueueMiddleware';
|
||||
|
||||
// ❌ 잘못된 import
|
||||
import { panelQueueMiddleware } from '../middleware/panelQueueMiddleware';
|
||||
// default export이므로 중괄호 없이 import해야 함
|
||||
```
|
||||
|
||||
**Step 3: 앱 재시작**
|
||||
```bash
|
||||
# 개발 서버 재시작
|
||||
npm start
|
||||
```
|
||||
|
||||
**Step 4: 브라우저 캐시 삭제**
|
||||
- Chrome: Ctrl+Shift+R (Windows) 또는 Cmd+Shift+R (Mac)
|
||||
|
||||
---
|
||||
|
||||
### 문제 5: 큐가 무한 루프에 빠짐
|
||||
|
||||
#### 증상
|
||||
```
|
||||
[panelQueueMiddleware] 🚀 ACTION_ENQUEUED
|
||||
[panelQueueMiddleware] ⚡ STARTING_QUEUE_PROCESS
|
||||
[panelReducer] 🟡 PROCESS_PANEL_QUEUE
|
||||
[panelReducer] 🟡 PROCESS_PANEL_QUEUE
|
||||
[panelReducer] 🟡 PROCESS_PANEL_QUEUE
|
||||
... (무한 반복)
|
||||
```
|
||||
|
||||
#### 원인
|
||||
1. 큐 처리 중에 다시 큐에 액션 추가
|
||||
2. `isProcessingQueue` 플래그가 제대로 설정되지 않음
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
**방법 1: 큐 액션 내부에서 일반 dispatch 사용**
|
||||
```javascript
|
||||
// ❌ 잘못된 사용 (무한 루프 발생)
|
||||
export const myAction = () => (dispatch) => {
|
||||
dispatch(pushPanelQueued({ name: 'PANEL_1' }));
|
||||
dispatch(pushPanelQueued({ name: 'PANEL_2' })); // 큐 처리 중 큐 추가
|
||||
};
|
||||
|
||||
// ✅ 올바른 사용
|
||||
export const myAction = () => (dispatch) => {
|
||||
dispatch(enqueueMultiplePanelActions([
|
||||
pushPanelQueued({ name: 'PANEL_1' }),
|
||||
pushPanelQueued({ name: 'PANEL_2' })
|
||||
]));
|
||||
};
|
||||
```
|
||||
|
||||
**방법 2: 리듀서 로직 확인**
|
||||
```javascript
|
||||
// panelReducer.js에서 확인
|
||||
case types.PROCESS_PANEL_QUEUE: {
|
||||
// 이미 처리 중이면 무시
|
||||
if (state.isProcessingQueue || state.panelActionQueue.length === 0) {
|
||||
return state; // ← 이 로직이 있는지 확인
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 문제 6: 큐 통계가 업데이트되지 않음
|
||||
|
||||
#### 증상
|
||||
```javascript
|
||||
store.getState().panels.queueStats
|
||||
// { totalProcessed: 0, failedCount: 0, averageProcessingTime: 0 }
|
||||
// 항상 0으로 유지됨
|
||||
```
|
||||
|
||||
#### 원인
|
||||
큐 처리가 정상적으로 완료되지 않음
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
**Step 1: 콘솔 로그 확인**
|
||||
```
|
||||
[panelReducer] ✅ QUEUE_ITEM_PROCESSED ← 이 로그가 보이는지 확인
|
||||
```
|
||||
|
||||
**Step 2: 에러 발생 확인**
|
||||
```javascript
|
||||
store.getState().panels.queueError
|
||||
// null이어야 정상
|
||||
```
|
||||
|
||||
**Step 3: 큐 처리 완료 여부 확인**
|
||||
```javascript
|
||||
store.getState().panels.isProcessingQueue
|
||||
// false여야 정상 (처리 완료)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 호출 문제
|
||||
|
||||
### 문제 7: API 성공인데 onFail이 호출됨
|
||||
|
||||
#### 증상
|
||||
```javascript
|
||||
// API 호출
|
||||
// HTTP 200, retCode: 0
|
||||
// 그런데 onFail이 호출됨
|
||||
```
|
||||
|
||||
#### 원인
|
||||
프로젝트 성공 기준을 이해하지 못함
|
||||
|
||||
#### 프로젝트 성공 기준
|
||||
**HTTP 200-299 + retCode 0/'0' 둘 다 만족해야 성공!**
|
||||
|
||||
```javascript
|
||||
// ✅ 성공 케이스
|
||||
{ status: 200, data: { retCode: 0, data: {...} } }
|
||||
{ status: 200, data: { retCode: '0', data: {...} } }
|
||||
|
||||
// ❌ 실패 케이스
|
||||
{ status: 200, data: { retCode: 1, message: '에러' } } // retCode가 0이 아님
|
||||
{ status: 500, data: { retCode: 0, data: {...} } } // HTTP 에러
|
||||
```
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
**방법 1: isApiSuccess 사용**
|
||||
```javascript
|
||||
import { isApiSuccess } from '../utils/asyncActionUtils';
|
||||
|
||||
const response = { status: 200 };
|
||||
const responseData = { retCode: 1, message: '에러' };
|
||||
|
||||
if (isApiSuccess(response, responseData)) {
|
||||
// 성공 처리
|
||||
} else {
|
||||
// 실패 처리 (retCode가 1이므로 실패!)
|
||||
}
|
||||
```
|
||||
|
||||
**방법 2: asyncActionUtils 사용**
|
||||
```javascript
|
||||
import { tAxiosToPromise } from '../utils/asyncActionUtils';
|
||||
|
||||
const result = await tAxiosToPromise(...);
|
||||
|
||||
if (result.success) {
|
||||
// HTTP 200-299 + retCode 0/'0'
|
||||
console.log(result.data);
|
||||
} else {
|
||||
// 실패
|
||||
console.error(result.error);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 문제 8: API 타임아웃이 작동하지 않음
|
||||
|
||||
#### 증상
|
||||
```javascript
|
||||
dispatch(enqueueAsyncPanelAction({
|
||||
asyncAction: (d, gs, onS, onF) => { /* 느린 API */ },
|
||||
timeout: 5000 // 5초
|
||||
}));
|
||||
// 10초가 지나도 타임아웃되지 않음
|
||||
```
|
||||
|
||||
#### 원인
|
||||
1. `withTimeout`이 적용되지 않음
|
||||
2. 타임아웃 값이 잘못 설정됨
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
**방법 1: enqueueAsyncPanelAction 사용 시**
|
||||
```javascript
|
||||
// ✅ timeout 옵션 사용
|
||||
dispatch(enqueueAsyncPanelAction({
|
||||
asyncAction: (dispatch, getState, onSuccess, onFail) => {
|
||||
TAxios(dispatch, getState, 'get', URL, {}, {}, onSuccess, onFail);
|
||||
},
|
||||
timeout: 5000, // 5초 (ms 단위)
|
||||
onFail: (error) => {
|
||||
if (error.code === 'TIMEOUT') {
|
||||
console.error('타임아웃 발생!');
|
||||
}
|
||||
}
|
||||
}));
|
||||
```
|
||||
|
||||
**방법 2: withTimeout 직접 사용**
|
||||
```javascript
|
||||
import { withTimeout, fetchApi } from '../utils/asyncActionUtils';
|
||||
|
||||
const result = await withTimeout(
|
||||
fetchApi('/api/slow-endpoint'),
|
||||
5000, // 5초
|
||||
'요청 시간이 초과되었습니다'
|
||||
);
|
||||
|
||||
if (result.error?.code === 'TIMEOUT') {
|
||||
console.error('타임아웃!');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 성능 문제
|
||||
|
||||
### 문제 9: 큐 처리가 너무 느림
|
||||
|
||||
#### 증상
|
||||
```javascript
|
||||
// 100개의 패널 액션을 큐에 추가
|
||||
// 처리하는데 10초 이상 소요
|
||||
```
|
||||
|
||||
#### 원인
|
||||
1. 각 액션이 복잡한 로직 수행
|
||||
2. 동기적으로 처리되어 병목 발생
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
**방법 1: 불필요한 액션 제거**
|
||||
```javascript
|
||||
// ❌ 잘못된 사용
|
||||
for (let i = 0; i < 100; i++) {
|
||||
dispatch(pushPanelQueued({ name: `PANEL_${i}` }));
|
||||
}
|
||||
|
||||
// ✅ 올바른 사용 - 필요한 것만
|
||||
dispatch(pushPanelQueued({ name: 'MAIN_PANEL' }));
|
||||
```
|
||||
|
||||
**방법 2: 배치 처리**
|
||||
```javascript
|
||||
// 한 번에 여러 액션 추가
|
||||
dispatch(enqueueMultiplePanelActions(
|
||||
panels.map(panel => pushPanelQueued(panel))
|
||||
));
|
||||
```
|
||||
|
||||
**방법 3: 병렬 처리가 필요하면 큐 사용 안함**
|
||||
```javascript
|
||||
// 순서가 중요하지 않은 경우
|
||||
dispatch(createParallelDispatch([
|
||||
fetchData1(),
|
||||
fetchData2(),
|
||||
fetchData3()
|
||||
]));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 문제 10: 메모리 누수
|
||||
|
||||
#### 증상
|
||||
```javascript
|
||||
// 오랜 시간 앱 사용 후
|
||||
store.getState().panels.completedAsyncActions.length
|
||||
// → 10000개 이상
|
||||
```
|
||||
|
||||
#### 원인
|
||||
완료된 비동기 액션 ID가 계속 누적됨
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
**방법 1: 주기적으로 클리어**
|
||||
```javascript
|
||||
// 일정 시간마다 완료된 액션 정리
|
||||
setInterval(() => {
|
||||
const state = store.getState().panels;
|
||||
|
||||
if (state.completedAsyncActions.length > 1000) {
|
||||
// 클리어 액션 dispatch
|
||||
dispatch({ type: types.CLEAR_COMPLETED_ASYNC_ACTIONS });
|
||||
}
|
||||
}, 60000); // 1분마다
|
||||
```
|
||||
|
||||
**방법 2: 리듀서에 최대 개수 제한 추가**
|
||||
```javascript
|
||||
// panelReducer.js
|
||||
case types.COMPLETE_ASYNC_PANEL_ACTION: {
|
||||
const newCompleted = [...state.completedAsyncActions, action.payload.actionId];
|
||||
|
||||
// 최근 100개만 유지
|
||||
const trimmed = newCompleted.slice(-100);
|
||||
|
||||
return {
|
||||
...state,
|
||||
completedAsyncActions: trimmed
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 디버깅 팁
|
||||
|
||||
### Tip 1: 콘솔 로그 활용
|
||||
|
||||
모든 헬퍼 함수와 미들웨어는 상세한 로그를 출력합니다:
|
||||
|
||||
```javascript
|
||||
// 큐 관련 로그
|
||||
[panelQueueMiddleware] 🚀 ACTION_ENQUEUED
|
||||
[panelQueueMiddleware] ⚡ STARTING_QUEUE_PROCESS
|
||||
[panelReducer] 🟡 PROCESS_PANEL_QUEUE
|
||||
[panelReducer] 🟡 PROCESSING_QUEUE_ITEM
|
||||
[panelReducer] ✅ QUEUE_ITEM_PROCESSED
|
||||
|
||||
// 비동기 액션 로그
|
||||
[queuedPanelActions] 🔄 ENQUEUE_ASYNC_PANEL_ACTION
|
||||
[queuedPanelActions] ⚡ EXECUTING_ASYNC_ACTION
|
||||
[queuedPanelActions] ✅ ASYNC_ACTION_SUCCESS
|
||||
|
||||
// asyncActionUtils 로그
|
||||
[asyncActionUtils] 🌐 FETCH_API_START
|
||||
[asyncActionUtils] 📊 API_RESPONSE
|
||||
[asyncActionUtils] ✅ TAXIOS_SUCCESS
|
||||
```
|
||||
|
||||
### Tip 2: Redux DevTools 사용
|
||||
|
||||
1. Chrome 확장 프로그램 설치: Redux DevTools
|
||||
2. 개발자 도구 → Redux 탭
|
||||
3. 액션 히스토리 확인
|
||||
4. State diff 확인
|
||||
|
||||
### Tip 3: 브레이크포인트 설정
|
||||
|
||||
```javascript
|
||||
// 디버깅용 브레이크포인트
|
||||
export const myAction = () => (dispatch, getState) => {
|
||||
debugger; // ← 여기서 멈춤
|
||||
|
||||
const state = getState();
|
||||
console.log('Current state:', state);
|
||||
|
||||
dispatch(action1());
|
||||
|
||||
debugger; // ← 여기서 다시 멈춤
|
||||
};
|
||||
```
|
||||
|
||||
### Tip 4: State 스냅샷
|
||||
|
||||
```javascript
|
||||
// 콘솔에서 실행
|
||||
const snapshot = JSON.parse(JSON.stringify(store.getState()));
|
||||
console.log(snapshot);
|
||||
|
||||
// 특정 부분만
|
||||
const panelsSnapshot = JSON.parse(JSON.stringify(store.getState().panels));
|
||||
console.log(panelsSnapshot);
|
||||
```
|
||||
|
||||
### Tip 5: 큐 상태 모니터링
|
||||
|
||||
```javascript
|
||||
// 콘솔에서 실행
|
||||
window.monitorQueue = setInterval(() => {
|
||||
const state = store.getState().panels;
|
||||
console.log('Queue status:', {
|
||||
queueLength: state.panelActionQueue.length,
|
||||
isProcessing: state.isProcessingQueue,
|
||||
stats: state.queueStats
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
// 중지
|
||||
clearInterval(window.monitorQueue);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 도움이 필요하신가요?
|
||||
|
||||
### 체크리스트
|
||||
|
||||
문제 해결 전에 다음을 확인하세요:
|
||||
|
||||
- [ ] panelQueueMiddleware가 store.js에 등록되어 있는가?
|
||||
- [ ] 필요한 파일들이 모두 존재하는가?
|
||||
- [ ] actionTypes.js에 필요한 타입들이 정의되어 있는가?
|
||||
- [ ] 콘솔 로그를 확인했는가?
|
||||
- [ ] Redux DevTools로 액션 흐름을 확인했는가?
|
||||
- [ ] 앱을 재시작했는가?
|
||||
- [ ] 브라우저 캐시를 삭제했는가?
|
||||
|
||||
### 추가 리소스
|
||||
|
||||
- [README.md](./README.md) - 전체 개요
|
||||
- [06-setup-guide.md](./06-setup-guide.md) - 설정 가이드
|
||||
- [05-usage-patterns.md](./05-usage-patterns.md) - 사용 패턴
|
||||
- [07-changelog.md](./07-changelog.md) - 변경 이력
|
||||
|
||||
---
|
||||
|
||||
**작성일**: 2025-11-10
|
||||
**최종 수정일**: 2025-11-10
|
||||
137
.docs/dispatch-async/README.md
Normal file
137
.docs/dispatch-async/README.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Dispatch 비동기 처리 순서 보장 솔루션
|
||||
|
||||
## 📋 목차
|
||||
|
||||
1. [문제 상황](./01-problem.md)
|
||||
2. [해결 방법 1: dispatchHelper.js](./02-solution-dispatch-helper.md)
|
||||
3. [해결 방법 2: asyncActionUtils.js](./03-solution-async-utils.md)
|
||||
4. [해결 방법 3: 큐 기반 패널 액션 시스템](./04-solution-queue-system.md)
|
||||
5. [사용 패턴 및 예제](./05-usage-patterns.md)
|
||||
6. [설정 가이드](./06-setup-guide.md) ⭐
|
||||
7. [변경 이력 (Changelog)](./07-changelog.md)
|
||||
8. [트러블슈팅](./08-troubleshooting.md) ⭐
|
||||
|
||||
## 🎯 개요
|
||||
|
||||
이 문서는 Redux-thunk 환경에서 여러 개의 dispatch를 순차적으로 실행할 때 순서가 보장되지 않는 문제를 해결하기 위해 구현된 솔루션들을 정리한 문서입니다.
|
||||
|
||||
## ⚙️ 설치 및 설정
|
||||
|
||||
### 필수: panelQueueMiddleware 등록
|
||||
|
||||
큐 기반 패널 액션 시스템을 사용하려면 **반드시** store에 미들웨어를 등록해야 합니다.
|
||||
|
||||
**파일**: `src/store/store.js`
|
||||
|
||||
```javascript
|
||||
import { applyMiddleware, combineReducers, createStore } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import { panelHistoryMiddleware } from '../middleware/panelHistoryMiddleware';
|
||||
import { autoCloseMiddleware } from '../middleware/autoCloseMiddleware';
|
||||
import panelQueueMiddleware from '../middleware/panelQueueMiddleware'; // ← 추가
|
||||
|
||||
// ... reducers ...
|
||||
|
||||
export const store = createStore(
|
||||
rootReducer,
|
||||
applyMiddleware(
|
||||
thunk,
|
||||
panelHistoryMiddleware,
|
||||
autoCloseMiddleware,
|
||||
panelQueueMiddleware // ← 추가 (맨 마지막에 위치)
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
**⚠️ 중요**: panelQueueMiddleware를 등록하지 않으면 큐 시스템이 작동하지 않습니다!
|
||||
|
||||
## 🚀 주요 솔루션
|
||||
|
||||
### 1. dispatchHelper.js (2025-11-05)
|
||||
Promise 체인 기반의 dispatch 순차 실행 헬퍼 함수 모음
|
||||
|
||||
- `createSequentialDispatch`: 순차적 dispatch 실행
|
||||
- `createApiThunkWithChain`: API 후 dispatch 자동 체이닝
|
||||
- `withLoadingState`: 로딩 상태 자동 관리
|
||||
- `createConditionalDispatch`: 조건부 dispatch
|
||||
- `createParallelDispatch`: 병렬 dispatch
|
||||
|
||||
### 2. asyncActionUtils.js (2025-11-06)
|
||||
Promise 기반 비동기 액션 처리 및 성공/실패 기준 명확화
|
||||
|
||||
- API 성공 기준: HTTP 200-299 + retCode 0/'0'
|
||||
- 모든 비동기 작업을 Promise로 래핑
|
||||
- reject 없이 resolve + success 플래그 사용
|
||||
- 타임아웃 지원
|
||||
|
||||
### 3. 큐 기반 패널 액션 시스템 (2025-11-06)
|
||||
미들웨어 기반의 액션 큐 처리 시스템
|
||||
|
||||
- `queuedPanelActions.js`: 큐 기반 패널 액션
|
||||
- `panelQueueMiddleware.js`: 자동 큐 처리 미들웨어
|
||||
- `panelReducer.js`: 큐 상태 관리
|
||||
|
||||
## 📊 커밋 히스토리
|
||||
|
||||
```
|
||||
f9290a1 [251106] fix: Dispatch Queue implementation
|
||||
- asyncActionUtils.js 추가
|
||||
- queuedPanelActions.js 확장
|
||||
- panelReducer.js 확장
|
||||
|
||||
5bd2774 [251106] feat: Queued Panel functions
|
||||
- queuedPanelActions.js 초기 구현
|
||||
- panelQueueMiddleware.js 추가
|
||||
|
||||
9490d72 [251105] feat: dispatchHelper.js
|
||||
- createSequentialDispatch
|
||||
- createApiThunkWithChain
|
||||
- withLoadingState
|
||||
- createConditionalDispatch
|
||||
- createParallelDispatch
|
||||
```
|
||||
|
||||
## 📂 관련 파일
|
||||
|
||||
### Core Files
|
||||
- `src/utils/dispatchHelper.js`
|
||||
- `src/utils/asyncActionUtils.js`
|
||||
- `src/actions/queuedPanelActions.js`
|
||||
- `src/middleware/panelQueueMiddleware.js`
|
||||
- `src/reducers/panelReducer.js`
|
||||
|
||||
### Example Files
|
||||
- `src/actions/homeActions.js`
|
||||
- `src/actions/cartActions.js`
|
||||
|
||||
## 🔑 핵심 개선 사항
|
||||
|
||||
1. ✅ **순서 보장**: Promise 체인과 큐 시스템으로 dispatch 순서 보장
|
||||
2. ✅ **에러 처리**: reject 대신 resolve + success 플래그로 체인 보장
|
||||
3. ✅ **성공 기준 명확화**: HTTP 상태 + retCode 둘 다 확인
|
||||
4. ✅ **타임아웃 지원**: withTimeout으로 응답 없는 API 처리
|
||||
5. ✅ **로깅**: 모든 단계에서 상세한 로그 출력
|
||||
6. ✅ **호환성**: 기존 코드와 완전 호환 (선택적 사용 가능)
|
||||
|
||||
## 🎓 학습 자료
|
||||
|
||||
각 솔루션에 대한 자세한 설명은 개별 문서를 참고하세요.
|
||||
|
||||
### 시작하기
|
||||
- **처음 시작한다면** → [06-setup-guide.md](./06-setup-guide.md) ⭐
|
||||
- **문제가 발생했다면** → [08-troubleshooting.md](./08-troubleshooting.md) ⭐
|
||||
|
||||
### 이해하기
|
||||
- 기존 코드의 문제점이 궁금하다면 → [01-problem.md](./01-problem.md)
|
||||
- dispatchHelper 사용법이 궁금하다면 → [02-solution-dispatch-helper.md](./02-solution-dispatch-helper.md)
|
||||
- asyncActionUtils 사용법이 궁금하다면 → [03-solution-async-utils.md](./03-solution-async-utils.md)
|
||||
- 큐 시스템 사용법이 궁금하다면 → [04-solution-queue-system.md](./04-solution-queue-system.md)
|
||||
|
||||
### 실전 적용
|
||||
- 실제 사용 예제가 궁금하다면 → [05-usage-patterns.md](./05-usage-patterns.md)
|
||||
- 변경 이력을 확인하려면 → [07-changelog.md](./07-changelog.md)
|
||||
|
||||
---
|
||||
|
||||
**작성일**: 2025-11-10
|
||||
**최종 수정일**: 2025-11-10
|
||||
Reference in New Issue
Block a user