Frontend/React

React Design Pattern - Compound Component Pattern

rachel_13 2024. 3. 30. 21:11

Compopund Component 패턴이란

 

부모 컴포넌트의 속성, 로직을 자식 컴포넌트와 공유할 때 사용하는 리액트 패턴이다.

주로 Select 박스, 드롭다운 메뉴, Modal 에서 자주 사용된다.

 

React Context API를 사용하게 되는데, Context API는 Provider에서 값이나 속성들을 정의하고, 하위 컴포넌트에서 이를 공유해서 재사용할 수 있도록 만들어진 구조이다. 

보통은 부모에서 props 로 자식에게 속성들을 전달하는 것이 일반적인데, 부모와 자식 사이의 간격이 너무 멀어질 경우, props를 계속해서 하위 컴포넌트로 넘겨줘야 하는 불편함이 발생한다.(props drilling)

 

그래서, 이러한 것들을 개선하기 위해 나온 개념으로, props로 자식에게 넘기지 않고도 하위에서 필요한 데이터들을 전달할 수 있다.

컨텍스트를 사용하면 상위 구성 요소가 그 아래의 전체 트리에 데이터를 제공할 수 있게 된다.

 

출처 : react.dev

 


Context API 사용법 짚고 넘어가기

 

  1. createContext

createContext(defaultValue)

 

- 컨텍스트를 생성한다.

- 컨텍스트 객체를 반환한다.

- 상위 컴포넌트에서 컨텍스트.Provider를 이용해서 값을 지정하고, 하위 컴포넌트에서 useContext(컨텍스트)를 호출해 그 값을 읽는다.

  이렇게 하면, props를 일일히 넘기지 않아도 멀리 떨어진 컨텍스트에서 부모의 속성들을 불러올 수 있게 된다.

import { createContext } from 'react';

const CounterContext = createContext();

 

  • Provider : 컨텍스트에 값/속성을 제공한다.
  • value : provider 내에서 컨텍스트를 읽는 모든 자식 컴포넌트들에게 전달하고자 하는 값이다.
                useContext(컨텍스트)로 호출하는 컴포넌트는 이 value 값을 받을 수 있다.
function Counter({ children }) {
  const [count, setCount] = useState(0);
  const increase = () => setCount((c) => c + 1);
  const decrease = () => setCount((c) => c - 1);

  return (
    <CounterContext.Provider value={{ count, increase, decrease }}>
      <span>{children}</span>
    </CounterContext.Provider>
  );
}

 

  2. useContext

useContext(컨텍스트)

 

- 컨텍스트를 읽는다.

- 컨텍스트 값을 반환한다. (컨텍스트.Provider에서 제공된 value를 반환한다.)

function Count() {
  const { count } = useContext(CounterContext);
  return <span>{count}</span>;
}

 


자 그럼, 다시 본론으로 돌아와서

 

Compound Component 패턴을 구현해보자

 

"Compound"의 사전적 의미는 복합적인, 혼합의 라는 뜻이다.

Compound Component Pattern 이란, 부모 컴포넌트 안에 관련 자식 컴포넌트를 캡슐화하여 재사용 가능하고 유연한 컴포넌트를 만들 수 있는 강력한 패턴이다.

 

즉, 부모의 어떤 상태를 자식에서 복합적으로 사용하고 싶을 때, 사용자가 컴포넌트와 상호작용할 수 있도록 깔끔하고 직관적인 API를 제공하면서 구현 세부 사항은 숨기는 것(캡슐화)이 핵심이다.

 

 

예제를 통해서 해당 패턴에 대해 더 자세히 알아보기로 한다.

 

가장 범용적인 예시인 모달을 구현해서 compound 패턴을 적용하기 전/후로 나눠보려고 한다.

 

[Before]

먼저, 일반적으로 만들 수 있는 모달 형태와, 모달을 사용하는 컴포넌트이다.

 

- 모달 구현부

function Modal({children, onClose}) {
  return <StyledModal onClick={onClose}>
      <button onClick={onClose}>X</button>
      <div>{children}</div>
    </StyledModal>;
}

 

- 모달 사용부

export default function App() {
  const [isOpenModal, setOpenModal] = useState(false);

  return (
      <StyledModalWrapper>
        <button onClick={() => setOpenModal(!isOpenModal)}>Open Modal</button>
        {isOpenModal && <Modal onClose={() => setOpenModal(false)}>
          <Form onClose={() => setOpenModal(false)}/>
        </Modal>}
      </StyledModalWrapper>
  );
}

 

 

🚀 여기서의 문제점은 무엇일까?

 

먼저 App 컴포넌트의 isOpenModal을 유심히 보자.

 

Modal이 닫혀있는지 열려있는지의 여부를 isOpenModal이라는 state 값으로 결정한다.

그리고 이 state는 사용부인 App 컴포넌트에서 관리되고 있다. (tracking)

 

사실, 이는 굉장히 비효율적이다.

App 컴포넌트는 Modal이 오픈되었는지 여부에 대해 관심이 없다.

몰라도 되는 속성을 단지 Modal을 보여주기 위한 용도로 state를 관리해야 하는 책임을 진다.

 

그렇다면 Modal 자체에서 상태를 관리하고 App 컴포넌트(사용부)에서는 Modal의 상태에 대해 몰라도 되는 것이 더 타당해 보인다.

즉 Modal 의 open 여부는 캡슐화 되는 것이 더 좋다.

 

자 그래서, 다시한번 결론내리자면

Modal이 보여지는지 말지는 App 컴포넌트에 관리하면 안되고, Modal 컴포넌트에서 현재 오픈되었는지 아닌지를 스스로 알고 있어야 한다. 그리고 opened 여부와 관련된 state도 내부적으로 관리되어야 할 것이다.

 

✅ Modal 컴포넌트의 역할 

- 내부적으로 캡슐화된 state 를 관리하고 추적한다.

- 모달을 on/off 할 수 있는 방법과 모달 내부의 콘텐츠를 표시하기 위한 방법을 제공해야 한다.

- 일반적으로, 버튼을 누르면 모달창이 열리니까 버튼, 모달 창 이 컴포넌트와 함께 관리되어야 한다.

 

 

[After]

- 모달 구현부

const ModalContext = createContext();

function Modal({children}) {
  const [openName, setOpenName] = useState('');

  const open = setOpenName;
  const close = () => setOpenName('');

  return (<ModalContext.Provider value={{openName, open, close}}>
    {children}
  </ModalContext.Provider>);
}

function Open({children, opens}) {
  const {open} = useContext(ModalContext);
  return cloneElement(children, {onClick: () => open(opens)});
}

function Window({children, name}) {
  const {openName, close} = useContext(ModalContext);

  if(name !== openName) return null;
  return createPortal(<Overlay onClick={close}>
    <StyledModal>
      <button onClick={close}>x</button>
      <div>{children}</div>
    </StyledModal>
  </Overlay>, document.body);
}

Modal.Open = Open;
Modal.Window = Window;

export default Modal;

 

 

- 모달 사용부

      <Modal>
        <Modal.Open opens={'name-form'}>
          <button>Open NameModal</button>
        </Modal.Open>
        <Modal.Window name={'name-form'}>
          <NameForm/>
        </Modal.Window>
      </Modal>

 

 

사용부가 훨씬 깔끔해진 걸 볼 수 있다.

 

 

 

 

 

 

 

 

* References

https://www.patterns.dev/react/compound-pattern/

https://react-ko.dev/reference/react/useContext

https://react-ko.dev/reference/react/createContext

https://react.dev/learn/passing-data-deeply-with-context

The Ultimate React Course 2024: React, Redux & More

 

 

 

 

 

 

 

 

 

 

 

React.createPortal( )

모달을 사용할 때 일반적인 HTML 구조를 살펴보면 다음과 같다. 페이지의 DOM 구조 내부에 모달이 있다. 그런데, portal을 사용하게 되면, DOM 구조 밖에 이 모달을 위치시킬 수 있다. 사용 방법 1. creat

rachelslab.tistory.com