이전글에서 useModal
훅을 만들어서 모달을 여러개 띄웠었다.
const App = () => {
const { Modal, closeModal, isOpen, openModal } = useModal();
return (
<>
<button onClick={openModal}></button>
<Modal
isOpen={isOpen}
onClose={closeModal}
>
<CategoryModal />
</Modal>
</>
);
};
export default App;
이런식으로 띄웠었는데 쓸때마다 isOpen
과 onClose
를 넣는게 정말 번거로웠다.
그래서 useModal
훅을 조금 개선하기로 했다.
그리고 열릴때와 닫힐때 애니메이션도 추가하기로 했다.
현재 스타일링을 위해 styled-components를 사용하고있다.
타입스크립트에서 사용하려면 npm으로 설치를 한다면 아래 두개를 설치한다.
▶︎ npm i styled-components
▶︎ npm i -D @types/styled-components
그리고서 우선 useModal
훅부터 수정을 시작한다.
import ModalFrame from 'components/Common/Modal';
import React, { useState } from 'react';
const useModal = () => {
const [isOpen, setIsOpen] = useState(false);
const closeModal = () => {
const showModal = () => {
setIsOpen(true);
};
const Modal = ({ children }: { children: React.ReactNode }) => (
<ModalFrame
isOpen={isOpen}
onClose={closeModal}
>
{children}
</ModalFrame>
);
return { Modal, showModal, closeModal };
};
export default useModal;
전에 바깥으로 isOpen
, onClose
를 받았었는데 다시 생각해보니 그럴 필요가 없었다.
useModal 훅에서 전부 처리해주면 되는데 귀찮은짓을 했었다.
이렇게만 사용해도 충분하지만 모달이 열릴때, 닫힐때 애니메이션을 넣고싶었다.
그래서 수많은 뻘짓 끝에 맘에드는 코드를 작성했다.
우선 styled-components
를 사용해 css에서 사용할 keyframes
를 외부에 작성해줄것이다.
// animations.ts
import { css } from 'styled-components';
export const Up100 = keyframes`
0% {bottom: -100%; }
100% {bottom: 0; }
`;
export const Down100 = keyframes`
0% {bottom: 0%; }
100% {bottom: -100%; }
`;
이렇게 선언한 애니메이션은 Modal에서 사용할것이다.
그리고 이제 Modal 컴포넌트는 열릴때, 닫힐때 애니메이션과 unuount
를 알리는 prop을 추가로 받는다.
unmount
가 됐는지 알 필요가 있는 이유는
컴포넌트가 unmount
되기전 닫히는 애니메이션을 실행 후 unmount
시키기 위해서다.
아래 코드를 보자.
import ModalFrame from 'components/Common/Modal';
import React, { useState } from 'react';
import { Keyframes } from 'styled-components';
const useModal = (
openAnime?: Keyframes, // 모달 열릴시 애니메이션
closeAnime?: Keyframes, // 모달 닫힐시 애니메이션
animeTimeMs = 300, // 애니메이션 실행시간
) => {
const [isOpen, setIsOpen] = useState(false);
const [isUnmount, setIsUnmount] = useState(false);
const closeModal = () => {
setIsUnmount(true);
setTimeout(() => {
setIsOpen(false); // animeTimeMs의 시간후 모달 닫음
}, animeTimeMs);
};
const showModal = () => {
setIsUnmount(false)
setIsOpen(true);
};
const Modal = ({ children }: { children: React.ReactNode }) => (
<ModalFrame
isOpen={isOpen}
onClose={closeModal}
isUnmount={isUnmount}
openAnimation={openAnime}
closeAnimation={closeAnime}
>
{children}
</ModalFrame>
);
return { Modal, showModal, closeModal };
};
export default useModal;
위 코드는 매개변수로 애니메이션과 애니메이션 시간을 받는다.
그러고서 Modal, showModal, closeModal을 리턴해준다.
이제 Modal을 사용하고싶으면 그냥 아래처럼 쓰면 된다
const {Modal, showModal, closeModal}
<button onClick={showModal}> open Modal </button>
<Modal>{넣고싶은 모달}</Modal>
훨씬 간단해졌다.
애니메이션도 들어가므로 Modal 컴포넌트도 수정이 필요하다.
import React, { MouseEvent } from 'react';
import { ModalInner, ModalWrapper } from './Modal.style';
import ModalPortal from './ModalPortal';
import { Keyframes } from 'styled-components';
interface ModalProps {
openAnimation?: Keyframes;
closeAnimation?: Keyframes;
isUnmount: boolean;
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}
const Modal = ({
openAnimation,
closeAnimation,
isUnmount,
isOpen,
onClose,
children,
}: ModalProps) => {
const handleClickInnerModal = (e: MouseEvent<HTMLDivElement>) => {
// ModalWrapper로 이벤트 전파 방지
e.stopPropagation();
};
return (
<>
{isOpen && (
<ModalPortal>
<ModalWrapper onClick={onClose}>
<ModalInner
onClick={onClose}
openAnimation={openAnimation}
closeAnimation={closeAnimation}
isUnmount={isUnmount}
>
<div onClick={handleClickInnerModal}>{children}</div>
</ModalInner>
</ModalWrapper>
</ModalPortal>
)}
</>
);
};
export default Modal;
그리고 애니메이션도 입력받아야 하므로 styled-components로 선언한 ModalInner
도 수정한다.
import styled, { Keyframes } from 'styled-components';
interface TModalInner {
openAnimation?: Keyframes;
closeAnimation?: Keyframes;
isUnmount: boolean;
}
export const ModalWrapper = styled.div`
position: fixed;
left: 0;
top: 0;
z-index: 1050;
width: 100%;
height: 100%;
background-color: #0b0b0bb8;
`;
export const ModalInner = styled.div<TModalInner>`
position: relative;
width: 100%;
height: 100%;
max-width: ${({ theme }) => theme.deviceSizes.mobile};
margin: 0 auto;
animation: ${({ isUnmount, openAnimation, closeAnimation }) =>
isUnmount ? closeAnimation : openAnimation}
0.3s ease-in-out;
`;
이렇게해서 이제 훨씬 간편하게 애니메이션과 함께 쓸 수 있게됐다.
사용은 이렇게 할 수 있다.
import useModal from 'hooks/useModal';
import { Down100, Up100 } from 'styles/animations';
interface CalanderPageProps {}
const CalanderPage = ({}: CalanderPageProps) => {
const { Modal, showModal, closeModal } = useModal(Up100, Down100, 300);
return (
<>
<button onClick={showModal}>click!!</button>
<Modal>
<SelectMonthModal/>
</Modal>
</>
);
};
export default CalanderPage;
SelectMonthModal은 별도로 만든 컴포넌트다.
이렇게 시작, 종료 애니메이션까지 넣고 실행하면 아래처럼 올라갔다 내려갔다 애니메이션이 잘 보이게 된다.
물론 SelectMonthModal에서 또 모달을 부른다면 중첩이 된다.