컨텍스트와 훅을 사용하는 리액트 모달 라이브러리
이 라이브러리는 <ModalProvider />
를 통해 컨텍스트와 모달 컨테이너를 제공합니다. 또한 모달을 열고 닫는 데 필요한 함수를 훅을 통해 제공합니다.
주된 목적은 어떤 모달이든 당신의 코드 어디서든 열 수 있는 타입-세이프한 방식을 제공하는 것입니다.
거기에 더해, 모달에 사용되는 코드가 모달이 열리기 전까지는 로드되지 않도록 합니다: 번들 사이즈를 줄이기 위한 것이죠.
npm install @reactleaf/modal
# 또는
yarn add @reactleaf/modal
이 라이브러리를 사용하기 위해서는 우선, 당신만의 모달 레지스터를 만들어야 합니다.
초기 로딩에 필요한 파일을 줄이기 위해, 동적 import 를 사용합니다.
레지스터에 등록된 모든 모달은 openModal
호출로 열릴 때 코드를 불러옵니다. app이나 page 코드에 번들링되지 않습니다.
const register = {
Alert: () => import("./Alert"),
Confirm: () => import("./Confirm"),
};
export default register;
하지만 특수한 경우에는 모달을 열기 전에 미리 불러와 두는 것이 유리할 수 있습니다. 그런 경우, 아래를 확인해주세요.
이제 만들어둔 레지스터를 당신의 앱에 넣어주세요.
우리는 이를 위해 <ModalProvider />
컴포넌트를 제공합니다. 이 컴포넌트는 모달 컨텍스트와 모달이 렌더링 될 모달 컨테이너를 한 번에 제공합니다.
얼마나 간단한가요!
import { ModalProvider } from "@reactleaf/modal";
import register from "./modals/register";
function App() {
...
return <ModalProvider register={register}>{...}</ModalProvider>
}
useModal()
훅은 직접 import 할 수 없습니다. createModalHook()
을 사용해서 만들어 사용해야 합니다.
register의 타입을 통해 모달의 type과 props가 서로 알맞게 입력되었는지 체크하기 위해서입니다.
// useModal.ts
import { createModalHook } from "@reactleaf/modal";
import register from "./register";
export const useModal = createModalHook<typeof register>();
useModal()
에서 제공하는 openModal()
함수는 당신이 register에 등록한 모달 type을 제대로 지정했는지 체크합니다.
import { useModal } from './modals/useModal'
const { openModal } = useModal()
function openAlert() {
openModal({ type: 'Confrim', props: { title: 'Hello', message: 'Wow' } })
^^^^ ^^
type 'Confrim' is not assignable to type 'Alert' | 'Confirm'
}
또한 적절한 타입에 맞는 적절한 props를 입력했는지 체크할 수 있습니다.
import { useModal } from './modals/useModal'
const { openModal } = useModal()
function openAlert() {
openModal({ type: 'Alert', props: { title: 'Hello' } })
^^^^^
property 'message' is missing
}
@reactleaf/modal
은 모달을 열 때, dynamic import를 통해 모달을 불러옵니다. 이 방식을 통해 code-splitting이 쉬워지고, 초기 번들 사이즈도 줄일 수 있습니다.
하지만 어떤 경우에는, 모달 코드가 페이지나 컴포넌트가 로딩될 시점에 함께 로드 되어있어야 할 수도 있습니다.
가령 모달이 열리는 애니메이션을 넣은 경우, 혹은 모달이나 모달이 의존하는 라이브러리가 너무 커서 불러오는 데에 시간이 좀 걸리는 경우, 등이 있겠죠.
그런 경우, 유저가 버튼을 클릭해 모달을 열기 전에, 모달 코드를 미리 불러와둘 수 있습니다.
preloadModal
을 사용하면, 레지스터에 등록된 import()
구문을 미리 실행해두어, openModal()
실행 시 코드를 불러오는데에 걸리는 시간을 없앱니다.
// [주의] preloadModal은 훅이 아닙니다.
import { createModalPreloader } from "@reactleaf/modal";
const preloadModal = createModalPreloader(register);
// 이 컴포넌트가 불러와졌을 때, 컴포넌트에서 사용할 모달을 미리 불러옵니다.
useEffect(() => {
preloadModal("Alert", "Confirm");
}, []);
register
- 위에서 만든 레지스터를 넣습니다.defaultOverlayOptions
- 매번 overlayOptions를 설정하지 말고, 기본 옵션을 설정하세요.
defaultOverlayOptions
은 아래와 같이 사용합니다.
type defaultOverlayOptions = Partial<OverlayOptions>;
// 별 설정이 필요 없다면, 기본 값을 사용합니다. 기본값은 아래, openModal과 함께 설명됩니다.
return (
<ModalProvider register={register}>
<App />
</ModalProvider>
);
// 모든 모달에 적용할 옵션이 필요하다면 이렇게 작성할 수 있습니다.
return (
<ModalProvider
register={register}
defaultOverlayOptions={{ closeDelay: 300 }}
>
<App />
</ModalProvider>
);
const useModal = createModalHook<typeof yourModalRegister>();
const { openModal, closeModal, closeAll, openedModals } = useModal();
지정된 타입의 모달을 열고, 주어진 props를 모달에 전달합니다.
interface OpenModalPayload {
type: keyof Register;
props?: Props;
overlayOptions?: OverlayOptions;
events?: ModalEvents;
}
function openModal(payload: OpenModalPayload);
Props
- type에 맞는 props를 제공합니다. 만약type === "Alert"
이라면, props는React.ComponentProps<Alert>
타입의 값이어야 합니다.OverlayOptions
export interface OverlayOptions {
className?: string; // 만약 서로 다른 모달을 구분하고 싶다면: 대개 모달마다 다른 애니메이션을 주고 싶다면, className을 통해 구분할 수 있습니다.
closeDelay?: number; // 기본값은 0 입니다. ms 단위를 사용합니다. 이 옵션을 설정할 경우, close()가 불린 뒤 모달이 실제로 unmount 되기까지 지연이 생깁니다. 모달을 닫는 애니메이션 같은 걸 구현할 때 굉장히 유용합니다.
closeOnOverlayClick?: boolean; // 기본값은 true 입니다. 모달 바깥, dim 영역을 클릭할 때 모달을 닫습니다.
dim?: boolean; // 기본값은 true 입니다. 오버레이 요소에 .dim 클래스를 포함합니다. 기본으로 제공되는 css 스타일을 사용한다면, 모달이 열렸을 때 모달 바깥이 어둡게 가려집니다.
preventScroll?: boolean; // 기본값은 true 입니다. 모달이 열렸을 때, body 의 스크롤을 막습니다.
}
ModalEvents
export interface ModalEvents {
onOpen?(payload: { type; props; id }): void; // 모달이 async하게 불러와 진 다음 열리기 때문에, openModal() 이후 실제로 모달이 마운트 된 순간을 알기 위해 사용합니다.
beforeClose?(): PromiseOr<void>; // 모달이 닫히기 전에 불립니다. 모달을 닫지 않으려면, throw Error 를 통해 모달이 닫히지 않도록 막을 수 있습니다. Promise를 반환해 모달이 닫히는 것을 지연시킬 수도 있습니다.
onClose?(): void; // 모달이 닫힐 때 불리는 콜백 함수입니다.
}
returns
- 방금 열었던 모달의 유니크한 "ID"를 반환합니다. 이 아이디를 closeModal에 전달하여, 특정 모달을 지정해서 닫을 수 있습니다.
특정 모달을 닫을 수 있습니다. 이 액션에서는 모달을 열 때 얻은 ID가 필요합니다.
모든 열려있는 모달을 닫습니다.
열려있는 모달 목록을 반환합니다. 모달이 하나라도 열려있는지, 특정 타입 모달이 열려있는지, 등을 체크할 수 있습니다.
모달의 오버레이 설정은 세 곳에서 이루어집니다.
openModal({ type: "...", overlayOptions: 여기 })
- 여기에 설정한 옵션은 이 함수를 통해 연 모달에만 적용됩니다.
- 모달 컴포넌트의 선언부
- 여기에 설정한 옵션은 이 컴포넌트 타입의 모달을 열 때, 적용됩니다.
// register
...
'common/Alert': () => import('./Alert'),
// ./Alert.tsx
export const defaultOverlayOptions: OverlayOptions;
export default function Alert(props) {
return ...
}
<MoalProvider />
에서
- 여기에 설정한 옵션은 모든 모달에 적용됩니다.
<ModalProvider register={register} defaultOverlayOptions={여기}>
<YourApp />
</ModalProvider>
첫 번째 것이 우선순위가 가장 높고, 마지막에 소개한 방식이 우선순위가 가장 낮습니다. 세 옵션은 Object.assign()
방식으로 합쳐집니다.
애니메이션이 돌아가게 하기 위해, 모달은 사실 openModal()
실행 시점보다 한 프레임 뒤늦게 열립니다.
그 덕분에, 오버레이에 아래와 같은 스타일을 줄 수 있습니다.
.modal-overlay {
opacity: 0;
transition: opacity 0.3s;
}
.modal-overlay.visible {
opacity: 1;
}
당신만의 커스텀 모달을 구현할 때에는, visible
속성을 활용하세요. 아래 에서 visible 속성에 대해 더 자세히 알아보세요.
닫히는 애니메이션을 구현할 때엔, closeDelay
옵션을 제대로 설정했는지 확인하세요.
애니메이션이 동작하는 예제는 Slideup 예제에서 확인할 수 있습니다.
.slideup {
transition: transform 500ms;
transform: translateY(100%);
}
.slideup.visible {
transform: translateY(0);
}
유저 경험을 위해, 웬만하면 모달은 스스로 닫는 방식으로 구현하는 것을 권장합니다. 아래에서 더 알아보세요.
물론 바깥에서도 닫을 수 있습니다.
const id = openModal(); closeModal({ id });
closeAll()
closeOnOverlayClick: true
- 유저가 모달 바깥(보통 어두운 색으로 가려두는)을 클릭한 경우, 가장 위에 열린 모달이 닫힙니다.
@reactleaf/modal
에서는 window.postMessage()
를 사용해, 모달을 열 수도 있습니다. 당신이 redux나 saga 같은 써드 파티 상태관리 라이브러리를 사용한다면, 컴포넌트 바깥에서 모달을 열어야 할 필요가 있을 수도 있습니다.
하지만 주의하세요: postMessage
를 사용할 때는 타입 체크를 할 수 없습니다. 또한, props에 함수를 전달할 수 없습니다. 만약 열려는 모달이 onConfirm
같은 함수를 받아야 한다면, postMessage
로 여는 데에 문제가 생깁니다.
postMessage 로는 모달을 열 수만 있습니다. 메시지로 모달을 닫을 수 없다는 점을 염두에 두세요.
window.postMessage({
to: "@reactleaf/modal",
payload: {
type: "Example",
props: {
warning: "postMessage는 Serializable 한 값만 전달할 수 있습니다.",
},
},
});
모달이 openModal()
에 의해 열리면, props로 전달했던 것 외에 두 가지 props가 추가로 삽입됩니다.
close(): void
visible: boolean
이 props를 활용하기 위해서, 다음 방식으로 구현하시기를 추천합니다.
import { BasicModalProps } from "@reactleaf/modal";
interface Props extends BasicModalProps {
title: string;
message: string;
}
const Alert = ({
title,
message,
visible, // injected by modal
close, // injected by modal
}: Props) => {
return (
<div className={cx("alert", "modal", { visible })}>
<p className="modal-title">{title}</p>
<div className="modal-body">
<p className="message">{message}</p>
</div>
<div className="modal-buttons">
<button onClick={close}>Close</button>
</div>
</div>
);
};
기본 스타일 CSS 파일을 제공하고 있습니다. 커스텀 스타일링이 필요 없으신 경우, 아래와 같이 import해서 사용할 수 있습니다.
import "@reactleaf/modal/style.css";
혹은, 아래 클래스에 대한 스타일을 정의해, 당신만의 스타일링을 적용할 수 있습니다.
.modal-overlay
- 모달 오버레이 요소.modal-overlay.dim
- 모달을 열 때overlayOption: { dim: true }
옵션을 설정한 경우 사용할 수 있습니다. dim 옵션은 기본값이true
입니다..modal-overlay.visible
-.visible
클래스는 모달이 열리고 한 프레임 뒤에 추가됩니다. 모달의 열리는 애니메이션을 적용할 경우, 이 셀렉터를 사용하세요.
아래 예제와 같은 방식으로 당신만의 스타일링을 정의할 수 있습니다.
.modal-overlay {
opacity: 0;
transition: opacity 0.3s;
}
.modal-overlay.dim {
background-color: rgba(0, 0, 0, 0.5);
}
.modal-overlay.visible {
opacity: 1;
}
OverlayOptions
에는 className
옵션도 있습니다. 열리는 각각의 모달을 구분하고, 서로 다른 애니메이션이나 dim 색상을 적용하기 위해 사용할 수 있습니다.
예제 폴더에서 확인하세요.