[문서화] Dispatch 비동기 처리 순서 보장 솔루션 문서 작성
dispatch 비동기 처리 순서 보장 문제와 해결 방법을 체계적으로 정리한 문서를 작성했습니다. ## 작성된 문서 1. README.md - 전체 개요 및 목차 2. 01-problem.md - 문제 상황 및 원인 분석 3. 02-solution-dispatch-helper.md - dispatchHelper.js 솔루션 4. 03-solution-async-utils.md - asyncActionUtils.js 솔루션 5. 04-solution-queue-system.md - 큐 기반 패널 액션 시스템 6. 05-usage-patterns.md - 사용 패턴 및 실전 예제 ## 주요 내용 ### 문제 - Redux-thunk에서 여러 dispatch 순서가 보장되지 않는 문제 - setTimeout(fn, 0) 임시방편의 한계 ### 해결 방법 1. **dispatchHelper.js** (2025-11-05) - createSequentialDispatch: Promise 체인 기반 순차 실행 - createApiThunkWithChain: API 후 dispatch 체이닝 - withLoadingState: 로딩 상태 자동 관리 2. **asyncActionUtils.js** (2025-11-06) - 성공 기준 명확화: HTTP 200-299 + retCode 0/'0' - reject 없이 resolve만 사용하여 Promise 체인 보장 - 타임아웃 지원 3. **큐 기반 패널 액션 시스템** (2025-11-06) - queuedPanelActions.js: 패널 액션 큐 - panelQueueMiddleware.js: 자동 큐 처리 - 비동기 액션 순차 실행 ## 관련 커밋 -9490d72[251105] feat: dispatchHelper.js -5bd2774[251106] feat: Queued Panel functions -f9290a1[251106] fix: Dispatch Queue implementation
This commit is contained in:
790
.docs/dispatch-async/05-usage-patterns.md
Normal file
790
.docs/dispatch-async/05-usage-patterns.md
Normal file
@@ -0,0 +1,790 @@
|
||||
# 사용 패턴 및 예제
|
||||
|
||||
## 📋 목차
|
||||
|
||||
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));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 체크리스트
|
||||
|
||||
### 기능 구현 전 확인사항
|
||||
|
||||
- [ ] 패널 관련 액션인가? → 큐 시스템 사용
|
||||
- [ ] API 호출이 포함되어 있는가? → createApiThunkWithChain 또는 createApiWithPanelActions
|
||||
- [ ] 로딩 상태 관리가 필요한가? → withLoadingState
|
||||
- [ ] 순차 실행이 필요한가? → createSequentialDispatch 또는 createAsyncPanelSequence
|
||||
- [ ] 타임아웃이 필요한가? → withTimeout 또는 timeout 옵션 설정
|
||||
|
||||
### 코드 리뷰 체크리스트
|
||||
|
||||
- [ ] setTimeout 사용 여부 확인
|
||||
- [ ] 에러 처리가 적절한가?
|
||||
- [ ] 로깅이 충분한가?
|
||||
- [ ] 타임아웃이 설정되어 있는가?
|
||||
- [ ] 상태 검증이 필요한가?
|
||||
- [ ] 재사용 가능한 구조인가?
|
||||
|
||||
---
|
||||
|
||||
**이전**: [← 해결 방법 3: 큐 기반 패널 액션 시스템](./04-solution-queue-system.md)
|
||||
**처음으로**: [← README](./README.md)
|
||||
Reference in New Issue
Block a user