[React] Modal 만들기

Ell!·2021년 12월 23일
2

react

목록 보기
19/28

Modal UI

한 줄 핵심

Yes -> modal 보여줌, no -> modal 안 보여줌. 대신 전역적으로 관리해줄 뿐.

우리 스크림도르 사이트에서는 모달을 굉장히 많이 사용한다. modal을 만들어주는 다양한 라이브러리가 있는 것을 알고 있지만 createPortal을 사용해서 직접 구현해보고 싶었다.

시작하기에 앞서 모달 구현보다는 관리하는 것이 더 어려웠다. 헷갈렸고. 이 점을 주의하면서 따라오면 한다.

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을 못하게 막는다.

Redux

위에서 만들어낸 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

마지막으로 모달창이 닫힐 때, 좀 더 자연스럽게 닫히면 좋겠다고 생각해서 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;
  }

이렇게 완성!

profile
더 나은 서비스를 고민하는 프론트엔드 개발자.

0개의 댓글