[React] React Portal

hyeondoonge·2023년 8월 30일
1

React Portal

부모 컴포넌트의 계층 구조 바깥에 있는 DOM노드로 자식을 렌더링할 수 있게하는 기술이다. 동시에 해당 노드는 부모 컴포넌트의 상태에 접근할 수 있다.

React에 createPortal API가 내장되어있고, 이것이 포탈을 만들기 위한 도구이다.

왜 써야할까?

모달이나 토스트와 같은 요소는 보통 뷰포트를 기준으로 레이아웃 하곤한다.

화면에서 부모 요소와 상관없이 톡 튀어나오는 것처럼 보이는 요소가 필요할 수 있는데, 이때 css 상속이나, 부모 스타일 등의 영향으로 css 설정이 번거로워질 가능성이 있다.

포탈을 이용하면 뷰포트를 기준으로 레이아웃하는게 가능해지기 때문에 css 설정이 보다 쉬워질 것이다. 간단한 앱에서는 굳이?라는 생각이 들지만, 대상 요소가 트리 상에 깊이 위치하고 css가 복잡한 경우에는 그냥 Portal로 올려보내서 설정하면 성가신 일이 줄어들 것이다.

CSS 상속이 적용되는 속성에는 visibility, opacity, text-align 등이 있다.

visibility, text-align을 부모에서 설정하면 하위에서 상속받고, 명시적인 설정을 통해 하위 요소의 설정을 변경할 수 있다. 부모의 CSS 상속을 피하기 위해 이렇게 해도되지만, 이를 고려해야한다는 점은 구현을 복잡하게 만든다.

음… position: fixed 또는 position: absolute로 스타일을 설정해주면 원래 문맥 흐름에서 제거되고 뷰포트 기준으로 UI를 설정할 수 있는 것 아닌가?

원래 문맥 흐름에서 제거되는 건 맞는 말이지만 후에 따라오는 말은 상황에 따라 옳지않다.

absolute
요소를 일반적인 문서 흐름에서 제거하고, 페이지 레이아웃에 공간도 배정하지 않습니다. 대신 가장 가까운 위치 지정 조상 요소에 대해 상대적으로 배치합니다. 단, 조상 중 위치 지정 요소가 없다면 초기 컨테이닝 블록을 기준으로 삼습니다. 최종 위치는 toprightbottomleft 값이 지정합니다.
z-index의 값이 auto가 아니라면 새로운 쌓임 맥락을 생성합니다.

만약 부모 요소가 position: relative으로 설정되어있다면, 대상 요소의 position: absolute를 위해 부모 요소의 position을 바꿔야할 것이다. 부모 요소의 position에 맞춰 스타일 해준 하위 요소들과 같이 영향받는 요소들의 스타일을 변경해야하는 어려움이 발생할 수 있다. 뿐만 아니라 부모 요소가 position: absolute가 한개가 아니라 여러개라면…? 줄줄이 바꿔야하는 불상사가 일어날 수도 있다.

이를 적용하는 것은 상위 요소가 position을 자유롭게 적용하는 것을 어렵게 한다.

그럼 position: fixed로 설정하는 것은 어떨까?

fixed
요소를 일반적인 문서 흐름에서 제거하고, 페이지 레이아웃에 공간도 배정하지 않습니다. 대신 뷰포트의 초기 컨테이닝 블록을 기준으로 삼아 배치합니다. 단, 요소의 조상 중 하나가 transformperspectivefilter 속성 중 어느 하나라도 none이 아니라면 (CSS Transforms 명세 참조) 뷰포트 대신 그 조상을 컨테이닝 블록으로 삼습니다. (perspective와 filter의 경우 브라우저별로 결과가 다름에 유의) 최종 위치는 toprightbottomleft 값이 지정합니다.
이 값은 항상 새로운 쌓임 맥락을 생성합니다.

position 속성보다 transform, perspective, filter는 덜 쓰는 속성같긴하다. 그렇지만 위처럼 transform, perspective, filter를 사용을 위해서는 하위 요소에 대상 요소가 툭 튀어나와 보이는 용도의 요소가 있는지를 파악하고 없을때 사용해야할 것이다.

사용 방법

하지만 Portal을 사용한다면 위의 예외사항들을 신경 쓸 이유가 사라질 것이다. 포탈 사용 시, 대상 요소를 위치하고자 하는 곳에 집어넣는다.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
  </head>
  <body>
    <div id="root"></div>
    <div id="modal"></div>
  </body>
</html>
// modal을 배치할 컨테이너 스타일
#modal {
    width: 100%;
    position: fixed;
    top: 0;
    z-index:2;
    display: flex;
    justify-content: center;
    align-items: center;
  }

포스팅을 시작할 때 Portal은 대상 요소를 부모 바깥에 그리게 해주기도 하면서도, React DOM 트리 상 부모의 상태에 접근할 수 있게 해준다고도 했다.

아래는 Portal을 이용해 모달 컴포넌트가 만들어졌고, props를 통해 message를 받아오는 모습이다. 이 message는 부모 컴포넌트가 전달한다. createPortal이 어떻게 사용되는지 그리고 message 상태를 전달하는 것 위주로 보면 되겠다.

export default function Modal({
  message
}: {
  message: string;
}) {
  return createPortal( // react 내장 API
      <div onClick={(event) => event.stopPropagation()}>
        <div className='message'>{message}</div>
        <div className='button'>
          <Button onClick={handleClose}>확인</Button>
        </div>
      </div>,
    document.getElementById('modal') as HTMLElement
  );
}

export default function Page() {
  const {
    template,
    error,
    handleClose
  } = useTemplate();

  return (
    <div>
      {error && <Modal message={error?.message} handleClose={handleClose} />}
     </div>
  );
}

이처럼 Portal을 부모 바깥으로 보내 최상단 계층으로 요소를 옮겨주면 고려해야할 부모 스타일이 확 줄어들어 대상 요소의 스타일링이 간단해진다.

앞으로 React 돔 트리 상에서 모달의 부모 요소들이 추가되고, 스타일이 복잡해져도 Modal은 전혀 영향을 받지않기 때문에 보다 스타일링하기 수월할 것이라 기대한다.

참고

https://react.dev/reference/react-dom/createPortal
https://developer.mozilla.org/ko/docs/Web/CSS/position
https://poiemaweb.com/css3-inheritance-cascading

0개의 댓글