Files
shoptime/com.twin.app.shoptime/.docs/dispatch-async/03-solution-async-utils.md
optrader fd5a171a28 restore: .docs 폴더 복원 및 .gitignore 수정
- 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>
2025-11-11 10:00:59 +09:00

17 KiB
Raw Blame History

해결 방법 2: asyncActionUtils.js

📦 개요

파일: src/utils/asyncActionUtils.js 작성일: 2025-11-06 커밋: f9290a1 [251106] fix: Dispatch Queue implementation

Promise 기반의 비동기 액션 처리와 상세한 성공/실패 기준을 제공합니다.

🎯 핵심 개념

프로젝트 특화 성공 기준

이 프로젝트에서 API 호출 성공은 2가지 조건을 모두 만족해야 합니다:

  1. HTTP 상태 코드: 200-299 범위
  2. retCode: 0 또는 '0'
// HTTP 200이지만 retCode가 1인 경우
{
  status: 200,        // ✅ HTTP는 성공
  data: {
    retCode: 1,       // ❌ retCode는 실패
    message: "권한이 없습니다"
  }
}
// → 이것은 실패입니다!

Promise 체인이 끊기지 않는 설계

핵심 원칙: 모든 비동기 작업은 reject 없이 resolve만 사용합니다.

// ❌ 일반적인 방식 (Promise 체인이 끊김)
return new Promise((resolve, reject) => {
  if (error) {
    reject(error);  // 체인이 끊김!
  }
});

// ✅ 이 프로젝트의 방식 (체인 유지)
return new Promise((resolve) => {
  if (error) {
    resolve({
      success: false,
      error: { code: 'ERROR', message: '에러 발생' }
    });
  }
});

🔑 핵심 함수

  1. isApiSuccess - API 성공 여부 판단
  2. fetchApi - Promise 기반 fetch 래퍼
  3. tAxiosToPromise - TAxios를 Promise로 변환
  4. wrapAsyncAction - 비동기 액션을 Promise로 래핑
  5. withTimeout - 타임아웃 지원
  6. executeParallelAsyncActions - 병렬 실행

1 isApiSuccess

설명

API 응답이 성공인지 판단하는 프로젝트 표준 함수입니다.

구현

파일: src/utils/asyncActionUtils.js:21-34

export const isApiSuccess = (response, responseData) => {
  // 1⃣ HTTP 상태 코드 확인 (200-299 성공 범위)
  if (!response.ok || response.status < 200 || response.status >= 300) {
    return false;
  }

  // 2⃣ retCode 확인 - 0 또는 '0'이어야 성공
  if (responseData && responseData.retCode !== undefined) {
    return responseData.retCode === 0 || responseData.retCode === '0';
  }

  // retCode가 없는 경우 HTTP 상태 코드만으로 판단
  return response.ok;
};

사용 예제

// 성공 케이스
isApiSuccess(
  { ok: true, status: 200 },
  { retCode: 0, data: { ... } }
);  // → true

isApiSuccess(
  { ok: true, status: 200 },
  { retCode: '0', data: { ... } }
);  // → true

// 실패 케이스
isApiSuccess(
  { ok: true, status: 200 },
  { retCode: 1, message: "권한 없음" }
);  // → false (retCode가 0이 아님)

isApiSuccess(
  { ok: false, status: 500 },
  { retCode: 0, data: { ... } }
);  // → false (HTTP 상태 코드가 500)

isApiSuccess(
  { ok: false, status: 404 },
  { retCode: 0 }
);  // → false (404 에러)

2 fetchApi

설명

표준 fetch API를 Promise로 래핑하여 프로젝트 성공 기준에 맞춰 처리합니다.

핵심 특징

  • 항상 resolve 사용 (reject 없음)
  • HTTP 상태 + retCode 모두 확인
  • JSON 파싱 에러도 처리
  • 네트워크 에러도 처리
  • 상세한 로깅

구현

파일: src/utils/asyncActionUtils.js:57-123

export const fetchApi = (url, options = {}) => {
  console.log('[asyncActionUtils] 🌐 FETCH_API_START', { url, method: options.method || 'GET' });

  return new Promise((resolve) => {  // ⚠️ 항상 resolve만 사용!
    fetch(url, {
      headers: {
        'Content-Type': 'application/json',
        ...options.headers
      },
      ...options
    })
    .then(response => {
      // JSON 파싱
      return response.json()
        .then(responseData => {
          console.log('[asyncActionUtils] 📊 API_RESPONSE', {
            status: response.status,
            ok: response.ok,
            retCode: responseData.retCode,
            success: isApiSuccess(response, responseData)
          });

          // ✅ 성공/실패 여부와 관계없이 항상 resolve
          resolve({
            response,
            data: responseData,
            success: isApiSuccess(response, responseData),
            error: !isApiSuccess(response, responseData) ? {
              code: responseData.retCode || response.status,
              message: responseData.message || getApiErrorMessage(responseData.retCode || response.status),
              httpStatus: response.status
            } : null
          });
        })
        .catch(parseError => {
          console.error('[asyncActionUtils] ❌ JSON_PARSE_ERROR', parseError);

          // ✅ JSON 파싱 실패도 resolve로 처리
          resolve({
            response,
            data: null,
            success: false,
            error: {
              code: 'PARSE_ERROR',
              message: '응답 데이터 파싱에 실패했습니다',
              originalError: parseError
            }
          });
        });
    })
    .catch(error => {
      console.error('[asyncActionUtils] 💥 FETCH_ERROR', error);

      // ✅ 네트워크 에러 등도 resolve로 처리
      resolve({
        response: null,
        data: null,
        success: false,
        error: {
          code: 'NETWORK_ERROR',
          message: error.message || '네트워크 오류가 발생했습니다',
          originalError: error
        }
      });
    });
  });
};

사용 예제

import { fetchApi } from '../utils/asyncActionUtils';

// 기본 사용
const result = await fetchApi('/api/products/123', {
  method: 'GET'
});

if (result.success) {
  console.log('성공:', result.data);
  // HTTP 200-299 + retCode 0/'0'
} else {
  console.error('실패:', result.error);
  // error.code, error.message 사용 가능
}

// POST 요청
const result = await fetchApi('/api/cart', {
  method: 'POST',
  body: JSON.stringify({ productId: 123 })
});

// 헤더 추가
const result = await fetchApi('/api/user', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer token123'
  }
});

반환 구조

// 성공 시
{
  response: Response,        // fetch Response 객체
  data: { ... },            // 파싱된 JSON 데이터
  success: true,            // 성공 플래그
  error: null               // 에러 없음
}

// 실패 시 (HTTP 에러)
{
  response: Response,
  data: { retCode: 1, message: "권한 없음" },
  success: false,
  error: {
    code: 1,
    message: "권한 없음",
    httpStatus: 200
  }
}

// 실패 시 (네트워크 에러)
{
  response: null,
  data: null,
  success: false,
  error: {
    code: 'NETWORK_ERROR',
    message: '네트워크 오류가 발생했습니다',
    originalError: Error
  }
}

3 tAxiosToPromise

설명

프로젝트에서 사용하는 TAxios를 Promise로 변환합니다.

구현

파일: src/utils/asyncActionUtils.js:138-204

export const tAxiosToPromise = (
  TAxios,
  dispatch,
  getState,
  method,
  baseUrl,
  urlParams,
  params,
  options = {}
) => {
  return new Promise((resolve) => {
    console.log('[asyncActionUtils] 🔄 TAXIOS_TO_PROMISE_START', { method, baseUrl });

    const enhancedOnSuccess = (response) => {
      console.log('[asyncActionUtils] ✅ TAXIOS_SUCCESS', { retCode: response?.data?.retCode });

      // TAxios 성공 콜백도 성공 기준 적용
      const isSuccess = response?.data && (
        response.data.retCode === 0 ||
        response.data.retCode === '0'
      );

      resolve({
        response,
        data: response.data,
        success: isSuccess,
        error: !isSuccess ? {
          code: response.data?.retCode || 'UNKNOWN_ERROR',
          message: response.data?.message || getApiErrorMessage(response.data?.retCode || 'UNKNOWN_ERROR')
        } : null
      });
    };

    const enhancedOnFail = (error) => {
      console.error('[asyncActionUtils] ❌ TAXIOS_FAIL', error);

      resolve({  // ⚠️ reject가 아닌 resolve
        response: null,
        data: null,
        success: false,
        error: {
          code: error.retCode || 'TAXIOS_ERROR',
          message: error.message || 'API 호출에 실패했습니다',
          originalError: error
        }
      });
    };

    try {
      TAxios(
        dispatch,
        getState,
        method,
        baseUrl,
        urlParams,
        params,
        enhancedOnSuccess,
        enhancedOnFail,
        options.noTokenRefresh || false,
        options.responseType
      );
    } catch (error) {
      console.error('[asyncActionUtils] 💥 TAXIOS_EXECUTION_ERROR', error);

      resolve({
        response: null,
        data: null,
        success: false,
        error: {
          code: 'EXECUTION_ERROR',
          message: 'API 호출 실행 중 오류가 발생했습니다',
          originalError: error
        }
      });
    }
  });
};

사용 예제

import { tAxiosToPromise } from '../utils/asyncActionUtils';
import { TAxios } from '../utils/TAxios';

export const getProductDetail = (productId) => async (dispatch, getState) => {
  const result = await tAxiosToPromise(
    TAxios,
    dispatch,
    getState,
    'get',
    URLS.GET_PRODUCT_DETAIL,
    {},
    { productId },
    {}
  );

  if (result.success) {
    dispatch({
      type: types.GET_PRODUCT_DETAIL,
      payload: result.data.data
    });
  } else {
    console.error('상품 조회 실패:', result.error);
  }
};

4 wrapAsyncAction

설명

비동기 액션 함수를 Promise로 래핑하여 표준화된 결과 구조를 반환합니다.

구현

파일: src/utils/asyncActionUtils.js:215-270

export const wrapAsyncAction = (asyncAction, context = {}) => {
  return new Promise((resolve) => {
    const { dispatch, getState } = context;

    console.log('[asyncActionUtils] 🎯 WRAP_ASYNC_ACTION_START');

    // 성공 콜백 - 항상 resolve 호출
    const onSuccess = (result) => {
      console.log('[asyncActionUtils] ✅ WRAP_ASYNC_SUCCESS', result);

      resolve({
        response: result.response || result,
        data: result.data || result,
        success: true,
        error: null
      });
    };

    // 실패 콜백 - 항상 resolve 호출 (reject 하지 않음)
    const onFail = (error) => {
      console.error('[asyncActionUtils] ❌ WRAP_ASYNC_FAIL', error);

      resolve({
        response: null,
        data: null,
        success: false,
        error: {
          code: error.retCode || error.code || 'ASYNC_ACTION_ERROR',
          message: error.message || error.errorMessage || '비동기 작업에 실패했습니다',
          originalError: error
        }
      });
    };

    try {
      // 비동기 액션 실행
      const result = asyncAction(dispatch, getState, onSuccess, onFail);

      // Promise를 반환하는 경우도 처리
      if (result && typeof result.then === 'function') {
        result
          .then(onSuccess)
          .catch(onFail);
      }
    } catch (error) {
      console.error('[asyncActionUtils] 💥 WRAP_ASYNC_EXECUTION_ERROR', error);
      onFail(error);
    }
  });
};

사용 예제

import { wrapAsyncAction } from '../utils/asyncActionUtils';

// 비동기 액션 정의
const myAsyncAction = (dispatch, getState, onSuccess, onFail) => {
  TAxios(dispatch, getState, 'get', URL, {}, {}, onSuccess, onFail);
};

// Promise로 래핑하여 사용
const result = await wrapAsyncAction(myAsyncAction, { dispatch, getState });

if (result.success) {
  console.log('성공:', result.data);
} else {
  console.error('실패:', result.error.message);
}

5 withTimeout

설명

Promise에 타임아웃을 적용합니다.

구현

파일: src/utils/asyncActionUtils.js:354-373

export const withTimeout = (
  promise,
  timeoutMs,
  timeoutMessage = '작업 시간이 초과되었습니다'
) => {
  return Promise.race([
    promise,
    new Promise((resolve) => {
      setTimeout(() => {
        console.error('[asyncActionUtils] ⏰ PROMISE_TIMEOUT', { timeoutMs });
        resolve({
          response: null,
          data: null,
          success: false,
          error: {
            code: 'TIMEOUT',
            message: timeoutMessage,
            timeout: timeoutMs
          }
        });
      }, timeoutMs);
    })
  ]);
};

사용 예제

import { withTimeout, fetchApi } from '../utils/asyncActionUtils';

// 5초 타임아웃
const result = await withTimeout(
  fetchApi('/api/slow-endpoint'),
  5000,
  '요청이 시간초과 되었습니다'
);

if (result.success) {
  console.log('성공:', result.data);
} else if (result.error.code === 'TIMEOUT') {
  console.error('타임아웃 발생');
} else {
  console.error('기타 에러:', result.error);
}

6 executeParallelAsyncActions

설명

여러 비동기 액션을 병렬로 실행하고 모든 결과를 기다립니다.

구현

파일: src/utils/asyncActionUtils.js:279-299

export const executeParallelAsyncActions = (asyncActions, context = {}) => {
  console.log('[asyncActionUtils] 🚀 EXECUTE_PARALLEL_START', { count: asyncActions.length });

  const promises = asyncActions.map(action =>
    wrapAsyncAction(action, context)
  );

  return Promise.all(promises)
    .then(results => {
      console.log('[asyncActionUtils] ✅ EXECUTE_PARALLEL_SUCCESS', {
        successCount: results.filter(r => r.success).length,
        failCount: results.filter(r => !r.success).length
      });
      return results;
    })
    .catch(error => {
      console.error('[asyncActionUtils] ❌ EXECUTE_PARALLEL_ERROR', error);
      return [];
    });
};

사용 예제

import { executeParallelAsyncActions } from '../utils/asyncActionUtils';

// 3개의 API를 동시에 호출
const results = await executeParallelAsyncActions([
  (dispatch, getState, onSuccess, onFail) => {
    TAxios(dispatch, getState, 'get', URL1, {}, {}, onSuccess, onFail);
  },
  (dispatch, getState, onSuccess, onFail) => {
    TAxios(dispatch, getState, 'get', URL2, {}, {}, onSuccess, onFail);
  },
  (dispatch, getState, onSuccess, onFail) => {
    TAxios(dispatch, getState, 'get', URL3, {}, {}, onSuccess, onFail);
  }
], { dispatch, getState });

// 결과 처리
results.forEach((result, index) => {
  if (result.success) {
    console.log(`API ${index + 1} 성공:`, result.data);
  } else {
    console.error(`API ${index + 1} 실패:`, result.error);
  }
});

📊 실제 사용 시나리오

시나리오 1: API 호출 후 후속 처리

import { tAxiosToPromise } from '../utils/asyncActionUtils';

export const addToCartAndRefresh = (productId) => async (dispatch, getState) => {
  // 1. 카트에 추가
  const addResult = await tAxiosToPromise(
    TAxios,
    dispatch,
    getState,
    'post',
    URLS.ADD_TO_CART,
    {},
    { productId },
    {}
  );

  if (addResult.success) {
    // 2. 카트 추가 성공 시 카트 정보 재조회
    dispatch({ type: types.ADD_TO_CART, payload: addResult.data.data });

    const cartResult = await tAxiosToPromise(
      TAxios,
      dispatch,
      getState,
      'get',
      URLS.GET_CART,
      {},
      { mbrNo: addResult.data.data.mbrNo },
      {}
    );

    if (cartResult.success) {
      dispatch({ type: types.GET_CART, payload: cartResult.data.data });
    }
  } else {
    console.error('카트 추가 실패:', addResult.error);
  }
};

시나리오 2: 타임아웃이 있는 API 호출

import { tAxiosToPromise, withTimeout } from '../utils/asyncActionUtils';

export const getLargeData = () => async (dispatch, getState) => {
  const result = await withTimeout(
    tAxiosToPromise(
      TAxios,
      dispatch,
      getState,
      'get',
      URLS.GET_LARGE_DATA,
      {},
      {},
      {}
    ),
    10000,  // 10초 타임아웃
    '데이터 조회 시간이 초과되었습니다'
  );

  if (result.success) {
    dispatch({ type: types.GET_LARGE_DATA, payload: result.data.data });
  } else if (result.error.code === 'TIMEOUT') {
    // 타임아웃 처리
    dispatch({ type: types.SHOW_TIMEOUT_MESSAGE });
  } else {
    // 기타 에러 처리
    console.error('조회 실패:', result.error);
  }
};

장점

  1. 성공 기준 명확화: HTTP + retCode 모두 확인
  2. 체인 보장: reject 없이 resolve만 사용하여 Promise 체인 유지
  3. 상세한 로깅: 모든 단계에서 로그 출력
  4. 타임아웃 지원: 응답 없는 API 처리 가능
  5. 에러 처리: 모든 에러를 표준 구조로 반환

⚠️ 주의사항

  1. Chrome 68 호환: async/await 사용 가능하지만 주의 필요
  2. 항상 resolve: reject 사용하지 않음
  3. success 플래그: 반드시 result.success 확인 필요

다음: 해결 방법 3: 큐 기반 패널 액션 시스템 →