IE에서 메가메뉴의 서브메뉴 z-index가 문제를 일으키는 것때문에 골머리를 썩은 일이 있었다. 어떻게 해결은 했었는데, 더 정확히 알 필요가 있다고 생각했다. 그 후 z-index에 관한 신뢰할 만한 글을 본 건 트위터에서였다. 누가 추천한 건지 기억은 안 나는데 ‘What no one told you about z-index’라는 글이 있었다. 언젠가 번역해야지 생각하고 있었는데 이제야 한다.

번역 중 [] 안의 내용은 이해를 돕기 위해 내가 추가한 내용이다. Stacking Context, Stacking Order는 모질라 한국 블로그의 용례를 따라 ‘쌓임 맥락’과 ‘쌓임 순서’로 번역했다.

번역 시작이다.


z-index의 문제는, 그게 실제로 작동하는 방식을 이해하는 사람이 너무 적다는 것이다. z-index가 복잡하지는 않다. 하지만 스펙을 읽어본 적이 없다면, 틀림없이 거기엔 당신이 전혀 모르는 결정적인 측면이 있을 것이다.

궁금하면 한 번 이 문제를 풀어 봐라.

문제

세 개의 <div> 요소가 있는 HTML이 있다. 각 <div>에는 <span> 요소가 하나씩 있다. 각 <span> 요소에는 배경색이 있다. 배경색은 각각 빨강, 초록, 파랑이다. 각 <span>은 또한 모두 문서의 좌측 상단 근처에 다른 <span>요소들과 살짝 겹쳐서 놓여 있다. 그래서 뭐가 더 앞에 있는지, 어떻게 쌓여 있는지 알 수 있다. 첫 번째 <span>은 z-index가 1이다. 다른 두 <span>에는 z-index가 없다.

HTML과 기본 CSS는 아래와 같다. 아래쪽에는 전체 CSS와 함께 실제 작동하는 데모(Codepen)를 배치해 뒀다.

<div>
  <span>Red</span>
</div>
<div>
  <span>Green</span>
</div>
<div>
  <span>Blue</span>
</div>


.red, .green, .blue {
  position: absolute;
}
.red {
  background: red;
  z-index: 1;
}
.green {
  background: green;
}
.blue {
  background: blue;
}

<pre class=”codepen”data-height=”268”data-type=”result”data-href=”ksBaI”data-user=”philipwalton”data-safe=”true”>Check out this Pen!</pre>

이제 문제다: 빨간 <span>을 아래 규칙을 깨지 않으면서 파랑과 초록 <span> 요소 밑으로 가게 해 봐라.

  • HTML 마크업을 어떤 식으로든 건드려선 안 된다.
  • 어떤 요소에도 z-index를 추가하거나 변경해선 안 된다.
  • 어떤 요소의 position 속성도 추가하거나 변경해선 안 된다.

해 볼 요량이라면 위의 코드펜으로 가서 잠깐 해 봐라. 만약 성공한다면 아래처럼 보여야 한다.

주의: 아래 예제의 CSS 탭을 누르지 마라. 누르면 답이 바로 보인다.

<pre class=”codepen”data-height=”268”data-type=”result”data-href=”dfCtb”data-user=”philipwalton”data-safe=”true”>Check out this Pen!</pre>

해법

해법은 첫 번째 <div>(빨간 <span>의 부모)에 opacity를 1보다 작게 주는 것이다. 이게 위의 코드펜에 추가한 CSS다.

div:first-child {
    opacity: .99;
}

요소들의 쌓임 순서(Stacking Order)에 opacity가 영향을 미친다는 사실에 충격을 받아 머리를 쥐어뜯고 있다면, 환영한다. 내가 처음 이 이슈에 걸렸을 때 나도 비슷한 충격을 받았다.

쌓임 순서(Stacking Order)

z-index는 아주 간단해 보인다. z-index가 높은 게 z-index가 낮은 것보다 앞에 나온다. 그렇지 않나? 흠, 실제로는 아니다. 이건 z-index의 문제 중 하나다. 되게 간단해 보이고, 그래서 많은 개발자들이 규약을 읽어 보지 않는다는 거다.

HTML 문서의 모든 요소는 다른 요소의 앞으로 나오거나 뒤로 들어갈 수 있다. 다들 이걸 쌓임 순서(stacking order)로 알고 있다. 이 순서를 정하는 규칙은 스펙에 상당히 명확하게 정의돼 있다. 하지만 내가 앞서 말했듯이, 대부분의 개발자들이 그걸 완전히 이해하고 있지 못하다.

z-index와 position 속성이 없을 때는, 규칙이 아주 단순하다. 기본적으로 쌓임 순서는 HTML에 나타나는 순서와 같다. (맞다, 실제로는 그거보다 조금 복잡하다. 하지만, 우리가 inline 요소를 덮으려고 음수 마진을 사용하지 않는 이상, 이런 예외적 경우를 만날 일은 없을 거다.)

position 속성을 요소들에 사용할 때, position 속성이 있는 모든 요소(와 그 자식 요소)는 position 속성이 없는 요소들 앞에 나타난다. (position 속성이 있다는 말은 static이 아닌 position 속성이 있다는 것이다. 예컨대,relativeabsolute 같은 것들.)

마지막으로, z-index가 연관되면, 좀더 복잡해진다. 처음엔, z-index 값이 더 높은 요소가 앞쪽에 올 것이고, z-index를 가진 요소는 z-index가 없는 요소보다 앞에 올 것이라고 가정하는 게 자연스럽다. 하지만, 그게 그렇게 간단치가 않다. 우선, z-index는 오직 position 속성이 있는 요소에서만 작동한다. position 속성이 지정되지 않은 요소에 z-index를 매겨 보면, 아무 일도 안 일어날 것이다. 두 번째로, z-index 값은 쌓임 맥락(stacking contexts)을 만들 수 있다. 그리고 이제 간단해 보이던 것이 갑자기 엄청나게 복잡해진다.

쌓임 맥락(Stacking Contexts)

같은 부모 밑에 있어서 쌓임 순서에 따라 함께 앞뒤로 한꺼번에 움직일 수 있는 요소들의 그룹은 쌓임 맥락(stacking context)으로 알려진 것을 만든다. 쌓임 맥락을 완전히 이해하는 것이 z-index와 쌓임 순서가 작동하는 방법을 진정으로 이해할 수 있는 열쇠다.

모든 쌓임 맥락에는 그것의 뿌리(root) 요소인 HTML 요소가 있다. 어떤 요소에서 쌓임 맥락이 새롭게 만들어질 때, 그 쌓임 맥락은 자식 요소들이 쌓임 순서에서 특정 범위를 벗어나지 못하도록 한계를 정하게 된다. 1 이것은, 맨 뒤의 쌓임 맥락에 있는 요소는 그보다 앞의 쌓임 맥락에 있는 요소보다 앞에 나올 수 없다는 것을 의미한다. z-index를 십만을 줘도 소용 없다. 2

쌓임 맥락은 다음 셋 중 하나에 속할 때 만들어진다.

  • 요소가 문서의 뿌리 요소일 때 (즉, <html> 요소)
  • 요소의 position 값이 static이 아니면서 z-index도 auto가 아닐 때
  • 요소의 opacity 값이 1보다 작을 때
  • [모바일 웹킷과 크롬 22 이상에서, position: fixed는 무조건 새로운 쌓임 맥락을 만든다. z-index가 “auto”여도 말이다.(이 글 참고)]

쌓임 맥락이 만들어지는 첫 번째와 두 번째 방법은 이해하기 쉽고, 웹 개발자들도 잘 알고 있다(뭐라고 불러야 할지는 모른다고 해도 말이다).

쌓임 순서(Stacking Order)에서 요소의 위치를 정하기

(선, 배경, 글자 노드 등을 포함해) 모든 페이지 요소에 대해 전체 쌓임 순서(global stacking order)를 실제로 정하는 것은 아주아주 복잡하고, 이 글의 범위를 넘어서는 것이다. (다시 한 번, 스펙 문서를 추천한다.)

하지만 대부분의 경우엔 [쌓임] 순서(the order)에 대한 기초적 이해만 있으면 많은 도움이 될 것이고, 예측 가능한 CSS 개발을 해 나가는 데 도움이 될 수 있다. 그러니 [쌓임] 순서(the order)를 파헤쳐 각각의 쌓임 맥락으로 들어가 보자.

같은 [단계의] 쌓임 맥락에서 쌓임 순서

한 쌓임 맥락에서 쌓임 순서를 정하는 기본적 규칙은 아래와 같다([아래 순서대로] 뒤에서 앞으로 [쌓인다]).

  1. 쌓임 맥락의 뿌리(root) 요소.
  2. position 값이 있고 z-index 값이 음수인 요소(와 자식들). (z-index 값이 높은 요소가 앞에 놓인다. 값이 같으면 HTML에 나타난 순서에 따라 나타난다.)
  3. position 값이 없는 요소(HTML에서 나타나는 순서를 따른다).
  4. position 값이 있고 z-index 값이 auto인 요소(와 그 자식들). (HTML에서 나타나는 순서에 따라)
  5. position 값이 있고 z-index 값이 양수인 요소(와 그 자식들). (z-index 값이 높은 요소가 앞에 놓인다. 값이 같으면 HTML에 나타난 순서에 따라 나타난다.)

알림: z-index가 음수면서 position 속성이 있는 요소는 쌓임 맥락에서 맨 먼저 쌓인다. 즉, 모든 다른 요소들보다 뒤에 있게 된다. 때문에 흔치 않은 일이 벌어지는데, 같은 쌓임 맥락 안에 있는 자기 부모보다 뒤에 놓일 수 있게 된다. 이것은 해당 요소의 부모가 같은 쌓임 맥락 안에 있는 경우에만 작동한다. [물론 그 요소도] 쌓임 맥락의 뿌리 요소보다 뒤로 갈 수는 없다. 이에 관한 훌륭한 예제는 니콜라스 갤라허(Nicolas Gallagher)의 이미지 없는 CSS 드롭 쉐도우다.

전체 쌓임 순서(Global Stacking Order)

언제 어떻게 새로운 쌓임 맥락이 만들어지는지에 대한 견고한 이해, 쌓임 맥락 안에서의 쌓임 순서에 대한 명확한 이해와 함께 특정 요소가 전체 쌓임 순서에서 어떻게 나타나는지를 이해하는 것도 나쁘지 않다.

실수를 피하는 열쇠는 언제 새로운 쌓임 맥락이 만들어지는지 알 수 있게 되는 것이다. z-index 값을 십만을 줬는데도 [그 요소가] 쌓임 순서에서 앞으로 나오지 않는다면, 그 조상 트리를 살피면서 그 부모가 쌓임 맥락을 만들지는 않는지 확인해 봐라. 그런 요소가 있다면, z-index 값 십만도 소용 없다.

감싸기(Wrapping Up)

원래 문제로 돌아가자. 각 태그에 쌓임 순서를 가리키는 주석을 추가했다. 이 순서는 원래 CSS를 바탕으로 한 것이다.

<div><!-- 1 -->
  <span><!-- 6 --></span>
</div>
<div><!-- 2 -->
  <span><!-- 4 --><span>
</div>
<div><!-- 3 -->
  <span><!-- 5 --></span>
</div>

우리가 첫 번재 <div>에 opacity 규칙을 추가했을 때, 쌓임 순서는 아래처럼 바뀌었다.

<div><!-- 1 -->
  <span><!-- 1.1 --></span>
</div>
<div><!-- 2 -->
  <span><!-- 4 --><span>
</div>
<div><!-- 3 -->
  <span><!-- 5 --></span>
</div>

span.red의 순서는 6이었지만, 1.1로 바뀌었다. 나는 새로운 쌓임 맥락이 생겼다는 것을 표시하기 위해 점(.)을 사용했고, span.red는 그 새로운 쌓임 맥락에서 현재 첫 번째 요소다.

이제 빨간 박스가 왜 다른 것들의 뒤로 갔는지 좀더 명확히 이해할 수 있게 됐다면 좋겠다. 원래 예제에는 쌓임 맥락이 두 개밖에 없었다. 뿌리(root) 요소와 span.red에서 만들어진 것이다. 우리가 span.red의 부모 요소에 opacity를 줬을 때, 우리는 세 번째 쌓임 맥락을 만든 것이고, 그 결과, span.red의 z-index는 오직 새로운 쌓임 맥락 안에서만 적용되게 되었다. 첫 번째 <div>(우리가 opacity를 매긴 놈)와 그 형제 요소들에 position이나 z-index 값이 없었기 때문에 그 쌓임 맥락[즉, 뿌리 요소(root)와 <div>들이 속해 있는 쌓임 맥락] 안의 모든 요소들은 HTML 소스 순서에 따라 쌓임 순서가 결정되었고, 그래서 첫번째 <div>와 그것의 쌓임 맥락 안에 있는 모든 요소들이 두 번째와 세 번째 <div>의 뒤에 그려진 것이다.

더 읽을 거리

Notes:

  1. When a new stacking context is formed on an element, that stacking context confines all of its child elements to a particular place in the stacking order.
  2. That means that if an element is contained in a stacking context at the bottom of the stacking order, there is no way to get it to appear in front of another element in a different stacking context that is higher in the stacking order, even with a z-index of a billion!