Files
shoptime/.docs/dispatch-async/02-solution-dispatch-helper.md
Claude f75860c8dd [문서화] 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
2025-11-10 09:26:40 +00:00

14 KiB
Raw Blame History

해결 방법 1: dispatchHelper.js

📦 개요

파일: src/utils/dispatchHelper.js 작성일: 2025-11-05 커밋: 9490d72 [251105] feat: dispatchHelper.js

Promise 체인 기반의 dispatch 순차 실행 헬퍼 함수 모음입니다.

🎯 핵심 함수

  1. createSequentialDispatch - 순차적 dispatch 실행
  2. createApiThunkWithChain - API 후 dispatch 자동 체이닝
  3. withLoadingState - 로딩 상태 자동 관리
  4. createConditionalDispatch - 조건부 dispatch
  5. createParallelDispatch - 병렬 dispatch

1 createSequentialDispatch

설명

여러 dispatch를 Promise 체인을 사용하여 순차적으로 실행합니다.

사용법

import { createSequentialDispatch } from '../utils/dispatchHelper';

// 기본 사용
dispatch(createSequentialDispatch([
  { type: types.SET_LOADING, payload: true },
  { type: types.UPDATE_DATA, payload: data },
  { type: types.SET_LOADING, payload: false }
]));

// thunk와 plain action 혼합
dispatch(createSequentialDispatch([
  { type: types.GET_HOME_TERMS, payload: response.data },
  { type: types.SET_TERMS_ID_MAP, payload: termsIdMap },
  getTermsAgreeYn() // thunk action
]));

// 옵션 사용
dispatch(createSequentialDispatch([
  fetchUserData(),
  fetchCartData(),
  fetchOrderData()
], {
  delay: 100,        // 각 dispatch 간 100ms 지연
  stopOnError: true  // 에러 발생 시 중단
}));

Before & After

Before (setTimeout 방식)

const onSuccess = (response) => {
  dispatch({ type: types.GET_HOME_TERMS, payload: response.data });
  dispatch({ type: types.SET_TERMS_ID_MAP, payload: termsIdMap });
  setTimeout(() => { dispatch(getTermsAgreeYn()); }, 0);
};

After (createSequentialDispatch)

const onSuccess = (response) => {
  dispatch(createSequentialDispatch([
    { type: types.GET_HOME_TERMS, payload: response.data },
    { type: types.SET_TERMS_ID_MAP, payload: termsIdMap },
    getTermsAgreeYn()
  ]));
};

구현 원리

파일: src/utils/dispatchHelper.js:96-129

export const createSequentialDispatch = (dispatchActions, options) =>
  (dispatch, getState) => {
    const config = options || {};
    const delay = config.delay || 0;
    const stopOnError = config.stopOnError !== undefined ? config.stopOnError : false;

    // Promise 체인으로 순차 실행
    return dispatchActions.reduce(
      (promise, action, index) => {
        return promise
          .then(() => {
            // delay가 설정되어 있고 첫 번째가 아닌 경우 지연
            if (delay > 0 && index > 0) {
              return new Promise((resolve) => setTimeout(resolve, delay));
            }
            return Promise.resolve();
          })
          .then(() => {
            // action 실행
            const result = dispatch(action);

            // Promise인 경우 대기
            if (result && typeof result.then === 'function') {
              return result;
            }
            return Promise.resolve(result);
          })
          .catch((error) => {
            console.error('createSequentialDispatch error at index', index, error);

            // stopOnError가 true면 에러를 다시 throw
            if (stopOnError) {
              throw error;
            }

            // stopOnError가 false면 계속 진행
            return Promise.resolve();
          });
      },
      Promise.resolve()
    );
  };

핵심 포인트:

  1. Array.reduce()로 Promise 체인 구성
  2. 각 action이 완료되면 다음 action 실행
  3. thunk가 Promise를 반환하면 대기
  4. 에러 처리 옵션 지원

2 createApiThunkWithChain

설명

API 호출 후 성공 콜백에서 여러 dispatch를 자동으로 체이닝합니다. TAxios의 onSuccess/onFail 패턴과 완벽하게 호환됩니다.

사용법

import { createApiThunkWithChain } from '../utils/dispatchHelper';

// 기본 사용
export const addToCart = (props) =>
  createApiThunkWithChain(
    (dispatch, getState, onSuccess, onFail) => {
      TAxios(dispatch, getState, 'post', URLS.ADD_TO_CART, {}, props, onSuccess, onFail);
    },
    [
      (response) => ({ type: types.ADD_TO_CART, payload: response.data.data }),
      (response) => getMyInfoCartSearch({ mbrNo: response.data.data.mbrNo })
    ]
  );

// 에러 처리 포함
export const registerDevice = (params) =>
  createApiThunkWithChain(
    (dispatch, getState, onSuccess, onFail) => {
      TAxios(dispatch, getState, 'post', URLS.REGISTER_DEVICE, {}, params, onSuccess, onFail);
    },
    [
      (response) => ({ type: types.REGISTER_DEVICE, payload: response.data.data }),
      getAuthenticationCode(),
      fetchCurrentUserHomeTerms()
    ],
    (error) => ({ type: types.API_ERROR, payload: error })
  );

Before & After

Before

export const addToCart = (props) => (dispatch, getState) => {
  const onSuccess = (response) => {
    dispatch({ type: types.ADD_TO_CART, payload: response.data.data });
    dispatch(getMyInfoCartSearch({ mbrNo }));
  };

  const onFail = (error) => {
    console.error(error);
  };

  TAxios(dispatch, getState, "post", URLS.ADD_TO_CART, {}, props, onSuccess, onFail);
};

After

export const addToCart = (props) =>
  createApiThunkWithChain(
    (d, gs, onS, onF) => TAxios(d, gs, 'post', URLS.ADD_TO_CART, {}, props, onS, onF),
    [
      (response) => ({ type: types.ADD_TO_CART, payload: response.data.data }),
      (response) => getMyInfoCartSearch({ mbrNo: response.data.data.mbrNo })
    ]
  );

구현 원리

파일: src/utils/dispatchHelper.js:170-211

export const createApiThunkWithChain = (
  apiCallFactory,
  successDispatchActions,
  errorDispatch
) => (dispatch, getState) => {
  const actions = successDispatchActions || [];

  const enhancedOnSuccess = (response) => {
    // 성공 시 순차적으로 dispatch 실행
    actions.forEach((action, index) => {
      setTimeout(() => {
        if (typeof action === 'function') {
          // action이 함수인 경우 (동적 action creator)
          // response를 인자로 전달하여 실행
          const dispatchAction = action(response);
          dispatch(dispatchAction);
        } else {
          // action이 객체인 경우 (plain action)
          dispatch(action);
        }
      }, 0);
    });
  };

  const enhancedOnFail = (error) => {
    console.error('createApiThunkWithChain error:', error);

    if (errorDispatch) {
      if (typeof errorDispatch === 'function') {
        const dispatchAction = errorDispatch(error);
        dispatch(dispatchAction);
      } else {
        dispatch(errorDispatch);
      }
    }
  };

  // API 호출 실행
  return apiCallFactory(dispatch, getState, enhancedOnSuccess, enhancedOnFail);
};

핵심 포인트:

  1. API 호출의 onSuccess/onFail 콜백을 래핑
  2. 성공 시 여러 action을 순차 실행
  3. response를 각 action에 전달 가능
  4. 에러 처리 action 지원

3 withLoadingState

설명

API 호출 thunk의 로딩 상태를 자동으로 관리합니다. changeAppStatusshowLoadingPanel을 자동 on/off합니다.

사용법

import { withLoadingState } from '../utils/dispatchHelper';

// 기본 로딩 관리
export const getProductDetail = (props) =>
  withLoadingState(
    (dispatch, getState) => {
      return TAxiosPromise(dispatch, getState, 'get', URLS.GET_PRODUCT_DETAIL, props, {})
        .then((response) => {
          dispatch({ type: types.GET_PRODUCT_DETAIL, payload: response.data.data });
        });
    }
  );

// 성공/에러 시 추가 dispatch
export const fetchUserData = (userId) =>
  withLoadingState(
    fetchUser(userId),
    {
      loadingType: 'spinner',
      successDispatch: [
        fetchCart(userId),
        fetchOrders(userId)
      ],
      errorDispatch: [
        (error) => ({ type: types.SHOW_ERROR_MESSAGE, payload: error.message })
      ]
    }
  );

Before & After

Before

export const getProductDetail = (props) => (dispatch, getState) => {
  // 로딩 시작
  dispatch(changeAppStatus({ showLoadingPanel: { show: true } }));

  const onSuccess = (response) => {
    dispatch({ type: types.GET_PRODUCT_DETAIL, payload: response.data.data });
    // 로딩 종료
    dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
  };

  const onFail = (error) => {
    console.error(error);
    // 로딩 종료
    dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
  };

  TAxios(dispatch, getState, 'get', URLS.GET_PRODUCT_DETAIL, props, {}, onSuccess, onFail);
};

After

export const getProductDetail = (props) =>
  withLoadingState(
    (dispatch, getState) => {
      return TAxiosPromise(dispatch, getState, 'get', URLS.GET_PRODUCT_DETAIL, props, {})
        .then((response) => {
          dispatch({ type: types.GET_PRODUCT_DETAIL, payload: response.data.data });
        });
    }
  );

구현 원리

파일: src/utils/dispatchHelper.js:252-302

export const withLoadingState = (thunk, options) => (dispatch, getState) => {
  const config = options || {};
  const loadingType = config.loadingType || 'wait';
  const successDispatch = config.successDispatch || [];
  const errorDispatch = config.errorDispatch || [];

  // 로딩 시작
  dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: loadingType } }));

  // thunk 실행
  const result = dispatch(thunk);

  // Promise인 경우 처리
  if (result && typeof result.then === 'function') {
    return result
      .then((res) => {
        // 로딩 종료
        dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));

        // 성공 시 추가 dispatch 실행
        successDispatch.forEach((action) => {
          if (typeof action === 'function') {
            dispatch(action(res));
          } else {
            dispatch(action);
          }
        });

        return res;
      })
      .catch((error) => {
        // 로딩 종료
        dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));

        // 에러 시 추가 dispatch 실행
        errorDispatch.forEach((action) => {
          if (typeof action === 'function') {
            dispatch(action(error));
          } else {
            dispatch(action);
          }
        });

        throw error;
      });
  }

  // 동기 실행인 경우
  dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
  return result;
};

핵심 포인트:

  1. 로딩 시작/종료를 자동 관리
  2. Promise 기반 thunk만 지원
  3. 성공/실패 시 추가 action 실행 가능
  4. 에러 발생 시에도 로딩 상태 복원

4 createConditionalDispatch

설명

getState() 결과를 기반으로 조건에 따라 다른 dispatch를 실행합니다.

사용법

import { createConditionalDispatch } from '../utils/dispatchHelper';

// 단일 action 조건부 실행
dispatch(createConditionalDispatch(
  (state) => state.common.appStatus.isAlarmEnabled === 'Y',
  addReservation(reservationData),
  deleteReservation(showId)
));

// 여러 action 배열로 실행
dispatch(createConditionalDispatch(
  (state) => state.common.appStatus.loginUserData.userNumber,
  [
    fetchUserProfile(),
    fetchUserCart(),
    fetchUserOrders()
  ],
  [
    { type: types.SHOW_LOGIN_REQUIRED_POPUP }
  ]
));

// false 조건 없이
dispatch(createConditionalDispatch(
  (state) => state.cart.items.length > 0,
  proceedToCheckout()
));

5 createParallelDispatch

설명

여러 API 호출을 병렬로 실행하고 모든 결과를 기다립니다. Promise.all을 사용합니다.

사용법

import { createParallelDispatch } from '../utils/dispatchHelper';

// 여러 API를 동시에 호출
dispatch(createParallelDispatch([
  fetchUserProfile(),
  fetchUserCart(),
  fetchUserOrders()
], { withLoading: true }));

📊 실제 사용 예제

homeActions.js 개선

// Before
export const getHomeTerms = (props) => (dispatch, getState) => {
  const onSuccess = (response) => {
    if (response.data.retCode === 0) {
      dispatch({ type: types.GET_HOME_TERMS, payload: response.data });
      dispatch({ type: types.SET_TERMS_ID_MAP, payload: termsIdMap });
      setTimeout(() => { dispatch(getTermsAgreeYn()); }, 0);
    }
  };
  TAxios(dispatch, getState, "get", URLS.GET_HOME_TERMS, ..., onSuccess, onFail);
};

// After
export const getHomeTerms = (props) =>
  createApiThunkWithChain(
    (d, gs, onS, onF) => TAxios(d, gs, "get", URLS.GET_HOME_TERMS, ..., onS, onF),
    [
      { type: types.GET_HOME_TERMS, payload: response.data },
      { type: types.SET_TERMS_ID_MAP, payload: termsIdMap },
      getTermsAgreeYn()
    ]
  );

cartActions.js 개선

// Before
export const addToCart = (props) => (dispatch, getState) => {
  const onSuccess = (response) => {
    dispatch({ type: types.ADD_TO_CART, payload: response.data.data });
    dispatch(getMyInfoCartSearch({ mbrNo }));
  };
  TAxios(dispatch, getState, "post", URLS.ADD_TO_CART, ..., onSuccess, onFail);
};

// After
export const addToCart = (props) =>
  createApiThunkWithChain(
    (d, gs, onS, onF) => TAxios(d, gs, 'post', URLS.ADD_TO_CART, {}, props, onS, onF),
    [
      (response) => ({ type: types.ADD_TO_CART, payload: response.data.data }),
      (response) => getMyInfoCartSearch({ mbrNo: response.data.data.mbrNo })
    ]
  );

장점

  1. 간결성: setTimeout 제거로 코드가 깔끔해짐
  2. 가독성: 의도가 명확하게 드러남
  3. 재사용성: 헬퍼 함수를 여러 곳에서 사용 가능
  4. 에러 처리: 옵션으로 에러 처리 전략 선택 가능
  5. 호환성: 기존 코드와 호환 (선택적 사용)

⚠️ 주의사항

  1. Promise 기반: 모든 함수가 Promise를 반환하도록 설계됨
  2. Chrome 68: async/await 없이 Promise.then() 사용
  3. 기존 패턴: TAxios의 onSuccess/onFail 패턴 유지

다음: 해결 방법 2: asyncActionUtils.js →