-
[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에 대해서는 다음 포스팅에서 다뤄볼 예정이다.
'React' 카테고리의 다른 글
[React] ErrorBoundary가 이벤트 핸들러와 비동기적 코드를 감지하지 못하는 이유 (0) 2025.03.15 변경에 유연한 컴포넌트 설계 (1) 2024.06.17 이미지 최적화 후 Lighthouse로 성능 검사하기 (0) 2024.05.10 컴포넌트 설계의 진짜 의미 (10) 2024.04.23 모던 리액트 Deep dive - 렌더링은 어떻게 일어나는가? & 메모이제이션 (0) 2024.03.19