ํ๋กํ ์์ ์ ์ํด ๋ชจ๋ฌ์ ๋ง๋ค์ด ๋ณด๊ณ ์ถ์ด์ ๊ตฌ๊ธ๋ง ํ๋ ์ค ์์ฃผ ์์ธํ๊ฒ ๋ชจ๋ฌ ๋ง๋๋ ๋ฐฉ๋ฒ์ ์๋ ค์ฃผ๋ ๋ธ๋ก๊ทธ๋ฅผ ๋ฐ๊ฒฌํ๋ค!
์๋์ ๊ฐ์ฌ :)๐
How to create a Modal Component in React from basic to advanced? | Thi Tran > Go To Read ๐
์ ์ฌ์ดํธ์์๋ ๋ชจ๋ฌ์ ์ฌ์ฉํ๋ ๊ธฐ์ด์ ์ด๊ณ ๋ค์ํ ๋ฐฉ๋ฒ์ ์ค๋ช ํด ์ฃผ๊ณ ์๋๋ฐ, ๋๋ ๋ด ํ๋ก์ ํธ์ ํ์ํ ์๋ ๊ธฐ๋ฅ์ ๊ตฌํํ๋๋ฐ ์์ด์ ๋์์ ๋ฐ์๋ค.

Modal ํด๋๋ฅผ ์์ฑํ์ฌ EditProfileModal.js ํ์ผ์ ๋ง๋ ๋ค.
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCamera, faTimes } from "@fortawesome/free-solid-svg-icons";
const EditProfileModal = () => {
return (
<div className="modal">
<div className="modal-content">
<div className="modal-header">
<div className="modal-header__box">
<div className="modal-x-btn">
<button className="btn--min btn--circle">
<FontAwesomeIcon icon={faTimes} />
</button>
</div>
<h4 className="modal-title">ํ๋กํ ์์ </h4>
<button className="btn btn--blue btn--border-zero">์ ์ฅ</button>
</div>
</div>
<div className="modal-body">
<div className="profile__user__header">
ํค๋ ์ด๋ฏธ์ง 598*200
<div className="btn--edit--container">
<div
aria-label="ํค๋ ์ฌ์ง ์ถ๊ฐํ๊ธฐ"
className="btn--edit--container--btn"
>
<button className="btn--change">
<FontAwesomeIcon icon={faCamera} />
</button>
</div>
<div
aria-label="ํค๋ ์ฌ์ง ์ญ์ ํ๊ธฐ"
className="btn--edit--container--btn"
>
<button className="btn--delete">
<FontAwesomeIcon icon={faTimes} />
</button>
</div>
</div>
</div>
<div className="profile__user__info">
<div className="profile__user__userImg">
<div className="profile__user__userImg__file userImg--sm img--edit--container">
<div
aria-label="ํ๋กํ ์ฌ์ง ์ถ๊ฐํ๊ธฐ"
className="img--edit--container--btn"
>
<button className="btn--change">
<FontAwesomeIcon icon={faCamera} />
</button>
</div>
</div>
</div>
</div>
<div className="modal-edit">
<form className="edit-form">
<label htmlFor="user-name"> ์ด๋ฆ </label>
<input
id="user-name"
aria-describedby="user-name"
name="user-name"
type="text"
placeholder="์ด๋ฆ"
className="btn btn--skyblue"
/>
<label htmlFor="self-introduction"> ์๊ธฐ ์๊ฐ </label>
<input
id="self-introduction"
aria-describedby="self-introduction"
name="self-introduction"
type="text"
placeholder="์๊ธฐ ์๊ฐ"
className="btn btn--skyblue"
/>
</form>
</div>
</div>
</div>
</div>
);
};
export default EditProfileModal;
๋ฐ๋ณต๋๋ ์คํ์ผ์ธ ๊ฒฝ์ฐ ๊ธฐ์กด์ ์์ฑํ๋ ํด๋์ค๋ช
์ ์ถ๊ฐํด ์คฌ๋ค.
์ผ๋จ styles.scss ์ํธ์ ๋ชจ๋ ์ ๊ณ ์๊ธฐ ๋๋ฌธ์ ํ๋ก์ ํธ ๋ค ์์ฑํ๊ณ ๋์ ํด๋ฆฌ๋ ์ฝ๋ํ์ ^3^~
/*๐ฌ EditProfileModal*/
.modal {
z-index: 99;
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
.modal-content {
width: 600px;
height: 70%;
background-color: white;
border-radius: 20px;
.modal-header {
padding: 0 16px;
display: flex;
flex-direction: row;
align-items: center;
height: 58px;
width: 100%;
.modal-header__box {
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
font-size: 20px;
.modal-x-btn {
min-width: 56px;
min-height: 32px;
display: flex;
flex-direction: column;
flex-basis: auto;
flex-shrink: 0;
align-self: stretch;
justify-content: center;
align-items: flex-start;
button {
font-size: 20px;
}
}
.modal-title {
font-weight: 800;
display: flex;
flex-grow: 1;
align-items: stretch;
}
button {
font-size: 15px;
}
}
}
.modal-body {
.profile__user__header {
display: flex;
justify-content: center;
align-items: center;
position: relative;
.btn--edit--container {
position: absolute;
display: flex;
flex-direction: row;
.btn--edit--container--btn {
border-radius: 50px;
width: 45px;
height: 45px;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-content: center;
box-sizing: border-box;
font-size: 20px;
button {
color: white;
}
}
.btn--edit--container--btn:last-child {
margin-left: 20px;
}
}
}
.profile__user__info {
.userImg-sm {
width: 200px;
height: 200px;
background-color: black;
}
.profile__user__userImg {
position: relative;
.img--edit--container {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
.img--edit--container--btn {
border-radius: 50px;
width: 45px;
height: 45px;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-content: center;
box-sizing: border-box;
font-size: 20px;
button {
color: white;
}
}
}
}
}
.modal-edit {
margin-top: 50px;
.edit-form {
display: flex;
flex-direction: column;
padding: 16px;
label {
padding-left: 15px;
}
input {
font-size: 13px;
font-weight: 500;
width: 300px;
margin-bottom: 15px;
border-radius: 5px;
cursor: auto;
}
input:focus {
background-color: #ebf5fe;
transition: 0.3s;
}
}
}
}
}
}
/Modal/EditProfileModal.js์ ์๋ ์ฝ๋๋ฅผ ์ถ๊ฐํ์ฌ ๋ชจ๋ฌ์ ์ด๊ณ ๋ซ์ ์ ์๊ฒ ํด์ฃผ์.import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCamera, faTimes } from "@fortawesome/free-solid-svg-icons";
const EditProfileModal = (isModalOpen) => {
if (!isModalOpen){
return null
}
return (
<div className="modal">
//์๋ต
๋ชจ๋ฌ ์ฐฝ์ด ์ด๋ฆฌ์ง ์์ ๊ฒฝ์ฐ, ์ฆ isModalOpen์ ๊ฐ์ด false์ธ ๊ฒฝ์ฐ null์ ๋ฆฌํดํ์ฌ ๋ชจ๋ฌ ๋ ์ด์์์ด ๋จ์ง ์๊ฒ ํ๋ค.
Profile.js์์ isModalOpen์ value ๊ด๋ฆฌํ๊ธฐ//๋ชจ๋ฌ ์ฐฝ ์ด๊ธฐ
<EditProfileModal isModalOpen={true} />
//๋ชจ๋ฌ ์ฐฝ ๋ซ๊ธฐ
<EditProfileModal isModalOpen={false} />
const Profile = ({ refreshUser, userObj }) => {
//์๋ต
const [isModalOpen, setIsModalOpen] = useState(false);
const handleEditModalOpen = () => {
setIsModalOpen((prev) => !prev);
};
return(
<button
className="btn btn--grey"
onClick={handleEditModalOpen}
>
ํ๋กํ ์์
</button>
<EditProfileModal
userObj={userObj}
isModalOpen={isModalOpen}
/>
);
/Modal/EditProfileModal.js์ onClick์ด๋ฒคํธ ์ถ๊ฐํ์ฌ ๋ชจ๋ฌ ๋ซ๊ธฐ ๋ฒํผ ์ด๋ฒคํธ ๋ฐ์ธ๋ฉํ๊ธฐ
const EditProfileModal = ({ isModalOpen, onClose }) => {
return(
//์๋ต
<div className="modal-x-btn">
<button className="btn--min btn--circle" onClick={onClose}>
<FontAwesomeIcon icon={faTimes} />
</button>
</div>
//์๋ต
);
};
Profile.js์์ ํ๊ธฐ ๋๋ฌธ์ onClose๋ ์ฌ๊ธฐ์์ ๊ด๋ฆฌํด์ผ ํ๋ค.const Profile = ({ refreshUser, userObj }) => {
//์๋ต
const [isModalOpen, setIsModalOpen] = useState(false);
const handleEditModalOpen = () => {
setIsModalOpen((prev) => !prev);
};
const handleEditModalClose = () => {
setIsModalOpen((prev) => !prev);
};
return(
<button
className="btn btn--grey"
onClick={handleEditModalOpen}
>
ํ๋กํ ์์
</button>
<EditProfileModal
userObj={userObj}
isModalOpen={isModalOpen}
onClose={handleEditModalClose}
/>
);
};
์ผ๋ฐ์ ์ผ๋ก ๋ชจ๋ฌ์ ์ฌ์ฉ์๊ฐ ์ธ๋ถ ํด๋ฆญ(๋ซ์ผ๋ ค๋ฉด ์ค๋ฒ๋ ์ด ํด๋ฆญ)์ ํตํด ๋ซ์ ์ ์๋ค. ๋์์ ๊ตฌํํด ๋ณด์.
/Modal/EditProfileModal.js์์ ๋ค์๊ณผ ๊ฐ์ด ์ฝ๋๋ฅผ ์์ฑํ๋ฉด ๋๋ค.
const EditProfileModal = ({ userObj, isModalOpen, onClose }) => {
//์ด๋ฒคํธ ์ ๋ฌ ๋ง๊ธฐ: modal ์ ์ฒด ํ๋ฉด ํด๋ฆฌ ์ onClose ์ด๋ฒคํธ ๋ฐ์ํ๋ modal-content ์์ญ ํด๋ฆญ์ ์๋๋์ง ์๋๋ก ์ด๋ฒคํธ ์ ๋ฌ ๋ง์
const handleStopPropagation = (event) => {
event.stopPropagation();
};
return (
<div className="modal" onClick={onClose}>
<div className="modal-content" onClick={handleStopPropagation}>
<div className="modal-header">
//์๋ต
);
};
์ค๋ฒ๋ ์ด๋ ๋ถ๋ถ์ ํด๋ฆญํ์ ๋ onClick ์ด๋ฒคํธ๋ฅผ ๋ฐ์ธ๋ฉํ์ฌ onClose๊ฐ ์คํ๋๋ค.
์ธ๋ถ ํด๋ฆญ ์์๋ง ๋ซ์์ผ ํ๊ธฐ ๋๋ฌธ์ modal-content ๋ด๋ถ ํด๋ฆญ ์์๋ ์ด ์ด๋ฒคํธ๊ฐ ์ ์ฉ๋์ง ์๋๋ก ์ด๋ฒคํธ ์ ๋ฌ ์ค์ง ๋ฉ์๋ event.stopPropagation()๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ค.
esc ํค๋ค์ด ์ด๋ฒคํธ๋ฅผ ์ฌ์ฉํ์ฌ ๋ชจ๋ฌ ์ฐฝ์ ๋ซ์๋ณด์.
/Modal/EditProfileModal.js์์ ๋ค์๊ณผ ๊ฐ์ด ์ฝ๋๋ฅผ ์์ฑํ๋ฉด ๋๋ค.
const EditProfileModal = ({ userObj, isModalOpen, onClose }) => {
const handleStopPropagation = (event) => {
event.stopPropagation();
};
const onEscapeKeyDown = (event) => {
if ((event.charCode || event.keyCode) === 27) {
onClose();
}
};
useEffect(() => {
document.body.addEventListener("keydown", onEscapeKeyDown);
return function cleanup() {
document.body.addEventListener("keydown", onEscapeKeyDown);
};
}, []);
//์๋ต
Escape ํค(์ด๋ค ํค ์ฝ๋๊ฐ 27์ธ์ง)๊ฐ ๋๋ ธ๋์ง ํ์ธํ๊ธฐ ์ํด ํค ๋ค์ด ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ์ ์ํ๋ค.
useEffect๋ฅผ ์ฌ์ฉํ์ฌ ํค๋ค์ด ๋ฆฌ์ค๋๋ฅผ ์ถ๊ฐํ๋ค.
CSS ์ ํ์ ์ฌ์ฉํ์ฌ ๋ชจ๋ฌ์ ์ ๋๋ฉ์ด์
ํํ ์ ์๋ค.
์ค๋ฒ๋ ์ด์ ๋ํด ํ์ด๋ ์ธ/ํ์ด๋ ์์์ ์ ์ฉํด ๋ณด์.
/Modal/EditProfileModal.js์์ ์์ฑํ๋ if(!isOpenModal){
return null
}
๋ถ๋ถ์ ์ญ์ ํ๊ณ ๋์ className์ด "modal"์ธ ์ฒซ ์ฒซ ๋ฒ์งธ div ์์์ ์ผํญ ์ฐ์ฐ์๋ฅผ ์๋์ ๊ฐ์ด ์ถ๊ฐํด ์ค๋ค.
return (
<div className={`modal ${isModalOpen ? "show" : ""}`} onClick={onClose}>
CSS์์ show/hide๋ฅผ ๊ด๋ฆฌํ๊ธฐ ์ํด ์ด๋ ๊ฒ ๋ณ๊ฒฝํด ์ฃผ๋ฉด ๋๋ค.
isModalOpen์ด true์ธ ๊ฒฝ์ฐ "show" ํด๋์ค๋ฅผ ์ถ๊ฐํด์ฃผ๋ ์์ด๋ค.
.modal {
//์๋ต
opacity: 0;
transition: all 0.15s ease-in-out;
pointer-events: none;
.modal.show {
opacity: 1;
pointer-events: visible;
}
opacity: 0;
๊ธฐ๋ณธ์ ์ผ๋ก ๋ชจ๋ฌ์ ํ์ด๋ ์์ํ๋ค.
transition: all 0.15s ease-in-out;
๋ถํฌ๋ช
๋์ ๋ํ ํธ๋์ง์
์ ์ฉ
pointer-events: none;
display: none;์ ํ๋ฉด ์์๋ ๋ฐ๋์ ์จ๊ฒจ์ง๋ค.
display: none;์ฒ๋ผ ์์๋ฅผ 100% ์จ๊ธฐ๋ฉด์ ํ์ด๋ ์ธ/์์์ ์ ๋๋ฉ์ด์
์ ์ฌ์ฉํ๊ธฐ ์ํด ๋ถํฌ๋ช
๋(opacity)๋ฅผ ์ฌ์ฉํ๊ณ ์๋ค. ๊ทธ๋ฐ๋ฐ .modal์ด ๋ชจ๋ ํ๋ฉด ๋ณด๋ค ์ ์ผ ์์ ์๊ธฐ ๋๋ฌธ์(z-index: 99;) ์์๊ฐ ๋ณด์ด์ง ์๋๋ค๊ณ ํ ์ง๋ผ๋ ์๋ ํ๋ฉด์ ์๋ ์์๋ค์ ํด๋ฆญํ ์ ์๊ฒ๋๋ค. ์ฆ, ํ๋กํ ์์ ๋ฒํผ์ ํด๋ฆญํ ์ ์๋ค.
pointer-events: none;๋ .modal ์๋์ ์๋ ๋ค๋ฅธ ์์๋ฅผ ํด๋ฆญํ ์ ์๋๋ก ๋์์ค๋ค.
.modal.show
.show ํด๋์ค๋ฅผ ์ถ๊ฐํ์ฌ opacity: 1๋ก ๋ณ๊ฒฝํ์ฌ ๋ชจ๋ฌ ์ฐฝ์ด ๋ณด์ด๊ฒ ๋ง๋ค๊ณ , pointer-events: visible;๋ก ๋ณ๊ฒฝํ์ฌ ๋ชจ๋ฌ ๋ด๋ถ๋ฅผ ํด๋ฆญํ ์ ์๊ฒ ํ๋ฉด ๋๋ค.
๐ฅฐ๐ฅ