개발 이야기/frontend

React Portals와 Refs 사용법: 모달 컴포넌트 최적화하기

thisisamrd 2024. 9. 13.

 

React 프로젝트에서 모달 컴포넌트를 구현할 때, UI의 구조를 더 깔끔하게 유지하고 접근성을 높이기 위해 React PortalsRefs를 활용하는 방법을 알아보겠습니다. 이번 글에서는 강좌에서 소개된 코드를 바탕으로 이 두 가지 기능을 효과적으로 사용하는 방법을 설명합니다.

 

 

 

1. React Portals의 필요성

 

React에서 모달을 구현할 때, 모달 컴포넌트가 다른 요소들 위에 시각적으로 떠 있도록 설정하는 경우가 많습니다. 일반적으로 모달 컴포넌트는 div 태그 안에 중첩되어 출력됩니다.

 

div 태그 안에 중첩되어 출력되는 모달(dialog)

 

하지만 이렇게 되면 스타일 문제나 접근성 문제를 초래할 수 있습니다. 이때 React Portals를 사용하면 모달을 실제 DOM의 더 상위 레벨, 예를 들어 body 바로 아래에 렌더링할 수 있습니다. 이렇게 하면 스타일링 및 접근성 문제를 해결할 수 있습니다.

 

 

 

 

 

 

2. React Portals란 무엇인가?

React Portals는 특정 컴포넌트의 렌더링 위치를 조정할 수 있는 기능입니다. 일반적으로 React 컴포넌트는 그 부모 컴포넌트의 DOM 트리 내부에 렌더링되지만, Portals를 사용하면 특정 컴포넌트의 렌더링 결과를 DOM 트리의 다른 위치에 삽입할 수 있습니다. 이는 모달처럼 화면 전체를 덮는 오버레이 요소를 구현할 때 유용합니다.

 

예를 들어, 모달 컴포넌트를 페이지의 최상위 레벨에 위치시키고 싶다면 createPortal 함수를 사용하여 이를 구현할 수 있습니다. 이 함수는 JSX 코드와 해당 코드를 렌더링할 DOM 요소를 인수로 받습니다.

 

 

 

 

3. React Portals 사용 방법

 

React Portals를 사용하려면 React DOM 라이브러리에서 제공하는 createPortal 함수를 사용해야 합니다. 이 함수는 두 가지 인수를 받습니다. 첫 번째는 렌더링할 JSX 코드이고, 두 번째는 해당 코드를 렌더링할 DOM 요소입니다.

 

 

예를 들어, 아래와 같은 코드를 통해 모달을 특정 div 요소에 렌더링할 수 있습니다.

저는 index.html에 이미 아래와 같이 작성해주었고, 해당 id에 연결할 예정입니다.

 

  <div id="modal"></div>

 

 

 

 

그리고 createPortal을 react-dom에서 불러와서 아래와 같이 작성하였습니다. 제가 만든 모달은 dialog이며, 이를 modal이라는 dom에 위치시키려고 합니다.

 

import { createPortal } from "react-dom";

const ResultModal = forwardRef(function ResultModal(
  { targetTime, remainingTime, onReset },
  ref
) {
  const dialog = useRef();

  useImperativeHandle(ref, () => {
    return {
      open() {
        dialog.current.showModal();
      },
    };
  });

 

 

{remainingTime <= 0 ?

You lost

:

Your Score: {score}

}

 

 

 

그리고 return 문에 createPortal을 적어주고, 맨 하단 컴포넌트에 쉼표를 붙여준 뒤 두번째 인자로 modal 의 dom값을 넣어줍니다.

 

  return createPortal(
      <div>
        // 생략
      </div>
  ,
    document.getElementById("modal")
  );
});

 

 

 

위 코드에서 createPortal 함수는 모달 컴포넌트를 id="modal"을 가진 div 요소 안에 렌더링합니다. 

 

이는 모달이 콘텐츠 구조의 깊은 중첩에서 벗어나, 더 상위 DOM 레벨에서 출력되도록 합니다.

 

상위 DOM 레벨인 modal 레벨에 중첩된 dialog

 

 

 

 

4. Refs와 useImperativeHandle로 모달 제어하기

 

모달의 상태를 제어하기 위해 React RefsuseImperativeHandle을 사용할 수 있습니다. 이 기능을 통해 모달을 열고 닫는 메서드를 외부에서 호출할 수 있습니다. 예를 들어, TimerChallenge 컴포넌트에서 모달을 제어하기 위해 아래와 같은 코드가 사용됩니다.

 

 

 

Refs란?

Refs는 React에서 특정 DOM 요소나 클래스 컴포넌트에 직접 접근할 수 있는 방법을 제공합니다. 주로 DOM 조작이나 포커스 관리, 애니메이션 트리거 등에 사용됩니다.

 

 

useImperativeHandle이란?

useImperativeHandle은 React Hook 중 하나로, 부모 컴포넌트가 자식 컴포넌트의 인스턴스 메서드를 사용할 수 있게 해줍니다. 이를 통해 부모 컴포넌트는 자식 컴포넌트의 내부 상태나 메서드를 외부에서 제어할 수 있습니다. 위 예시에서, 모달을 열기 위한 open 메서드를 정의하고 있습니다.

 

 

TimerChallenge 컴포넌트의 예제 코드

TimerChallenge 컴포넌트는 ResultModal 컴포넌트를 참조(Ref)하고, 사용자가 버튼을 클릭할 때마다 모달을 열거나 닫도록 제어합니다.

 

import { useState, useRef } from "react";
import ResultModal from "./ResultModal";

export default function TimerChallenge({ title, targetTime }) {
  const timer = useRef();
  const dialog = useRef();
  const [timeRemaining, setTimeRemaining] = useState(targetTime * 1000);
  const timerIsActive = timeRemaining > 0 && timeRemaining < targetTime * 1000;

  if (timeRemaining <= 0) {
    clearInterval(timer.current);
    dialog.current.open();
  }

  function handleReset() {
    setTimeRemaining(targetTime * 1000);
  }

  function handleStart() {
    timer.current = setInterval(() => {
      setTimeRemaining((prevTimeRemainig) => prevTimeRemainig - 10);
    }, 10);
  }

  function handleStop() {
    dialog.current.open();
    clearInterval(timer.current);
  }

  return (
    <>
    {title}
    {targetTime} second{targetTime > 1 ? "s" : ""}

    {timerIsActive ? "Stop" : "Start"} Challenge
    {timerIsActive ? "Time is running..." : " Timer inactive"}
    </>
  );
}

 

 

 

위 코드에서 ResultModal 컴포넌트는 ref를 통해 제어되고, 사용자가 타이머를 중지하거나 시간이 다 되었을 때 모달을 보여주도록 합니다. 이렇게 Refs와 Portals를 결합하여 더 나은 사용자 경험을 제공할 수 있습니다.

댓글