ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React] 늘어나는 props, 합성으로 해결해보자
    React 2025. 5. 4. 19:45

     

     

    이번 우테코 미션은 바로 React Module 이다.

     

    이 미션의 학습 목표는,

    모달을 만들고 npm에 배포해보는 것. 

     

    미션에서 얻어야 할 여러 경험이 있겠지만,

    그 중에서 나는 해결해야 할 문제를 아래와 같이 정의했다.

     

     

    1. 사용자에게 모달 구조에 대한 자율성을 넘겨줄 수 있다.
    2. 사용자가 사용하기에 편리한 모달이어야 한다.  

     

     

     

    아래는 실제 미션 LMS 파일에 나와있는 예시이다.

     

     

     

    생각해보자.

     

    재사용에 용이한 모달 라이브러리를 만들어야 하는데, 저런 구조는 무조건 props가 늘어날 것 같다.

     

    하지만 페어프로그래밍 특성 상, 일단 빠르게 구현해야했다.

     

    초기 우리의 모달은 아래와 같은 인터페이스를 가졌다. 

     

    export interface ModalProps extends PropsWithChildren {
      onOpenChange?: (isOpen: boolean) => void;
      title?: string;
    }

     

    비교적 간단하다.

     

    다만, 요구사항을 다시 읽어보니 bottomSheet에도 같은 모달이 대응해야 하고,

    어떤 경우에는 closeButton이 보이고 또 어떤 경우에는 보이지 않아야 한다. 

     

    이제 우리의 모달 인터페이스는 이렇게 되었다.

    export interface ModalProps extends PropsWithChildren {
      defaultOpen?: boolean;
      onOpenChange?: (isOpen: boolean) => void;
      position?: 'center' | 'bottom';
      maxWidth?: number;
      title?: string;
      isVisibleCloseButton?: boolean;
    }

     

     

    슬슬 불안해지기 시작했다.

     

    이걸 라이브러리로 배포한다면, 아무도 사용하지 않을 것이라고 백프로 확신이 들었다.

     

    컴포넌트 합성으로 위에서 정의한 1번에 대한 문제를 해결해보자.

     

    우리가 희망하는 모달 인터페이스는 아래와 같다.

          <Dialog>
              <Dialog.Trigger>
                Open Compound Modal
              </Dialog.Trigger>
              <Dialog.Root>
                <Dialog.Overlay />
                  <Dialog.Content position="bottom">
                    <Dialog.Header>
                      <h3>Custom Compound Modal</h3>
                      <Dialog.CloseButton>close Modal</Dialog.CloseButton>
                    </Dialog.Header>                
                  </Dialog.Content>
              </Dialog.Root>
           </Dialog>

     

     

    이러한 인터페이스를 보니, 훨씬 유연한 모달을 만들 수 있을 것 같다.

    이제 실제 구현을 진행해보자.

     

    실제 구현을 진행하기 전에, 

    위에서 정의한 2번문제에 대해 고민하는 시간을 잠시 가지고 넘어가려 한다. 

     

    2. 사용자가 사용하기에 편리한 모달이어야 한다.  

     

    상상해보자. 보통 모달을 열고 닫을땐, 

     

    const [isOpen, setIsOpen] = useState(false)
    
    return (
    	isOpen && <Modal />
    )

     

    이런 isOpen 상태를 이용하여 모달을 열고 닫아야 한다.

     

    라이브러리를 사용하는 입장에서 항상 저런 상태를 만들어줘야 한다는 것이 얼마나 불편한가?

     

    이런 문제를 해결하기 위해서,

     

    가장 바깥에서 감싸고 있는 <Dialog>에 컨텍스트를 내려준다. 

     

    export function Dialog({ children }: { children: React.ReactNode }) {
      const { value: isOpen, setTrue: open, setFalse: close } = useBoolean(false);
    // useBoolean 훅은 단순히 value를 true와 false로 변환해주는 훅이다.
    
      return (
        <DialogContext.Provider value={{ isOpen, open, close }}>
          {children}
        </DialogContext.Provider>
      );
    }

     

    이제 Trigger를 보자. 

     

    Trigger는 해당 버튼을 클릭 했을 때 모달이 떠야한다.

    function Trigger({
      children,
      className,
    }: {
      children: React.ReactNode;
      className?: string;
    }) {
      const { open } = useDialogContext();
    
      return (
        <button onClick={open} className={className}>
          {children}
        </button>
      );
    }

     

    이러한 방식으로 모든 부속 컴포넌트에 children을 이용하여 만든다면, 우리가 원하는 모달을 만들 수 있다.

     

    전체 코드를 보고 싶다면 여기로

    https://github.com/ExceptAnyone/react-modules/blob/step1/components/src/lib/Dialog.tsx

     

    react-modules/components/src/lib/Dialog.tsx at step1 · ExceptAnyone/react-modules

    Contribute to ExceptAnyone/react-modules development by creating an account on GitHub.

    github.com

     

     

    근데 이러한 구조에서도 문제가 있다.

     

    만약 내 라이브러리가 제공하는 HTML 요소 대신 다른 태그로 대체하고 싶다면 어떻게 해야할까?

     

    //내 라이브러리에서 제공하는 trigger
    <Dialog.Trigger>Open</Dialog.Trigger>
    
    
    //만약 커스텀된 버튼을 사용하고 싶다면? 
    <Dialog.Trigger>
      <MyCustomButton>Open</MyCustomButton>
    </Dialog.Trigger>

     

     

    이에 대한 문제를 해결하기 위해 Radix에서는 asChild 패턴을 사용하고 있다.

     

    간단히 얘기하자면,

     

    <Dialog.Trigger asChild>
      <MyCustomButton>Open</MyCustomButton>
    </Dialog.Trigger>
    
    //asChild가 true면 기존 button을 렌더링하지 않고 자식 엘리먼트를 직접 받아서 렌더링

     

     

    radix의 asChild 패턴과 Slot에 대해서는 다음 포스팅에서 다뤄볼 예정이다.

Designed by Tistory.