React에서의 Modal
2024. 7. 4. 21:53ㆍ공부내용 공유하기
리액트의 모달 컴포넌트는 다양한 방식으로 사용된다.
모달 렌더링에 대한 책임을 버튼 컴포넌트에 넘기는 방식. 모달의 열림/닫힘 상태를 버튼에서 관리하여 보일러 플레이트 코드를 줄이기 좋다. Props에 대한 타입 전달이 자연스러럽게 되는 장점도 있다.
<ModalControlButton Modal={ShareModal} modalProps={{ url }}>Share!</ModalControlButton>
모달의 열림/닫힘 상태를 제어하면서 모달 컴포넌트를 렌더링하는 방식. 가장 일반적인 방식이고 러닝커브가 없다.
const modal = useModal();
//...
<Button onClick={modal.open}>Share!</Button>
<ShareModal isOpen={modal.isOpen} close={modal.close} url={url} />
모달을 함수 호출 시점에 렌더링하는 방식. 호출부와 렌더링 코드가 붙어있어 응집도가 높은 편이다.
const { openModal } = useModal();
const handleClick = () => openModal(ShareModal, { url });
//...
<Button onClick={handleClick}>Share!</Button>
모달을 함수 호출 시점에 렌더링하고 JSX구조를 유지하는 방식. 위의 방식과 유사하다.
import modal from "@~";
//...
const handleClick = () => modal.open((isOpen, close) => <ShareModal isOpen={isOpen} close={close} url={{ url }} />, );
//...
<Button onClick={handleClick}>Share!</Button>
모달 구현체는 일반적으로 Portal을 사용하거나 전용 Provider를 통해 렌더링 위치를 제어한다.
`createRoot().render()`를 직접 호출해서 전용 FiberTree를 만들어 제어하는 방식도 가능하다.
이 중에서 개인적으로 편하게 사용하고 있는 ModalControlButton의 구현체를 살펴보자
import { FC, useCallback, useState } from 'react';
import { Button, type ButtonProps } from '@/compoents/Button';
type PropsFrom<TComponent> = TComponent extends FC<infer Props> ? Props : never;
type WithModalProps<ModalRenderer> = {} extends RemoveOptional<
Omit<PropsFrom<ModalRenderer>, 'close'>
>
? { modalProps?: Omit<PropsFrom<ModalRenderer>, 'close' | 'isOpen'> }
: { modalProps: Omit<PropsFrom<ModalRenderer>, 'close' | 'isOpen'> };
type ModalControlButtonProps<ModalRenderer> = {
ModalRenderer: ModalRenderer;
} & WithModalProps<ModalRenderer> &
Omit<ButtonProps, 'onClick'>;
export default function ModalControlButton<
ModalRenderer extends FC<any & { close: () => unknown }>,
>({ ModalRenderer, children, modalProps, ...restProps }: ModalControlButtonProps<ModalRenderer>) {
const [isOpen, setIsOpen] = useState<boolean>(false);
const open = useCallback(() => {
setIsOpen(true);
}, []);
const close = useCallback(() => {
setIsOpen(false);
}, []);
return (
<>
<Button onClick={open} {...restProps}>
{children}
</Button>
<ModalRenderer {...(modalProps as any)} isOpen={isOpen} close={close} />
</>
);
}
/**
* 타입 T의 키 K를 조건부로 포함하는 맵 타입을 작성합니다.
* {}가 타입 T에서 K 키를 가진 속성을 Pick한 것에 확장할 수 있는 경우 never를 반환하고, 그렇지 않으면 K를 반환합니다.
* 결국 필수 속성만 K로 선정됩니다.
*/
type RequiredKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];
/**
* 주어진 타입 T에서 선택적 속성을 제거한 새 타입을 생성합니다.
* 필수 속성들로만 이루어진 타입을 반환합니다.
*/
type RemoveOptional<T> = Pick<T, RequiredKeys<T>>;