Yes -> modal 보여줌, no -> modal 안 보여줌. 대신 전역적으로 관리해줄 뿐.
우리 스크림도르 사이트에서는 모달을 굉장히 많이 사용한다. modal을 만들어주는 다양한 라이브러리가 있는 것을 알고 있지만 createPortal
을 사용해서 직접 구현해보고 싶었다.
시작하기에 앞서 모달 구현보다는 관리하는 것이 더 어려웠다. 헷갈렸고. 이 점을 주의하면서 따라오면 한다.
react는 id = 'root'
인 div 하나에 jsx파일에서 만들어낸 모든 컴포넌트들을 끼워넣는다. public폴더의 index.html
에서 div하나 밖에 없는 이유이다. 하지만 이렇게 하나의 div에서는 다양한 UI를 표현하는데에 제약이 있을 수밖에 없다. 가장 대표적인 것이 현재의 계층 구조를 탈피해서 무엇인가를 보여주고 싶을 때이다. (모달이라든가 alert 창)
그래서 사용하는 것이 createPortal
이다. 사용방법은 간단하다.
const ModalPortal = ({ children }) => {
return ReactDOM.createPortal(children, document.getElementById('modal'));
};
이 한 줄의 코드로 modal
이라는 이름의 div를 생성해냈다. 이제 우리가 children
으로 넘겨주는 컴포넌트들은 root
가 아니라 modal
이라는 div에서 나타날 것이다.
이 ModalPortal을 바로 사용하지는 않고 몇 가지 처리를 좀 해주었다.
// Modal.js
const Modal = ({ children, closeModal, isSolid }) => {
// 모달 닫을 수 있는 로직 모아둔 hook
useModalClose(closeModal, isSolid);
return (
<ModalPortal>
<ModalBackground className="modalspace">{children}</ModalBackground>
</ModalPortal>
);
};
// useModalClose.js
const useModalClose = (closeModal, isSolid) => {
// ESC key 누르면 닫기
useEffect(() => {
const closeWithESC = e => {
if (e.key === 'Escape') {
closeModal();
}
};
isSolid || window.addEventListener('keydown', closeWithESC);
return () => window.removeEventListener('keydown', closeWithESC);
}, []);
// modal 창 열리면 외부 scroll 금지
useEffect(() => {
document.body.style.cssText = "overflow: 'hidden'";
return () => (document.body.style.cssText = "overflow :'unset'");
}, []);
return;
};
useModalClose
는 커스텀 훅으로 두가지 기능을 한다.
1. esc 키를 누르면 모달창이 꺼진다. 이는 closeModal
이라는 함수를 prop으로 받아 실행한다.
2. 모달이 켜져있는 동안 바깥 화면에서 scroll을 못하게 막는다.
위에서 만들어낸 Modal
컴포넌트를 이제 사용하기만 하면 된다. 위에서 언급했듯이 모달의 핵심은 state를 통한 true / false로 보여주고 / 안 보여주고를 결정하는 것이다. 이걸 이번 프로젝트에서는 redux를 통해서 관리했다.
page의 기본틀이 되는 BaseTemplate
에 다음 컴포넌트를 넣어주었다.
return (
<MainContainer>
<MainBackground withoutNav={withoutNav}>
{withoutNav || <Navigation />}
{children}
{widthOverMobileLandScape ||
(mobileEventState.roomListMobile && (
<Blind
onClick={() => {
dispatch(triggerRoomListMobile());
}}
/>
))}
{withoutNav || <RightSideBar />}
<ModalTemplate /> // 여기!
</MainBackground>
</MainContainer>
);
// ModalTemplate
const dispatch = useDispatch();
const modalState = useSelector(state => state.modal);
return (
{modalState.profileUpdate && (
<Modal closeModal={() => dispatch(triggerProfileUpdateModal())}>
<ProfileUpdateModalContent />
</Modal>
)}
)
modalState.profileUpdate
가 true일 때, 모달을 보여주는 구조이다. triggerProfileUpdateModal
는 boolean을 이전과 반대로 바꿔주는 역할을 한다.
ProfileUpdateModalContent
에서도 redux로 dispatch 함수를 불러와서 closeModal
을 만들어주면 창을 닫을 수 있다.
위의 구조까지 만들었으면 모달창이 나타나고 사라지는 작업을 완료했다. 하지만 뭔가 어색하다. modal 뒤에 검은색 배경이 있어야 진짜 모달처럼 보일 것 같다.
이 뒤 배경을 Dimmed
라는 컴포넌트로 만들어주었다.
Header
까지도 포함해서 전체 화면을 덮어주어야해서 App.js
에서 구현해주었다. position : absolute
로 구현해주면 되는데, 이때 조금 문제가 생긴다.
만약 modal위에 또 modal을 띄워야 한다면?? 흔히 있는 구조는 아니었지만 우리 사이트에서는 이런 UI를 만들어야했기 때문에 고민이 되었다.
결론적으로 말하면 redux의 modal state를 가져와서 true (모달 창이 켜진 개수)를 Dimmed
로 넘겨주었다.
// App.js
/* 현재 열린 모달*/
/* 현재 열려 있는 모달 갯수에 따라 다른 dim 제공 */
// modal state 내에서 true인 state가 2개인 순간 이미 하나가 열렸다는 의미니깐.
const modalState = useSelector(state => state.modal);
let modalCount = Object.values(modalState).filter(
state => state === true,
).length;
// Dimmed styles
// 전체화면이지만 modal container 밖, 검은색 색깔 칠해진 박스
export const Dimmed = styled.div`
width: 100%;
height: 100%;
background-color: ${({ modalCount }) =>
modalCount > 2 ? 'rgb(0, 0, 0, 0.3)' : 'rgb(0, 0, 0, 0.4)'};
z-index: ${({ modalCount }) => modalCount * 100};
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
`;
모달창이 새로 열릴 때마다 z-index를 계산해서 덮어씌우니깐 새로 모달이 열릴 때마다 z-index가 100씩 높아져서 열릴 것이다.
마지막으로 모달창이 닫힐 때, 좀 더 자연스럽게 닫히면 좋겠다고 생각해서 React-Transition-Group
을 사용해서 애니메이션 효과를 주었다.
위의 ModalTemplate
파일을 조금 바꿔주었다.
<CSSTransition
in={modalState.profileUpdate}
timeout={300}
classNames="modal"
unmountOnExit
onEnter={() => dispatch(triggerProfileUpdateModal)}
onExited={() => dispatch(triggerProfileUpdateModal)}
>
<Modal closeModal={() => dispatch(triggerProfileUpdateModal())}>
<ProfileUpdateModalContent />
</Modal>
</CSSTransition>
전역 스타일 파일에서도 다음 코드를 넣어주었다.
// modal transition exit 만
.modal-exit {
opacity: 1;
}
.modal-exit-active {
opacity: 0;
transform: scale(0.4);
transition: opacity 200ms, transform 200ms;
}
.dimmed-exit {
opacity: 1;
transform : translate(0);
}
.dimmed-exit-active {
opacity: 0;
transform : scale(0.3);
transition: opacity 200ms transform 200ms;
}
이렇게 완성!