가이드: 레이어로 모달 쌓기
실제 앱에서는 모달 안에서 다시 모달을 엽니다. 확인을 묻는 설정 모달이나 선택기를 여는 폼이 그렇습니다. 이 가이드는 React Snappy Modal이 쌓인 모달의 순서를 정하는 방식과 원하는 모달을 닫는 방법을 설명합니다.
서로 다른 세 가지 설정
모달을 쌓을 때는 헷갈리기 쉬운 세 가지 설정이 관여합니다.
| 설정 | 의미 | 용도 |
|---|---|---|
layer | 숫자 옵션이며 기본값은 0입니다. 열린 모달은 layer 순으로 정렬되고, 높은 레이어가 나중에 렌더링되어 위에 표시됩니다. | 모달 그룹의 순서를 정하고 close(value, layer)로 그룹을 지정합니다. |
modalId | show() 호출마다 만들어지는 고유 ID이며 useCurrentModal()로 얻습니다. | 특정 모달 하나를 닫습니다. |
zIndex | 모달 영역의 CSS z-index이며 기본값은 1000입니다. | sticky 헤더 같은 페이지 요소보다 모달을 위에 둡니다. |
흔한 실수: 레이어로 닫기
다른 값을 전달하지 않으면 모든 모달은 레이어 0에서 열립니다. 모달이 두 번째 모달을 열고, 두 번째 모달이 SnappyModal.close()를 호출하면 라이브러리는 레이어 0의 첫 번째 모달, 즉 부모를 찾습니다. 부모가 닫히고 자식은 화면에 남습니다.
1function Child() {
2 // Looks for the first modal on layer 0 — that is the Parent
3 return <button onClick={() => SnappyModal.close('child done')}>Done</button>;
4}
5
6function Parent() {
7 return <button onClick={() => SnappyModal.show(<Child />)}>Open child</button>;
8}
9
10SnappyModal.show(<Parent />); // layer 0
11// Clicking "Done" closes Parent; Child stays on screen해결 1: useCurrentModal로 안에서 닫기
resolveModal()은 자신이 렌더링된 모달을 ID로 찾아 닫으므로, 같은 레이어에 모달이 몇 개 있든 정확합니다. 모든 모달 컴포넌트의 기본 방식으로 사용하세요. 부모는 자식을 await하고, 자식이 답한 뒤에만 스스로 닫을 수도 있습니다.
1function Child() {
2 const { resolveModal } = useCurrentModal();
3 return <button onClick={() => resolveModal('child done')}>Done</button>;
4}
5
6function Parent() {
7 const { resolveModal } = useCurrentModal();
8
9 const openChild = async () => {
10 const answer = await SnappyModal.show(<Child />);
11 if (answer) resolveModal(answer); // close Parent only after Child answered
12 };
13
14 return <button onClick={openChild}>Open child</button>;
15}해결 2: 중첩 모달에 별도 레이어 지정하기
타이머나 전역 단축키처럼 모달 바깥에서 닫아야 한다면, 단계마다 별도 레이어로 열고 그 레이어를 close()에 전달하세요. 레이어 이름을 상수 하나에 모아 두면 순서를 읽기 쉽습니다.
1const LAYER = { page: 0, picker: 1, alert: 2 } as const;
2
3SnappyModal.show(<ColorPicker />, { layer: LAYER.picker });
4
5// Later, from anywhere (a timer, a keyboard shortcut, ...)
6SnappyModal.close(undefined, LAYER.picker);해결 3: ID로 특정 모달 닫기
useCurrentModal()은 modalId도 반환합니다. 모달이 effect에서 이 값을 알려 주게 하고, 값을 받은 뒤 SnappyModal.close(value, modalId)에 전달하면 같은 레이어에 다른 모달이 있어도 그 모달만 닫습니다. ID를 받기 전에 닫지 마세요. 빈 ID는 레이어 0으로 해석되어 그 레이어의 첫 모달이 닫힙니다.
1function UploadProgress({ onReady }: { onReady: (id: string) => void }) {
2 const { modalId } = useCurrentModal();
3 useEffect(() => onReady(modalId), [modalId, onReady]);
4 return <p>Uploading…</p>;
5}
6
7const opened = Promise.withResolvers<string>();
8SnappyModal.show(<UploadProgress onReady={opened.resolve} />, { allowOutsideClick: false });
9
10// Wait for the ID too: closing with an empty ID would target layer 0 instead
11const [modalId] = await Promise.all([opened.promise, upload(file)]);
12SnappyModal.close('uploaded', modalId);여러 모달에서의 바깥 클릭과 스크롤 잠금
바깥 클릭은 클릭된 영역의 모달을 ID로 닫고 Promise를 undefined로 resolve합니다. 그 아래의 모달은 열린 채로 남습니다.
열린 모달 중 하나라도 allowScroll: false이면 페이지는 잠긴 상태로 유지됩니다. 그런 모달이 모두 닫힌 뒤에야 잠금이 풀리고 스크롤 위치가 복원됩니다.
스택 변화에 반응하기
useSnappyModalState()는 스택이 바뀔 때마다 리렌더링합니다. 모달이 열려 있는 동안 전역 단축키를 멈추거나, 쌓인 모달 수를 보여 줄 때 사용하세요.
1function useEscapeToGoBack() {
2 const { isShow } = useSnappyModalState();
3
4 useEffect(() => {
5 if (isShow) return; // let the open modal handle the keyboard
6 const onKey = (event: KeyboardEvent) => {
7 if (event.key === 'Escape') history.back();
8 };
9 window.addEventListener('keydown', onKey);
10 return () => window.removeEventListener('keydown', onKey);
11 }, [isShow]);
12}체크리스트
한 화면에서 모달이 두 개 이상 열릴 수 있다면 다음을 확인하세요.
- 모달 컴포넌트는
useCurrentModal()로 스스로 닫습니다. - 인자 없는
SnappyModal.close()는 모달이 하나만 열릴 수 있을 때만 사용합니다. - 바깥에서 닫는 모달은 별도
layer를 갖거나modalId로 닫습니다. zIndex는 모달끼리의 순서가 아니라 모달이 아닌 페이지 요소보다 위에 두는 데만 사용합니다.