08.30.2023
1. 처음 디렉토리 구조
- root
-- src
--- A component
---- Modal
--- B component
---- Modal
--- C component
---- Modal
모달컴포넌트가 중복된다. 그래서 목표는 Modal을 컴포넌트로 재사용할 수 있게 리팩토링하는 것이다.
목표하고 있는 디렉토리 구조는 다음과 같다.
- root
-- src
--- Modal
--- A component
---- Content
--- B component
---- Content
--- C component
---- Content
Content는 Modal 안에 들어갈 컴포넌트이다.
2. Modal Componentization
Modal에서는 기본적으로 Parent로부터 setModalOpen과 Content를 전달받아야한다.
또한, Modal의 크기는 항상 같지 않으므로 width, height까지 props로 전달받는다.
Modal.tsx
import { useRef } from 'react';
import styles from './Modal.module.css';
interface Props {
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
Content: React.ReactElement;
mywidth: string;
myheight: string;
}
function Modal({
setModalOpen,
Content,
mywidth,
myheight,
}: Props): JSX.Element {
const bgRef = useRef<HTMLDivElement>(null);
const size = { width: mywidth, height: myheight };
const handleClickBackground = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === bgRef.current) {
closeModal();
}
};
const closeModal = () => {
setModalOpen(false);
document.body.style.overflow = 'unset';
};
return (
<div
ref={bgRef}
className={styles.background}
onClick={handleClickBackground}
>
<div className={styles.container} style={size}>
<button className={styles.closebtn} type="button" onClick={closeModal}>
x
</button>
<div className={styles.content_wrapper}> {Content} </div>
</div>
</div>
);
}
export default Modal;
3. Button 안에서 Modal을 호출했을 때의 Issue
A component는 좋아요 버튼이다. 좋아요 버튼을 클릭했을 때 모달이 띄워지게 하고 싶었는데 Modal이 켜지기만 하고 꺼지지는 않았다.
console.log를 통해 디버그해보니 state가 안바뀌는 것을 확인할 수 있었다.
Before_Likebutton.tsx
import { useState } from 'react';
import Modal from 'components/Modal/Modal';
import ModalContent from './ModalContent/ModalContent';
import EmptyHeart from '../../assets/images/emptyheart.png';
import FullHeart from '../../assets/images/fullheart.png';
import styles from './LikeButton.module.css';
function LikeButton(): JSX.Element {
const [imgSrc, setImgSrc] = useState<string>(EmptyHeart);
const [modalOpen, setModalOpen] = useState<boolean>(false);
const onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
setImgSrc(imgSrc === EmptyHeart ? FullHeart : EmptyHeart);
setModalOpen(true);
document.body.style.overflow = 'hidden';
console.log(modalOpen);
};
return (
<button type="button" className={styles.container} onClick={onClick}>
<img alt="add" src={imgSrc} className={styles.img} />
{modalOpen && (
<Modal
setModalOpen={setModalOpen}
Content={<ModalContent />}
mywidth="500px"
myheight="500px"
/>
)}
</button>
);
}
export default LikeButton;
그 이유는 HTML에서 nested interactive elements를 허용하지 않기 때문이다.
Modal 컴포넌트는 닫기 버튼 구현을 위해 button element가 있었기 때문에 예상치 못한 오류가 나고 state가 제대로 바뀌지 않았던 것이다.
이 문제를 해소하기 위해 Modal이 쓰인 구문을 button바깥쪽으로 빼내주었다.
After_Likebutton.tsx
import { useState } from 'react';
import Modal from 'components/Modal/Modal';
import ModalContent from './ModalContent/ModalContent';
import styles from './LikeButton.module.css';
function LikeButton(): JSX.Element {
const [modalOpen, setModalOpen] = useState<boolean>(false);
const onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
setModalOpen(true);
document.body.style.overflow = 'hidden';
};
return (
<div>
<button type="button" className={styles.container} onClick={onClick} />
{modalOpen && (
<Modal
setModalOpen={setModalOpen}
Content={<ModalContent />}
mywidth="500px"
myheight="500px"
/>
)}
</div>
);
}
export default LikeButton;
* document.body.style.overflow = 'hidden'의 역할!
이 코드는 Modal 안의 document.body.style.overflow = 'unset'과 세트이다.
Modal이 띄워져있고 Modal 내에서 스크롤이 필요한 경우 백그라운드의 스크롤 이벤트를 막는 것이 UX에 훨씬 좋다.
Modal이 true로 세팅될 때는 웹페이지의 최상단 dom(body)의 overflow를 'hidden'으로 바꿔 스크롤을 막고, Modal이 닫히면 'unset'으로 바꿔 스크롤 이벤트를 다시 생성한다.
4. Link / a 태그 안의 Modal
마지막(이길바라는) 문제가 생겼다. 현재 Linkbutton 컴포넌트는 Link로 감싸여져있다.
Modal을 열 때는 정상적으로 열리지만 Modal이 닫이면서 navigation event가 trigger되며 Link에 연결되어있는 페이지로 넘어가는 것이다.
해결방법은 간단하게도 Modal에서 Button입력이 들어올 때 preventDefault()를 해주면 된다.
Modal.tsx
import { useRef } from 'react';
import styles from './Modal.module.css';
interface Props {
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
Content: React.ReactElement;
mywidth: string;
myheight: string;
}
function Modal({
setModalOpen,
Content,
mywidth,
myheight,
}: Props): JSX.Element {
const bgRef = useRef<HTMLDivElement>(null);
const size = { width: mywidth, height: myheight };
const handleClickBackground = (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
if (e.target === bgRef.current) {
setModalOpen(false);
document.body.style.overflow = 'unset';
}
};
const closeModal = () => {
e.preventDefault();
setModalOpen(false);
document.body.style.overflow = 'unset';
};
return (
<div
ref={bgRef}
className={styles.background}
onClick={handleClickBackground}
>
<div className={styles.container} style={size}>
<button className={styles.closebtn} type="button" onClick={closeModal}>
x
</button>
<div className={styles.content_wrapper}> {Content} </div>
</div>
</div>
);
}
export default Modal;
만약 e.preventDefault()를 handleClickButton안의 if문 안으로 집어넣으면, Modal창 밖이 아닌 안을 클릭할 때 이벤트 버블링 일어나게 되므로 바깥으로 빼준다.
마지막으로 리팩토링까지 끝내면 Modal Refactoring이 모두 끝나게 된다.
5. 최종 Modal.tsx
Modal.tsx
import { useRef } from 'react';
import styles from './Modal.module.css';
interface Props {
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
Content: React.ReactElement;
mywidth: string;
myheight: string;
}
function Modal({
setModalOpen,
Content,
mywidth,
myheight,
}: Props): JSX.Element {
const bgRef = useRef<HTMLDivElement>(null);
const size = { width: mywidth, height: myheight };
const closeModal = (
e: React.MouseEvent<HTMLDivElement> | React.MouseEvent<HTMLButtonElement>
) => {
e.preventDefault();
if (e.target === bgRef.current || e.target instanceof HTMLButtonElement) {
setModalOpen(false);
document.body.style.overflow = 'unset';
}
};
return (
<div
ref={bgRef}
className={styles.background}
onClick={(e) => closeModal(e)}
>
<div className={styles.container} style={size}>
<div className={styles.closebtn_wrapper}>
<button
className={styles.closebtn}
type="button"
onClick={(e) => closeModal(e)}
>
x
</button>
</div>
<div className={styles.content_wrapper}> {Content} </div>
</div>
</div>
);
}
export default Modal;
'Project > Time Map' 카테고리의 다른 글
| [문제 해결] 리액트에서 최신 상태 보장 방법 (0) | 2023.09.24 |
|---|---|
| 메모이제이션이란?(useMemo, useCallback) in React.js (0) | 2023.06.26 |