쇼핑몰 제작 프로젝트를 진행하며 구현한 모달 창 입니다.
👉 목표
- 취소/확인 이 있는 모달창 컴포넌트화 하기
- 모달 영역 외 배경은 어둡게 처리하기
- 모달이 닫히는 조건
- 영역 밖 클릭 시
- 취소/ 확인 버튼 클릭 시
- 모달이 열리는 조건
- 버튼 클릭 시
👉 고려 사항
- 다른 페이지에서 재사용할 것인데, 텍스트 내용이 다를 수 있다.
- 확인 버튼 클릭 시 주문취소 인 경우 주문번호가, 반품신청 인 경우 각 주문번호+알파가 백앤드에 전달되야 한다. (나중 구현 예정)
🌝 구현
1️⃣
모달 창을 먼저 컴포넌트화 했습니다.
modal의 css 파일을 따로 만들면 css 파일이 많아지고 각 클래스 작명을 잘해야 한다는 귀찮음이 있어 styled-components를 이용했습니다.
참고로 styled-components 을 잘 다루진 못해서 불필요하게 중복되는 코드가 있을 수 있어요.
// 확인, 취소 버튼이 있는 popup modal 입니다.
import styled from 'styled-components';
function ConfirmCancelModal({isOpen, onClose, children}) {
if (!isOpen) {
return null;
}
return (
<ModalBackdrop onClick={onClose}>
<ModalView onClick={(e) => e.stopPropagation()}>
<TextArea>{children}</TextArea>
<BtnArea>
<CancelBtn onClick={onClose}>취소</CancelBtn>
<ConfirmBtn onClick={onClose}>확인</ConfirmBtn>
</BtnArea>
</ModalView>
</ModalBackdrop>
);
}
export const ModalBackdrop = styled.div`
// Modal이 떴을 때의 배경을 깔아주는 CSS
z-index: 1; //위치지정 요소
position: fixed;
display : flex;
justify-content : center;
align-items : center;
background-color: rgba(0,0,0,0.4);
border-radius: 10px;
top : 0;
left : 0;
right : 0;
bottom : 0;
`;
export const ModalView = styled.div.attrs((props) => ({
// attrs 메소드를 이용해서 아래와 같이 div 엘리먼트에 속성을 추가할 수 있다.
role: 'dialog',
}))`
// Modal CSS
display: flex;
align-items: center;
flex-direction: column;
width: 400px;
height: 231px;
border-radius: 5px;
background: #FFF;
box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.25);
`;
export const TextArea = styled.div`
display: flex;
flex-basis: 180px;
text-align: center;
align-items: center;
color: #000;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: normal;
`
export const BtnArea= styled.div`
display: flex;
flex-direction: row;
height: 51px;
`
export const CancelBtn = styled.button`
width: 200px;
outline: none;
border: none;
border-radius: 0 0 0 5px;
background: #F0F0F0;
&:active,
&:hover,
&:focus {
background: #e1e4e6;
}
`
export const ConfirmBtn = styled.button`
width: 200px;
outline: none;
border: none;
border-radius: 0 0 5px 0;
background: #FFD465;
&:active,
&:hover,
&:focus {
background: #ffb84a;
}
`
export default ConfirmCancelModal;
2️⃣
주문취소 버튼 클릭 시 모달 창이 뜨게 하기 위해 onClick 이벤트를 주었습니다.
function OrderHistoryDetail() {
const [isOpen, setIsOpen] = useState(false);
const openModalHandler = () => {
// isOpen 상태 변경
setIsOpen(true);
};
const closeModalHandler = () => {
setIsOpen(false);
};
return (
<>
<OrderCancelBtn onClick={openModalHandler}>주문취소</OrderCancelBtn>
<ConfirmCancelModal isOpen={isOpen} onClose={closeModalHandler}>
전체 상품에 대한 주문이 취소됩니다.
</ConfirmCancelModal>
</>
);
}
3️⃣
OrderCancelBtn 는 버튼 컴포넌트 입니다.
일반적으로 쓰는 Button onClick 처럼 onClick 이벤트를 추가했더니 읽지를 못하더라구요.
OrderCancelBtn 에 onClick props 를 추가해줍니다.
import styled from "styled-components";
function OrderCancelBtn({ disabled, onClick, children }) {
return <StyledButton disabled={disabled} onClick={onClick}>{children}</StyledButton>;
}
const StyledButton = styled.button`
font-family: "Noto Sans KR", sans-serif;
border-radius: 4px;
border: 1px solid #505050;
outline: none;
background-color: #FFFFFF;
cursor: pointer;
font-size: 16px;
font-style: normal;
line-height: normal;
margin-left: auto;
padding: 8px 19px;
color: #505050;
font-weight: 400;
&:active,
&:hover,
&:focus {
background: #e1e4e6;
border: 1px solid #191919;
}
`;
export default OrderCancelBtn;
😑 그럼 useRef 는 언제..?
이전에도 이와 비슷한 모달창을 구현했었는데, 그때는 모달 영역 밖 클릭 시 닫히게 하기 위해 useRef 를 이용했습니다.
그때 사용했던 코드를 잠깐 보면
import styles from './nav.module.css'
import {useEffect, useRef, useState} from "react";
function App() {
// isModalOpen이 true 이면 모달창 열림
const [isModalOpen, setIsModalOpen] = useState(false);
// navModal 영역 밖 클릭 시 모달창 닫기
const modalRef = useRef();
useEffect(()=>{
const handleClickOutside=(e)=>{
// 모달이 open 상태이고
// 모달 밖 영역 클릭 시
if (isModalOpen && !modalRef.current.contains(e.target)) {
setIsModalOpen(false);
};
window.addEventListener("click", handleClickOutside);
return()=>{
window.removeEventListener("click", handleClickOutside);
};
}, [isModalOpen]);
return (
<>
<div className={styles.navigation}>
<button
className={styles[`hamburger-menu`]}
onClick={(e) => {
e.stopPropagation();
setIsModalOpen(true);
}}
/>
</div>
<div ref={modalRef}>
{
isModalOpen && (<Modal />)
}
</div>
</>
);
}
const Modal = () => {
return (
<div className={styles[`nav-modal`]}>
<div className={styles.tag}>
<div className={styles.tagList}>태그목록</div>
<ul>
<li>전체보기 <p>(50)</p></li>
<li>JavaScript <p>(12)</p></li>
<li>Algorithm <p>(26)</p></li>
<li>SpringBoot <p>(4)</p></li>
</ul>
</div>
<div className={styles.link}>
<div className={styles.linkList}>링크</div>
<ul>
<li>브런치 스토리</li>
<li>깃허브</li>
</ul>
</div>
</div>
);
}
export default App;
✅ useRef 를 이용해 modal 영역 지정
✅ useEffect 로 handleClickOutside 발생 시 모달 닫기
이렇게 했었군요.
사실 저렇게 코드를 작성했을때 하나 해결 못한 것이 있었습니다.
모달이 올라올 때 배경을 어둡게 주고 싶었는데 그럼 어두운 전체 화면 위에 모달을 올리는 형식이었고, 그럼 모달의 영역이 어두운 전체화면이 되는 거라고 생각했습니다.
그럼 모달의 영역이 전체화면으로 잡혀 useRef 를 못쓰게 되기 때문에 모달에 그림자를 짙게 주는 것으로 만족했었습니다.
그럼 전체 영역을 미리 잡아두고 isOpen이 true 상태가 되면 어두워지게 상태관리를 하면 되는 것 아니냐? 하실 수 있지만 그때 코드가 조금이라도 길어지는 것을 극도로 싫어하기도 했고(게다가 useRef, event bubbling 등 삽질을 많이 해 지친 상태였습니다.), 화면이 어두워지는 것과 모달을 싸잡아서 컴포넌트화 하고 싶었어요.
이번에 다시 모달을 만들 기회가 있어 만들어 봤는데 개인적으론 useRef, useEffect 등 사용하는 것 보다 깔끔한 것 같아 좋습니다.
다음번에 시간이 되면 useRef 를 이용해서 다 하지 못한 과제를 해봐야겠군요.