## 코드 수정 - store.js에 panelQueueMiddleware import 및 등록 - 큐 기반 패널 액션 시스템이 작동하기 위해 필수 - applyMiddleware에 panelQueueMiddleware 추가 ## 문서 수정 1. README.md - "설치 및 설정" 섹션 추가 - panelQueueMiddleware 등록 필수 사항 명시 2. 04-solution-queue-system.md - "사전 요구사항" 섹션 추가 - 미들웨어 등록 코드 예제 포함 3. 05-usage-patterns.md - "초기 설정 확인사항" 체크리스트 추가 - panelQueueMiddleware 등록 여부 확인 항목 추가 ## 이유 - panelQueueMiddleware가 등록되지 않으면 큐 시스템이 작동하지 않음 - ENQUEUE_PANEL_ACTION dispatch 시 자동으로 PROCESS_PANEL_QUEUE가 실행되지 않음 - 문서에 설정 방법이 명확하지 않아 사용자가 놓칠 수 있음 ## 관련 파일 - src/store/store.js (수정) - .docs/dispatch-async/README.md (수정) - .docs/dispatch-async/04-solution-queue-system.md (수정) - .docs/dispatch-async/05-usage-patterns.md (수정)
21 KiB
21 KiB
사용 패턴 및 예제
📋 목차
어떤 솔루션을 선택할까?
의사결정 플로우차트
패널 관련 액션인가?
├─ 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
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)
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+)
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
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
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
dispatch(pushPanel({ name: panel_names.SEARCH }));
setTimeout(() => {
dispatch(updatePanel({ results: [...] }));
setTimeout(() => {
dispatch(popPanel(panel_names.LOADING));
}, 0);
}, 0);
After
dispatch(enqueueMultiplePanelActions([
pushPanelQueued({ name: panel_names.SEARCH }),
updatePanelQueued({ results: [...] }),
popPanelQueued(panel_names.LOADING)
]));
패턴 4: 조건부 dispatch
Before
export const checkAndFetch = () => (dispatch, getState) => {
const state = getState();
if (state.user.isLoggedIn) {
dispatch(fetchUserProfile());
dispatch(fetchUserCart());
} else {
dispatch({ type: types.SHOW_LOGIN_POPUP });
}
};
After
export const checkAndFetch = () =>
createConditionalDispatch(
(state) => state.user.isLoggedIn,
[
fetchUserProfile(),
fetchUserCart()
],
[
{ type: types.SHOW_LOGIN_POPUP }
]
);
실전 예제
예제 1: 검색 기능
// 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: 장바구니 추가
// 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: 로그인 플로우
// 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: 다단계 폼 제출
// 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: 병렬 데이터 로딩
// 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 변경
// Before
import { pushPanel, popPanel, updatePanel } from '../actions/panelActions';
// After
import { pushPanelQueued, popPanelQueued, updatePanelQueued }
from '../actions/queuedPanelActions';
import { createApiThunkWithChain, withLoadingState }
from '../utils/dispatchHelper';
Step 2: 기존 코드 점진적 마이그레이션
// 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 패턴 제거
// Before
dispatch(action1());
setTimeout(() => {
dispatch(action2());
setTimeout(() => {
dispatch(action3());
}, 0);
}, 0);
// After
dispatch(createSequentialDispatch([
action1(),
action2(),
action3()
]));
Step 4: API 패턴 개선
// 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. 명확한 에러 처리
// ✅ 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. 타임아웃 설정
// ✅ 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. 로깅 활용
// ✅ 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. 상태 검증
// ✅ 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. 재사용 가능한 액션
// ✅ 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에 등록되어 있는가? (큐 시스템 사용 시 필수!)
// 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: 큐 기반 패널 액션 시스템 처음으로: ← README