Files
shoptime/.docs/dispatch-async/05-usage-patterns.md
Claude c12cc91a39 [수정] panelQueueMiddleware 등록 및 문서 업데이트
## 코드 수정
- 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 (수정)
2025-11-10 09:30:22 +00:00

21 KiB

사용 패턴 및 예제

📋 목차

  1. 어떤 솔루션을 선택할까?
  2. 공통 패턴
  3. 실전 예제
  4. 마이그레이션 가이드
  5. 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

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