클릭했을 때 모달이 띄워져야 한다.
주변 배경을 클릭할 수 없다
모달을 닫는 close 버튼과 이후 작업을 수행할 submit 버튼이 있다
재사용이 가능해야 한다
모달창은 전역 상태로 구현한다.
context를 사용해도 되지만 나의 경우 Zustand를 사용했다
전역 모달에 사용되는 타입은 다음과 같다
interface ModalStore {
// 모달의 렌더링 유무 플래그
isOpen: boolean;
// 모달을 렌더링 하는 콜백
openModal: (modalData: ModalData) => unknown;
// 모달을 닫는 콜백
closeModal: () => unknown;
// 모달에 사용될 로직과 컨텐츠들
modalData: {
children?: ReactNode; // 모달에 사용될 컨텐츠
onCancel?: () => unknown; // 모달을 닫을 때 트리거 할 로직
onSubmit?: () => unknown; // 모달을 submit할 때 트리거 할 로직
};
}
출처 : https://velog.io/@dev-redo/React-전역상태관리를-통해-모달을-띄우는-게시물을-만들어보자
위 타입을 바탕으로 전역 모달에 대한 상태를 다루는 store를 생성한다
// modal.ts
import { ReactNode } from 'react';
import { create } from 'zustand';
// 모달에 사용될 로직과 컨텐츠 타입을 따로 분리
type ModalData = {
children?: ReactNode;
onCancel?: () => unknown;
onSubmit?: () => unknown;
};
interface ModalStore {
isOpen: boolean;
openModal: (modalData: ModalData) => unknown;
closeModal: () => unknown;
modalData: ModalData;
}
export const useModalStore = create<ModalStore>((set) => ({
isOpen: false,
modalData: {} as ModalData,
// 모달을 열면 isOpen를 true, 인자로 받은 ModalData를 modalData에 할당
openModal: (modalData: ModalData) => {
set((state) => ({ isOpen: true, modalData: { ...modalData } }));
},
// 모달을 닫으면 isOpen를 false, modalData초기화
closeModal: () => {
set((state) => ({ isOpen: false, modalData: {} }));
},
}));
import { useModalStore } from './store/modal';
import styles from './Modal.module.scss';
const Modal = () => {
const { isOpen, modalData, openModal, closeModal } = useModalStore();
const { children, onCancel, onSubmit } = modalData;
if (!isOpen) {
return <></>;
}
const onCancelInternal = () => {
onCancel?.(); // onCancel이 있을때만 진행
closeModal();
};
const onSubmitInternal = () => {
onSubmit?.(); // onSubmit이 있을때만 진행
closeModal();
};
// openModal()로 받은 ModalData타입의 내용을 여기서 렌더링
// 코드를 보니 모든 모달을 전부 커스텀해서 사용하기보다
// 여기서 기본 UI를 고정하기 때문에 같은 종류(UI적으로)의 모달들을 띄우는듯
return (
<div className={styles.modalContainer}> **// 스타일링은 아직 안함**
<div className={styles.modalContent}> **// 스타일링은 아직 안함**
<div style={{ color: 'red', cursor: 'pointer' }} onClick={closeModal}>
x
</div>
<div>
<div>{children}</div>
<div>
<button onClick={onCancelInternal}>cancel</button>
<button onClick={onSubmitInternal}>submit</button>
</div>
</div>
</div>
</div>
);
};
export default Modal;
onCancelInternal
onSubmitInternal
import React, { memo } from 'react';
import { useModalStore } from './store/modal';
import Modal from './Modal';
import styles from './FirstCardList.module.scss';
// 모달을 사용하는 컴포넌트
const FirstCardList = memo(() => {
const { openModal } = **useModalStore**();
const openModalWithData = (data: number) =>
// ModalData 생성
openModal({
children: <p>{data}</p>,
onSubmit: () => console.log('submit'),
onCancel: () => console.log('cancel'),
});
return (
<div className={styles.container}>
<div className={styles.cards}>
{[1, 2, 3].map((data) => (
<div key={data} className={styles.singleCard}>
<p onClick={() => **openModalWithData(data)**}>{data}</p>
</div>
))}
</div>
</div>
);
});
export default FirstCardList;
// FirstCardList.module.scss
.container {
height: calc(100dvh - (200px * 2)); **// 헤더와 푸터 제외한 높이**
background-color: #68a9f3;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.cards {
display: flex;
flex-direction: column;
cursor: pointer;
}
전체 순서도
openModal 함수 호출
전역 모달 상태 변경
해당 상태를 구독하고 있는 모달 컴포넌트 리렌더링 및
리렌더링 된 값을 기반으로 모달 생성
import App from './App';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import FirstCardList from './FirstCardList';
import SecondCardList from './SecondCardList';
const AppRouter = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
<Route path="/FirstCardList" element={<FirstCardList />} />
</Routes>
</BrowserRouter>
);
};
export default AppRouter;
import styles from './App.module.scss';
import AppRouter from './AppRouter';
import Modal from './Modal';
const App = () => {
return (
<div className={styles.container}>
<header className={styles.header}>나 헤더인데</header>
<AppRouter />
<footer className={styles.footer}>나 푸터인데</footer>
**<Modal /> // 모달을 최상단에 두고 사용**
</div>
);
};
export default App;
// App.module.scss
.container {
height: 100dvh;
width: 100dvw;
}
.header {
display: flex;
justify-content: center;
align-items: center;
background-color: green;
width: 100%;
height: 200px;
color: white;
}
.footer {
display: flex;
justify-content: center;
align-items: center;
background-color: navy;
width: 100%;
height: 200px;
color: white;
}
각 숫자를 클릭하면 모달창이 나타나게 된다
그렇지만 아직 모달에 별다른 스타일링을 진행하지 않았으므로 맨 아래부분에 나타나게 된다
confirm 모달 ( 전체 화면 + 뒷 배경 클릭을 방지)
.modalContainer {
position: absolute; // body에 걸리게 되므로 body기준
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
// .modalContent 중앙 정렬
display: flex;
justify-content: center;
align-items: center;
}
.modalContent {
width: 500px;
height: 700px;
background: #ffffff;
border-radius: 20px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
화면 일부만 차지 + 뒷 배경 클릭도 가능한 경우
대표적으로 확인버튼을 눌렀을 때, “제출되었습니다” 와 같은
1회성 알림창 용도로 쓰인다
.modalContainer {
position: absolute; // body기준
background-color: red; // 이해를 위해 추가
top: 20%;
left: 50%;
width: 200px;
height: 100px;
transform: translate(-50%, -50%);
}
.modalContent {
width: 100%;
height: 100%;
border-radius: 20px;
background: #ffffff;
display: flex;
justify-content: center;
align-items: center;
}
- position: absolute; 으로 body를 기준으로 잡는다
그렇지만 이번엔 전체화면으로 사용하지 않고 실제 사용 크기만
지정하고 위치만 조정한다.
이렇게 하면 뒷 배경은 여전히 클릭이 가능한 상태로 화면의 일부에만 모달창을 사용할 수 있다
세로 스크롤이 생길때 해당 모달이 따라오길 원한다면
position을 absolute 말고 fixed로 사용하면 된다
기본적으로 position속성에서
static 속성과 그 외(relative, absolute, fixed, sticky)의 속성이 만나면
static이 아닌 요소가
position 속성이 static인 요소 위로올라간다
그러므로 굳이 z-index없이도 모달 컨테이너에 absolute 를 부여해서
동일한 효과를 부여했다.
을 최상위에 선언함으로써 absolute를 부여하면 최대한
별 탈 없이 body에 걸리게 한 것이다
에 absolute를 부여하면 위로 타고타고 올라가다가 body가 아닌
중간에 staic이 아닌것에 걸리면 또 복잡해지니까
❗그렇지만 가끔은 z-index를 적용해야 하는 경우가 있다
아래 사진처럼, 모달을 띄웠는데도 뒤에 있는 작은 x 버튼이 눌리고 있다.
(HTML에서 모달 선언 부분이 x버튼의 선언 부분보다 위에 있다고 가정한 상황이다)
즉, 서로 position 속성이 static이 아닌 경우로 동일 할 때,
이런 경우에는 z-index를 통해 모달을 위로 올려야 한다
나 조민호인데..