본문 바로가기

FrontEnd

[CSS] 쌓임 맥락(스태킹 컨텍스트)과 z-index

반응형

TL;DR

z-index를 요소의 순서를 변경하는 방법으로 생각해서는 안 됩니다.
해당 선언이 존재하는 요소의 자식 주위에 그룹을 형성하는 방법으로 생각해야 합니다.
그룹이 형성되지 않으면 z-index가 작동하지 않습니다.

 
CSS에서는 HTML 요소의 쌓임 순서를 명시적으로 제어하는 ​​도구인 z-index가 제공됩니다.
더 높은 값을 가진 요소가 맨 위에 표시됩니다.
<style>
  .box {
    position: relative;
  }
  .first.box {
    z-index: 2;
    background-color: peachpuff;
  }
  .second.box {
    z-index: 1;
    margin-top: -20px;
    margin-left: 20px;
  }
</style>

<div class="first box"></div>
<div class="second box"></div>

 

.first.box는 .second.box보다 z-인덱스가 더 크기 때문에 앞에 쌓입니다.
z-index 선언을 제거하면 뒤에 쌓입니다.

 

그러나 상황이 항상 그렇게 간단하지는 않습니다.
때로는 더 큰 z-인덱스 값이 이기지 못합니다.
<style>
  header {
    position: relative;
    z-index: 2;
  }
  .tooltip {
    position: absolute;
    z-index: 999999;
  }
  main {
    position: relative;
    z-index: 1;
  }
</style>

<header>
  My Cool Site
</header>
<main>
  <div class="tooltip">
    A tooltip
  </div>
  <p>Some main content</p>
</main>

 
.tooltip은 헤더보다 훨씬 더 큰 z-index를 가지고 있습니다,
그렇다면 왜 헤더가 맨 위에 있을까요?
이 미스터리를 풀기 위해 우리는 모호하지만 기본적인 CSS 메커니즘인 쌓임 맥락에 대해 배워야 합니다.
이 기사에서 우리는 그것들이 무엇인지, 어떻게 작동하는지, 어떻게 유리하게 사용할 수 있는지 탐구할 것입니다.

레이어와 그룹

Photoshop 또는 Figma와 같은 이미지 편집 소프트웨어를 사용해 본 적이 있다면 레이어 개념에 익숙할 것입니다.

우리 이미지에는 팬케이크처럼 쌓인 3개의 개별 캔버스가 있습니다.
맨 아래 레이어는 고양이 사진이고 맨 위에는 세부 사항을 추가하는 2개의 레이어가 있습니다.
이 레이어를 병합하여 최종 구성을 완성합니다.

이 프로그램에서는 레이어를 그룹화할 수도 있습니다.

폴더에 있는 파일과 마찬가지로 그룹을 사용하면 레이어를 분할할 수 있습니다.
쌓는 순서와 관련하여 레이어는 그룹 간에 "혼합"될 수 없습니다.
모든 개의 레이어가 모든 고양이 레이어 위에 나타납니다.

CSS도 비슷하게 동작합니다.
요소는 쌓임 맥락으로 그룹화됩니다.
요소에 z-인덱스을 지정하면
해당 값은 동일한 컨텍스트의 다른 요소와만 비교됩니다.
z-인덱스 값은 전역이 아닙니다.
 
기본적으로 일반 HTML 문서에는 모든 노드를 포함하는 단일 스택 컨텍스트가 있습니다.
그러나 추가 컨텍스트를 만들 수 있습니다!
스택 컨텍스트를 만드는 방법은 여러 가지가 있지만 가장 일반적인 방법은 다음과 같습니다.

position: relative와 z-index 같이 사용하기

.some-element {
  position: relative;
  z-index: 1;
}
이 두 선언을 결합하면 비밀 메커니즘이 트리거됩니다.
쌓임 맥락이 생성되어 이 요소와 모든 자식 주위에 그룹을 형성합니다.
맨 위의 문제를 다시 살펴봅시다.
<style>
  header {
    position: relative;
    z-index: 2;
  }
  .tooltip {
    position: absolute;
    z-index: 999999;
  }
  main {
    position: relative;
    z-index: 1;
  }
</style>
<header>
  My Cool Site
</header>
<main>
  <div class="tooltip">
    A tooltip
  </div>
  <p>Some main content</p>
</main>
이 스니펫에서 생성되는 스태킹 컨텍스트를 매핑할 수 있습니다.
  • The root context
    • <header>
    • <main>
      • <div class="tooltip">

 

우리의 .tooltip 요소는 999999의 z-인덱스를 가지고 있지만
그 값은 <main> 스택 컨텍스트 내에서만 관련이 있습니다.
툴팁이 인접한 <p> 태그 위 또는 아래에 표시되는지만 영향을 미칩니다.
다른곳에는 영향을 미칠 수 없습니다.

 

한편, 부모 컨텍스트에서는 <header>와 <main>이 비교됩니다.
<main>은 z-index가 더 작기 때문에 <header> 아래에 표시됩니다.
모든 아이들이 함께 부모 컨텍스트 위에 타고 있습니다.
 

시맨틱 버저닝과의 유사점

모든 사람이 Photoshop / Figma / Sketch와 같은 소프트웨어 경험이 있는 것은 아닙니다.

여러분이 더 잘 알고 있을 것 같은 또 다른 비유가 있습니다.

바로 시맨틱 버저닝 입니다.

 

시맨틱 버전 관리에서 버전의 다른 "계층(티어)"은 점으로 구분됩니다. (메이저.마이너.패치)

예를 들어 패키지 버전 2.0은 1.0보다 큰 버전이지만 1.999보다 큰 버전이기도 합니다.

 

z-인덱스는 마이너 버전과 유사하고 쌓임 맥락은 메이저 버전과 유사합니다.

쌓임 맥락이 생성될 때마다 버전에 점을 추가합니다.

<header> <!-- 2.0 -->
  My Cool Site
</header>
<main> <!-- 1.0 -->
  <div class="tooltip"> <!-- 1.999999 -->
    A tooltip
  </div>
</main>
1.999999가 2.0보다 낮은 버전이기 때문에 툴팁이 <header> 아래에 표시됩니다.
마이너 버전에 9를 추가하는 것은 중요하지 않습니다.
더 큰 메이저 버전을 능가하지는 않습니다.

우리의 예제 고치기

툴팁 문제를 어떻게 해결합니까? 이 경우 우리는 사실 <main>에 스태킹 컨텍스트를 생성할 필요가 없습니다:
<style>
  header {
    position: relative;
    z-index: 2;
  }
  .tooltip {
    position: absolute;
    z-index: 999999;
  }
  main {
    position: relative;
    /* No more z-index here! */
  }
</style>

<header>
  My Cool Site
</header>
<main>
  <div class="tooltip">
    A tooltip
  </div>
  <p>Some main content</p>
</main>
Z-인덱스가 없으면 <main>은 쌓임 맥락을 생성하지 않습니다. 그러면 계층 구조는 다음과 같습니다.
  • The root context
    • <header>
    • <div class="tooltip">
헤더와 툴팁이 이제 동일한 컨텍스트에 있기 때문에 z-인덱스 값은 서로 동일하며, 툴팁이 승자로 나타납니다.
부모/자식 관계는 상관없습니다.

 

툴팁이 헤더보다 더 깊게 중첩되어도 상관 없습니다. 브라우저는 스택 컨텍스트에만 관심이 있습니다.

예외사항

이 인위적인 예에서 우리는 <main>이 실제로 아무 것도 하지 않았기 때문에 z-index를 제거할 수 있습니다.
하지만 실제로 <main>이 z-index를 사용하거나 쌓임 맥락을 생성해야 한다면 어떻게 될까요?
CSS의 규칙에 따르면 쌓임 맥락의 법칙에서 벗어날 수 있는 방법은 없습니다.
즉, 한 스택 컨텍스트 내의 요소는 다른 스택 컨텍스트의 요소와 비교할 수 없습니다.
그러나 약간의 독창적인* 사고를 통해 원하는 결과를 얻을 수 있습니다.
툴팁을 <body> 태그에 추가하여 <main> 외부에서 툴팁을 렌더링할 수 있습니다.
그런 다음 일부 CSS를 사용하여 적절하게 배치하여 해당 요소의 자식인 것처럼 보이게 할 수 있습니다.
이것은 고급 기술이며 키보드를 사용하여 웹을 탐색하는 사용자의 경험을 실수로 중단하지 않도록 신중한 계획이 필요합니다.
고맙게도 Reach UI와 같은 라이브러리는 이 기술을 내부적으로 사용하고 모든 접근성 및 사용성 문제를 해결합니다.
이 튜토리얼의 범위를 벗어나지만 더 자세히 알아보고 싶다면
포털이 React에서 어떻게 작동하는지 조사하고(how Portals work in React) Reach UI의 소스 코드(Reach UI's source code)를 확인하십시오.

쌓임 맥락 만들기

상대 위치 또는 절대 위치를 z-인덱스와 결합하여 스택 컨텍스트를 만드는 방법을 살펴보았지만 유일한 방법은 아닙니다!
다음은 몇 가지 다른 방법입니다.
  • opacity를 1보다 작은 값으로 설정
  • position를 fixed 또는 sticky으로 설정(이 값에는 z-index가 필요하지 않습니다!)
  • normal 모드 대신 mix-blend-mode 사용
  • display: flex 또는 display: grid 컨테이너 내부의 자식에 z-인덱스 추가
  • transform,filter,clip-path,perspective 사용
  • opacity 또는 transform과 같은 값으로 will-change 사용
  • isolation:isolate를 사용하여 컨텍스트를 명시적으로 생성

 

몇 가지 다른 방법도 있습니다. 전체 목록은 MDN에서 찾을 수 있습니다. (the full list on MDN)

이로 인해 몇 가지 놀라운 상황이 발생할 수 있습니다. 여기에서 무슨 일이 일어나고 있는지 확인하십시오:

<style>
  header {
    position: relative;
    z-index: 2;
  }
  .tooltip {
    position: absolute;
    z-index: 999999;
  }
  main {
    position: relative;
    /*
      No more z-index…
      but it's still broken??
    */
    will-change: transform;
  }
</style>

<header>
  My Cool Site
</header>
<main>
  <div class="tooltip">
    A tooltip
  </div>
  <p>Some main content</p>
</main>​

2 vs 1.99999의 재림

main은 더 이상 z-인덱스를 설정하지 않지만, 스택 컨텍스트를 자체적으로 생성할 수 있는 속성인 will-change를 사용합니다.

 

z-index에 대한 흔한 오해

z-index가 작동하려면 position을 relative나 absolute로 설정해야 하나요?
정확하게는 아닙니다. 여길 보세요
<style>
  .wrapper {
    display: flex;
  }
  .second.box {
    z-index: 1;
    background: hotpink;
    margin-top: 20px;
    margin-left: -20px;
    margin-right: -20px;
  }
</style>

<div class="wrapper">
  <div class="first box"></div>
  <div class="second box"></div>
  <div class="third box"></div>
</div>

두 번째 상자는 z-색인을 사용하여 형제 위로 들어 올려집니다. 하지만 스니펫 어디에도 position 선언이 없습니다!

 

일반적으로 z-인덱스는 "위치 지정" 요소(기본 "정적"이 아닌 다른 것으로 위치를 설정하는 요소)에서만 작동합니다.
그러나 Flexbox 사양은 예외를 추가합니다. flex 자식은 정적으로 배치된 경우에도 z-index를 사용할 수 있습니다.

잠깐...

여기에 이상한 일이 있습니다. 1~2분 동안 생각해 볼 가치가 있다고 생각합니다.
Photoshop 비유에서는 그룹과 레이어가 명확하게 구분됩니다.
모든 시각적 요소는 레이어이며 그룹은 이를 포함하는 구조적 도우미로 사용될 수 있습니다. 그들은 뚜렷한 아이디어입니다.
그러나 웹에서는 구분이 다소 명확하지 않습니다.
z-인덱스를 사용하는 모든 요소는 쌓임 맥락도 생성해야 합니다.
요소에 z-인덱스를 설정 할때
우리의 목표는 일반적으로 상위 스택 컨텍스트에서 다른 요소 위/아래로 해당 요소를 올리거나 내리는 것입니다.
우리는 해당 요소에 대한 스택 컨텍스트를 생성할 의도가 없습니다!
그러나 우리가 그것을 고려하는 것이 중요합니다.
스택 컨텍스트가 생성되면 모든 하위 항목을 "평면화"합니다.
그 자식들은 여전히 ​​내부적으로 재배열될 수 있지만 우리는 본질적으로 그 자식들을  하나의 컨텍스트 안에 가두었습니다.
이전의 마크업을 다시 살펴보겠습니다.
<header>
  My Cool Site
</header>
<main>
  <div class="tooltip">
    A tooltip
  </div>
  <p>Some main content</p>
</main>
기본적으로 HTML 요소는 DOM 순서에 따라 쌓입니다.
CSS 간섭 없이 메인은 헤더 위에 렌더링됩니다.
헤더에 z-인덱스를 제공하여 헤더를 앞으로 들어올릴 수 있지만 모든 자식을 병합하지 않고는 불가능합니다.
이 메커니즘은 앞에서 논의한 버그로 이어졌습니다.

 

z-index를 요소의 순서를 변경하는 방법으로 생각해서는 안 됩니다.
해당 선언이 존재하는 요소의 자식 주위에 그룹을 형성하는 방법으로 생각해야 합니다.
그룹이 형성되지 않으면 z-index가 작동하지 않습니다.
 
 

믿거나 말거나 이것은 좋은 일입니다.

툴팁 데모에서 보았듯이 스택 컨텍스트는 미묘하고 진단하기 어려운 버그를 유발할 수 있습니다.
대신 z-index 값을 전역적으로 비교하면 좋지 않을까요?
나는 그렇게 생각하지 않으며 다음과 같은 몇 가지 이유를 생각할 수 있습니다.
 
  • 그대로, z-지수 인플레이션(거대한 z-지수 값이 계속 증가하는 추세)은 전염병입니다.
    • z-색인이 있는 모든 단일 요소가 동일한 축척에 맞아야 한다면 얼마나 더 나빠질지 상상해 보십시오.
    • (주: 1번부터 모든 페이지에서 차례대로 존재해야 함)
 
  • 저는 브라우저 엔지니어는 아니지만 스택 컨텍스트가 성능에 좋다고 생각합니다.
    • 그것들이 없으면 브라우저는 z-인덱스가 있는 모든 항목을 z-인덱스가 있는 다른 모든 항목과 비교해야 합니다.
 
  • 스택 컨텍스트를 이해하면 요소를 "봉인"하는 데 유리하게 사용할 수 있습니다.
    • 이것은 React와 같은 컴포넌트 중심 프레임워크에서 특히 강력한 패턴입니다.
마지막 포인트가 특히 흥미롭습니다. 더 깊이 파헤쳐 보겠습니다.

"격리"를 사용한 완벽한 추상화 (Airtight abstractions with “isolation”)

내가 가장 좋아하는 CSS 속성 중 하나는 가장 모호(obscure)한 속성이기도 합니다.
언어의 숨겨진 보석인 isolation 속성을 소개합니다.
.wrapper {
  isolation: isolate;
}
이 선언을 요소에 적용하면 정확히 한 가지 작업을 수행합니다. 새 쌓임 맥락을 생성합니다.
스태킹 컨텍스트를 생성하는 다양한 방법이 있는데 왜 또 다른 컨텍스트가 필요한가요?
글쎄, 다른 모든 방법을 사용하면 다른 변경의 결과로 스태킹 컨텍스트가 암시적으로 생성됩니다.
isolation은 가능한 가장 순수한 방법으로 스택 컨텍스트를 생성합니다.
 
  • z-index 값을 처방할 필요가 없습니다.
  • 정적으로 배치된* 요소에 사용할 수 있습니다.
  • 어떤 식으로든 자식의 렌더링에 영향을 미치지 않습니다.

이것은 요소의 자식을 "봉인"할 수 있기 때문에 매우 유용합니다.

예를 들어 보겠습니다. 아래와 같은 봉투 컴포넌트가 있습니다.

몇개의 계층으로 되어있습니다.

이 효과를 React 컴포넌트인 <Envelope>에 패키징했습니다.
function Envelope({ children }) {
  return (
    <div>
      <BackPane style={{ zIndex: 1 }} />
      <Letter style={{ zIndex: 3 }}>
        {children}
      </Letter>
      <Shell style={{ zIndex: 4 }} />
      <Flap style={{ zIndex: isOpen ? 2 : 5 }} />
    </div>
  )
}
(Flap에 동적 z-index가 있는 이유가 궁금하다면 봉투를 열었을 때 문자 뒤로 이동해야 하기 때문입니다.)
좋은 React 컴포넌트는 우주복처럼 환경과 격리되어 있습니다.
하지만 이 우주복은 공기가 새어나오네요
z-index: 3이 있는 <header> 근처에서 사용하면 어떻게 되는지 확인하세요.

 

<Envelope> 컴포넌트는 div에 4개의 레이어를 래핑하지만 스택 컨텍스트를 생성하지는 않습니다.
따라서 이 컴포넌트의 레이어는 다른 컴포넌트와 연결될 수 있습니다.
 
<Envelope> 내의 최상위 요소에 isolation: isolate를 사용하여 그룹으로 배치되도록 보장합니다.
function Envelope({ children }) {
  return (
    <div style={{ isolation: 'isolate' }}>
      <BackPane style={{ zIndex: 1 }} />
      <Letter style={{ zIndex: 3 }}>
        {children}
      </Letter>
      <Shell style={{ zIndex: 4 }} />
      <Flap style={{ zIndex: isOpen ? 2 : 5 }} />
    </div>
  )
}

position: relative; z-index: 1를 사용하여 구식 방식으로 스택 컨텍스트를 생성하지 않는 이유는 무엇입니까?

글쎄요, React 컴포넌트는 재사용이 가능해야 합니다.
1은 모든 상황에서 이 컴포넌트에 대해 정말 올바른 z-인덱스 값입니까?
isolation의 장점은 컴포넌트를 편견 없이 유연하게 유지한다는 것입니다.
점점 더, 나는 z-index가 !important와 유사한 탈출구라고 믿기 시작했습니다.
internet explorer는 isolation 못씀. transform:translate(0px) 사용

쌓임 맥락 이슈 디버깅

불행히도 스택 컨텍스트 문제를 디버그하는 데 도움이 되는 많은 도구를 찾지 못했습니다.
Microsoft Edge에는 스택 컨텍스트를 볼 수 있는 흥미로운 "3D 보기"가 있습니다.

https://docs.microsoft.com/en-us/microsoft-edge/devtools-guide-chromium/3d-view/

 

Navigate webpage layers, z-index, and DOM using the 3D View tool - Microsoft Edge Development

How to use the 3D View tool, including navigating the 3D canvas and using the Composited Layers tab, Z-index tab, and DOM tab.

docs.microsoft.com

이것은 야심 찬 아이디어이지만 솔직히 나는 그것이 꽤 과하다고 생각합니다.
이 보기에서 특정 요소를 찾기가 어렵고 내 앱의 스태킹 컨텍스트를 이해하는 데 도움이 되지 않는 것 같습니다.

 

때때로 사용할 수 있는 또 다른 멋진 트릭이 있습니다: offsetParent.
const element = document.querySelector('.tooltip');
console.log(element.offsetParent); // <main>
 
offsetParent는 static이 아닌 position 값으로 렌더링된 가장 가까운 조상을 반환합니다.
relative / absolute / fixed / sticky 조상을 찾는 트리를 기어 올라갑니다.
이것은 완벽한 솔루션이 아닙니다.
모든 스태킹 컨텍스트가 positioned 레이아웃을 사용하는 것은 아니며
배치된 모든 요소가 스태킹 컨텍스트를 생성하는 것은 아닙니다!
하지만, 실제로 이 두 가지 사이에는 꽤 강한 상관관계가 있는 경향이 있습니다. 적어도 시작점입니다.
여기에서 도움이 될 수 있는 도구를 알고 있다면(또는 만들면!) Twitter에서 알려주십시오.
업데이트: Felix Becker는 스택 컨텍스트가 생성될 때 강조 표시되는 VSCode 확장을 공유하기 위해 연락했습니다.

https://marketplace.visualstudio.com/items?itemName=felixfbecker.css-stacking-contexts 

 

CSS Stacking Contexts - Visual Studio Marketplace

Extension for Visual Studio Code - Highlights stacking contexts in CSS and ineffective z-index declarations.

marketplace.visualstudio.com

이 확장은 .css 및 .scss 파일에서 작동합니다(CSS-in-JS 지원 없음).
업데이트 2: Giuseppe Gurgone이 devtools에 새로운 "z-index" 창을 추가하는 이 Chrome 확장 프로그램에 대해 알려주기 위해 연락했습니다.

Chrome extension

 

z-context

A Chrome DevTools Extension that displays stacking contexts and z-index values in the elements panel

chrome.google.com

업데이트 3: Andrea Dragotta는 z-색인 및 스택 컨텍스트에 대한 매우 중요한 정보를 추가하는 놀라운 브라우저 확장 프로그램을 만들었습니다.

이것은 멋진 도구이며 저는 정기적으로 사용하고 있습니다. CSS Stacking Context Inspector 설치:

For Chrome

 

CSS Stacking Context inspector

Helps inspecting the css stacking contexts and solving the z-index war.

chrome.google.com


스태킹 컨텍스트 vs 레이어

Chrome devtools에는 개별 요소 레이어를 표시하는 "레이어" 창이 있습니다. 레이어는 스택 컨텍스트와 동일한가요?
불행히도. 곁눈질로 보면 비슷하지만 근본적으로 다른 개념입니다.
스택 컨텍스트는 CSS 사양에서 "thing"입니다.
그것들은 언어 작동 방식의 기본적인 부분이며 브라우저는 사양에 따라 이를 구현하도록 되어 있습니다.
 
반면에 레이어는 사양에 언급되어 있지 않습니다.
일부 브라우저(예: Chrome)가 성능을 최적화하기 위해 사용하는 구현 세부 사항입니다.
 
예를 들어 애니메이션을 적용할 요소를 자체 레이어로 승격함으로써
브라우저는 해당 작업을 GPU로 지연시켜 보다 부드러운 전환을 유도할 수 있습니다.
이에 대해 더 자세히 알고 싶다면 article about CSS transitions 를 참조하세요
 
언뜻 보기에 이 개념은 스태킹 컨텍스트와 매우 유사하게 들리지만 별개의 메커니즘입니다.
레이어가 생성될 때마다 스태킹 컨텍스트도 정의해야 하지만
그 반대가 반드시 사실은 아닙니다(여러 스태킹 컨텍스트가 동일한 레이어에서 렌더링될 수 있음).
Chrome 웹 블로그의 Mariko Kosako의 이 멋진 시리즈에서 브라우저가 레이어를 사용하는 방법에 대해 자세히 알아볼 수 있습니다.

https://developer.chrome.com/blog/inside-browser-part3/

 

Inside look at modern web browser (part 3) - Chrome Developers

Inner workings of a browser rendering engine

developer.chrome.com

 

반응형