최근 스터디를 하면서 React Portal이라는 개념을 처음 알게 되었다. 평소에 z-index를 아무리 높게 줘도 모달이나 툴팁이 의도한 대로 안 보이는 경우가 종종 있었는데 이게 Portal로 깔끔하게 해결된다는 걸 배웠다. 그래서 이참에 왜 z-index가 안 먹히는지, Portal이 어떻게 그 문제를 해결하는지 정리해 봤다.
z-index가 안 먹히는 진짜 이유
z-index를 아무리 높게 줘도 원하는 대로 안 되는 상황을 겪어본 적이 있다. 모달이 다른 요소 뒤에 숨거나, 툴팁이 카드 밖으로 나가지 못하고 잘려버리는 경우다.
원인은 stacking context(쌓임 맥락) 때문이다. z-index는 같은 stacking context 안에서만 비교된다. 문제는 부모 요소가 아래 속성 중 하나라도 가지면 새로운 stacking context를 만든다는 점이다.
- position: relative/absolute/fixed/sticky + z-index 값이 있는 경우
- opacity가 1보다 작은 경우
- transform, filter, will-change 등이 적용된 경우
- overflow: hidden이 적용된 경우
예를 들어 이런 구조라면 문제가 생긴다.
<div style={{ position: 'relative', zIndex: 1, overflow: 'hidden' }}>
<Tooltip style={{ position: 'absolute', zIndex: 9999 }}>
안녕하세요
</Tooltip>
</div>
Tooltip의 z-index: 9999는 어디까지나 부모 div가 만든 stacking context 안에서만 의미가 있다. 부모가 overflow: hidden이면 부모 밖으로 삐져나간 부분은 그냥 잘려버리고, z-index를 아무리 키워도 부모 바깥의 요소 위로는 올라갈 수 없다. 부모라는 틀 자체에 갇혀있는 것이다.
React Portal이란?
React Portal은 컴포넌트를 부모 DOM 트리 바깥의 다른 DOM 노드에 렌더링 할 수 있게 해주는 기능이다. react-dom에서 제공하는 createPortal을 사용한다.
import { createPortal } from 'react-dom';
function MyModal({ children }) {
return createPortal(
children,
document.getElementById('modal-root')
);
}
createPortal(child, container) 형태로 호출하며, 첫 번째 인자는 렌더링 할 React 노드, 두 번째 인자는 실제로 삽입될 DOM 컨테이너다.
Portal이 문제를 해결하는 방식
Portal은 z-index 싸움으로 해결하는 게 아니라 DOM 위치를 통째로 옮겨서 해결한다.
React 트리상으로는 여전히 부모 컴포넌트의 자식이지만, 실제로 브라우저에 그려지는 DOM 위치는 body 바로 아래 같은 곳으로 빠져나간다.
일반 렌더링 Portal 사용
──────────────── ────────────────
body body
└── Card div ├── Card div
└── Tooltip (잘림) └── Tooltip (portal로 렌더링)
이렇게 되면 Card의 overflow: hidden이나 stacking context와 완전히 무관해지기 때문에 z-index 문제가 근본적으로 사라진다.
실무에서는 document.body에 직접 박기보다, index.html에 전용 컨테이너를 만들어 거기에 렌더링 하는 방식을 많이 쓴다.
<!-- index.html -->
<body>
<div id="root"></div>
<div id="portal-root"></div>
</body>
createPortal(children, document.getElementById('portal-root'))
DOM 트리는 분리되지만 React 트리는 그대로
Portal을 처음 보면 헷갈릴 수 있는 부분이다. DOM 위치는 분리되어도 React 컴포넌트 트리에서는 호출한 컴포넌트의 자식으로 그대로 존재한다.
그래서 아래 동작이 모두 유지된다.
- 부모의 Context(useContext로 가져오는 값들)가 정상적으로 전달된다.
- Portal 안에서 발생한 이벤트가 React 트리를 따라 부모로 버블링 된다. DOM상으로는 멀리 떨어져 있어도 마찬가지다.
- React DevTools에서도 원래 위치의 자식으로 표시된다.
이 점이 단순히 document.body.appendChild(div)로 DOM을 직접 조작하는 것과 본질적으로 다른 이유다. 직접 조작하면 React의 상태 관리, 이벤트 시스템, Context 전부와 단절되어 버린다.

어디에 쓰나?
Portal이 주로 쓰이는 곳은 부모의 레이아웃 제약을 벗어나 화면 최상단에 떠야 하는 UI들이다.
- 모달
- 툴팁
- 드롭다운 메뉴
- 토스트 / 알림
- 컨텍스트 메뉴
정리
z-index를 아무리 올려도 해결이 안 될 때는 숫자를 더 키우는 게 아니라 stacking context 문제를 의심해야 한다. Portal은 이 문제를 z-index 싸움이 아닌 DOM 위치 자체를 바꾸는 방식으로 근본적으로 해결해 준다. 그러면서도 React 트리상의 관계는 그대로 유지되기 때문에 Context나 이벤트 처리를 따로 신경 쓸 필요가 없다는 점이 핵심이다.
'💻 STUDY > Frontend' 카테고리의 다른 글
| MVC부터 React의 단방향 데이터 흐름까지 (0) | 2026.06.29 |
|---|---|
| Next.js App Router의 렌더링 방식 이해하기 (0) | 2026.06.11 |
| Next.js는 React의 어떤 문제를 해결할까? (0) | 2026.06.08 |
| React 클라이언트 전역 상태 관리 (0) | 2026.06.07 |
| Storybook이란? — props가 많아질수록 개발이 느려진다고 느꼈다면 (0) | 2026.06.01 |