ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리액트 디자인 패턴] Compound Component Pattern (합성 컴포넌트 패턴) 알아보기
    Frontend 2022. 7. 24. 20:10

    https://unsplash.com/photos/QiWOG1MA9rE

     

     

    이 글은 Compound Pattern과 관련된 글의 한글 번역입니다.

     

    원문 링크: https://www.patterns.dev/posts/compound-pattern/

     

    Compound Pattern

    Create multiple components that work together to perform a single task

    www.patterns.dev

     

     


     

    Compound Pattern

    어플리케이션에서 우리는 종종 서로에게 속한 컴포넌트를 가지게 된다. 이 컴포넌트들은 공통의 상태를 통해 서로에게 의존되어 있고, 로직을 함께 공유한다. 여러분은 종종 select, dropdown 컴포넌트 또는 메뉴 아이템들에서 이러한 형태를 볼 수 있다. Compound component pattern은 task를 수행하기 위해 다 같이 함께 작동하는 컴포넌트를 생성할 수 있도록 해준다.

     

     


     

    Context API

    다음 예제를 살펴보자: 다람쥐 이미지 목록이 있다! 단순히 다람쥐 이미지들을 보여주는 것 외에, 유저가 이미지를 수정하거나 삭제할 수 있도록 버튼을 추가하려고 한다. 우리는 유저가 컴포넌트를 토글했을 때, 목록을 보여주는 FlyOut 컴포넌트를 사용할 수 있다.

     

     

     

     

    FlyOut 컴포넌트는 내부에 기본적으로 다음 3가지를 가지고 있다:

    • 토글 버튼과 List를 가지고 있는 FlyOut wrapper
    • List를 토글하는 Toggle 버튼
    • 메뉴 아이템을 가지고 있는 List

     

    Compound component pattern을 리액트의 Context API와 사용하는 것은 이 예제에 완벽하게 들어맞는다!

     

     

    먼저, FlyOut 컴포넌트를 생성해보자. 이 컴포넌트는 상태를 가지고 있고, 모든 children에게 전달 할 toggle value를 가지고 있는 FlyOutProvider를 리턴한다.

     

    const FlyOutContext = createContext();
    
    function FlyOut(props) {
      const [open, toggle] = useState(false);
    
      const providerValue = { open, toggle };
    
      return (
        <FlyOutContext.Provider value={providerValue}>
          {props.children}
        </FlyOutContext.Provider>
      );
    }
    

     

    우리는 이제 자식에게 opentoggle이라는 value를 전달할 수 있는 stateful한 FlyOut 컴포넌트를 가지게 된다.

     

     

     

    이제 Toggle 컴포넌트를 만들어보자. 이 컴포넌트는 단순히 유저가 메뉴를 토글하기 위해 클릭할 수 있는 컴포넌트를 렌더한다.

     

    function Toggle() {
      const { open, toggle } = useContext(FlyOutContext);
    
      return (
        <div onClick={() => toggle(!open)}>
          <Icon />
        </div>
      );
    }
    

     

    ToggleFlyOutContext provider에 실제로 접근할 수 있도록 만들려면, 이 컴포넌트를 FlyOut의 자식 컴포넌트로 렌더해야 한다!

    물론 단순히 자식 컴포넌트로 렌더할 수도 있다.

     

     

    그러나 Toggle 컴포넌트를 FlyOut 컴포넌트의 속성으로 만들 수도 있다!

     

    const FlyOutContext = createContext();
    
    function FlyOut(props) {
      const [open, toggle] = useState(false);
    
      return (
        <FlyOutContext.Provider value={{ open, toggle }}>
          {props.children}
        </FlyOutContext.Provider>
      );
    }
    
    function Toggle() {
      const { open, toggle } = useContext(FlyOutContext);
    
      return (
        <div onClick={() => toggle(!open)}>
          <Icon />
        </div>
      );
    }
    
    FlyOut.Toggle = Toggle;
    

     

    이 말인즉슨 만약 우리가 FlyOut을 어떤 파일이든 사용하려고 할 때, 우리는 오직 FlyOut만을 import하면 된다는 뜻이다!

     

     

    import React from "react";
    import { FlyOut } from "./FlyOut";
    
    export default function FlyoutMenu() {
      return (
        <FlyOut>
          <FlyOut.Toggle />
        </FlyOut>
      );
    }
    

     

    토글만 있는 것은 충분하지 않다.

     

     

    open이라는 value를 바탕으로 열리고 닫히며, list 아이템을 가진 List 또한 필요하다.

     

    function List({ children }) {
      const { open } = React.useContext(FlyOutContext);
      return open && <ul>{children}</ul>;
    }
    
    function Item({ children }) {
      return <li>{children}</li>;
    }
    

     

    List 컴포넌트는 open이라는 값이 true인지 false인지에 따라 자식 컴포넌트를 렌더한다.

     

     

    Toggle 컴포넌트에 했듯이 List와 Item 또한 FlyOut 컴포넌트의 속성으로 만들자.

     

    const FlyOutContext = createContext();
    
    function FlyOut(props) {
      const [open, toggle] = useState(false);
    
      return (
        <FlyOutContext.Provider value={{ open, toggle }}>
          {props.children}
        </FlyOutContext.Provider>
      );
    }
    
    function Toggle() {
      const { open, toggle } = useContext(FlyOutContext);
    
      return (
        <div onClick={() => toggle(!open)}>
          <Icon />
        </div>
      );
    }
    
    function List({ children }) {
      const { open } = useContext(FlyOutContext);
      return open && <ul>{children}</ul>;
    }
    
    function Item({ children }) {
      return <li>{children}</li>;
    }
    
    FlyOut.Toggle = Toggle;
    FlyOut.List = List;
    FlyOut.Item = Item;
    

     

    이제 이것들을 FlyOut 컴포넌트의 속성으로 사용할 수 있다!

    이 예제에서 우리는 유저에게 두 가지 옵션을 주려고 한다: EditDelete이다.

     

     

    FlyOut.List를 생성하여 두 개의 FlyOut.Item 컴포넌트를 렌더해보자.

    하나는 Edit 옵션을, 그리고 나머지 하나는 Delete 옵션을 말이다.

     

    import React from "react";
    import { FlyOut } from "./FlyOut";
    
    export default function FlyoutMenu() {
      return (
        <FlyOut>
          <FlyOut.Toggle />
          <FlyOut.List>
            <FlyOut.Item>Edit</FlyOut.Item>
            <FlyOut.Item>Delete</FlyOut.Item>
          </FlyOut.List>
        </FlyOut>
      );
    }
    

     

    완벽하다! 우리는 방금 어떠한 상태도 FlyOutMenu에 추가하지 않고, 전체 FlyOut 컴포넌트를 만들었다!

     

    Compound pattern은 여러분이 컴포넌트 라이브러리를 구축하는데 도움이 된다.

    종종 이 패턴을 Semantic UI와 같은 UI 라이브러리를 사용할 때 볼 수 있을 것이다.

     


    React.Children.map

     

    우리는 또한 Compound Component pattern을 해당 컴포넌트의 자식을 mapping하는 방식으로 적용할 수도 있다. 해당 컴포넌트를 추가적인 props와 함께 복제함으로써 opentoggle 속성을 추가할 수 있다.

     

    export function FlyOut(props) {
      const [open, toggle] = React.useState(false);
    
      return (
        <div>
          {React.Children.map(props.children, child =>
            React.cloneElement(child, { open, toggle })
          )}
        </div>
      );
    }

     

    모든 자식 컴포넌트는 복제되고, opentoggle value를 전달받는다.

    이 전 예제에서와 같이 Context API를 사용하는 대신, 이 두 value를 이제 props를 통해 접근할 수 있게 된다.

     

     

     

     


    장점

    Compound 컴포넌트들은 내부적으로 상태를 다루며 몇몇 자식 컴포넌트들 사이에서만 공유한다.

    Compound 컴포넌트를 사용하면 우리 자신의 별도 상태를 다루는 것에 대해서 걱정할 필요가 없다.

     

    Compound 컴포넌트를 import할 때, 해당 컴포넌트에 필요한 자식 컴포넌트를 명시적으로 import할 필요도 없다.

     

    import { FlyOut } from "./FlyOut";
    
    export default function FlyoutMenu() {
      return (
        <FlyOut>
          <FlyOut.Toggle />
          <FlyOut.List>
            <FlyOut.Item>Edit</FlyOut.Item>
            <FlyOut.Item>Delete</FlyOut.Item>
          </FlyOut.List>
        </FlyOut>
      );
    }
    

     

    단점

    value를 제공하기 위해 React.Children.map 을 사용하면 컴포넌트 nesting에 제한이 생긴다.

    오직 부모 컴포넌트의 직접적인 자식들만 opentoggle props에 대한 접근성이 생기므로, 이 컴포넌트를 다른 어떠한 컴포넌트로도 감쌀 수 없게 된다.

     

    export default function FlyoutMenu() {
      return (
        <FlyOut>
          {/* This breaks */}
          <div>
            <FlyOut.Toggle />
            <FlyOut.List>
              <FlyOut.Item>Edit</FlyOut.Item>
              <FlyOut.Item>Delete</FlyOut.Item>
            </FlyOut.List>
          </div>
        </FlyOut>
      );
    }
    

     

     

    React.cloneElement를 사용하여 엘리먼트를 복제하는 것은 얕은 병합(shallow merge)을 수행한다. 이미 존재하는 props가 우리가 전달하는 새로운 props와 병합될 것이다. 만약 이미 존재하는 prop과 같은 이름의 prop을 React.cloneElement에 전달한다면 이 방식이 네이밍 충돌을 일으킬 수 있다. props가 얕에 병합됨에 따라, prop의 value는 우리가 전달한 최신 값으로 덮어씌워질 것이다.

     

     


    References

    반응형

    COMMENT