본문 바로가기
React

Portal과 Compound 패턴으로 Modal 만들기

by oagree0123 2025. 4. 29.

지난 글에서는 Select 컴포넌트를 Compound Component 패턴으로 관리하는 방법을 소개했다. 이번에는 이 패턴을 Modal 컴포넌트에도 적용해보려고 한다.

Modal은 프로젝트 어디에서나 자주 사용된다. 그래서 더더욱 잘 구조화해서 만들어두는 게 중요하다. 이번 글에서는 Portal을 이용하고, Compound Component 패턴을 적용해 Modal을 공통 컴포넌트로 깔끔하게 만드는 방법을 정리해본다.

Modal을 만들 때 고려한 기본 원칙은 세 가지였다:

  1. Portal을 이용해 Modal을 body 바깥에 띄운다.
  2. useModalStore 같은 전역 상태 훅을 사용해 열고 닫을 수 있게 한다.
  3. Compound Component 패턴으로 구조를 나눠 자유롭게 조합할 수 있게 한다.

왜 Portal을 사용했을까?

Modal은 항상 시각적으로 화면 최상단에 떠야 하고, 다른 레이아웃 요소들의 영향을 받아서는 안 된다. Portal을 사용하면 다음과 같은 장점이 있다:

  • 레이아웃 간섭이 없다: 모달이 컴포넌트 트리 어디에 있든 body에 직접 붙어 렌더링된다.
  • 스타일 충돌 최소화: 부모 요소의 CSS(position, overflow 등) 영향을 받지 않는다.
  • 구조적 분리: 렌더링 구조를 논리 구조와 분리해 유지보수가 쉬워진다.

Portal 없이 모달을 구현하면 부모의 overflow: hidden 같은 스타일에 의해 모달이 잘리거나 스크롤이 막히는 문제가 생길 수 있다. 그래서 Portal은 Modal에서 사실상 필수다.


왜 상태 관리를 전역으로 했을까?

Modal은 다양한 위치, 다양한 이벤트에서 열릴 수 있어야 한다. 매번 컴포넌트마다 상태를 따로 관리하면 관리 포인트가 늘어나고 일관성도 떨어진다.

그래서 Modal의 열림/닫힘 상태를 전역 상태로 관리했다.

상태 관리 구조 예시:

// useModalStore.ts
import { create } from 'zustand';

type ModalState = {
  isOpen: boolean;
  modalProps: Partial<ModalProps>;
};

const initialState: ModalState = {
  isOpen: false,
  modalProps: {},
};

export const useModalStore = create<ModalState>(() => ({
  ...initialState,
}));

export const modalActions = {
  openModal: (props: Partial<ModalProps>) =>
    useModalStore.setState({ isOpen: true, modalProps: props }),
  closeModal: () =>
    useModalStore.setState({ isOpen: false, modalProps: {} }),
};

사용 예시:

modalActions.openModal({
  title: '삭제 확인',
  description: '삭제하면 복구할 수 없습니다.',
  onConfirm: handleDelete,
});

 

어디서든 modalActions.openModal()을 호출하면 모달을 열 수 있고, 상태는 전역으로 일관되게 관리된다.


Modal 기본 구조

Portal과 전역 상태를 이용해 기본적인 Modal 구조를 만들었다. 핵심은 다음과 같다:

  • Portal을 통해 #portal DOM에 렌더링
  • 모달 외부를 클릭하면 닫히고, 내부 클릭은 이벤트 버블링을 막는다

이렇게 하면 사용자 경험을 깔끔하게 유지할 수 있다.


Compound Component로 구조 나누기

Modal은 상황에 따라 다양한 모습으로 사용된다.

  • 제목이 있을 수도, 없을 수도 있고
  • 버튼이 하나일 수도, 두 개일 수도 있으며
  • 본문에는 단순 텍스트뿐 아니라 입력 폼, 이미지 등 다양한 콘텐츠가 들어갈 수 있다

이를 위해 Modal을 다음과 같이 쪼갰다:

컴포넌트 역할

Modal.Root Portal을 통한 전체 구조
Modal.Header 제목을 표시하는 영역
Modal.Body 본문을 표시하는 영역
Modal.Footer 버튼 영역 (Confirm/Cancel)

필요한 조합만 골라서 사용할 수 있다. 예를 들면:

<Modal>
  <Modal.Header title="정말 삭제하시겠습니까?" />
  <Modal.Body>
    삭제하면 복구할 수 없습니다.
  </Modal.Body>
  <Modal.Footer
    onConfirm={handleDelete}
    onCancel={modalActions.closeModal}
    confirmText="삭제"
    cancelText="취소"
  />
</Modal>

복잡한 Modal도 쉽게 만들 수 있다

Compound 패턴을 적용하면 입력폼이 들어간 복잡한 Modal도 쉽게 만들 수 있다.

<Modal>
  <Modal.Header title="회원가입" />
  <Modal.Body>
    <input type="text" placeholder="이름 입력" />
    <input type="email" placeholder="이메일 입력" />
  </Modal.Body>
  <Modal.Footer
    onConfirm={handleSubmit}
    confirmText="가입하기"
    type="alert"
  />
</Modal>

Body 영역 안에는 자유롭게 폼, 이미지, 설명 등을 넣을 수 있고, Modal 컴포넌트 자체를 수정할 필요가 없다.


왜 Compound Component 패턴을 썼을까?

Modal을 하나의 컴포넌트로 만들면 다양한 옵션을 props로 관리해야 해서 컴포넌트가 비대해지고, 조건 분기 로직이 복잡해진다.

반면 Compound 패턴은:

  • 필요한 조합만 골라서 깔끔하게 사용 가능
  • 레이아웃과 동작을 자유롭게 조합 가능
  • 유지보수성과 확장성 향상

특히 Modal처럼 다양한 변형이 필요한 UI에서는 Compound 패턴의 이점이 매우 크다.


마무리

Modal 컴포넌트를 Portal과 Compound Component 패턴을 활용해 구조화하면서 느낀 점은 이렇다.

다양한 형태와 사용 방법을 지원해야 하는 컴포넌트라면, Compound Component 패턴은 충분히 고려할 가치가 있다.

 

모든 상황에 무조건 적용해야 하는 것은 아니지만, Modal처럼 확장 가능성과 변형 가능성이 중요한 컴포넌트에는
초기에 이런 구조를 잡아두는 것이 장기적으로 큰 도움이 된다.

지금 프로젝트를 돌아보면서, 특정 UI를 더 유연하고 확장성 있게 관리하고 싶다면
Compound Component 패턴을 한 번 고려해보는 것도 좋은 방법일 것이다.

댓글