본문 바로가기

z-index가 작동하지 않는 진짜 이유 (Stacking Context 정리)

📑 목차

    웹 개발을 하다 보면 한 번쯤은 이런 경험을 해보셨을 겁니다. "분명히 `z-index` 값을 엄청 높게 설정했는데, 왜 이 요소는 다른 요소 뒤에 숨어있지?" 혹은 "모달 팝업이 다른 UI 요소 위에 나타나지 않고 어딘가에 가려져 있어!"

    이러한 상황은 개발자들에게 큰 좌절감을 안겨주곤 합니다. 마치 `z-index`가 마법처럼 작동하지 않는 것처럼 느껴지죠. 하지만 걱정하지 마세요. `z-index`가 작동하지 않는 것처럼 보이는 진짜 이유는 마법이 아니라, 바로 'Stacking Context'(스태킹 컨텍스트)라는 CSS의 중요한 개념 때문입니다. 이 가이드에서는 Stacking Context가 무엇인지, 왜 `z-index`와 밀접하게 관련되어 있는지, 그리고 실생활에서 발생하는 문제들을 어떻게 해결할 수 있는지 종합적으로 알려드리겠습니다.

    z-index는 무엇이며 어떻게 작동해야 하는가

    `z-index`는 웹 페이지에서 요소들의 겹침 순서를 제어하는 CSS 속성입니다. 이름 그대로 'z축' 방향, 즉 화면을 뚫고 나오는 듯한 깊이 방향의 순서를 결정한다고 생각하시면 쉽습니다. 일반적으로 `z-index` 값이 높을수록 화면의 '앞쪽'에 나타나고, 값이 낮을수록 '뒤쪽'에 나타납니다.

    하지만 `z-index`가 작동하기 위한 필수 조건이 있습니다. 바로 해당 요소의 `position` 속성이 `static`이 아니어야 한다는 점입니다. `position: relative`, `position: absolute`, `position: fixed`, `position: sticky` 중 하나로 설정되어야 `z-index`가 비로소 의미를 가집니다. `position: static`인 요소에는 `z-index`를 아무리 높게 주어도 아무런 효과가 없습니다.

    간단한 예시를 들어보겠습니다.

    <div style="position: relative; background-color: lightblue; width: 200px; height: 200px; z-index: 1;">박스 1</div>

    <div style="position: absolute; top: 100px; left: 100px; background-color: lightcoral; width: 200px; height: 200px; z-index: 2;">박스 2</div>

    
    

     

    
    

    이 코드에서는 '박스 2'가 '박스 1' 위에 겹쳐서 나타날 것입니다. '박스 2'의 `z-index`가 2로 '박스 1'의 1보다 높기 때문입니다. 이처럼 단순한 경우에는 `z-index`가 예상대로 잘 작동합니다.

    Stacking Context의 등장 왜 중요할까

    앞서 설명한 단순한 예시를 벗어나 복잡한 웹 페이지를 만들다 보면 `z-index`가 말을 듣지 않는 상황에 직면합니다. 이때 등장하는 개념이 바로 Stacking Context입니다. Stacking Context는 웹 페이지의 3D 공간에서 요소들이 겹쳐지는 순서를 결정하는 독립적인 '층' 또는 '영역'이라고 생각할 수 있습니다.

    가장 중요한 사실은 `z-index`는 오직 같은 Stacking Context 내부에서만 유효하다는 것입니다. 서로 다른 Stacking Context에 속한 요소들은 각각의 Stacking Context 규칙에 따라 겹쳐지며, 상위 Stacking Context의 순서가 하위 Stacking Context의 모든 `z-index` 값보다 우선합니다.

    예를 들어, 부모 요소가 하나의 Stacking Context를 생성하고, 그 안에 자식 요소들이 있다고 가정해봅시다. 자식 요소들은 아무리 `z-index` 값을 높게 주어도 부모 Stacking Context의 겹침 순서를 넘어설 수 없습니다. 마치 건물의 층과 비슷합니다. 1층에 있는 아무리 높은 물건이라도 2층에 있는 낮은 물건보다 위로 올라갈 수 없는 것과 같습니다.

    이러한 특성 때문에 `z-index`가 예상대로 작동하지 않을 때, 가장 먼저 확인해야 할 것이 바로 해당 요소들이 어떤 Stacking Context에 속해 있는지 파악하는 것입니다.

    Stacking Context는 언제 생성될까

    Stacking Context는 특정 CSS 속성들이 적용될 때 자동으로 생성됩니다. 이러한 속성들을 이해하는 것이 `z-index` 문제를 해결하는 핵심 열쇠입니다. 다음은 새로운 Stacking Context를 생성하는 대표적인 CSS 속성들입니다.

    • `position`과 `z-index`
    • `position` 속성이 `static`이 아니면서 (`relative`, `absolute`, `fixed`, `sticky`), `z-index` 값이 `auto`가 아닌 경우 (숫자 값) 새로운 Stacking Context를 생성합니다. 이것이 가장 일반적인 Stacking Context 생성 조건입니다.
    • `opacity`
    • `opacity` 값이 `1`보다 작은 경우 (예: `opacity: 0.99`) 새로운 Stacking Context를 생성합니다.
    • `transform`
    • `transform` 속성이 `none`이 아닌 경우 (예: `transform: translateX(0)`) 새로운 Stacking Context를 생성합니다. 심지어 아무런 시각적 변화를 주지 않는 `transform: translateX(0)` 같은 값도 Stacking Context를 만듭니다.
    • `filter`
    • `filter` 속성이 `none`이 아닌 경우 (예: `filter: blur(1px)`) 새로운 Stacking Context를 생성합니다.
    • `perspective`
    • `perspective` 속성이 `none`이 아닌 경우 새로운 Stacking Context를 생성합니다.
    • `will-change`
    • `will-change` 속성이 `opacity`, `transform`, `filter` 등 Stacking Context를 생성하는 다른 속성 중 하나로 설정된 경우 새로운 Stacking Context를 생성합니다. 이 속성은 브라우저에게 요소의 특정 속성이 변경될 것임을 미리 알려 성능 최적화를 돕는 역할을 하지만, 부수적으로 Stacking Context를 만들 수 있습니다.
    • Flex 또는 Grid 컨테이너 내의 `z-index`
    • Flex 컨테이너 또는 Grid 컨테이너의 자식 요소에 `z-index` 속성이 `auto`가 아닌 값으로 적용될 경우, 해당 자식 요소는 새로운 Stacking Context를 생성합니다.
    • `isolation`
    • `isolation: isolate` 속성은 명시적으로 새로운 Stacking Context를 생성하도록 지시합니다. 이는 다른 Stacking Context 생성 조건과 상관없이 독립적인 Stacking Context를 만들고자 할 때 사용됩니다.
    • `mix-blend-mode`
    • `mix-blend-mode` 속성이 `normal`이 아닌 경우 새로운 Stacking Context를 생성합니다.

    이러한 속성들이 Stacking Context를 생성하는 이유는 주로 브라우저의 렌더링 최적화와 관련이 있습니다. 특히 GPU 가속을 사용하는 경우, 브라우저는 특정 요소를 독립적인 레이어로 분리하여 렌더링함으로써 성능을 향상시킵니다. 이 독립적인 레이어가 바로 Stacking Context가 되는 경우가 많습니다.

    실생활에서 z-index가 작동하지 않는 사례 및 해결 방법

    이제 실제 개발 환경에서 흔히 겪는 `z-index` 문제를 Stacking Context 개념을 적용하여 해결하는 방법을 알아보겠습니다.

    부모 요소의 Stacking Context에 갇힌 자식 요소

    문제 상황

    모달 팝업이나 툴팁이 다른 콘텐츠 위에 나타나야 하는데, 특정 부모 요소 안에서만 겹쳐지고 페이지의 다른 부분 위로 올라오지 못하는 경우가 있습니다. 예를 들어, 특정 섹션(`section`) 내에 모달이 있고, 이 `section`이 `position: relative`와 `z-index: 10`을 가지고 있다고 가정해봅시다. 모달 자체에 `z-index: 9999`를 주어도, 이 모달은 `section`이라는 부모 Stacking Context의 한계를 벗어나지 못합니다. 다른 `section` 위에 나타나기는커녕, 같은 `section` 내의 다른 요소 뒤에 숨을 수도 있습니다.

    <div style="position: relative; z-index: 10; background: lightgray; padding: 20px;">

    <p>부모 Stacking Context 요소</p>

    
    

    <div style="position: absolute; top: 50px; left: 50px; background: lightblue; width: 150px; height: 150px; z-index: 1;">

    
    

    <p>부모 안의 일반 요소</p>

    
    

    </div>

    
    

    <!-- 이 모달은 부모 Stacking Context에 갇힙니다 -->

    
    

    <div style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 9999; display: flex; justify-content: center; align-items: center;">

    
    

    <div style="background: white; padding: 20px; border-radius: 5px;">

    
    

    <p>모달 팝업 (하지만 부모를 벗어나지 못함)</p>

    
    

    </div>

    
    

    </div>

    
    

    </div>

    
    

    <div style="margin-top: 50px; background: lightgreen; padding: 20px;">

    
    

    <p>다른 섹션 요소</p>

    
    

    </div>

    
    

     

    
    

    해결 방법

    가장 확실한 해결책은 문제가 되는 요소를 Stacking Context를 생성하는 부모 요소 밖으로 이동시키는 것입니다. 예를 들어, 모달 팝업은 DOM 트리의 가장 최상위 근처(예: `body` 태그 바로 아래)에 배치하는 것이 일반적입니다. 이렇게 하면 모달은 가장 상위 Stacking Context에 속하게 되어, 페이지의 어떤 요소 위로도 자유롭게 나타날 수 있습니다.

    <div style="position: relative; z-index: 10; background: lightgray; padding: 20px;">

    <p>부모 Stacking Context 요소</p>

    
    

    <div style="position: absolute; top: 50px; left: 50px; background: lightblue; width: 150px; height: 150px; z-index: 1;">

    
    

    <p>부모 안의 일반 요소</p>

    
    

    </div>

    
    

    </div>

    
    

    <div style="margin-top: 50px; background: lightgreen; padding: 20px;">

    
    

    <p>다른 섹션 요소</p>

    
    

    </div>

    
    

    <!-- 모달 팝업을 body 바로 아래로 이동 -->

    
    

    <div style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 9999; display: flex; justify-content: center; align-items: center;">

    
    

    <div style="background: white; padding: 20px; border-radius: 5px;">

    
    

    <p>모달 팝업 (이제 모든 요소 위에 나타남)</p>

    
    

    </div>

    
    

    </div>

    
    

     

    
    

    `opacity`, `transform` 등으로 생성된 Stacking Context

    문제 상황

    `z-index`를 사용하지 않았거나, 부모 요소에 `position` 속성을 주지 않았는데도 요소가 다른 요소 뒤에 숨는 경우가 있습니다. 이때는 `opacity: 0.99`, `transform: translateX(0)`, `filter: blur(0px)`와 같이 시각적으로 큰 변화가 없는 속성들이 의도치 않게 Stacking Context를 생성했을 가능성이 높습니다.

    <div style="background: lightgray; padding: 20px;">

    <div style="background: lightblue; width: 200px; height: 100px; margin-bottom: -50px;">

    
    

    <p>일반 요소</p>

    
    

    </div>

    
    

    <!-- 이 요소는 transform 때문에 새로운 Stacking Context를 만듭니다 -->

    
    

    <div style="background: lightcoral; width: 200px; height: 100px; transform: translateX(0px); z-index: 10;">

    
    

    <p>Transform으로 Stacking Context 생성. z-index 10은 이 컨텍스트 안에서만 유효.</p>

    
    

    </div>

    
    

    </div>

    
    

    <div style="position: relative; top: -70px; left: 100px; background: lightgreen; width: 150px; height: 150px; z-index: 5;">

    
    

    <p>이 요소는 z-index 5인데도 위의 transform 요소 위에 나타날 수 있습니다.</p>

    
    

    </div>

    
    

     

    
    

    위 예시에서 `lightcoral` 박스는 `transform: translateX(0px)` 때문에 새로운 Stacking Context를 생성합니다. 따라서 그 안의 `z-index: 10`은 그 Stacking Context 안에서만 유효합니다. 이와 별개로 생성된 `lightgreen` 박스가 `z-index: 5`임에도 불구하고 `lightcoral` 박스 위에 나타날 수 있습니다. 왜냐하면 `lightgreen` 박스와 `lightcoral` 박스의 부모 요소가 동일한 Stacking Context에 속하거나, `lightgreen` 박스가 더 높은 Stacking Context에 속하기 때문입니다.

    해결 방법

    개발자 도구를 사용하여 해당 요소에 적용된 CSS 속성들을 꼼꼼히 확인합니다. 특히 `opacity`, `transform`, `filter`, `perspective`, `will-change`, `isolation`, `mix-blend-mode` 등의 속성이 의도치 않게 적용되어 있는지 확인해야 합니다. 만약 해당 속성이 시각적으로 필수적이지 않다면, 제거하거나 다른 방법으로 대체하는 것을 고려합니다.

    예를 들어, 애니메이션을 위해 `transform`을 사용했는데 `z-index` 문제가 발생했다면, `transform`을 부여하는 부모 요소의 `z-index`를 조정하거나, 애니메이션 대상 요소를 다른 Stacking Context로 이동시키는 등의 방법을 고려할 수 있습니다.

    Flex 또는 Grid 컨테이너 내의 z-index

    문제 상황

    Flexbox나 CSS Grid 레이아웃을 사용하면서 `z-index`를 적용할 때 예상과 다르게 작동할 수 있습니다. Flex 또는 Grid 컨테이너의 자식 요소에 `z-index`가 `auto`가 아닌 값으로 적용되면, 그 자식 요소는 새로운 Stacking Context를 생성합니다. 이 때문에 컨테이너 내의 다른 아이템과의 겹침 순서는 예상대로 작동할 수 있지만, 컨테이너 외부의 요소와 겹칠 때는 문제가 발생할 수 있습니다.

    <div style="display: flex; position: relative; z-index: 10; background: lightgray; padding: 20px;">

    <div style="width: 100px; height: 100px; background: lightblue; z-index: 2;">Flex Item 1</div>

    
    

    <div style="width: 100px; height: 100px; background: lightcoral; margin-left: -50px; z-index: 1;">Flex Item 2</div>

    
    

    </div>

    
    

    <div style="position: relative; top: -70px; left: 150px; background: lightgreen; width: 150px; height: 150px; z-index: 5;">

    
    

    <p>외부 요소 (z-index: 5)</p>

    
    

    </div>

    
    

     

    
    

    여기서 `Flex Item 1`은 `z-index: 2`, `Flex Item 2`는 `z-index: 1`이므로, `Flex Item 1`이 `Flex Item 2` 위에 나타납니다. 하지만 이 Flex 컨테이너 자체가 `z-index: 10`인 Stacking Context를 생성했습니다. 만약 `lightgreen` 외부 요소가 `z-index: 5`라고 해도, 이 외부 요소는 Flex 컨테이너의 `z-index` 순서에 따라 겹쳐지게 됩니다. 즉, `lightgreen` 요소는 Flex 컨테이너의 뒤에 나타날 가능성이 높습니다. Flex Item 자체의 `z-index`는 외부 요소에 직접적인 영향을 주지 못합니다.

    해결 방법

    Flex 또는 Grid 아이템에 `z-index`를 적용할 때는 해당 컨테이너 자체가 어떤 Stacking Context에 속해 있는지, 그리고 컨테이너 외부의 다른 요소들과의 겹침 순서를 어떻게 가져갈지 함께 고려해야 합니다. 필요한 경우 Flex/Grid 아이템 자체의 `z-index`를 `auto`로 두고, 컨테이너의 `z-index`나 DOM 구조를 조정하는 것이 더 효과적일 수 있습니다.

    Stacking Context 시각화 및 디버깅 팁

    `z-index` 문제를 해결하는 가장 좋은 방법은 Stacking Context를 명확하게 이해하고 시각적으로 확인하는 것입니다.

    브라우저 개발자 도구 활용

    • Elements 탭과 Styles 탭
    • 요소를 선택하고 Styles 탭에서 `position`, `z-index`, `opacity`, `transform`, `filter` 등 Stacking Context를 생성할 수 있는 모든 속성을 확인합니다. 부모 요소들을 차례로 거슬러 올라가면서 어떤 요소가 Stacking Context를 생성하는지 파악하는 것이 중요합니다.
    • Computed 탭
    • Computed 탭에서는 최종적으로 적용된 CSS 속성 값을 볼 수 있습니다. `z-index`가 `auto`인지, 아니면 특정 숫자로 계산되었는지 확인하고, `position` 속성도 함께 살펴봅니다.
    • Layers 패널 (Chrome 개발자 도구)
    • Chrome 개발자 도구의 'More tools'에서 'Layers' 패널을 열어볼 수 있습니다. 이 패널은 웹 페이지의 모든 렌더링 레이어를 시각적으로 보여줍니다. 여기서 각 레이어가 어떤 Stacking Context에 속해 있는지, 그리고 어떤 속성(예: `transform`, `opacity`) 때문에 새로운 레이어가 생성되었는지 확인할 수 있습니다. 이는 Stacking Context를 디버깅하는 데 매우 강력한 도구입니다.

    DOM 구조 단순화

    문제가 복잡하게 얽혀 있다면, 의심되는 요소를 제외한 다른 요소들을 임시로 주석 처리하거나 제거하여 문제를 단순화합니다. 이렇게 하면 어떤 요소가 `z-index` 문제를 일으키는지 더 쉽게 격리할 수 있습니다.

    경계 시각화

    문제가 되는 요소와 그 주변 요소에 임시로 `border`나 `background-color`를 추가하여 각 요소의 실제 크기와 위치, 그리고 겹침 관계를 시각적으로 명확하게 확인합니다. `outline` 속성도 요소를 선택했을 때 시각적으로 구별하는 데 도움이 됩니다.

    흔한 오해와 사실 관계

    `z-index`와 Stacking Context에 대한 몇 가지 흔한 오해들을 바로잡아 보겠습니다.

    • 오해 `z-index 9999`는 항상 최상위다
    • 사실 `z-index: 9999`는 해당 요소가 속한 Stacking Context 내에서만 최상위입니다. 만약 이 요소의 부모 요소가 `z-index: 10`인 다른 Stacking Context를 생성했다면, 아무리 9999를 주어도 부모 Stacking Context의 한계를 벗어날 수 없습니다. Stacking Context 간의 순서는 부모 Stacking Context의 `z-index` 값에 의해 결정됩니다.
    • 오해 `position static` 요소에도 `z-index`를 적용할 수 있다
    • 사실 `z-index` 속성은 `position` 속성이 `static`이 아닌 요소 (`relative`, `absolute`, `fixed`, `sticky`)에만 유효합니다. `position: static`인 요소에 `z-index`를 적용하더라도 아무런 효과가 없습니다.
    • 오해 Stacking Context는 성능에만 영향 미친다
    • 사실 Stacking Context는 브라우저 렌더링 성능 최적화와 밀접하게 관련되어 있지만, 직접적으로 요소들의 시각적 겹침 순서를 결정하는 핵심적인 규칙입니다. 즉, 성능뿐만 아니라 웹 페이지의 시각적 구성에 직접적인 영향을 미칩니다.

    전문가를 위한 고급 팁과 조언

    • 명시적인 Stacking Context 생성 `isolation isolate`
    • `isolation: isolate` 속성은 해당 요소에 대해 명시적으로 새로운 Stacking Context를 생성하도록 지시합니다. 이는 다른 Stacking Context 생성 조건(예: `position`, `opacity`, `transform`)이 없더라도 독립적인 Stacking Context를 만들 수 있어, 복잡한 겹침 문제를 해결하거나 특정 영역의 `z-index`를 완전히 분리해야 할 때 유용하게 사용될 수 있습니다. 하지만 불필요하게 남용하면 오히려 디버깅을 어렵게 만들 수 있으므로 신중하게 사용해야 합니다.
    • DOM 구조의 중요성
    • 복잡한 `z-index` 문제를 최소화하려면 모달, 툴팁, 드롭다운 메뉴와 같이 항상 다른 요소 위에 나타나야 하는 UI 컴포넌트들은 가능한 한 DOM 트리의 높은 곳(예: `body` 태그 바로 아래)에 배치하는 것이 좋습니다. 이렇게 하면 해당 요소들이 가장 상위 Stacking Context에 속하게 되어, `z-index` 값을 쉽게 제어할 수 있습니다.
    • CSS 변수 활용
      :root {
      --z-index-base: 1;--z-index-dropdown: 100;--z-index-fixed-header: 200;--z-index-modal: 1000;--z-index-tooltip: 1010;}.modal {z-index: var(--z-index-modal);}
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    • 복잡한 프로젝트에서 `z-index` 값을 일관성 있게 관리하기 위해 CSS 변수를 활용하는 것을 고려해볼 수 있습니다. 예를 들어, `--z-index-modal`, `--z-index-dropdown`, `--z-index-fixed-header`와 같이 의미 있는 변수 이름을 사용하여 `z-index` 값을 정의하고, 이를 필요한 곳에 적용합니다. 이렇게 하면 `z-index`의 우선순위를 한눈에 파악하기 쉽고, 수정이 필요할 때 한 곳에서만 변경하면 되므로 유지보수성이 향상됩니다.
    • Stacking Context의 '부모화'
    • 때로는 자식 요소의 `z-index`가 특정 부모 요소의 범위 내에서만 작동하도록 제어해야 할 때가 있습니다. 이런 경우, 부모 요소에 `position: relative`와 적절한 `z-index` 값을 부여하여 새로운 Stacking Context를 생성하면, 그 자식 요소들의 `z-index`는 이 부모 Stacking Context 안에서만 유효하게 됩니다. 이는 특정 컴포넌트의 내부 겹침 순서를 외부와 분리하여 관리할 때 효과적입니다.

    자주 묻는 질문과 답변

    • Q `z-index auto`는 무엇인가요
    • A `z-index: auto`는 새로운 Stacking Context를 생성하지 않습니다. 대신, 해당 요소가 속한 부모의 Stacking Context에 포함되어 부모와 동일한 `z-index` 레벨에서 렌더링됩니다. 즉, 부모 요소의 겹침 순서에 따라 자식 요소의 겹침 순서가 결정됩니다. 대부분의 경우 `z-index`를 명시적으로 지정할 필요가 없다면 `auto` 상태를 유지하는 것이 좋습니다.
    • Q `z-index`에 음수 값을 사용할 수 있나요
    • A 네, 사용할 수 있습니다. `z-index`에 음수 값을 부여하면 해당 요소는 부모 Stacking Context의 배경이나 다른 콘텐츠 뒤로 숨겨질 수 있습니다. 예를 들어, 특정 배경 이미지나 패턴을 콘텐츠 아래에 배치하고 싶을 때 유용하게 사용됩니다. 하지만 음수 `z-index`는 해당 요소가 속한 Stacking Context의 뒤로만 갈 수 있다는 점을 기억해야 합니다.