RoomE / React portal로 스택킹 컨텍스트 문제 해결하기

프로젝트 기록트러블 슈팅스택킹 컨텍스트React Portal
avatar
2025.03.10
·
8 min read

3779

오늘의 이슈

메인 페이지를 QA 하면서 z-index가 이상하게 적용되는 문제를 겪었다. 헤더는 z-index 50, 네임 바와 도구 버튼은 20이었고, 모달과 모달 배경은 999였는데 다음과 같이 인덱스 값이 더 낮은 헤더, 도구 버튼, 네임 바가 모달 배경 위로 렌더링되는 문제를 겪었다.

3781

스택킹 컨텍스트란?

스택킹 컨텍스트(Stacking Context)는 HTML 요소들의 3차원 공간에서의 렌더링 순서를 결정하는 개념이다. 쉽게 말해, 어떤 요소가 다른 요소 위에 나타날지를 결정하는 규칙의 집합이다.

새롭게 스택킹 컨텍스트가 생성되는 조건은 다음과 같다.

스택킹 컨텍스트가 생성되는 조건

  • HTML 문서의 루트 요소 (<html>)

  • position: relative 또는 position: absolute와 함께 z-index가 auto가 아닌 요소

  • position: fixed 또는 position: sticky인 요소

  • opacity가 1보다 작은 요소

  • transformfilterperspective 등의 CSS 속성을 가진 요소

  • isolation: isolate를 사용한 요소

스택킹 컨텍스트의 쌓임 순서

같은 스택킹 컨텍스트 내에서 요소들은 다음 순서로 쌓인다. (아래에서 위 방향으로)

  1. 배경과 테두리(background and borders)

  2. 음수 z-index를 가진 요소들

  3. 블록 레벨의 일반 흐름에 속한 요소들

  4. 플로팅(floating) 요소들

  5. 인라인 요소들

  6. z-index: 0 또는 auto인 위치가 지정된 요소들

  7. 양수 z-index를 가진 요소들

중요한 점은 각 스택킹 컨텍스트 내에서만 z-index 값이 의미를 갖는다는 것이다. 즉, 서로 다른 스택킹 컨텍스트에 속한 요소들은 단순히 z-index 값의 크기만으로 겹침 순서를 비교할 수 없다.

과정

처음에는 단순히 z-index 값을 높여보는 시도를 했다. 하지만 이것만으로는 해결되지 않았다. 문제의 원인을 찾기 위해 앱 구조를 자세히 분석했더니 두 가지 핵심 문제점을 발견했다:

  1. BaseLayout 컴포넌트에서 헤더가 모달보다 나중에 렌더링되고 있었다.

  2. 모달과 헤더 모두 fixed 포지션을 사용하고 있어서 DOM의 렌더링 순서가 중요했다.

이 문제를 해결하기 위해 먼저 렌더링 순서를 바꾸고 스택킹 컨텍스트를 수정하는 접근법을 시도했다. BaseLayout의 구조를 변경하고, 적절한 포지셔닝과 z-index 값을 설정했다. 하지만 이 방법도 완벽히 해결되지 않았다.

여러 시도 끝에 결국 React Portal이라는 해결책을 찾았다. Portal을 사용하면 컴포넌트를 DOM 트리의 다른 부분에 렌더링할 수 있어, 모달을 최상위 레벨로 끌어올릴 수 있다는 것을 알게 되었다.

해결방안

최종적으로 React의 createPortal을 사용하여 문제를 해결했다. 모달 컴포넌트를 수정하여 모달 내용을 document.body에 직접 렌더링함으로써 DOM 계층 구조와 상관없이 항상 최상위에 표시되도록 했다.

import React from 'react';
import { createPortal } from 'react-dom';

const ModalBackground = React.memo(
  ({
    children,
    onClose,
  }: {
    children?: React.ReactNode;
    onClose?: () => void;
  }) => {
    // 모달 닫기 핸들러
    const handlecloseModal = (
      event: React.MouseEvent<HTMLButtonElement | HTMLDivElement, MouseEvent>,
    ) => {
      event.stopPropagation();
      onClose();
    };

    // 모달 콘텐츠 정의
    const modalContent = (
      <div
        className='fixed inset-0 z-[99] w-full h-full flex justify-center items-center bg-[#1E3675CC] backdrop-blur-xs'
        onClick={handlecloseModal}>
        <div onClick={(e) => e.stopPropagation()}>
          {children}
        </div>
      </div>
    );

    // Portal을 사용하여 body에 직접 렌더링
    return createPortal(
      modalContent,
      document.body,
    );
  },
);

export default ModalBackground;

이렇게 수정하니 모달이 헤더를 포함한 모든 요소 위에 완벽하게 표시되었다. z-index 값도 더 이상 엄청 높게 설정할 필요가 없어졌다.

배운 점

  1. CSS의 스택킹 컨텍스트는 생각보다 복잡하다. position, z-index, 렌더링 순서 등 여러 요소가 영향을 미친다.

  2. 같은 z-index 값을 가진 요소들은 DOM에서의 순서(나중에 나오는 요소가 위에 렌더링됨)에 따라 쌓이게 된다. 이것이 헤더가 모달보다 나중에 렌더링되어 문제가 발생했던 이유이다.

  3. 서로 다른 스택킹 컨텍스트에 속한 요소들 간에는 단순히 z-index 값의 크기 비교만으로 겹침 순서를 결정할 수 없다. 이는 부모 요소의 스택킹 컨텍스트가 자식 요소들의 z-index 범위를 제한하기 때문이다.

  4. React에서 컴포넌트의 렌더링 위치와 실제 DOM에서의 위치는 다를 수 있으며, Portal을 사용하면 이를 효과적으로 제어할 수 있다.

  5. 문제 해결을 위해서는 여러 접근 방식을 시도해보는 것이 중요하다. 처음에는 단순히 CSS 속성만 변경하려 했지만, 결국 React의 기능을 활용해 더 깔끔한 해결책을 찾았다.

  6. 모달 같은 UI 요소는 앱의 기본 레이아웃과 독립적으로 존재해야 할 때가 많으며, Portal은 이런 상황에 최적의 솔루션이다.

이번 경험을 통해 React와 CSS의 상호작용에 대해 더 깊이 이해하게 되었고, 앞으로 비슷한 문제가 발생하면 더 효율적으로 해결할 수 있을 것 같다.







- 컬렉션 아티클