z-index: -1 배경 레이어가 안 보인다 — body 배경색이 캔버스로 승격될 때
position:fixed + z-index:-1 로 깐 배경이 Chromium 에서 body 배경색에 덮인다. getComputedStyle 로는 전부 정상으로 보여서 스크린샷 없이는 못 잡는다.
전체화면 배경 레이어를 흔한 방식으로 만들었다. 콘텐츠 로드 전 흰 화면이 깜빡이는 걸 막으려고 fallback 색을 깔고,
html, body { background: #8fd8ea; }
그 위에 실제 배경을 얹었다.
<div style={{ position: 'fixed', inset: 0, zIndex: -1, background: 'url(bg.png) center/cover' }} />
배경 이미지가 전혀 안 보였다. <body> 의 단색만 화면을 덮고 있었다.
스타일은 다 정상이라고 나온다
가장 먼저 의심한 건 이미지 로드 실패였는데 아니었다. getComputedStyle 로 확인하면 전부 멀쩡하다.
background-image설정됨background-position/size정상getBoundingClientRect()가 뷰포트 전체 크기visibility: visible,opacity: 1
스타일 계산은 정상인데 페인트만 안 된다. 그래서 스타일을 assert 하는 테스트로는 절대 안 잡힌다.
여기에 함정이 하나 더 있었다. 처음에는 배경이 CSS 그라데이션이었는데, 색이 <body> fallback 색과 비슷해서 잘 나오는 줄 알았다. 팔레트가 확연히 다른 실제 이미지로 바꾸고 나서야 “통째로 안 보인다”가 드러났다.
원인 — 캔버스 배경
<html> 에 배경이 없고 <body> 에만 배경색이 있으면, 그 색이 캔버스(canvas) 배경으로 승격되어 뷰포트 전체를 채운다. 이건 스펙에 정의된 동작이다.
문제는 Chromium 구현상 이 캔버스 배경 페인트가 음수 z-index 를 가진 포지션 엘리먼트보다 위에 그려지는 경우가 있다는 것이다. z-index: -1 이 “루트 스택 컨텍스트의 배경보다도 뒤”로 밀려버린다.
최소 재현
await page.setContent(`
<html><head><style>html,body{margin:0;background:#8fd8ea;}</style></head>
<body>
<div style="position:fixed;inset:0;z-index:-1;background:linear-gradient(red,blue);"></div>
</body></html>
`);
// 스크린샷: 그라데이션 없이 #8fd8ea 단색
<body> 의 배경색만 빼면 같은 코드가 정상 동작한다. 트리거 조건은 정확히 “body 배경색 + 자식의 음수 z-index” 조합이다.
해결 — 음수 z-index 를 쓰지 않는다
배경을 DOM 순서상 첫 번째로 두고 z-index 를 아예 안 준다.
<>
<div style={{ position: 'fixed', inset: 0, background: 'url(bg.png) center/cover' }} />
<MainContent />
</>
position: fixed + z-index: auto 인 엘리먼트끼리는 DOM 순서로 페인트 순서가 정해진다. 나중에 오는 게 위다. 배경을 맨 앞에 두면 캔버스 배경 위에는 정상적으로 그려지고, 뒤에 오는 콘텐츠보다는 아래에 깔린다. 음수 z-index 없이 원하는 “맨 뒤”가 나온다.
이것만으로는 부족한 경우
MainContent 가 position 을 안 쓰는 일반 static 콘텐츠라면 아직 안 끝났다.
CSS 페인트 순서 규칙(CSS 2.1 Appendix E)상, 같은 스택 컨텍스트 안에서 positioned 엘리먼트는 position 없는 in-flow 콘텐츠보다 항상 나중에 그려진다. z-index: auto 나 0 이어도 그렇고, DOM 순서와 무관하다.
즉 배경 레이어가 DOM 상 먼저 와도, 콘텐츠가 plain <div> 들이면 여전히 배경이 그 위에 얹힌다.
이 상태가 얼마나 안 보이냐면
시각적으로는 멀쩡해 보일 수 있다. 배경이 투명한 부분이 많으면 콘텐츠가 그대로 보인다. 그런데 클릭만 배경 레이어가 가로챈다.
실제로 겪은 증상은 이랬다.
elementFromPoint가 버튼이 아니라 배경<div>를 반환locator.click()이 “element is visible, enabled and stable” 인데도 계속 타임아웃
스크린샷 검증으로는 못 잡고 클릭 상호작용 테스트에서만 드러난다.
해결
콘텐츠 래퍼를 같은 “positioned + 명시적 z-index” 그룹으로 올린다.
<>
<div style={{ position: 'fixed', inset: 0, background: 'url(bg.png) center/cover' }} />
<div style={{ position: 'relative', zIndex: 1 }}> {/* 이 한 줄이 핵심 */}
<MainContent />
</div>
</>
그러면 그룹 안에서 DOM 순서가 타이브레이커로 작동해서 나중에 오는 콘텐츠가 이긴다.
배경 레이어 패턴의 완성형
두 가지를 둘 다 챙겨야 한다.
- 배경 레이어에는 z-index 를 쓰지 않는다 — 캔버스 배경 문제 회피
- 그 위 콘텐츠 래퍼에는
position: relative+zIndex를 명시한다 — static vs positioned 문제 회피
하나만 하면 증상이 다른 형태로 남는다.
디버깅 요령
getComputedStyle 과 getBoundingClientRect() 로는 이 버그가 안 잡힌다. 스타일 계산과 페인트 결과가 분리돼 있기 때문이다. 실제 스크린샷을 눈으로 봐야 발견된다. 헤드리스 브라우저로 스타일만 assert 하는 테스트는 이런 페인트 레이어링 버그를 그냥 통과시킨다.
의심되면 page.setContent() 로 body 배경색 유무만 바꿔가며 최소 재현을 만드는 게 가장 빠르다. 실제 앱에서 뺄셈으로 좁히려 들면 컴포넌트 트리가 방해한다.