- 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>
17 KiB
17 KiB
해결 방법 2: asyncActionUtils.js
📦 개요
파일: src/utils/asyncActionUtils.js
작성일: 2025-11-06
커밋: f9290a1 [251106] fix: Dispatch Queue implementation
Promise 기반의 비동기 액션 처리와 상세한 성공/실패 기준을 제공합니다.
🎯 핵심 개념
프로젝트 특화 성공 기준
이 프로젝트에서 API 호출 성공은 2가지 조건을 모두 만족해야 합니다:
- ✅ HTTP 상태 코드: 200-299 범위
- ✅ 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: '에러 발생' }
});
}
});
🔑 핵심 함수
isApiSuccess- API 성공 여부 판단fetchApi- Promise 기반 fetch 래퍼tAxiosToPromise- TAxios를 Promise로 변환wrapAsyncAction- 비동기 액션을 Promise로 래핑withTimeout- 타임아웃 지원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);
}
};
✅ 장점
- 성공 기준 명확화: HTTP + retCode 모두 확인
- 체인 보장: reject 없이 resolve만 사용하여 Promise 체인 유지
- 상세한 로깅: 모든 단계에서 로그 출력
- 타임아웃 지원: 응답 없는 API 처리 가능
- 에러 처리: 모든 에러를 표준 구조로 반환
⚠️ 주의사항
- Chrome 68 호환: async/await 사용 가능하지만 주의 필요
- 항상 resolve: reject 사용하지 않음
- success 플래그: 반드시
result.success확인 필요