홈으로 돌아가기

React Modal Stack: IT 아키텍처

이 기사는 React 애플리케이션을 위한 타입화된 모달 스택 아키텍처를 설명합니다. Redux 슬라이스, 오버레이 훅, 소스 래퍼를 통해 DOM 중복 문제, prop drilling, 종속성을 해결합니다. 코드 예제와 확장 전략을 제공합니다.

복잡한 UI를 위한 React 타입화된 모달 스택
Advertisement 728x90

확장 가능한 앱을 위한 React 모달 스택 아키텍처

복잡한 React 앱에서 모달은 금세 골칫거리로 변합니다. 기본 useState로 구현한 단일 모달은 독립적으로는 잘 작동하지만, 프로젝트가 커지면 중복 컴포넌트, prop drilling, 얽힌 상태 의존성이 생깁니다. 해결책? 트리거 소스별로 로직을 분리하는 타입화된 오버레이 스택입니다.

전형적인 마켓플레이스 관리자 대시보드의 상품 테이블을 생각해보세요. 각 행마다 IMEI, SGTIN 같은 식별자 추가 모달이 필요하고, 주문 테이블, 유효성 검사, Excel 업로드, 다단계 마법사까지 포함됩니다.

로컬 렌더링의 문제점

테이블 행마다 IdentificationModal을 렌더링하면 수천 개의 숨겨진 DOM 요소가 생성됩니다. 이 컴포넌트는 테이블, 마스킹 입력, RTK Query 훅, 스테퍼로 가득 차 있어 메모리 사용량이 폭증합니다.

Google AdInline article slot

나쁜 예시:

const ProductRow = ({ product }) => {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <tr>
      <td>{product.name}</td>
      <td>
        <Button onClick={() => setIsOpen(true)}>식별자 추가</Button>
        <IdentificationModal
          isOpen={isOpen}
          onClose={() => setIsOpen(false)}
          productId={product.id}
        />
      </td>
    </tr>
  );
};

핵심 포인트: 모달은 페이지 최상위에서 한 번만 렌더링해야 합니다.

Prop Drilling과 전역 상태

모달을 트리 상위로 이동하면 DOM 팽창은 해결되지만, 4~5개 컴포넌트 레이어를 거쳐 콜백을 전달해야 합니다:

Google AdInline article slot
  • ProductsPageProductsTableTableBodyProductRowActionsMenu

Redux에서 openIdent(productId) 디스패치로 prop을 없앨 수 있지만, 여러 컨텍스트에서 복잡해집니다.

다중 데이터 소스

모달은 서로 다른 API를 가진 세 곳에서 데이터를 가져옵니다:

| 트리거 소스 | 주문 | 데이터 로딩 | Excel 템플릿 |

Google AdInline article slot

|-------------|------|-------------|--------------|

| 테이블 헤더 | 전체 주문 | ordersApi.getIdentOrders() | ordersApi.getExcel() |

| 테이블 행 | 단일 주문 | orderId로 필터링 | ordersApi.getExcel() |

| 장바구니 | 장바구니 주문 | cartsApi.getCartIdentOrders(cartId) | cartsApi.getExcel() |

모달에 로직을 몰아넣으면 if문, 캐시 무효화, 분석으로 가득 찬 God Object가 됩니다. 반대로 순수 UI 모달에 ordersonSave prop만 주면 페이지가 컨텍스트를 미리 알 수 없어 실패합니다.

모달 의존성

식별자 모달을 열면 벌크 액션 패널이 숨겨지고, 닫으면 다시 나타납니다. "저장되지 않은 변경사항이 있어요?" 확인 모달을 추가하면 플래그를 수동으로 관리해야 합니다:

openConfirmClose: (state) => {
  state.isConfirmOpen = true;
  state.isIdentOpen = false;
},
cancelConfirmClose: (state) => {
  state.isConfirmOpen = false;
  state.isIdentOpen = true;
},

평면적인 플래그 구조는 스택 순서나 롤백 전략을 담지 못합니다.

해결 아키텍처: 타입화된 스택

이 시스템은 세 가지 문제를 한 번에 해결합니다:

  • 단일 렌더링을 위한 Redux 스택과 오버레이 큐.
  • 컨텍스트 분리를 위한 소스 래퍼.
  • 자동 의존성 관리를 위한 스택 규칙.

스택용 Redux Slice

export enum EOverlayName {
  BulkActions = 'bulkActions',
  IdentFromHeader = 'identFromHeader',
  IdentFromRow = 'identFromRow',
  IdentFromCart = 'identFromCart',
  EditProduct = 'editProduct',
  // ...
}

type OverlayItem<TData = unknown> = {
  name: EOverlayName;
  data: TData;
  priority?: number;
};

type OverlayStack = OverlayItem[];

스택은 타입화된 데이터와 순서를 유지합니다. 비동기 액션으로 페이로드와 함께 오버레이를 푸시합니다.

트리거용 오버레이 훅

const useOverlay = () => {
  const dispatch = useDispatch();

  const open = <TData>(name: EOverlayName, data: TData) => {
    dispatch(openOverlay({ name, data }));
  };

  const close = (name: EOverlayName) => {
    dispatch(closeOverlay(name));
  };

  return { open, close };
};

행 버튼: open('identFromRow', { productId, orderId }). 모달은 스택에서 읽어 prop drilling 없음.

컨텍스트용 소스 래퍼

각 소스는 어댑터를 export:

// identFromRow.adapter.ts
export const useIdentFromRowAdapter = () => {
  const { open } = useOverlay();

  const openIdent = (productId: string, orderId: string) => {
    const orders = ordersApi.useGetIdentOrdersQuery();
    const filtered = orders.data?.filter(o => o.id === orderId);
    open('identFromRow', { orders: filtered, productId });
  };

  return { openIdent };
};

모달은 소스를 모르고 준비된 ordersonSave를 받습니다.

열기/닫기 전략

스택이 규칙을 강제합니다:

  • 교체: ident 열기 시 bulkActions 닫기.
  • 일시정지: 닫으면 이전 오버레이 재개.
  • 초기화: 확인 시 스택 리셋.
const openOverlay = createAsyncThunk(
  'overlay/open',
  async ({ name, data }, { getState }) => {
    const stack = selectStack(getState());
    // 교체/우선순위 로직
    return { name, data };
  }
);

주요 이점

  • 스택이 prop drilling, DOM 중복, 상태 의존성을 한 아키텍처로 제거.
  • 소스 래퍼가 API 로직 격리; 모달은 순수 UI 유지.
  • OverlayItem<TData> 타입으로 데이터 안전성 보장.
  • 비동기 전략으로 열기/닫기 순서 자동화.
  • 리듀서 팽창 없이 수십 개 모달 확장 가능.

— Editorial Team

Advertisement 728x90

다음 읽기