[React] Context API를 이용한 합성 컴포넌트 만들기 - 컴파운드 패턴

728x90

컴파운드 패턴이란?

 

Compound Pattern

Create multiple components that work together to perform a single task

www.patterns.dev

컴파운드 컴포넌트(Compound Component) 패턴은 서로 관계가 있는 여러 개의 컴포넌트가 하나의 상위 컴포넌트를 통해 연결되고 이 상위 컴포넌트가 공통된 상태나 메서드를 제공한다. 사용자는 상위 컴포넌트만 사용하면서도 하위 컴포넌트들이 자연스럽게 상호작용하도록 할 수 있다. 보통 셀렉트 박스나 드롭다운 컴포넌트, 메뉴 항목과 같은 곳에서 이런 패턴을 볼 수 있다. 컴파운드 컴포넌트 패턴은 이러한 컴포넌트들이 함께 작동하여 특정 작업을 수행할 수 있게 해준다.

 

컴파운드 패턴 구현 예시

컴파운드 패턴 Flyout

 

 

언제 어떻게 컴파운드 컴포넌트 패턴을 사용하나?

리액트로 사이드 프로젝트를 진행중에 모달을 만들어야 하는데 문득 든 생각이 있다.

"이거 그냥 반복되는 모양에 하위 컴포넌트만 바뀌는 구조인데  하나씩 다 컴포넌트로 만들어야 하나..?"

그래서 리액트 디자인 패턴에 대해 찾던 중 컴포넌트 패턴에 대해 알게 되었고 현재 프로젝트에 적용해보기 좋다고 생각했다.

아래 내용들을 보면 언제 사용하면 좋을지 간단하게 살펴볼 수 있다.

  • 여러 하위 컴포넌트가 하나의 상위 컴포넌트에 의존해야 하는 경우
  • 상위 컴포넌트가 특정 상태나 함수를 제공하여 하위 컴포넌트에서 필요로 할 때
  • UI 구성 요소가 밀접하게 연결된 경우 (예: Tab, Dropdown, Modal 등)

 

Context API와 컴파운드 패턴의 결합

 

createContext – React

The library for web and native user interfaces

react.dev

먼저 컴파운드 패턴은 컴포넌트 내부에서 데이터를 처리하기 위해 Context API를 사용한다.

Context API는 부모와 자식 간의 깊은 관계에서 props를 계속 전달하는 대신 전역으로 상태를 관리하여 여러 컴포넌트에서 쉽게 공유할 수 있도록 도와준다. Context API와 컴파운드 패턴을 결합하면 상태 관리가 수월해지고 가독성이 높아져 유지보수가 편리해진다.

 

컴파운드 모달 컴포넌트 만들기

modal.jsx

import { createContext, useState, useContext } from 'react';
import { createPortal } from 'react-dom';

const ModalContext = createContext();

const Modal = ({ children }) => {
  const [isOpen, setIsOpen] = useState(false);

  const openModal = () => setIsOpen(true);

  const closeModal = () => setIsOpen(false);

  return (
    <ModalContext.Provider value={{ isOpen, openModal, closeModal }}>
      {children}
    </ModalContext.Provider>
  );
};

const useModal = () => useContext(ModalContext);

const OpenButton = ({ children }) => {
  const { openModal } = useModal();
  return (
    <button
      onClick={openModal}
      className='block text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 rounded-lg text-sm px-6 py-2.5 shadow-md transition duration-300'
      type='button'
    >
      {children}
    </button>
  );
};

const ModalPortal = ({ children }) => {
  const element = document.getElementById('modal-root');
  return createPortal(children, element);
};

const Content = ({ children }) => {
  const { isOpen } = useModal();

  return (
    isOpen && (
      <div className='fixed inset-0 flex items-center justify-center bg-black bg-opacity-60 z-50'>
        <div className='bg-white rounded-lg shadow-lg w-80 md:w-96 p-6 text-gray-800 relative'>
          {children}
        </div>
      </div>
    )
  );
};

const CloseButton = ({ children }) => {
  const { closeModal } = useModal();

  return (
    <button
      onClick={closeModal}
      className=' text-white bg-red-600 hover:bg-red-700 focus:ring-4 focus:ring-red-300 rounded-lg text-sm px-5 py-2.5 transition duration-300'
    >
      {children}
    </button>
  );
};

const SubmitButton = ({ children }) => {
  const { closeModal } = useModal();

  return (
    <button
      className='text-white bg-green-600 hover:bg-green-700 focus:ring-4 focus:ring-green-300 rounded-lg text-sm px-5 py-2.5 shadow-md transition duration-300'
      onClick={() => {
        closeModal();
      }}
    >
      {children}
    </button>
  );
};

const ModalTitle = ({ children }) => {
  return <h2 className='text-xl font-semibold mb-4 text-center'>{children}</h2>;
};

// Modal 컴포넌트에 자식 컴포넌트 추가
Modal.OpenButton = OpenButton;
Modal.Content = Content;
Modal.ModalPortal = ModalPortal;
Modal.CloseButton = CloseButton;
Modal.ModalTitle = ModalTitle;
Modal.SubmitButton = SubmitButton;

export { Modal, useModal };

 

App.jsx

import { Modal } from "./components/modal";
import "./styles.css";

export default function App() {
  return (
    <div className="App">
      <h2>합성 컴포넌트로 모달 만들기</h2>
      <Modal>
        <Modal.OpenButton>모달1</Modal.OpenButton>
        <Modal.ModalPortal>
          <Modal.Content>
            <div>데이터 1입니다. 정말 삭제하시겠습니까?</div>
            <Modal.CloseButton>취소</Modal.CloseButton>
            <Modal.SubmitButton>확인</Modal.SubmitButton>
          </Modal.Content>
        </Modal.ModalPortal>
      </Modal>
      <Modal>
        <Modal.OpenButton>모달2</Modal.OpenButton>
        <Modal.ModalPortal>
          <Modal.Content>
            <div>데이터 2입니다. 정말 삭제하시겠습니까?</div>
            <Modal.CloseButton>취소</Modal.CloseButton>
            <Modal.SubmitButton>확인</Modal.SubmitButton>
          </Modal.Content>
        </Modal.ModalPortal>
      </Modal>
    </div>
  );
}

 

index.jsx

프로젝트에서 모달을 DOM 트리 구조를 다른 위치에 렌더링할 수 있게 하기 위해 ReactDOM.createPortal을 사용해 "modal-root"에 모달을 렌더링한다.

포탈이 필요한 이유
React 애플리케이션에서는 주로 모달, 툴팁, 드롭다운 같은 컴포넌트가 부모 컴포넌트 밖에서 렌더링되어야 할 때가 많다.
이런 컴포넌트들은 레이아웃과 관계없이 화면의 최상위에 위치하여 다른 요소들에 가려지지 않고 독립적으로 보여야 하기 때문에 모달을 사용한다.
<!doctype html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>모달 만들기 - 컴파운드</title>
</head>

<body>
  <div id="modal-root"></div>
  <div id="root"></div>
  <script type="module" src="/src/main.jsx"></script>
</body>

</html>

 

컴파운드 패턴 Modal

위와같이 구현하고 원하는 디자인으로 미리 스타일을 지정 후 자식 요소만 추가하면 된다.

 

참고문헌

 

Compound Pattern

Create multiple components that work together to perform a single task

www.patterns.dev

 

React Hooks: Compound Components

Stay up to date Stay up to date All rights reserved © Kent C. Dodds 2024

kentcdodds.com

 

 

728x90