[251106] fix: Dispatch Queue implementation

🕐 커밋 시간: 2025. 11. 06. 20:46:35

📊 변경 통계:
  • 총 파일: 7개
  • 추가: +398줄
  • 삭제: -1줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/utils/advancedAsyncPanelExamples.js
  + com.twin.app.shoptime/src/utils/asyncActionUtils.js
  + com.twin.app.shoptime/src/utils/asyncPanelQueueExamples.js
  + com.twin.app.shoptime/src/utils/compatibleAsyncPanelExamples.js

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/actionTypes.js
  ~ com.twin.app.shoptime/src/actions/queuedPanelActions.js
  ~ com.twin.app.shoptime/src/reducers/panelReducer.js

🔧 주요 변경 내용:
  • 타입 시스템 안정성 강화
  • 핵심 비즈니스 로직 개선
  • 공통 유틸리티 함수 최적화
  • 대규모 기능 개발
  • 모듈 구조 개선
This commit is contained in:
2025-11-06 20:46:36 +09:00
parent 46ab30930d
commit f9290a12ba
7 changed files with 2712 additions and 1 deletions

View File

@@ -18,6 +18,11 @@ export const types = {
CLEAR_PANEL_QUEUE: 'CLEAR_PANEL_QUEUE',
SET_QUEUE_PROCESSING: 'SET_QUEUE_PROCESSING',
// 🔽 [251106] 비동기 액션 완료 처리
ENQUEUE_ASYNC_PANEL_ACTION: 'ENQUEUE_ASYNC_PANEL_ACTION',
COMPLETE_ASYNC_PANEL_ACTION: 'COMPLETE_ASYNC_PANEL_ACTION',
FAIL_ASYNC_PANEL_ACTION: 'FAIL_ASYNC_PANEL_ACTION',
// device actions
GET_AUTHENTICATION_CODE: 'GET_AUTHENTICATION_CODE',
REGISTER_DEVICE: 'REGISTER_DEVICE',

View File

@@ -152,3 +152,294 @@ export const createPanelSequence = (sequence) => {
dispatch(enqueueMultiplePanelActions(queuedActions));
};
};
// ===================================================================
// 🔽 [251106] 비동기 액션 완료 처리 기능
// ===================================================================
/**
* 비동기 패널 액션을 큐에 추가합니다.
* API 호출이나 다른 비동기 작업이 포함된 패널 액션을 순차적으로 처리합니다.
*
* @param {Object} config - 비동기 액션 설정
* @param {string} config.id - 액션 고유 ID
* @param {Function} config.asyncAction - 비동기 액션 (dispatch, getState, onSuccess, onFail) => void
* @param {Function} config.onSuccess - 성공 콜백 (response) => void
* @param {Function} config.onFail - 실패 콜백 (error) => void
* @param {Function} config.onFinish - 완료 콜백 (isSuccess, result) => void
* @param {number} config.timeout - 타임아웃 (ms, 기본값: 10000)
* @returns {Function} Redux thunk
*/
export const enqueueAsyncPanelAction = (config) => {
return (dispatch, getState) => {
const actionId = config.id || `async_action_${++queueItemId}_${Date.now()}`;
console.log('[queuedPanelActions] 🔄 ENQUEUE_ASYNC_PANEL_ACTION', {
actionId,
timestamp: Date.now()
});
dispatch({
type: types.ENQUEUE_ASYNC_PANEL_ACTION,
payload: {
id: actionId,
asyncAction: config.asyncAction,
onSuccess: config.onSuccess,
onFail: config.onFail,
onFinish: config.onFinish,
timeout: config.timeout || 10000,
timestamp: Date.now(),
status: 'pending'
}
});
// 비동기 액션 실행
executeAsyncAction(dispatch, getState, actionId);
};
};
/**
* 비동기 액션을 실행하고 완료 처리를 합니다.
* @param {Function} dispatch - Redux dispatch
* @param {Function} getState - Redux getState
* @param {string} actionId - 액션 ID
*/
const executeAsyncAction = (dispatch, getState, actionId) => {
const state = getState();
const asyncAction = state.panels?.panelActionQueue?.find(item => item.id === actionId);
if (!asyncAction) {
console.warn('[queuedPanelActions] ⚠️ ASYNC_ACTION_NOT_FOUND', actionId);
return;
}
console.log('[queuedPanelActions] ⚡ EXECUTING_ASYNC_ACTION', actionId);
// 비동기 액션을 Promise로 래핑하여 실행
import('../utils/asyncActionUtils').then(({ wrapAsyncAction, withTimeout }) => {
const actionPromise = wrapAsyncAction(asyncAction.asyncAction, { dispatch, getState });
const timeoutPromise = withTimeout(actionPromise, asyncAction.timeout);
timeoutPromise
.then(result => {
console.log('[queuedPanelActions] 📊 ASYNC_ACTION_RESULT', {
actionId,
success: result.success,
hasError: !!result.error,
errorCode: result.error?.code
});
if (result.success) {
// 성공 처리
console.log('[queuedPanelActions] ✅ ASYNC_ACTION_SUCCESS', actionId);
// 사용자 정의 성공 콜백 실행
if (asyncAction.onSuccess) {
try {
asyncAction.onSuccess(result.data);
} catch (error) {
console.error('[queuedPanelActions] ❌ USER_ON_SUCCESS_ERROR', error);
}
}
// 완료 콜백 실행
if (asyncAction.onFinish) {
try {
asyncAction.onFinish(true, result.data);
} catch (error) {
console.error('[queuedPanelActions] ❌ USER_ON_FINISH_ERROR', error);
}
}
// Redux 상태 업데이트
dispatch({
type: types.COMPLETE_ASYNC_PANEL_ACTION,
payload: {
actionId,
result: result.data,
timestamp: Date.now()
}
});
} else {
// 실패 처리
console.error('[queuedPanelActions] ❌ ASYNC_ACTION_FAILED', {
actionId,
error: result.error,
errorCode: result.error?.code
});
// 사용자 정의 실패 콜백 실행
if (asyncAction.onFail) {
try {
asyncAction.onFail(result.error);
} catch (callbackError) {
console.error('[queuedPanelActions] ❌ USER_ON_FAIL_ERROR', callbackError);
}
}
// 완료 콜백 실행
if (asyncAction.onFinish) {
try {
asyncAction.onFinish(false, result.error);
} catch (callbackError) {
console.error('[queuedPanelActions] ❌ USER_ON_FINISH_ERROR', callbackError);
}
}
// Redux 상태 업데이트
dispatch({
type: types.FAIL_ASYNC_PANEL_ACTION,
payload: {
actionId,
error: result.error,
timestamp: Date.now()
}
});
}
})
.catch(error => {
console.error('[queuedPanelActions] 💥 ASYNC_ACTION_EXECUTION_ERROR', { actionId, error });
// 치명적인 에러 처리
if (asyncAction.onFail) {
try {
asyncAction.onFail(error);
} catch (callbackError) {
console.error('[queuedPanelActions] ❌ USER_ON_FAIL_ERROR', callbackError);
}
}
if (asyncAction.onFinish) {
try {
asyncAction.onFinish(false, error);
} catch (callbackError) {
console.error('[queuedPanelActions] ❌ USER_ON_FINISH_ERROR', callbackError);
}
}
dispatch({
type: types.FAIL_ASYNC_PANEL_ACTION,
payload: {
actionId,
error: {
code: 'EXECUTION_ERROR',
message: error.message || '비동기 액션 실행 중 치명적인 오류 발생'
},
timestamp: Date.now()
}
});
});
}).catch(error => {
console.error('[queuedPanelActions] 💥 ASYNC_UTILS_IMPORT_ERROR', error);
// 유틸리티 import 실패 시 기본 처리
if (asyncAction.onFail) {
asyncAction.onFail(error);
}
if (asyncAction.onFinish) {
asyncAction.onFinish(false, error);
}
});
};
/**
* API 호출 후 패널 액션을 실행하는 헬퍼 함수
* @param {Object} config - 설정
* @param {Function} config.apiCall - API 호출 함수 (dispatch, getState, onSuccess, onFail) => void
* @param {Array} config.panelActions - API 성공 후 실행할 패널 액션들
* @param {Function} config.onApiSuccess - API 성공 콜백 (선택)
* @param {Function} config.onApiFail - API 실패 콜백 (선택)
* @returns {Function} Redux thunk
*/
export const createApiWithPanelActions = (config) => {
return enqueueAsyncPanelAction({
asyncAction: (dispatch, getState, onSuccess, onFail) => {
console.log('[queuedPanelActions] 🌐 API_CALL_START');
config.apiCall(dispatch, getState, onSuccess, onFail);
},
onSuccess: (response) => {
console.log('[queuedPanelActions] 🎯 API_SUCCESS_EXECUTING_PANELS');
// API 성공 콜백 실행
if (config.onApiSuccess) {
config.onApiSuccess(response);
}
// 패널 액션들 순차 실행
if (config.panelActions && config.panelActions.length > 0) {
config.panelActions.forEach((panelAction, index) => {
setTimeout(() => {
if (typeof panelAction === 'function') {
dispatch(panelAction(response));
} else {
dispatch(panelAction);
}
}, index * 10); // 10ms 간격으로 실행
});
}
},
onFail: (error) => {
console.log('[queuedPanelActions] 🚫 API_FAILED', error);
// API 실패 콜백 실행
if (config.onApiFail) {
config.onApiFail(error);
}
},
onFinish: (isSuccess, result) => {
console.log('[queuedPanelActions] 🏁 API_WITH_PANELS_COMPLETE', { isSuccess });
}
});
};
/**
* 여러 비동기 액션들을 순차적으로 실행합니다.
* @param {Array} asyncConfigs - 비동기 액션 설정 배열
* @returns {Function} Redux thunk
*/
export const createAsyncPanelSequence = (asyncConfigs) => {
return (dispatch, getState) => {
let currentIndex = 0;
const executeNext = () => {
if (currentIndex >= asyncConfigs.length) {
console.log('[queuedPanelActions] 🎊 ASYNC_SEQUENCE_COMPLETE');
return;
}
const config = asyncConfigs[currentIndex];
console.log('[queuedPanelActions] 📋 EXECUTING_ASYNC_SEQUENCE_ITEM', {
index: currentIndex,
total: asyncConfigs.length
});
// 현재 액션에 다음 액션 실행 로직 추가
const enhancedConfig = {
...config,
onFinish: (isSuccess, result) => {
// 원래 onFinish 콜백 실행
if (config.onFinish) {
config.onFinish(isSuccess, result);
}
// 성공한 경우에만 다음 액션 실행
if (isSuccess) {
currentIndex++;
setTimeout(executeNext, 50); // 50ms 후 다음 액션 실행
} else {
console.error('[queuedPanelActions] ⛔ ASYNC_SEQUENCE_STOPPED_ON_ERROR', {
index: currentIndex,
error: result
});
}
}
};
dispatch(enqueueAsyncPanelAction(enhancedConfig));
};
// 첫 번째 액션 실행
executeNext();
};
};

View File

@@ -14,7 +14,12 @@ const initialState = {
totalProcessed: 0, // 총 처리된 액션 수
failedCount: 0, // 실패한 액션 수
averageProcessingTime: 0 // 평균 처리 시간
}
},
// [251106] 비동기 액션 관련 상태
asyncActions: {}, // 실행 중인 비동기 액션들 { actionId: { ... } }
completedAsyncActions: [], // 완료된 비동기 액션 ID들
failedAsyncActions: [], // 실패한 비동기 액션 ID들
};
// last one will be on top
@@ -332,6 +337,102 @@ export const panelsReducer = (state = initialState, action) => {
};
}
// [251106] 비동기 패널 액션 관련 reducer 케이스들
case types.ENQUEUE_ASYNC_PANEL_ACTION: {
console.log('[panelReducer] 🟠 ENQUEUE_ASYNC_PANEL_ACTION', {
actionId: action.payload.id,
timestamp: action.payload.timestamp,
});
return {
...state,
asyncActions: {
...state.asyncActions,
[action.payload.id]: {
...action.payload,
status: 'running',
startTime: Date.now(),
}
},
queueError: null, // 에러 초기화
};
}
case types.COMPLETE_ASYNC_PANEL_ACTION: {
console.log('[panelReducer] ✅ COMPLETE_ASYNC_PANEL_ACTION', {
actionId: action.payload.actionId,
timestamp: action.payload.timestamp,
});
const asyncAction = state.asyncActions[action.payload.actionId];
const executionTime = asyncAction ? Date.now() - asyncAction.startTime : 0;
// 실행 중인 액션에서 제거하고 완료된 액션에 추가
const newAsyncActions = { ...state.asyncActions };
delete newAsyncActions[action.payload.actionId];
return {
...state,
asyncActions: newAsyncActions,
completedAsyncActions: [
...state.completedAsyncActions,
{
actionId: action.payload.actionId,
result: action.payload.result,
executionTime,
completedAt: action.payload.timestamp,
}
].slice(-100), // 최근 100개만 유지
queueError: null,
queueStats: {
...state.queueStats,
totalProcessed: state.queueStats.totalProcessed + 1,
averageProcessingTime: Math.round(
((state.queueStats.averageProcessingTime * state.queueStats.totalProcessed) + executionTime) /
(state.queueStats.totalProcessed + 1) * 100
) / 100,
},
};
}
case types.FAIL_ASYNC_PANEL_ACTION: {
console.log('[panelReducer] ❌ FAIL_ASYNC_PANEL_ACTION', {
actionId: action.payload.actionId,
error: action.payload.error?.message || 'Unknown error',
timestamp: action.payload.timestamp,
});
const asyncAction = state.asyncActions[action.payload.actionId];
const executionTime = asyncAction ? Date.now() - asyncAction.startTime : 0;
// 실행 중인 액션에서 제거하고 실패한 액션에 추가
const newAsyncActions = { ...state.asyncActions };
delete newAsyncActions[action.payload.actionId];
return {
...state,
asyncActions: newAsyncActions,
failedAsyncActions: [
...state.failedAsyncActions,
{
actionId: action.payload.actionId,
error: action.payload.error,
executionTime,
failedAt: action.payload.timestamp,
}
].slice(-100), // 최근 100개만 유지
queueError: {
actionId: action.payload.actionId,
error: action.payload.error,
timestamp: action.payload.timestamp,
},
queueStats: {
...state.queueStats,
failedCount: state.queueStats.failedCount + 1,
},
};
}
default:
return state;
}

View File

@@ -0,0 +1,720 @@
/**
* [251106] 고급 비동기 패널 액션 예시
*
* 이 파일은 Promise 기반의 정교한 비동기 처리와
* 프로젝트 특화된 성공/실패 기준을 적용한 예시들을 보여줍니다.
*/
import {
enqueueAsyncPanelAction,
createApiWithPanelActions,
createAsyncPanelSequence
} from '../actions/queuedPanelActions';
import {
pushPanelQueued,
popPanelQueued,
updatePanelQueued,
clearPanelQueue
} from '../actions/queuedPanelActions';
import {
fetchApi,
tAxiosToPromise,
wrapAsyncAction,
executeParallelAsyncActions,
withTimeout,
processAsyncResult
} from './asyncActionUtils';
import { panel_names } from './Config';
/**
* 예시 1: Promise 기반 fetch API 사용
*
* 200 + retCode 0/'0' 성공 기준 적용
*/
export const promiseBasedFetchExample = (dispatch, getState, searchQuery) => {
console.log('🌐 Promise Based Fetch Example');
dispatch(enqueueAsyncPanelAction({
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
try {
const result = await fetchApi(`/api/search?q=${encodeURIComponent(searchQuery)}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
// 항상 resolve를 호출하여 상세 제어
onSuccess(result);
} catch (error) {
onFail(error);
}
},
onSuccess: (result) => {
console.log('✅ Fetch Success:', {
httpStatus: result.response?.status,
retCode: result.data?.retCode,
success: result.success
});
// 성공 시 패널 액션들
dispatch(pushPanelQueued({
name: panel_names.SEARCH_PANEL,
panelInfo: {
query: searchQuery,
results: result.data?.data || [],
isLoading: false
}
}));
},
onFail: (error) => {
console.error('❌ Fetch Failed:', {
code: error.code,
message: error.message,
httpStatus: error.httpStatus
});
// 실패 시 에러 패널
dispatch(pushPanelQueued({
name: panel_names.ERROR_PANEL,
panelInfo: {
error: error.message,
errorCode: error.code,
canRetry: true,
retryAction: () => promiseBasedFetchExample(dispatch, getState, searchQuery)
}
}));
},
onFinish: (isSuccess, result) => {
console.log('🏁 Fetch Complete:', {
isSuccess,
hasData: !!result?.data,
errorCode: result?.error?.code
});
}
}));
};
/**
* 예시 2: TAxios 방식을 Promise로 변환
*
* 기존 TAxios 코드와 호환되면서 Promise 기반 제어
*/
export const tAxiosPromiseExample = (dispatch, getState, productId) => {
console.log('🔄 TAxios Promise Example');
dispatch(enqueueAsyncPanelAction({
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
try {
// TAxios 동적 import
const { TAxios } = await import('../api/TAxios');
const result = await tAxiosToPromise(
TAxios,
dispatch,
getState,
'get',
'/api/products',
{ productId }, // URL params
{}, // request params
{
noTokenRefresh: false,
responseType: 'json'
}
);
onSuccess(result);
} catch (error) {
onFail(error);
}
},
onSuccess: (result) => {
console.log('✅ TAxios Success:', {
retCode: result.data?.retCode,
success: result.success,
hasData: !!result.data?.data
});
if (result.success) {
dispatch(pushPanelQueued({
name: panel_names.DETAIL_PANEL,
panelInfo: {
product: result.data?.data,
isLoading: false
}
}));
} else {
// retCode가 0이 아닌 경우 실패 처리
dispatch(pushPanelQueued({
name: panel_names.ERROR_PANEL,
panelInfo: {
error: result.error?.message || '제품 정보를 가져오는데 실패했습니다',
errorCode: result.error?.code
}
}));
}
},
onFail: (error) => {
console.error('❌ TAxios Failed:', error);
dispatch(pushPanelQueued({
name: panel_names.ERROR_PANEL,
panelInfo: {
error: error.message,
isSystemError: true
}
}));
}
}));
};
/**
* 예시 3: 복잡한 비동기 워크플로우 (Promise.all)
*
* 여러 API를 병렬로 호출하고 모두 완료되면 패널 조작
*/
export const parallelAsyncWorkflow = (dispatch, getState, userId) => {
console.log('🚀 Parallel Async Workflow');
dispatch(enqueueAsyncPanelAction({
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
try {
// 병렬로 실행할 비동기 액션들
const asyncActions = [
// 사용자 프로필 로드
async (dispatch, getState, success, fail) => {
const result = await fetchApi(`/api/users/${userId}/profile`);
success(result);
},
// 장바구니 정보 로드
async (dispatch, getState, success, fail) => {
const result = await fetchApi(`/api/users/${userId}/cart`);
success(result);
},
// 위시리스트 로드
async (dispatch, getState, success, fail) => {
const result = await fetchApi(`/api/users/${userId}/wishlist`);
success(result);
}
];
// 병렬 실행
const results = await executeParallelAsyncActions(asyncActions, { dispatch, getState });
// 모든 결과 포장
const combinedResult = {
response: { status: 200 },
data: {
profile: results[0].data,
cart: results[1].data,
wishlist: results[2].data,
errors: results.filter(r => !r.success).map(r => r.error)
},
success: results.every(r => r.success),
error: results.some(r => !r.success) ? {
code: 'PARTIAL_FAILURE',
message: '일부 정보 로드에 실패했습니다',
errors: results.filter(r => !r.success).map(r => r.error)
} : null
};
onSuccess(combinedResult);
} catch (error) {
onFail(error);
}
},
onSuccess: (result) => {
console.log('✅ Parallel Workflow Success:', {
allSuccess: result.success,
hasData: !!result.data,
errorCount: result.data?.errors?.length || 0
});
// 사용자 패널 표시
dispatch(pushPanelQueued({
name: panel_names.USER_PANEL,
panelInfo: {
user: result.data.profile?.data,
cartItems: result.data.cart?.data?.items || [],
wishlistItems: result.data.wishlist?.data?.items || [],
hasErrors: !!result.error,
errors: result.data?.errors || []
}
}));
},
onFail: (error) => {
console.error('❌ Parallel Workflow Failed:', error);
dispatch(pushPanelQueued({
name: panel_names.ERROR_PANEL,
panelInfo: {
error: '사용자 정보 로드에 실패했습니다',
canRetry: true,
retryAction: () => parallelAsyncWorkflow(dispatch, getState, userId)
}
}));
}
}));
};
/**
* 예시 4: 순차적 비동기 체이닝
*
* 각 단계의 성공 여부에 따라 다음 단계 실행/중단
*/
export const sequentialAsyncChaining = (dispatch, getState, orderId) => {
console.log('🔗 Sequential Async Chaining');
const asyncSequence = [
{
name: 'loadOrder',
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
const result = await withTimeout(
fetchApi(`/api/orders/${orderId}`),
5000,
'주문 정보 로드 시간이 초과되었습니다'
);
onSuccess(result);
},
onSuccess: (result) => {
console.log('✅ Order loaded');
if (result.success) {
dispatch(pushPanelQueued({
name: panel_names.ORDER_PANEL,
panelInfo: {
order: result.data?.data,
step: 'loaded'
}
}));
}
},
onFail: (error) => {
console.error('❌ Order load failed:', error);
// 실패 시 체이닝 중단
dispatch(pushPanelQueued({
name: panel_names.ERROR_PANEL,
panelInfo: {
error: '주문 정보를 찾을 수 없습니다',
errorCode: error.code
}
}));
}
},
{
name: 'loadPayment',
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
const result = await withTimeout(
fetchApi(`/api/orders/${orderId}/payment`),
3000,
'결제 정보 로드 시간이 초과되었습니다'
);
onSuccess(result);
},
onSuccess: (result) => {
console.log('✅ Payment loaded');
if (result.success) {
dispatch(updatePanelQueued({
name: panel_names.ORDER_PANEL,
panelInfo: {
payment: result.data?.data,
step: 'payment_loaded'
}
}));
}
},
onFail: (error) => {
console.error('❌ Payment load failed:', error);
// 결제 정보 실패는 주문에는 영향 없음
dispatch(updatePanelQueued({
name: panel_names.ORDER_PANEL,
panelInfo: {
paymentError: error.message,
step: 'payment_failed'
}
}));
}
},
{
name: 'loadShipping',
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
const result = await withTimeout(
fetchApi(`/api/orders/${orderId}/shipping`),
3000,
'배송 정보 로드 시간이 초과되었습니다'
);
onSuccess(result);
},
onSuccess: (result) => {
console.log('✅ Shipping loaded');
if (result.success) {
dispatch(updatePanelQueued({
name: panel_names.ORDER_PANEL,
panelInfo: {
shipping: result.data?.data,
step: 'completed'
}
}));
}
},
onFail: (error) => {
console.error('❌ Shipping load failed:', error);
dispatch(updatePanelQueued({
name: panel_names.ORDER_PANEL,
panelInfo: {
shippingError: error.message,
step: 'shipping_failed'
}
}));
}
}
];
// 순차적 실행
dispatch(createAsyncPanelSequence(asyncSequence));
};
/**
* 예시 5: 재시도 로직과 에러 복구
*
* 실패 시 자동 재시도 및 폴백 로직
*/
export const retryAndFallbackExample = (dispatch, getState, productId) => {
console.log('🔄 Retry and Fallback Example');
const maxRetries = 3;
let retryCount = 0;
const attemptLoad = () => {
retryCount++;
console.log(`📡 Attempt ${retryCount}/${maxRetries}`);
dispatch(enqueueAsyncPanelAction({
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
try {
const result = await withTimeout(
fetchApi(`/api/products/${productId}`),
3000,
'제품 정보 로드 시간이 초과되었습니다'
);
// 성공/실패 관계없이 항상 resolve
onSuccess(result);
} catch (error) {
onSuccess({
response: null,
data: null,
success: false,
error: {
code: 'NETWORK_ERROR',
message: error.message,
originalError: error
}
});
}
},
onSuccess: (result) => {
if (result.success) {
console.log('✅ Product loaded successfully');
// 성공 시 패널 표시
dispatch(pushPanelQueued({
name: panel_names.DETAIL_PANEL,
panelInfo: {
product: result.data?.data,
source: 'api',
retryCount
}
}));
} else {
console.error('❌ Product load failed:', result.error);
if (retryCount < maxRetries) {
// 재시도
console.log(`🔄 Retrying in 1 second... (${retryCount}/${maxRetries})`);
setTimeout(() => attemptLoad(), 1000);
} else {
// 최대 재시도 도달 - 폴백 로직
console.log('📦 Max retries reached, using fallback');
// 캐시된 데이터나 기본값 사용
dispatch(pushPanelQueued({
name: panel_names.DETAIL_PANEL,
panelInfo: {
product: {
id: productId,
name: '제품 정보 로드 실패',
placeholder: true
},
source: 'fallback',
error: result.error?.message,
retryCount
}
}));
}
}
},
timeout: 4000 // 각 시도 타임아웃
}));
};
// 첫 번째 시도
attemptLoad();
};
/**
* 예시 6: 실제 프로젝트 시나리오 - 장바구니 결제 과정
*
* 실제 비즈니스 로직에 적용 가능한 복합 예시
*/
export const realWorldCheckoutFlow = (dispatch, getState, cartItems) => {
console.log('🛒 Real World Checkout Flow');
dispatch(clearPanelQueue()); // 기존 큐 정리
const checkoutWorkflow = [
{
name: 'validateCart',
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
try {
const result = await fetchApi('/api/cart/validate', {
method: 'POST',
body: JSON.stringify({ items: cartItems })
});
onSuccess(result);
} catch (error) {
onSuccess({
response: null,
data: null,
success: false,
error: { code: 'VALIDATION_ERROR', message: error.message }
});
}
},
onSuccess: (result) => {
if (result.success) {
dispatch(pushPanelQueued({
name: panel_names.CHECKOUT_PANEL,
panelInfo: {
step: 'validated',
items: cartItems,
validationResult: result.data
}
}));
} else {
dispatch(pushPanelQueued({
name: panel_names.CART_PANEL,
panelInfo: {
step: 'validation_failed',
items: cartItems,
errors: result.error
}
}));
}
}
},
{
name: 'calculateShipping',
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
try {
const result = await fetchApi('/api/shipping/calculate', {
method: 'POST',
body: JSON.stringify({
items: cartItems,
address: getState().user?.defaultAddress
})
});
onSuccess(result);
} catch (error) {
onSuccess({
response: null,
data: null,
success: false,
error: { code: 'SHIPPING_ERROR', message: error.message }
});
}
},
onSuccess: (result) => {
if (result.success) {
dispatch(updatePanelQueued({
name: panel_names.CHECKOUT_PANEL,
panelInfo: {
step: 'shipping_calculated',
shippingOptions: result.data?.options || []
}
}));
} else {
dispatch(updatePanelQueued({
name: panel_names.CHECKOUT_PANEL,
panelInfo: {
step: 'shipping_error',
shippingError: result.error?.message
}
}));
}
}
},
{
name: 'processPayment',
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
try {
const result = await fetchApi('/api/payment/process', {
method: 'POST',
body: JSON.stringify({
items: cartItems,
shipping: getState().panels?.panels?.find(p => p.name === panel_names.CHECKOUT_PANEL)?.panelInfo?.selectedShipping,
paymentMethod: 'credit_card'
})
});
onSuccess(result);
} catch (error) {
onSuccess({
response: null,
data: null,
success: false,
error: { code: 'PAYMENT_ERROR', message: error.message }
});
}
},
onSuccess: (result) => {
if (result.success) {
// 결제 성공 - 주문 완료 패널
dispatch(pushPanelQueued({
name: panel_names.ORDER_COMPLETE_PANEL,
panelInfo: {
orderData: result.data,
step: 'completed'
}
}));
// 장바구니 패널 제거
dispatch(popPanelQueued(panel_names.CHECKOUT_PANEL));
} else {
// 결제 실패 - 에러 표시
dispatch(updatePanelQueued({
name: panel_names.CHECKOUT_PANEL,
panelInfo: {
step: 'payment_failed',
paymentError: result.error?.message,
canRetry: true
}
}));
}
}
}
];
// 체크아웃 워크플로우 실행
dispatch(createAsyncPanelSequence(checkoutWorkflow));
};
/**
* 예시 7: 기존 방식과 새로운 방식의 비교
*/
export const comparisonExample = (dispatch, getState) => {
console.log('📊 Comparison Example - Old vs New');
// 기존 방식 (복잡한 콜백 체인, 성공 기준 불명확)
console.log('❌ Old way (complex callbacks, unclear success criteria):');
setTimeout(() => {
// 복잡한 콜백 체인
TAxios(
dispatch,
getState,
'get',
'/api/user/profile',
{},
{},
(response) => {
// 성공 여부 불명확 - 단순히 콜백 호출만 확인
if (response.data && response.data.retCode === 0) {
// 성공 처리
dispatch({ type: 'PUSH_PANEL', payload: { name: 'USER_PANEL' }});
// 또 다른 API 호출 - 중첩 콜백
TAxios(
dispatch,
getState,
'get',
'/api/user/cart',
{},
{},
(cartResponse) => {
if (cartResponse.data && cartResponse.data.retCode === 0) {
dispatch({ type: 'UPDATE_PANEL', payload: {
name: 'USER_PANEL',
panelInfo: { cart: cartResponse.data.data }
}});
}
},
(error) => {
console.error('Cart load failed:', error);
}
);
} else {
// 실패 처리 - retCode 확인 필요
console.error('Profile load failed, retCode:', response.data?.retCode);
}
},
(error) => {
console.error('Profile load failed:', error);
dispatch({ type: 'PUSH_PANEL', payload: { name: 'ERROR_PANEL' }});
}
);
}, 1000);
// 새로운 방식 (Promise 기반, 명확한 성공 기준)
console.log('✅ New way (Promise-based, clear success criteria):');
setTimeout(() => {
const newWayWorkflow = [
{
name: 'loadProfile',
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
// 자동 성공 기준 적용 (200 + retCode 0/'0')
const result = await fetchApi('/api/user/profile');
onSuccess(result);
},
onSuccess: (result) => {
if (result.success) {
dispatch(pushPanelQueued({ name: 'USER_PANEL' }));
}
}
},
{
name: 'loadCart',
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
const result = await fetchApi('/api/user/cart');
onSuccess(result);
},
onSuccess: (result) => {
if (result.success) {
dispatch(updatePanelQueued({
name: 'USER_PANEL',
panelInfo: { cart: result.data?.data }
}));
}
}
}
];
dispatch(createAsyncPanelSequence(newWayWorkflow));
}, 2000);
};

View File

@@ -0,0 +1,385 @@
/**
* [251106] 비동기 액션 유틸리티
*
* 이 파일은 Promise 기반의 비동기 액션 처리와 상세한 성공/실패 기준을 제공합니다.
* 프로젝트 특화된 성공 기준과 에러 처리 로직을 포함합니다.
*/
import { ERROR_MESSAGES_GROUPS } from './Config';
/**
* API 응답 성공 여부를 확인하는 함수
*
* 성공 기준:
* 1. HTTP 상태 코드가 200-299 범위여야 함
* 2. 응답 데이터의 retCode가 0 또는 '0'이어야 함
*
* @param {Response} response - fetch API 응답 객체
* @param {Object} responseData - 파싱된 응답 데이터
* @returns {boolean} 성공 여부
*/
export const isApiSuccess = (response, responseData) => {
// HTTP 상태 코드 확인 (200-299 성공 범위)
if (!response.ok || response.status < 200 || response.status >= 300) {
return false;
}
// retCode 확인 - 0 또는 '0'이어야 성공
if (responseData && responseData.retCode !== undefined) {
return responseData.retCode === 0 || responseData.retCode === '0';
}
// retCode가 없는 경우 HTTP 상태 코드만으로 판단
return response.ok;
};
/**
* API 에러 메시지를 가져오는 함수
* @param {number} code - 에러 코드
* @returns {string} 에러 메시지
*/
export const getApiErrorMessage = (code) => {
const errorGroup = ERROR_MESSAGES_GROUPS.find(group =>
group.codes && group.codes.includes(code)
);
return errorGroup ? errorGroup.message : `오류가 발생했습니다 (코드: ${code})`;
};
/**
* Promise 기반 fetch API 래퍼
* 프로젝트 성공 기준에 맞춰 응답을 처리합니다.
*
* @param {string} url - API URL
* @param {Object} options - fetch 옵션
* @returns {Promise<Object>} 응답 데이터를 포함한 Promise
*/
export const fetchApi = (url, options = {}) => {
console.log('[asyncActionUtils] 🌐 FETCH_API_START', { url, method: options.method || 'GET' });
return new Promise((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
}
});
});
});
};
/**
* TAxios 방식의 API 호출을 Promise로 변환
*
* @param {Function} TAxios - TAxios 함수
* @param {Function} dispatch - Redux dispatch
* @param {Function} getState - Redux getState
* @param {string} method - HTTP 메소드
* @param {string} baseUrl - 기본 URL
* @param {Object} urlParams - URL 파라미터
* @param {Object} params - 요청 파라미터
* @param {Object} options - 추가 옵션
* @returns {Promise<Object>} Promise 기반 응답
*/
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({
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
}
});
}
});
};
/**
* 비동기 액션을 Promise 기반으로 래핑하는 헬퍼 함수
* 모든 비동기 작업이 Promise를 반환하도록 보장합니다.
*
* @param {Function} asyncAction - 비동기 액션 함수
* @param {Object} context - 실행 컨텍스트 (dispatch, getState 등)
* @returns {Promise<Object>} Promise 기반 결과
*/
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);
// result의 구조를 표준화
const normalizedResult = {
response: result.response || result,
data: result.data || result,
success: true,
error: null
};
resolve(normalizedResult);
};
// 실패 콜백 - 항상 resolve 호출 (reject 하지 않음)
const onFail = (error) => {
console.error('[asyncActionUtils] ❌ WRAP_ASYNC_FAIL', error);
// error 객체를 표준화
const normalizedError = {
response: null,
data: null,
success: false,
error: {
code: error.retCode || error.code || 'ASYNC_ACTION_ERROR',
message: error.message || error.errorMessage || '비동기 작업에 실패했습니다',
originalError: error
}
};
resolve(normalizedError);
};
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);
}
});
};
/**
* 여러 비동기 액션을 병렬로 실행하고 모든 결과를 기다립니다.
*
* @param {Array} asyncActions - 비동기 액션 배열
* @param {Object} context - 실행 컨텍스트
* @returns {Promise<Array>} 모든 결과 배열
*/
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);
// Promise.all이 실패해도 빈 배열 반환
return [];
});
};
/**
* 비동기 액션 실행 결과를 검증하고 처리하는 헬퍼 함수
*
* @param {Object} result - 비동기 액션 실행 결과
* @param {Object} options - 처리 옵션
* @returns {Object} 처리된 결과
*/
export const processAsyncResult = (result, options = {}) => {
const {
throwOnError = false,
defaultErrorMessage = '작업에 실패했습니다',
logErrors = true
} = options;
if (result.success) {
console.log('[asyncActionUtils] 🎉 PROCESS_RESULT_SUCCESS');
return {
...result,
processed: true
};
}
const errorMessage = result.error?.message || defaultErrorMessage;
if (logErrors) {
console.error('[asyncActionUtils] ❌ PROCESS_RESULT_ERROR', {
code: result.error?.code,
message: errorMessage,
originalError: result.error?.originalError
});
}
const processedResult = {
...result,
processed: true,
errorMessage
};
if (throwOnError) {
throw new Error(errorMessage);
}
return processedResult;
};
/**
* 타임아웃이 있는 Promise 래퍼
*
* @param {Promise} promise - 원본 Promise
* @param {number} timeoutMs - 타임아웃 시간 (ms)
* @param {string} timeoutMessage - 타임아웃 메시지
* @returns {Promise} 타임아웃 적용된 Promise
*/
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);
})
]);
};
// 기본 export
export default {
fetchApi,
tAxiosToPromise,
wrapAsyncAction,
executeParallelAsyncActions,
processAsyncResult,
withTimeout,
isApiSuccess,
getApiErrorMessage
};

View File

@@ -0,0 +1,488 @@
/**
* [251106] 비동기 패널 액션 큐 사용 예시
*
* 이 파일은 API 호출이나 다른 비동기 작업이 포함된 패널 액션들을
* 어떻게 순차적으로 처리하는지 보여줍니다.
*/
import {
enqueueAsyncPanelAction,
createApiWithPanelActions,
createAsyncPanelSequence
} from '../actions/queuedPanelActions';
import {
pushPanelQueued,
popPanelQueued,
updatePanelQueued
} from '../actions/queuedPanelActions';
import { panel_names } from './Config';
/**
* 예시 1: TAxios API 호출 후 패널 액션 실행
*
* TAxios 방식의 API 호출을 큐 기반으로 처리합니다.
*/
export const tAxiosWithPanelExample = (dispatch, getState) => {
console.log('🎯 TAxios + Panel Actions Example');
dispatch(createApiWithPanelActions({
// TAxios 방식의 API 호출
apiCall: (dispatch, getState, onSuccess, onFail) => {
import('../api/TAxios').then(({ TAxios }) => {
TAxios(
dispatch,
getState,
'post', // method
'/api/search', // URL
{}, // URL params
{ query: 'product search' }, // request params
onSuccess, // 성공 콜백
onFail, // 실패 콜백
false, // noTokenRefresh
);
});
},
// API 성공 후 실행할 패널 액션들
panelActions: [
{ type: 'PUSH_PANEL', payload: { name: panel_names.SEARCH_PANEL } },
(response) => updatePanelQueued({
name: panel_names.SEARCH_PANEL,
panelInfo: {
searchResults: response.data.results,
query: response.data.query
}
})
],
// API 성공 콜백 (선택)
onApiSuccess: (response) => {
console.log('🎉 API Success:', response.data);
},
// API 실패 콜백 (선택)
onApiFail: (error) => {
console.error('💥 API Failed:', error);
}
}));
};
/**
* 예시 2: Redux Thunk 비동기 액션과 패널 조합
*
* Redux Thunk 방식의 비동기 액션을 큐 기반으로 처리합니다.
*/
export const thunkWithPanelExample = (dispatch, getState) => {
console.log('🔄 Thunk + Panel Actions Example');
dispatch(enqueueAsyncPanelAction({
// 비동기 액션 정의
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
try {
// Redux Thunk 방식의 비동기 작업
const response = await fetch('/api/products/123');
const data = await response.json();
if (response.ok) {
onSuccess(data);
} else {
onFail(new Error(data.message || 'API Error'));
}
} catch (error) {
onFail(error);
}
},
// 성공 콜백 - 패널 액션들 실행
onSuccess: (response) => {
console.log('✅ Thunk Success:', response);
// 성공 후 패널 액션들 순차 실행
dispatch(pushPanelQueued({ name: panel_names.DETAIL_PANEL }));
dispatch(updatePanelQueued({
name: panel_names.DETAIL_PANEL,
panelInfo: {
product: response.product,
isLoading: false
}
}));
},
// 실패 콜백
onFail: (error) => {
console.error('❌ Thunk Failed:', error);
// 에러 패널 표시
dispatch(pushPanelQueued({
name: panel_names.ERROR_PANEL,
panelInfo: {
error: error.message,
retryAction: () => thunkWithPanelExample(dispatch, getState)
}
}));
},
// 완료 콜백 (성공/실패 모두 호출)
onFinish: (isSuccess, result) => {
console.log('🏁 Thunk Complete:', { isSuccess, result });
if (isSuccess) {
// 성공했을 때만 추가적인 패널 액션
dispatch(pushPanelQueued({
name: panel_names.RECOMMENDATION_PANEL,
panelInfo: { productId: result.product.id }
}));
}
}
}));
};
/**
* 예시 3: 여러 비동기 액션들을 순차적으로 실행
*
* 복잡한 비동기 워크플로우를 순차적으로 처리합니다.
*/
export const complexAsyncSequenceExample = (dispatch, getState) => {
console.log('🔀 Complex Async Sequence Example');
const asyncActions = [
{
// 1단계: 사용자 정보 로드
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
try {
const response = await fetch('/api/user/profile');
const data = await response.json();
if (response.ok) {
onSuccess(data);
} else {
onFail(new Error('Failed to load user profile'));
}
} catch (error) {
onFail(error);
}
},
onSuccess: (response) => {
console.log('👤 User profile loaded:', response);
dispatch(updatePanelQueued({
name: panel_names.USER_PANEL,
panelInfo: { user: response.user }
}));
}
},
{
// 2단계: 장바구니 정보 로드 (1단계 성공 시에만 실행)
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
try {
const response = await fetch('/api/cart/items');
const data = await response.json();
if (response.ok) {
onSuccess(data);
} else {
onFail(new Error('Failed to load cart items'));
}
} catch (error) {
onFail(error);
}
},
onSuccess: (response) => {
console.log('🛒 Cart items loaded:', response);
dispatch(pushPanelQueued({
name: panel_names.CART_PANEL,
panelInfo: { items: response.items }
}));
}
},
{
// 3단계: 추천 상품 로드 (2단계 성공 시에만 실행)
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
try {
const response = await fetch('/api/recommendations');
const data = await response.json();
if (response.ok) {
onSuccess(data);
} else {
onFail(new Error('Failed to load recommendations'));
}
} catch (error) {
onFail(error);
}
},
onSuccess: (response) => {
console.log('⭐ Recommendations loaded:', response);
dispatch(updatePanelQueued({
name: panel_names.CART_PANEL,
panelInfo: { recommendations: response.recommendations }
}));
}
}
];
// 비동기 시퀀스 실행
dispatch(createAsyncPanelSequence(asyncActions));
};
/**
* 예시 4: 타임아웃과 에러 처리
*
* 타임아웃 설정과 상세한 에러 처리를 보여줍니다.
*/
export const timeoutAndErrorHandlingExample = (dispatch, getState) => {
console.log('⏰ Timeout and Error Handling Example');
dispatch(enqueueAsyncPanelAction({
// 5초 타임아웃 설정
timeout: 5000,
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
// 의도적으로 오래 걸리는 작업 시뮬레이션
await new Promise(resolve => setTimeout(resolve, 3000));
// 랜덤으로 성공/실패 시뮬레이션
if (Math.random() > 0.5) {
onSuccess({ message: 'Success after delay' });
} else {
onFail(new Error('Random failure occurred'));
}
},
onSuccess: (response) => {
console.log('🎉 Success with timeout handling:', response);
dispatch(pushPanelQueued({
name: panel_names.SUCCESS_PANEL,
panelInfo: { message: response.message }
}));
},
onFail: (error) => {
console.error('💥 Error with timeout handling:', error);
dispatch(pushPanelQueued({
name: panel_names.ERROR_PANEL,
panelInfo: {
error: error.message,
isTimeout: error.message.includes('timeout')
}
}));
},
onFinish: (isSuccess, result) => {
console.log('🏁 Operation completed:', {
isSuccess,
result,
timestamp: Date.now()
});
}
}));
};
/**
* 예시 5: 기존 방식과 새로운 방식의 비교
*
* 기존의 callback 방식과 새로운 큐 기반 방식을 비교합니다.
*/
export const comparisonExample = (dispatch, getState) => {
console.log('📊 Async Comparison Example');
// 기존 방식 (순서 보장되지 않음, 복잡한 콜백 체인)
console.log('❌ Old way (complex callbacks, no order guarantee):');
setTimeout(() => {
// API 호출
TAxios(
dispatch,
getState,
'get',
'/api/products',
{},
{},
(response) => {
// 성공 콜백
dispatch({ type: 'PUSH_PANEL', payload: { name: 'PRODUCT_PANEL' } });
dispatch({ type: 'UPDATE_PANEL', payload: {
name: 'PRODUCT_PANEL',
panelInfo: { products: response.data }
}});
// 추가 API 호출
TAxios(
dispatch,
getState,
'get',
'/api/recommendations',
{},
{},
(recResponse) => {
// 중첩 콜백
dispatch({ type: 'UPDATE_PANEL', payload: {
name: 'PRODUCT_PANEL',
panelInfo: { recommendations: recResponse.data }
}});
},
(error) => {
// 에러 처리
console.error('Recommendation load failed:', error);
}
);
},
(error) => {
// 에러 처리
console.error('Product load failed:', error);
dispatch({ type: 'PUSH_PANEL', payload: { name: 'ERROR_PANEL' }});
}
);
}, 1000);
// 새로운 방식 (순서 보장, 깔끔한 구조)
console.log('✅ New way (ordered, clean structure):');
setTimeout(() => {
const asyncActions = [
{
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
try {
const response = await fetch('/api/products');
const data = await response.json();
onSuccess(data);
} catch (error) {
onFail(error);
}
},
onSuccess: (response) => {
dispatch(pushPanelQueued({ name: 'PRODUCT_PANEL' }));
dispatch(updatePanelQueued({
name: 'PRODUCT_PANEL',
panelInfo: { products: response.data }
}));
}
},
{
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
try {
const response = await fetch('/api/recommendations');
const data = await response.json();
onSuccess(data);
} catch (error) {
onFail(error);
}
},
onSuccess: (response) => {
dispatch(updatePanelQueued({
name: 'PRODUCT_PANEL',
panelInfo: { recommendations: response.data }
}));
}
}
];
dispatch(createAsyncPanelSequence(asyncActions));
}, 2000);
};
/**
* 예시 6: 실제 프로젝트 적용 예시
*
* 실제 프로젝트에서 발생할 수 있는 시나리오를 보여줍니다.
*/
export const realWorldExample = (dispatch, getState, productId) => {
console.log('🌍 Real World Example - Product Detail Flow');
const productDetailFlow = [
{
// 1. 제품 상세 정보 로드
name: 'loadProductDetail',
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
try {
const response = await fetch(`/api/products/${productId}`);
const data = await response.json();
if (response.ok) {
onSuccess(data);
} else {
onFail(new Error(data.message || 'Product not found'));
}
} catch (error) {
onFail(error);
}
},
onSuccess: (response) => {
dispatch(pushPanelQueued({
name: panel_names.DETAIL_PANEL,
panelInfo: {
product: response.product,
isLoading: false
}
}));
},
onFail: (error) => {
dispatch(pushPanelQueued({
name: panel_names.ERROR_PANEL,
panelInfo: {
error: error.message,
retryCallback: () => realWorldExample(dispatch, getState, productId)
}
}));
}
},
{
// 2. 관련 상품 로드
name: 'loadRelatedProducts',
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
try {
const response = await fetch(`/api/products/${productId}/related`);
const data = await response.json();
if (response.ok) {
onSuccess(data);
} else {
onFail(new Error('Failed to load related products'));
}
} catch (error) {
onFail(error);
}
},
onSuccess: (response) => {
dispatch(updatePanelQueued({
name: panel_names.DETAIL_PANEL,
panelInfo: {
relatedProducts: response.products
}
}));
}
},
{
// 3. 재고 확인
name: 'checkStock',
asyncAction: async (dispatch, getState, onSuccess, onFail) => {
try {
const response = await fetch(`/api/products/${productId}/stock`);
const data = await response.json();
if (response.ok) {
onSuccess(data);
} else {
onFail(new Error('Failed to check stock'));
}
} catch (error) {
onFail(error);
}
},
onSuccess: (response) => {
dispatch(updatePanelQueued({
name: panel_names.DETAIL_PANEL,
panelInfo: {
stockInfo: response.stock,
canPurchase: response.stock.available > 0
}
}));
}
}
];
dispatch(createAsyncPanelSequence(productDetailFlow));
};

View File

@@ -0,0 +1,721 @@
/**
* [251106] Chrome 68 호환 비동기 패널 액션 예시
*
* 이 파일은 async/await 없이 Promise/.then 체이닝만 사용하여
* Chrome 68 호환성을 완벽하게 보장하는 예시들을 보여줍니다.
* TAxios는 그대로 사용하면서 성공 기준(200 + retCode 0/'0')을 적용합니다.
*/
import {
enqueueAsyncPanelAction,
createApiWithPanelActions,
createAsyncPanelSequence
} from '../actions/queuedPanelActions';
import {
pushPanelQueued,
popPanelQueued,
updatePanelQueued,
clearPanelQueue
} from '../actions/queuedPanelActions';
import { panel_names } from './Config';
/**
* 예시 1: TAxios Promise 래핑 (Chrome 68 호환)
*
* 기존 TAxios를 그대로 사용하면서 Promise 기반 제어
*/
export const compatibleTaxiosExample = (dispatch, getState, searchQuery) => {
console.log('🔄 Compatible TAxios Example');
dispatch(enqueueAsyncPanelAction({
asyncAction: (dispatch, getState, onSuccess, onFail) => {
// TAxios는 수정하지 않고 그대로 사용
import('../api/TAxios').then(({ TAxios }) => {
// TAxios를 Promise로 래핑하는 유틸리티 사용
import('../utils/asyncActionUtils').then(({ tAxiosToPromise }) => {
tAxiosToPromise(
TAxios, // 기존 TAxios (수정 없음)
dispatch, getState,
'post', // HTTP 메소드
'/api/search', // URL
{}, // URL params
{ query: searchQuery }, // request params
{ noTokenRefresh: false } // 옵션
)
.then(result => {
console.log('[CompatibleExample] 📊 TAxios Result:', {
retCode: result.data?.retCode,
success: result.success,
hasData: !!result.data?.data
});
onSuccess(result);
})
.catch(error => {
// tAxiosToPromise은 reject하지 않지만, 혹시 모를 예외 처리
onSuccess({
response: null,
data: null,
success: false,
error: {
code: 'TAXIOS_WRAPPER_ERROR',
message: error.message || 'TAxios 래퍼 오류'
}
});
});
}).catch(error => {
// 유틸리티 import 실패
console.error('[CompatibleExample] ❌ Utils Import Error:', error);
onSuccess({
response: null,
data: null,
success: false,
error: {
code: 'UTILS_IMPORT_ERROR',
message: '유틸리티 import 실패'
}
});
});
}).catch(error => {
// TAxios import 실패
console.error('[CompatibleExample] ❌ TAxios Import Error:', error);
onSuccess({
response: null,
data: null,
success: false,
error: {
code: 'TAXIOS_IMPORT_ERROR',
message: 'TAxios import 실패'
}
});
});
},
onSuccess: (result) => {
if (result.success) {
// HTTP 200 + retCode 0/'0' 성공
console.log('[CompatibleExample] ✅ Search Success:', result.data?.data);
dispatch(pushPanelQueued({
name: panel_names.SEARCH_PANEL,
panelInfo: {
query: searchQuery,
results: result.data?.data || [],
isLoading: false
}
}));
} else {
// retCode가 0이 아닌 경우 (504, 6xx, 9xx 등)
console.error('[CompatibleExample] ❌ Search Failed:', {
code: result.error?.code,
message: result.error?.message,
httpStatus: result.error?.httpStatus
});
dispatch(pushPanelQueued({
name: panel_names.ERROR_PANEL,
panelInfo: {
error: result.error?.message || '검색에 실패했습니다',
errorCode: result.error?.code,
canRetry: true,
retryAction: () => compatibleTaxiosExample(dispatch, getState, searchQuery)
}
}));
}
},
onFail: (error) => {
console.error('[CompatibleExample] 💥 Critical Error:', error);
dispatch(pushPanelQueued({
name: panel_names.ERROR_PANEL,
panelInfo: {
error: '치명적인 오류가 발생했습니다',
isSystemError: true
}
}));
},
timeout: 10000 // 10초 타임아웃
}));
};
/**
* 예시 2: 순차적 TAxios 호출 체이닝
*
* 여러 TAxios 호출을 순차적으로 실행하고 각 결과에 따라 패널 조작
*/
export const sequentialTaxiosChaining = (dispatch, getState, userId) => {
console.log('🔗 Sequential TAxios Chaining');
dispatch(clearPanelQueue()); // 큐 정리
const workflow = [
{
name: 'loadUserProfile',
asyncAction: (dispatch, getState, onSuccess, onFail) => {
// Promise 체이닝으로 순차적 처리
Promise.all([
import('../api/TAxios'),
import('../utils/asyncActionUtils')
])
.then(([{ TAxios }, { tAxiosToPromise }]) => {
return tAxiosToPromise(
TAxios, dispatch, getState, 'get',
'/api/user/profile',
{ userId }, {}, {}
);
})
.then(result => {
console.log('[Sequential] 👤 Profile Loaded:', {
retCode: result.data?.retCode,
success: result.success
});
onSuccess(result);
})
.catch(error => {
console.error('[Sequential] ❌ Profile Load Error:', error);
onSuccess({
response: null,
data: null,
success: false,
error: {
code: 'PROFILE_LOAD_ERROR',
message: '프로필 로드 실패'
}
});
});
},
onSuccess: (result) => {
if (result.success) {
dispatch(pushPanelQueued({
name: panel_names.USER_PANEL,
panelInfo: {
user: result.data?.data,
step: 'profile_loaded'
}
}));
} else {
// 실패 시 체이닝 중단
dispatch(pushPanelQueued({
name: panel_names.ERROR_PANEL,
panelInfo: {
error: result.error?.message || '프로필 로드 실패',
stopChain: true
}
}));
}
}
},
{
name: 'loadCartItems',
asyncAction: (dispatch, getState, onSuccess, onFail) => {
Promise.all([
import('../api/TAxios'),
import('../utils/asyncActionUtils')
])
.then(([{ TAxios }, { tAxiosToPromise }]) => {
return tAxiosToPromise(
TAxios, dispatch, getState, 'get',
'/api/user/cart',
{ userId }, {}, {}
);
})
.then(result => {
console.log('[Sequential] 🛒 Cart Loaded:', {
retCode: result.data?.retCode,
itemCount: result.data?.data?.items?.length
});
onSuccess(result);
})
.catch(error => {
console.error('[Sequential] ❌ Cart Load Error:', error);
onSuccess({
response: null,
data: null,
success: false,
error: {
code: 'CART_LOAD_ERROR',
message: '장바구니 로드 실패'
}
});
});
},
onSuccess: (result) => {
if (result.success) {
dispatch(updatePanelQueued({
name: panel_names.USER_PANEL,
panelInfo: {
cartItems: result.data?.data?.items || [],
step: 'completed'
}
}));
} else {
// 장바구니 로드 실패는 사용자 패널은 유지
dispatch(updatePanelQueued({
name: panel_names.USER_PANEL,
panelInfo: {
cartError: result.error?.message,
step: 'cart_failed'
}
}));
}
}
}
];
// 순차적 워크플로우 실행
dispatch(createAsyncPanelSequence(workflow));
};
/**
* 예시 3: 병렬 TAxios 호출
*
* 여러 API를 동시에 호출하고 모두 완료되면 결과를 종합
*/
export const parallelTaxiosCalls = (dispatch, getState, productId) => {
console.log('🚀 Parallel TAxios Calls');
dispatch(enqueueAsyncPanelAction({
asyncAction: (dispatch, getState, onSuccess, onFail) => {
// 필요한 모듈 import
Promise.all([
import('../api/TAxios'),
import('../utils/asyncActionUtils')
])
.then(([{ TAxios }, { tAxiosToPromise, executeParallelAsyncActions }]) => {
// 병렬로 실행할 TAxios 호출들
const parallelActions = [
// 제품 상세 정보
(dispatch, getState, success, fail) => {
tAxiosToPromise(
TAxios, dispatch, getState, 'get',
'/api/products/detail',
{ productId }, {}, {}
).then(result => success(result))
.catch(error => success({
response: null,
data: null,
success: false,
error: { code: 'DETAIL_ERROR', message: error.message }
}));
},
// 관련 상품
(dispatch, getState, success, fail) => {
tAxiosToPromise(
TAxios, dispatch, getState, 'get',
'/api/products/related',
{ productId }, {}, {}
).then(result => success(result))
.catch(error => success({
response: null,
data: null,
success: false,
error: { code: 'RELATED_ERROR', message: error.message }
}));
},
// 리뷰 정보
(dispatch, getState, success, fail) => {
tAxiosToPromise(
TAxios, dispatch, getState, 'get',
'/api/products/reviews',
{ productId }, {}, {}
).then(result => success(result))
.catch(error => success({
response: null,
data: null,
success: false,
error: { code: 'REVIEWS_ERROR', message: error.message }
}));
}
];
// 병렬 실행
return executeParallelAsyncActions(parallelActions, { dispatch, getState });
})
.then(results => {
console.log('[Parallel] 📊 All Results:', {
total: results.length,
success: results.filter(r => r.success).length,
failed: results.filter(r => !r.success).length
});
// 결과 종합
const combinedResult = {
response: { status: 200 },
data: {
detail: results[0].data,
related: results[1].data,
reviews: results[2].data,
errors: results.filter(r => !r.success).map(r => ({
endpoint: ['detail', 'related', 'reviews'][results.indexOf(r)],
error: r.error
}))
},
success: results.every(r => r.success),
error: results.some(r => !r.success) ? {
code: 'PARTIAL_FAILURE',
message: '일부 정보 로드 실패'
} : null
};
onSuccess(combinedResult);
})
.catch(error => {
console.error('[Parallel] 💥 Parallel Execution Error:', error);
onSuccess({
response: null,
data: null,
success: false,
error: {
code: 'PARALLEL_EXECUTION_ERROR',
message: '병렬 실행 실패'
}
});
});
},
onSuccess: (result) => {
if (result.success) {
// 모든 API 성공
dispatch(pushPanelQueued({
name: panel_names.DETAIL_PANEL,
panelInfo: {
product: result.data.detail?.data,
relatedProducts: result.data.related?.data?.items || [],
reviews: result.data.reviews?.data?.reviews || [],
isLoading: false
}
}));
} else {
// 부분 실패 - 성공한 데이터는 표시
dispatch(pushPanelQueued({
name: panel_names.DETAIL_PANEL,
panelInfo: {
product: result.data.detail?.data,
relatedProducts: result.data.related?.data?.items || [],
reviews: result.data.reviews?.data?.reviews || [],
hasErrors: true,
errors: result.data.errors,
isLoading: false
}
}));
}
},
timeout: 15000 // 15초 타임아웃 (병렬 호출이므로 더 길게)
}));
};
/**
* 예시 4: 재시도 로직 (Promise 체이닝)
*
* 실패 시 자동 재시도 - Promise 체이닝으로 구현
*/
export const retryWithPromiseChaining = (dispatch, getState, productId) => {
console.log('🔄 Retry with Promise Chaining');
const maxRetries = 3;
let retryCount = 0;
const attemptLoad = () => {
retryCount++;
console.log(`[Retry] 📡 Attempt ${retryCount}/${maxRetries}`);
dispatch(enqueueAsyncPanelAction({
asyncAction: (dispatch, getState, onSuccess, onFail) => {
Promise.all([
import('../api/TAxios'),
import('../utils/asyncActionUtils')
])
.then(([{ TAxios }, { tAxiosToPromise, withTimeout }]) => {
const apiPromise = tAxiosToPromise(
TAxios, dispatch, getState, 'get',
'/api/products/detail',
{ productId }, {}, {}
);
// 타임아웃 적용
return withTimeout(apiPromise, 3000, '제품 정보 로드 시간 초과');
})
.then(result => {
console.log('[Retry] 📊 Load Result:', {
success: result.success,
errorCode: result.error?.code,
retryCount
});
// 항상 resolve
onSuccess(result);
})
.catch(error => {
console.error('[Retry] 💥 Load Error:', error);
// 예외도 resolve로 처리
onSuccess({
response: null,
data: null,
success: false,
error: {
code: 'LOAD_EXCEPTION',
message: error.message || '로드 중 예외 발생'
}
});
});
},
onSuccess: (result) => {
if (result.success) {
// 성공 시 패널 표시
console.log('[Retry] ✅ Load Success after', retryCount, 'attempts');
dispatch(pushPanelQueued({
name: panel_names.DETAIL_PANEL,
panelInfo: {
product: result.data?.data,
source: 'api',
retryCount
}
}));
} else {
console.error('[Retry] ❌ Load Failed:', result.error);
if (retryCount < maxRetries) {
// 재시도
console.log(`[Retry] 🔄 Retrying in 1 second... (${retryCount}/${maxRetries})`);
setTimeout(() => {
attemptLoad();
}, 1000);
} else {
// 최대 재시고 도달 - 폴백
console.log('[Retry] 📦 Max retries reached, using fallback');
dispatch(pushPanelQueued({
name: panel_names.DETAIL_PANEL,
panelInfo: {
product: {
id: productId,
name: '제품 정보 로드 실패',
placeholder: true
},
source: 'fallback',
error: result.error?.message,
retryCount,
canRetry: true,
retryAction: () => {
retryCount = 0; // 재시도 카운트 초기화
attemptLoad();
}
}
}));
}
}
},
timeout: 5000 // 각 시도 타임아웃
}));
};
// 첫 번째 시도
attemptLoad();
};
/**
* 예시 5: 실제 비즈니스 시나리오 - 제품 검색 및 상세 보기
*
* 검색 → 상제 선택 → 상세 로드 → 관련 상품 로드
*/
export const productSearchToDetailFlow = (dispatch, getState, searchQuery) => {
console.log('🔍 Product Search to Detail Flow');
// 1단계: 검색
dispatch(enqueueAsyncPanelAction({
asyncAction: (dispatch, getState, onSuccess, onFail) => {
Promise.all([
import('../api/TAxios'),
import('../utils/asyncActionUtils')
])
.then(([{ TAxios }, { tAxiosToPromise }]) => {
return tAxiosToPromise(
TAxios, dispatch, getState, 'post',
'/api/search',
{}, {},
{ query: searchQuery, limit: 20 }
);
})
.then(result => {
console.log('[SearchFlow] 🔍 Search Results:', {
retCode: result.data?.retCode,
resultCount: result.data?.data?.results?.length
});
onSuccess(result);
})
.catch(error => {
onSuccess({
response: null,
data: null,
success: false,
error: { code: 'SEARCH_ERROR', message: error.message }
});
});
},
onSuccess: (result) => {
if (result.success && result.data?.data?.results?.length > 0) {
// 검색 성공 - 결과 패널 표시
dispatch(pushPanelQueued({
name: panel_names.SEARCH_RESULTS_PANEL,
panelInfo: {
query: searchQuery,
results: result.data.data.results,
isLoading: false
}
}));
// 첫 번째 결과에 대한 상세 정보 자동 로드
const firstProduct = result.data.data.results[0];
loadProductDetail(dispatch, getState, firstProduct.id);
} else {
// 검색 결과 없음
dispatch(pushPanelQueued({
name: panel_names.SEARCH_RESULTS_PANEL,
panelInfo: {
query: searchQuery,
results: [],
isEmpty: true,
message: '검색 결과가 없습니다'
}
}));
}
},
timeout: 8000
}));
};
// 제품 상세 로드 함수 (Promise 체이닝)
const loadProductDetail = (dispatch, getState, productId) => {
console.log('[SearchFlow] 📦 Loading Product Detail:', productId);
dispatch(enqueueAsyncPanelAction({
asyncAction: (dispatch, getState, onSuccess, onFail) => {
Promise.all([
import('../api/TAxios'),
import('../utils/asyncActionUtils')
])
.then(([{ TAxios }, { tAxiosToPromise }]) => {
return tAxiosToPromise(
TAxios, dispatch, getState, 'get',
'/api/products/detail',
{ productId }, {}, {}
);
})
.then(result => {
console.log('[SearchFlow] 📦 Product Detail Loaded:', {
retCode: result.data?.retCode,
success: result.success
});
onSuccess(result);
})
.catch(error => {
onSuccess({
response: null,
data: null,
success: false,
error: { code: 'DETAIL_LOAD_ERROR', message: error.message }
});
});
},
onSuccess: (result) => {
if (result.success) {
// 상세 정보 성공 - 상세 패널 표시
dispatch(pushPanelQueued({
name: panel_names.DETAIL_PANEL,
panelInfo: {
product: result.data?.data,
isLoading: false
}
}));
// 관련 상품 로드 (배경에서)
loadRelatedProducts(dispatch, getState, productId);
} else {
// 상세 정보 로드 실패
dispatch(updatePanelQueued({
name: panel_names.SEARCH_RESULTS_PANEL,
panelInfo: {
detailError: result.error?.message,
selectedProductId: productId
}
}));
}
},
timeout: 5000
}));
};
// 관련 상품 로드 함수
const loadRelatedProducts = (dispatch, getState, productId) => {
console.log('[SearchFlow] 🔗 Loading Related Products:', productId);
dispatch(enqueueAsyncPanelAction({
asyncAction: (dispatch, getState, onSuccess, onFail) => {
Promise.all([
import('../api/TAxios'),
import('../utils/asyncActionUtils')
])
.then(([{ TAxios }, { tAxiosToPromise }]) => {
return tAxiosToPromise(
TAxios, dispatch, getState, 'get',
'/api/products/related',
{ productId }, {}, {}
);
})
.then(result => {
console.log('[SearchFlow] 🔗 Related Products Loaded:', {
retCode: result.data?.retCode,
count: result.data?.data?.items?.length
});
onSuccess(result);
})
.catch(error => {
onSuccess({
response: null,
data: null,
success: false,
error: { code: 'RELATED_LOAD_ERROR', message: error.message }
});
});
},
onSuccess: (result) => {
if (result.success) {
// 관련 상품 성공 - 상세 패널 업데이트
dispatch(updatePanelQueued({
name: panel_names.DETAIL_PANEL,
panelInfo: {
relatedProducts: result.data?.data?.items || []
}
}));
}
// 관련 상품 실패는 무시 (메인 기능에 영향 없음)
},
timeout: 3000 // 더 짧은 타임아웃 (부가 기능)
}));
};