본문 바로가기

개발일지/디스턴스

React에서 모달창 구현하기 (with Ref& createPortal)

반응형

목차

  1. Ref란 무엇인가
  2. 모달창 구현 화면
  3. 모달창 구현하기
  4. 기타 마무리 - createPortal

1.  Ref란 무엇인가

Ref가 무엇인지 자세히 알고 싶다면 전에 작성했던 글을 참조하길 바란다.

 

React Ref 개념과 사용하는 방법

목차 Ref란 무엇인가 Ref 사용 시에 주의해야 하는 점 Ref는 언제 사용해야 할까 Ref 사용하는 방법 1. Ref란 무엇인가 Ref를 한 줄로 간단하게 설명하면 아래와 같이 이야기 할 수 있다. Ref는 render 메

sol-aftercoding.tistory.com

 

 

2.  모달창 구현 화면

홈 화면에서 카드를 클릭하면 모달창이 뜨도록 구현해보려고 한다.

 

모달 구현 화면

 

 

3.  모달창 구현하기

위의 화면을 구현하기 위해서 홈 페이지(HomeIndexPage.jsx)와 모달 컴포넌트(Modal.jsx) 파일을 만들었다.

CSS는 생략하고 설명해보자면, Dialog 태그로 모달창을 구현하였고 ref로 해당 태그를 참조하였다.

import { useRef, forwardRef, useImperativeHandle } from "react";
import { createPortal } from "react-dom";

const Modal = 
  (
    { children, buttonLabel, buttonClickHandler, buttonColor = "#FF625D" },
  ) => {
    const dialog = useRef();
    
    const handleCloseModal = () => {
      dialog.current.close();
    };

    return (
      <>
        <StyledDialog ref={dialog}>
          <CloseButton
            onClick={handleCloseModal}
            src={"/assets/cancel-button-gray.svg"}
            alt="Close"
          />
          {children}
          {/* <button/> 태그 위치 */}
        </StyledDialog>
      </>
    )
  };

export default Modal;

 

그리고 Home 화면에서 Modal 컴포넌트를 가져와서 ref를 연결해서 모달을 열고 닫으면 된다.

UI 구조상 홈화면의 프로필 컴포넌트를 클릭할 때 모달이 열려야 하므로 Profile 컴포넌트의 클릭 이벤트 안에서 모달을 열면 된다.

const HomeIndexPage = () => {
  const profileModal = useRef();
  
  const handleSelectProfile = (profile) => {
    setSelectedProfile(profile);
    profileModal.current.open();
  };

return (
    <>
        <Profile
        key={index}
        profile={profile}
        onClick={() => handleSelectProfile(profile)}
        />
        <Moda ref={profileModal}></Moda>
    </>
    );
}

 

 

완성된 것 같지만 이렇게 구현하면 오류가 난다.

빠뜨린 것이 2가지 있기 때문이다.

첫 번째, ref를 자식 컴포넌트로 전달할 때에는 자식 컴포넌트를 forwardRef로 감싸줘야 한다.

 

Modal 컴포넌트(자식)를 Home 페이지(부모)에서 호출해서 사용하고 있기 때문에

Modal 컴포넌트에 forwardRef를 사용해줘야 한다.

forwardRef의 첫 번째 인자는 props를 넣고, 두 번째 인자에 전달 받을 ref를 넣는다.

 

두 번째, useImperativeHandle을 이용하여 부모 컴포넌트에 자식 컴포넌트의 핸들을 노출시켜서 모달을 열고 닫아야 한다.

기본적으로 컴포넌트는 자식 컴포넌트의 DOM 노드를 부모 컴포넌트에 노출하지 않는다.

때문에 해당 훅을 사용할 때에는 신중하게 사용해야 하며 prop으로 표현할 수 있는 내용이면 되도록 prop을 사용하는것이

리액트 생명주기에 적합하다.

하지만 지금 상황에서는 모달을 열고 닫는 함수를 노출하여 모달을 핸들링할 수 있도록 만들어줘야 하므로

useImperativeHandle에 open과 close 2개의 함수를 선언해 놓았다.

import { useRef, forwardRef, useImperativeHandle } from "react";
import { createPortal } from "react-dom";

const Modal = forwardRef(
  (
    { children, buttonLabel, buttonClickHandler, buttonColor = "#FF625D" },
    ref
  ) => {
    const dialog = useRef();
    
    useImperativeHandle(ref, () => {
      return {
        open() {
          dialog.current.showModal();
        },
        close() {
          dialog.current.close();
        },
      };
    });

    const handleCloseModal = () => {
      dialog.current.close();
    };

    return (
      //컴포넌트 내용
  }
);

export default Modal;

 

 

4.  기타 마무리 - createProtal

모달창은 UI 구조상 가장 최상단 레이어에 위치한다. 따라서 DOM 트리에 상단에 위치시켜야 자연스럽다.

createPortal을 사용하면 모달 컴포넌트의 DOM 상의 위치를 포털을 여는 것처럼 이동시킬 수 있다.

const Modal = forwardRef(
  (
    { children, buttonLabel, buttonClickHandler, buttonColor = "#FF625D" },
    ref
  ) => {
    return createPortal(
      <>
        //컴포넌트 내용
      </>, document.getElementById('modal'));
   }
);

 

사용 방법은 간단하다. return문 안에 적어놓은 컴포넌트 내용을 createPortal로 감싸주면 된다.

그리고 두번째 인자에 이동시킬 위치를 적어주면 되는데 나는 index.html 문서의 body태그 안에서

가장 상단에 id 값이 modal인 div태그를 추가해주었다.

<body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="modal"></div>
    <div id="root"></div>
</body>

이렇게 해서 모달 창이 root보다 상단에 위치하도록 이동시켜주었다.


참고로 ref는 state와 다르게 값을 변경해도 컴포넌트 리렌더링을 촉발하지 않는다.

따라서 ref는 컴포넌트의 시각적 출력에 영향을 미치지 않는 정보를 저장하는데 적합하다.

시각적으로 보여지는 값은 state로, 안 보여지지만 참조가 필요한 값은 ref로 처리하면 되겠다.