- claude/ 브랜치에서 누락된 .docs 폴더 복원 완료 - dispatch-async 관련 문서 9개 파일 복원 * 01-problem.md, 02-solution-dispatch-helper.md * 03-solution-async-utils.md, 04-solution-queue-system.md * 05-usage-patterns.md, 06-setup-guide.md * 07-changelog.md, 08-troubleshooting.md, README.md - MediaPlayer.v2 관련 문서 4개 파일 복원 * MediaPlayer-v2-README.md, MediaPlayer-v2-Required-Changes.md * MediaPlayer-v2-Risk-Analysis.md, PR-MediaPlayer-v2.md - 기타 분석 문서 2개 파일 복원 * modal-transition-analysis.md, video-player-analysis-and-optimization-plan.md - .gitignore에서 .docs 항목 제거로 문서 추적 가능하도록 수정 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: GLM 4.6 <noreply@z.ai>
805 lines
21 KiB
Markdown
805 lines
21 KiB
Markdown
# 사용 패턴 및 예제
|
|
|
|
## 📋 목차
|
|
|
|
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)
|