## 코드 수정 - store.js에 panelQueueMiddleware import 및 등록 - 큐 기반 패널 액션 시스템이 작동하기 위해 필수 - applyMiddleware에 panelQueueMiddleware 추가 ## 문서 수정 1. README.md - "설치 및 설정" 섹션 추가 - panelQueueMiddleware 등록 필수 사항 명시 2. 04-solution-queue-system.md - "사전 요구사항" 섹션 추가 - 미들웨어 등록 코드 예제 포함 3. 05-usage-patterns.md - "초기 설정 확인사항" 체크리스트 추가 - panelQueueMiddleware 등록 여부 확인 항목 추가 ## 이유 - panelQueueMiddleware가 등록되지 않으면 큐 시스템이 작동하지 않음 - ENQUEUE_PANEL_ACTION dispatch 시 자동으로 PROCESS_PANEL_QUEUE가 실행되지 않음 - 문서에 설정 방법이 명확하지 않아 사용자가 놓칠 수 있음 ## 관련 파일 - src/store/store.js (수정) - .docs/dispatch-async/README.md (수정) - .docs/dispatch-async/04-solution-queue-system.md (수정) - .docs/dispatch-async/05-usage-patterns.md (수정)
16 KiB
16 KiB
해결 방법 3: 큐 기반 패널 액션 시스템
📦 개요
관련 파일:
src/actions/queuedPanelActions.jssrc/middleware/panelQueueMiddleware.jssrc/reducers/panelReducer.jssrc/store/store.js(미들웨어 등록 필요)
작성일: 2025-11-06 커밋:
5bd2774 [251106] feat: Queued Panel functionsf9290a1 [251106] fix: Dispatch Queue implementation
미들웨어 기반의 액션 큐 처리 시스템으로, 패널 액션들을 순차적으로 실행합니다.
⚠️ 사전 요구사항
큐 시스템을 사용하려면 반드시 store에 panelQueueMiddleware를 등록해야 합니다.
파일: src/store/store.js
import panelQueueMiddleware from '../middleware/panelQueueMiddleware';
export const store = createStore(
rootReducer,
applyMiddleware(thunk, panelHistoryMiddleware, autoCloseMiddleware, panelQueueMiddleware)
);
미들웨어를 등록하지 않으면 큐에 액션이 추가되어도 자동으로 처리되지 않습니다!
🎯 핵심 개념
왜 큐 시스템이 필요한가?
패널 관련 액션들은 특히 순서가 중요합니다:
// 문제 상황
dispatch(pushPanel({ name: 'SEARCH' })); // 검색 패널 열기
dispatch(updatePanel({ results: [...] })); // 검색 결과 업데이트
dispatch(popPanel('LOADING')); // 로딩 패널 닫기
// 실제 실행 순서 (문제!)
// → popPanel이 먼저 실행될 수 있음
// → updatePanel이 pushPanel보다 먼저 실행될 수 있음
큐 시스템의 동작 방식
[큐에 추가] → [미들웨어 감지] → [순차 처리] → [완료]
↓ ↓ ↓ ↓
ENQUEUE 자동 감지 시작 PROCESS_QUEUE 다음 액션
🔑 주요 컴포넌트
1. queuedPanelActions.js
패널 액션을 큐에 추가하는 액션 크리에이터들
2. panelQueueMiddleware.js
큐에 액션이 추가되면 자동으로 처리를 시작하는 미들웨어
3. panelReducer.js
큐 상태를 관리하는 리듀서
📋 기본 패널 액션
1. pushPanelQueued
패널을 큐에 추가하여 순차적으로 열기
import { pushPanelQueued } from '../actions/queuedPanelActions';
// 기본 사용
dispatch(pushPanelQueued(
{ name: panel_names.SEARCH_PANEL },
false // duplicatable
));
// 중복 허용
dispatch(pushPanelQueued(
{ name: panel_names.PRODUCT_DETAIL, productId: 123 },
true // 중복 허용
));
2. popPanelQueued
패널을 큐를 통해 제거
import { popPanelQueued } from '../actions/queuedPanelActions';
// 마지막 패널 제거
dispatch(popPanelQueued());
// 특정 패널 제거
dispatch(popPanelQueued(panel_names.SEARCH_PANEL));
3. updatePanelQueued
패널 정보를 큐를 통해 업데이트
import { updatePanelQueued } from '../actions/queuedPanelActions';
dispatch(updatePanelQueued({
name: panel_names.SEARCH_PANEL,
panelInfo: {
results: [...],
totalCount: 100
}
}));
4. resetPanelsQueued
모든 패널을 초기화
import { resetPanelsQueued } from '../actions/queuedPanelActions';
// 빈 패널로 초기화
dispatch(resetPanelsQueued());
// 특정 패널들로 초기화
dispatch(resetPanelsQueued([
{ name: panel_names.HOME }
]));
5. enqueueMultiplePanelActions
여러 패널 액션을 한 번에 큐에 추가
import { enqueueMultiplePanelActions, pushPanelQueued, updatePanelQueued, popPanelQueued }
from '../actions/queuedPanelActions';
dispatch(enqueueMultiplePanelActions([
pushPanelQueued({ name: panel_names.SEARCH_PANEL }),
updatePanelQueued({ name: panel_names.SEARCH_PANEL, panelInfo: { query: 'test' } }),
popPanelQueued(panel_names.LOADING_PANEL)
]));
🚀 비동기 패널 액션
1. enqueueAsyncPanelAction
비동기 작업(API 호출 등)을 큐에 추가하여 순차 실행
파일: src/actions/queuedPanelActions.js:173-199
import { enqueueAsyncPanelAction } from '../actions/queuedPanelActions';
dispatch(enqueueAsyncPanelAction({
id: 'search_products_123', // 고유 ID
// 비동기 액션 (TAxios 등)
asyncAction: (dispatch, getState, onSuccess, onFail) => {
TAxios(
dispatch,
getState,
'post',
URLS.SEARCH_PRODUCTS,
{},
{ keyword: 'test' },
onSuccess,
onFail
);
},
// 성공 콜백
onSuccess: (response) => {
console.log('검색 성공:', response);
dispatch(pushPanelQueued({
name: panel_names.SEARCH_RESULT,
results: response.data.results
}));
},
// 실패 콜백
onFail: (error) => {
console.error('검색 실패:', error);
dispatch(pushPanelQueued({
name: panel_names.ERROR,
message: error.message
}));
},
// 완료 콜백 (성공/실패 모두 호출)
onFinish: (isSuccess, result) => {
console.log('검색 완료:', isSuccess ? '성공' : '실패');
},
// 타임아웃 (ms)
timeout: 10000 // 10초
}));
동작 흐름
1. enqueueAsyncPanelAction 호출
↓
2. ENQUEUE_ASYNC_PANEL_ACTION dispatch
↓
3. executeAsyncAction 자동 실행
↓
4. wrapAsyncAction으로 Promise 래핑
↓
5. withTimeout으로 타임아웃 적용
↓
6. 결과에 따라 onSuccess 또는 onFail 호출
↓
7. COMPLETE_ASYNC_PANEL_ACTION 또는 FAIL_ASYNC_PANEL_ACTION dispatch
🔗 API 호출 후 패널 액션
createApiWithPanelActions
API 호출 후 여러 패널 액션을 자동으로 실행
파일: src/actions/queuedPanelActions.js:355-394
import { createApiWithPanelActions } from '../actions/queuedPanelActions';
dispatch(createApiWithPanelActions({
// API 호출
apiCall: (dispatch, getState, onSuccess, onFail) => {
TAxios(
dispatch,
getState,
'post',
URLS.SEARCH_PRODUCTS,
{},
{ keyword: 'laptop' },
onSuccess,
onFail
);
},
// API 성공 후 실행할 패널 액션들
panelActions: [
// Plain action
{ type: 'PUSH_PANEL', payload: { name: panel_names.SEARCH_PANEL } },
// Dynamic action (response 사용)
(response) => updatePanelQueued({
name: panel_names.SEARCH_PANEL,
panelInfo: {
results: response.data.results,
totalCount: response.data.totalCount
}
}),
// 또 다른 패널 액션
popPanelQueued(panel_names.LOADING_PANEL)
],
// API 성공 콜백
onApiSuccess: (response) => {
console.log('API 성공:', response.data.totalCount, '개 검색됨');
},
// API 실패 콜백
onApiFail: (error) => {
console.error('API 실패:', error);
dispatch(pushPanelQueued({
name: panel_names.ERROR,
message: '검색에 실패했습니다'
}));
}
}));
사용 예제: 상품 검색
export const searchProducts = (keyword) =>
createApiWithPanelActions({
apiCall: (dispatch, getState, onSuccess, onFail) => {
TAxios(
dispatch,
getState,
'post',
URLS.SEARCH_PRODUCTS,
{},
{ keyword },
onSuccess,
onFail
);
},
panelActions: [
// 1. 로딩 패널 닫기
popPanelQueued(panel_names.LOADING_PANEL),
// 2. 검색 결과 패널 열기
(response) => pushPanelQueued({
name: panel_names.SEARCH_RESULT,
results: response.data.results
}),
// 3. 검색 히스토리 업데이트
(response) => updatePanelQueued({
name: panel_names.SEARCH_HISTORY,
panelInfo: { lastSearch: keyword }
})
],
onApiSuccess: (response) => {
console.log(`${response.data.totalCount}개의 상품을 찾았습니다`);
}
});
🔄 비동기 액션 시퀀스
createAsyncPanelSequence
여러 비동기 액션을 순차적으로 실행
파일: src/actions/queuedPanelActions.js:401-445
import { createAsyncPanelSequence } from '../actions/queuedPanelActions';
dispatch(createAsyncPanelSequence([
// 첫 번째 비동기 액션
{
asyncAction: (dispatch, getState, onSuccess, onFail) => {
TAxios(dispatch, getState, 'get', URLS.GET_USER_INFO, {}, {}, onSuccess, onFail);
},
onSuccess: (response) => {
console.log('사용자 정보 조회 성공');
dispatch(pushPanelQueued({
name: panel_names.USER_INFO,
userInfo: response.data.data
}));
},
onFail: (error) => {
console.error('사용자 정보 조회 실패:', error);
}
},
// 두 번째 비동기 액션 (첫 번째 완료 후 실행)
{
asyncAction: (dispatch, getState, onSuccess, onFail) => {
const userInfo = getState().user.info;
TAxios(
dispatch,
getState,
'get',
URLS.GET_CART,
{},
{ mbrNo: userInfo.mbrNo },
onSuccess,
onFail
);
},
onSuccess: (response) => {
console.log('카트 정보 조회 성공');
dispatch(updatePanelQueued({
name: panel_names.USER_INFO,
panelInfo: { cartCount: response.data.data.length }
}));
},
onFail: (error) => {
console.error('카트 정보 조회 실패:', error);
}
},
// 세 번째 비동기 액션 (두 번째 완료 후 실행)
{
asyncAction: (dispatch, getState, onSuccess, onFail) => {
TAxios(dispatch, getState, 'get', URLS.GET_ORDERS, {}, {}, onSuccess, onFail);
},
onSuccess: (response) => {
console.log('주문 정보 조회 성공');
dispatch(pushPanelQueued({
name: panel_names.ORDER_LIST,
orders: response.data.data
}));
},
onFail: (error) => {
console.error('주문 정보 조회 실패:', error);
// 실패 시 시퀀스 중단
}
}
]));
동작 흐름
Action 1 실행 → 성공? → Action 2 실행 → 성공? → Action 3 실행
↓ ↓
실패 시 실패 시
중단 중단
⚙️ 미들웨어: panelQueueMiddleware
동작 원리
파일: src/middleware/panelQueueMiddleware.js
const panelQueueMiddleware = (store) => (next) => (action) => {
const result = next(action);
// 큐에 액션이 추가되면 자동으로 처리 시작
if (action.type === types.ENQUEUE_PANEL_ACTION) {
console.log('[panelQueueMiddleware] 🚀 ACTION_ENQUEUED', {
action: action.payload.action,
queueId: action.payload.id,
});
// setTimeout을 사용하여 현재 액션이 완전히 처리된 후에 큐 처리 시작
setTimeout(() => {
const currentState = store.getState();
if (currentState.panels) {
// 이미 처리 중이 아니고 큐에 액션이 있으면 처리 시작
if (!currentState.panels.isProcessingQueue &&
currentState.panels.panelActionQueue.length > 0) {
console.log('[panelQueueMiddleware] ⚡ STARTING_QUEUE_PROCESS');
store.dispatch({ type: types.PROCESS_PANEL_QUEUE });
}
}
}, 0);
}
// 큐 처리가 완료되고 남은 큐가 있으면 계속 처리
if (action.type === types.PROCESS_PANEL_QUEUE) {
setTimeout(() => {
const currentState = store.getState();
if (currentState.panels) {
// 처리 중이 아니고 큐에 남은 액션이 있으면 계속 처리
if (!currentState.panels.isProcessingQueue &&
currentState.panels.panelActionQueue.length > 0) {
console.log('[panelQueueMiddleware] 🔄 CONTINUING_QUEUE_PROCESS');
store.dispatch({ type: types.PROCESS_PANEL_QUEUE });
}
}
}, 0);
}
return result;
};
주요 특징
- ✅ 자동 시작: 큐에 액션 추가 시 자동으로 처리 시작
- ✅ 연속 처리: 한 액션 완료 후 자동으로 다음 액션 처리
- ✅ 중복 방지: 이미 처리 중이면 새로 시작하지 않음
- ✅ 로깅: 모든 단계에서 로그 출력
📊 리듀서 상태 구조
panelReducer.js의 큐 관련 상태
{
panels: [], // 실제 패널 스택
lastPanelAction: 'push', // 마지막 액션 타입
// 큐 관련 상태
panelActionQueue: [ // 처리 대기 중인 큐
{
id: 'queue_item_1_1699999999999',
action: 'PUSH_PANEL',
panel: { name: 'SEARCH_PANEL' },
duplicatable: false,
timestamp: 1699999999999
},
// ...
],
isProcessingQueue: false, // 큐 처리 중 여부
queueError: null, // 큐 처리 에러
queueStats: { // 큐 통계
totalProcessed: 0, // 총 처리된 액션 수
failedCount: 0, // 실패한 액션 수
averageProcessingTime: 0 // 평균 처리 시간 (ms)
},
// 비동기 액션 상태
asyncActions: { // 실행 중인 비동기 액션들
'async_action_1': {
id: 'async_action_1',
status: 'pending', // 'pending' | 'success' | 'failed'
timestamp: 1699999999999
}
},
completedAsyncActions: [ // 완료된 액션 ID들
'async_action_1',
'async_action_2'
],
failedAsyncActions: [ // 실패한 액션 ID들
'async_action_3'
]
}
🎯 실제 사용 시나리오
시나리오 1: 검색 플로우
export const performSearch = (keyword) => (dispatch) => {
// 1. 로딩 패널 열기
dispatch(pushPanelQueued({ name: panel_names.LOADING }));
// 2. 검색 API 호출 후 결과 표시
dispatch(createApiWithPanelActions({
apiCall: (dispatch, getState, onSuccess, onFail) => {
TAxios(dispatch, getState, 'post', URLS.SEARCH, {}, { keyword }, onSuccess, onFail);
},
panelActions: [
popPanelQueued(panel_names.LOADING),
(response) => pushPanelQueued({
name: panel_names.SEARCH_RESULT,
results: response.data.results
})
]
}));
};
시나리오 2: 다단계 결제 프로세스
export const processCheckout = (orderInfo) =>
createAsyncPanelSequence([
// 1단계: 주문 검증
{
asyncAction: (dispatch, getState, onSuccess, onFail) => {
TAxios(dispatch, getState, 'post', URLS.VALIDATE_ORDER, {}, orderInfo, onSuccess, onFail);
},
onSuccess: () => {
dispatch(updatePanelQueued({
name: panel_names.CHECKOUT,
panelInfo: { step: 1, status: 'validated' }
}));
}
},
// 2단계: 결제 처리
{
asyncAction: (dispatch, getState, onSuccess, onFail) => {
TAxios(dispatch, getState, 'post', URLS.PROCESS_PAYMENT, {}, orderInfo, onSuccess, onFail);
},
onSuccess: (response) => {
dispatch(updatePanelQueued({
name: panel_names.CHECKOUT,
panelInfo: { step: 2, paymentId: response.data.data.paymentId }
}));
}
},
// 3단계: 주문 확정
{
asyncAction: (dispatch, getState, onSuccess, onFail) => {
const state = getState();
const paymentId = state.panels.panels.find(p => p.name === panel_names.CHECKOUT)
.panelInfo.paymentId;
TAxios(
dispatch,
getState,
'post',
URLS.CONFIRM_ORDER,
{},
{ ...orderInfo, paymentId },
onSuccess,
onFail
);
},
onSuccess: (response) => {
dispatch(popPanelQueued(panel_names.CHECKOUT));
dispatch(pushPanelQueued({
name: panel_names.ORDER_COMPLETE,
orderId: response.data.data.orderId
}));
}
}
]);
✅ 장점
- 완벽한 순서 보장: 큐 시스템으로 100% 순서 보장
- 자동 처리: 미들웨어가 자동으로 큐 처리
- 비동기 지원: API 호출 등 비동기 작업 완벽 지원
- 타임아웃: 응답 없는 작업 자동 처리
- 에러 복구: 에러 발생 시에도 다음 액션 계속 처리
- 통계: 큐 처리 통계 자동 수집
⚠️ 주의사항
- 미들웨어 등록: store에 panelQueueMiddleware 등록 필요
- 리듀서 확장: panelReducer에 큐 관련 상태 추가 필요
- 기존 코드: 기존 pushPanel 등과 병행 사용 가능
다음: 사용 패턴 및 예제 →