# 사용 패턴 및 예제 ## 📋 목차 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)