본문 바로가기

FrontEnd

[번역] z-index와 쌓임 맥락에 대한 오해

반응형
z-index와 CSS에서 가장 잘못 이해되고 있는 메커니즘 중 하나인 쌓임 맥락(stacking context)에 대해 알아봅니다.
아래 게시물의 번역입니다.
https://www.joshwcomeau.com/css/stacking-contexts/
 

What The Heck, z-index??

The z-index property can be a tricky little bugger. Sometimes, no matter how much you crank up the number, the element never rises to the top! In this article, we explore stacking contexts, and see how they can thwart our efforts to use z-index. We'll also

www.joshwcomeau.com

z-index는 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>

z-index가 클수록 위에 표시됩니다.

하지만 때때로 더 큰 z-index 값을 갖고 있는 요소가 뒤에 표시되는 경우가 있습니다.

<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를 갖고 있는데 header 뒤에 나타납니다.

tooltip이 더 큰 z-index를 갖고 있는데 뒤에 나타납니다.

그러면 header가 맨 위에 있는 이유는 뭐죠?

 

이 미스터리를 풀기 위해선, 맥락(stacking contexts)에 대해 알아야 합니다.


레이어와 그룹(Layers and groups)

Photoshop이나 Figma와 같은 이미지 편집 소프트웨어에는 layer 개념이 있습니다.

각 레이어를 쌓아 하나의 이미지를 만듭니다.

layer의 개념
레이어를 쌓아 만든 이미지

또한 해당 프로그램에서는 여러 레이러르 하나의 그룹으로 묶을 수도 있죠.

레이어를 그룹으로

폴더의 파일, 그롭과 레이어는 유사합니다.

 

레이어 내의 순서는 결정적입니다. 즉 그룹 내에 순서가 있습니다.

  • 즉, 어떤 것은 반드시 어떤 것의 앞에 나오며 겹치지 않습니다.
  • 폴더의 파일의 경우 > 파일명 순서

또한 레이어 간에도 순서가 있습니다

  • 즉 어떤 그룹은 어떤 그룹 앞에 쌓입니다.
    • 폴더 간에도 폴더명 규칙 및 정렬 순서가 있습니다.
  • 다른 레이어에 있는 요소가 다른 레이어를 침범하지 않습니다.

모지쓴 개가 고양이 그림을 가리고 있습니다.

이 그룹은 CSS의 쌓임 맥락이 됩니다.

z-index는 해당 그룹의 레이어가 됩니다.

즉 z-index(레이어)의 유효범위는 전역이 아닙니다!

 

html 문서에는 전역 쌓임 맥락이 존재하지만,

우리는 상황에 따라 다른 쌓임 맥락을 추가로 생성할 수 있습니다.
가장 일반적인 쌓임 맥락 생성 방법은 다음과 같습니다.

.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의 z-index가 9999여도 <main/> 쌓임 맥락 내에서만 의미가 있습니다.
즉 <p>보다 위에 쌓이냐만 결정하지 외부 요소에는 영향을 미치지 않습니다.

물론 그 부모 컨텍스트에서는 header와 main이 같은 컨텍스트에 있기에, 둘을 비교합니다

또한 z-index는 레이어 내에 한정됩니다.

만약 header가 main의 앞에 나온다면, main 안에 있는 요소는 header 앞으로 올라올 수 없습니다.

 

간단하게 생각해서 쌓임 맥락을 메이저 버전, z-index를 마이너 버전으로 생각할 수 있습니다.

아무리 마이너 버전을 올려도 메이저 버전은 안올라갈 수 있습니다. 

<header> <!-- 2.0 -->
  My Cool Site
</header>
<main> <!-- 1.0 -->
  <div class="tooltip"> <!-- 1.999999 -->
    A tooltip
  </div>
</main>

이전 예제 고치기

z-index가 없으면 <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>

변경된 계층 구조는 다음과 같습니다.

 

루트 컨텍스트(The root context)

  • <header>
  • <div class="tooltip">

헤더와 툴팁이 이제 같은 컨텍스트에 있기 때문에 Z-인덱스 비교가 이제 의미를 갖습니다.
여기서 중요한 것은 부모 자식 관계가 아닙ㅂ니다.
툴팁이 헤더보다 더 깊은 곳에 있는건 아무런 영향을 미치지 않습니다.
브라우저는 쌓임 맥락과 z-index만 관심이 있습니다.

 

만약 main에 실제로 z-index:1이 필요하다면,

이제 main안에 있는 tooltip을 header 위로 올릴 수 있는 방법은 없습니다.

이러한 경우는 React Portal과 같은 방법을 사용하여, 돔의 위치와 시각적 요소의 위치를 변경하는 꼼수를 사용합니다.

실제로 다양한 UI 라이브러리는 접근성을 위해 이러한 꼼수를 활용하고 있습니다.


쌓임 맥락 만들기(Creating stacking contexts)

이전에 position:relative(or absolute)와 z-index:1(auto가 아닌 값)을 사용하여 쌓임 맥락을 만들었었는데요,

이 외에도 여러 방법으로 쌓임 맥락을 만들 수 있습니다.

  • opacity를 1보다 작게 설졍
  • position을 fixed나 sticky로 설정
    • 이 값들은 추가적인 z-index가 불필요
  • mix-blend-mode의 값을 normal 외로 설정
  • display:flex or display:grid인 컨테이너의 자식
    • 자식이 z-index가 auto가 아닌 값을 갖는 경우
  • z-index가 auto가 아닌 값+ (position:relativeor absolute)
  • transform, filter, clip-path, perspective 속성 사용
  • will-change 속성 사용(opacity,transform과 주로 사용)
  • isolation:isolate 사용
  • 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>

다시 main에 쌓임 맥락이 생겨 툴팁 위로 올라가 버립니다.

main에 다시 쌓임 맥락 생성


z-index에 대한 오해

z-index를 사용하려면 반드시 position:relative나 position:relative를 사용해야 한다?

no! flexbox도 위 두개 속성과 유사한 역할을 한다!

<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>

second-index에 z-index가 작동한 모습

일반적으로 z-index는 위치 지정 요소를 위한 속성이지만, Flexbox 사양은 예외를 허용합니다.

flexbox의 자식은 정적 배치되었을 때에도 z-index를 사용할 수 있습니다.

신기하게도 자식 요소에 z-index를 설정할 때, 부모 컨테이너에 쌓임 맥락이 형성됩니다.

즉, 자식 요소는 위치 지정 요소 혹은 그 자식이 아닌데도 z-index를 사용할 수 있습니다.

 

https://developer.mozilla.org/en-US/docs/Web/CSS/z-index

z-index CSS 속성은 위치지정 요소 그 자식 혹은 플렉스 아이템의 z-index를 설정합니다.
더 큰 z-index가 있는 중첩 요소는 더 작은 인덱스를 가진 요소를 덮습니다.

잠깐만요...

우리는 이전에 포토샵의 개념을 사용했는데요.

사실 포토샵에서는 그룹과 레이어를 동시에 같은 레벨로 취급할 수 있습니다.

하지만 웹에서는 반드시 쌓임 맥락이 있어야만 레이어(z-index)를 사용할 수 있습니다.

 

또한 사실 우리의 의도는 요소를 위아래로 내리는 것이지,

쌓임 맥락을 만드려는 것이 아닙니다.

그렇지만 쌓임 맥락이 형성되면 모든 자식을 단일 레이어화 하는 것과 유사한 현상이 발생한다는 것은 기억하세요

 

다시 맨 처음믜 예제를 보면

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

z-index와 쌓임 맥락이 없으면, 해당 마크업은 단일 평면에 불과합니다.

하지만 이전의 문제가 발생했던 원인은,

header와 main이 두 개의 다른 평면으로 분리되어 있고, header가 main 레이어 위에 있었으며,

tooltip은 main위에 쌓여있지만 header의 아래에 있는 구조였기 때문이었습니다.

즉 main과 header간의 순서를 변경할 때, 자식을 병합한 형태가 아니면 순서 변경이 불가능합니다.

 

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

 

이는 때론 좋은 것입니다.

  • 렌더링 성능 측면에서 레이어 비교 단순화
  • z-index 범위가 과도하게 증가하는것 방지
  • 컴포넌트 내로 z-index 효과 봉인

isolation과 z-index 추상화

해당 속성을 사용하면 쌓임 맥락이 생성됩니다.

즉 해당 맥락 안에서 z-index 범위는 고유한 효과를 같습니다.

즉 다른 요소와 z-index 충돌 걱정을 안해도 됩니다.

.wrapper {
  isolation: isolate;
}

해당 속성은 이런 장점이 있습니다.

  • z-index 값을 규정할 필요가 없습니다.
  • 정적으로 배치된* 요소에 사용할 수 있습니다.
    • position:static 아닌 요소는 동적임
  • 자식의 렌더링에 영향을 미치지 않습니다.

즉 이 CSS는 자식을 컴포넌트 안에 봉인해 줍니다.

 

아래와 같은 레이어로 구성된 컴포넌트가 있다고 칩시다.

봉투 컴포넌트

해당 컴포넌트는 z-index 충돌 걱정 없이 아무데서나 안전하게 사용할 수 있습니다.

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은 z-index:1이 사실 디폴트가 아니라 항상 안전하지 않습니다.

z-index는 !important와 비슷하다고 생각하세요

 

Internet Explorer를 지원해야 한다면 transform: translate(0px);를 사용할 것이지만,

IE가 아내면 해당 속성의 사용에 대해 걱정할 필요가 없습니다.(very good browser support)


쌓임 맥락 디버깅


쌓임 맥락 VS 레이어

Chrome devtools에는 개별 요소 레이어를 표시하는 "레이어" 창이 있습니다.
레이어는 스택 컨텍스트와 같은 걸까요?

 

답은 아닙니다.

 

스택 컨텍스트는 CSS 사양에 존재합니다.
CSS 언어 동작의 근본적인 부분이며 브라우저는 사양에 따라 이를 구현합니다.

 

반면 레이어는 사양에서 언급되지 않습니다.

오히려 성능을 최적화하기 위해,
Chrome과 같은 일부 브라우저에서 사용하는 구현 세부 사항입니다.

 

예를 들어, 애니메이션이 적용되는 요소를 자체 레이어로 승격하면
브라우저가 해당 작업을 GPU에 맡길 수 있으므로 전환이 더 원활해집니다.
이에 대해 더 자세히 알아보고 싶다면 확인하세요!(article about CSS transitions)
 
레이어가 생성될 때마다 스택 컨텍스트도 정의해야 하지만
그 반대가 반드시 참인 것은 아닙니다(동일한 레이어가 여러 스택 컨텍스트를 렌더링할 수 있음)
 
레이어에 대해 더 자세히 알고 싶으면 참조하세요(this wonderful series by Mariko Kosako)


 

반응형