- Published on
Chrome 렌더링 깨짐? contain·will-change 함정
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버나 데이터 문제처럼 보이는데 UI만 이상하게 깨지는 경우가 있습니다. 특히 Chrome에서만 다음 같은 증상이 간헐적으로 나타납니다.
- 텍스트가 갑자기 흐려지거나(서브픽셀 안티앨리어싱이 꺼진 듯) 두께가 달라짐
- 스크롤 중 요소가 1px씩 떨리거나 깜빡임
position: sticky나 툴팁/드롭다운이 잘리거나, z-index가 먹히지 않는 것처럼 보임- transform 애니메이션 중 경계가 깨지거나, 배경이 비쳐 보이는 합성 아티팩트
이런 경우 “GPU 가속”이나 “Chrome 버그”로 뭉뚱그리기 쉽지만, 실제로는 우리가 성능 최적화 목적으로 넣은 contain과 will-change가 새로운 레이어/격리 컨텍스트를 만들면서 부작용을 일으키는 경우가 많습니다. 이 글은 그 함정들을 증상 중심으로 정리하고, 안전하게 되돌리는 방법까지 다룹니다.
관련해서 스크롤/레이아웃 특이점은 브라우저별로도 자주 다릅니다. iOS Safari의 sticky 이슈는 별도로 정리해 둔 글도 참고하세요: Safari iOS에서 position - sticky 깨짐 원인·해결
contain과 will-change가 ‘깨짐’을 만드는 이유
contain은 “격리”를 강제한다
contain은 브라우저에 “이 요소는 외부에 영향을 덜 주니 최적화해도 된다”고 힌트를 주는 속성입니다. 대표적으로 다음을 분리합니다.
contain: layout레이아웃 계산 범위 축소contain: paint페인팅 범위 격리(자식이 밖으로 그리기 어려움)contain: size크기 계산을 독립적으로 처리contain: strict위를 모두 강하게 적용
문제는 contain: paint 계열이 들어가면 오버플로우/클리핑/스택킹 컨텍스트가 예상과 달라지고, sticky/fixed/포털 UI가 잘리는 형태로 나타날 수 있다는 점입니다.
will-change는 “곧 바뀔 테니 레이어를 준비하라”는 신호다
will-change: transform 같은 값을 주면 Chrome은 성능을 위해 해당 요소를 별도 합성 레이어로 올리려는 경향이 있습니다. 이때:
- 텍스트 렌더링이 레이어 합성 방식으로 바뀌며 흐려 보일 수 있음
- 불필요한 레이어가 늘어나면 오히려 프레임 드랍/깜빡임이 생김
- 레이어 경계에서 1px seam, aliasing, 클리핑 등 아티팩트가 드러날 수 있음
즉, contain과 will-change는 “최적화” 도구지만, UI 일관성과 페인팅/합성 안정성을 희생할 수 있습니다.
빠른 진단 체크리스트(Chrome DevTools)
- DevTools
Rendering패널에서Paint flashing을 켜서 과도한 repaint가 있는지 확인 Layers패널로 레이어가 과도하게 생성되는지 확인- 문제 요소의 computed style에서
contain과will-change적용 여부 확인 - 문제가 “스크롤 중만” 발생한다면, 스크롤 컨테이너에
transform또는contain: paint가 있는지 확인
특히 will-change가 상시 적용되어 있으면, 레이어가 항상 유지되며 부작용이 지속됩니다.
대표 증상 1: 텍스트가 흐려짐(특히 transform/will-change)
재현 예시
아래처럼 카드에 will-change: transform을 상시 적용하고, hover 시 transform을 주면 일부 환경에서 텍스트가 살짝 흐려지거나 두께가 달라 보일 수 있습니다.
<div class="card">Chrome에서 글자가 흐려질 수 있는 카드</div>
.card {
font-size: 18px;
padding: 16px;
border-radius: 12px;
background: white;
box-shadow: 0 8px 30px rgba(0,0,0,0.12);
/* 함정: 상시 will-change */
will-change: transform;
transition: transform 200ms ease;
}
.card:hover {
transform: translateY(-2px);
}
왜 이런가
텍스트가 그려지는 경로가 바뀌며(레이어 합성), 서브픽셀 렌더링이 제한되거나 안티앨리어싱이 달라질 수 있습니다. “버그”라기보다는 합성 정책 변화의 부작용에 가깝습니다.
해결 패턴
1) will-change를 상시 주지 말고 “필요할 때만”
const el = document.querySelector('.card');
el.addEventListener('mouseenter', () => {
el.style.willChange = 'transform';
});
el.addEventListener('mouseleave', () => {
// 애니메이션이 끝난 뒤 제거
setTimeout(() => {
el.style.willChange = 'auto';
}, 250);
});
2) 애니메이션 대상 최소화
카드 전체가 아니라 “그림자/배경”만 transform하는 식으로 레이어 영향 범위를 줄입니다.
3) 정말 필요할 때만 레이어 승격
will-change는 “성능 핫스팟”에만 제한적으로 쓰는 것이 안전합니다. 무분별한 적용은 레이어 폭증으로 이어집니다.
대표 증상 2: 드롭다운/툴팁이 잘림(특히 contain: paint)
재현 예시
<div class="panel">
<button class="btn">메뉴</button>
<div class="menu">드롭다운이 잘릴 수 있음</div>
</div>
.panel {
position: relative;
padding: 24px;
border: 1px solid #ddd;
/* 함정: paint 격리로 인해 자식이 밖으로 그리기 어려워짐 */
contain: paint;
}
.menu {
position: absolute;
top: 48px;
left: 0;
width: 240px;
padding: 12px;
background: white;
border: 1px solid #ccc;
box-shadow: 0 12px 40px rgba(0,0,0,0.18);
}
해결 패턴
1) 오버레이 UI는 contain: paint 영역 밖으로 “포털”
React라면 createPortal로 body 하위에 렌더링해 클리핑/스택킹 영향을 피합니다.
import { createPortal } from 'react-dom';
export function MenuPortal({ open, anchorRect }: { open: boolean; anchorRect: DOMRect }) {
if (!open) return null;
return createPortal(
<div
style={{
position: 'fixed',
top: anchorRect.bottom,
left: anchorRect.left,
width: 240,
background: 'white',
border: '1px solid #ccc',
boxShadow: '0 12px 40px rgba(0,0,0,0.18)',
padding: 12,
zIndex: 9999
}}
>
드롭다운
</div>,
document.body
);
}
2) contain을 더 약하게
가능하면 contain: content나 특정 축만 격리하는 방식으로 줄입니다. “페인트 격리”가 꼭 필요한지부터 재검토하세요.
대표 증상 3: sticky가 이상해짐(스크롤 컨테이너 + contain/transform)
position: sticky는 “가까운 스크롤 컨테이너”와 “조상 요소의 페인팅/변환 컨텍스트” 영향을 강하게 받습니다. 조상에 contain: paint나 transform이 들어가면 sticky가 기대와 다르게 동작하거나, 경계에서 깜빡일 수 있습니다.
점검 포인트
- sticky 요소의 조상 중
overflow: hidden/auto/scroll이 있는지 - 조상 중
transform: translateZ(0)같은 레이어 트릭이 있는지 - 조상 중
contain: paint또는contain: strict가 있는지
sticky 이슈는 브라우저별 차이가 커서, 동일한 CSS라도 Safari/iOS에서 더 자주 터집니다. 위에서 링크한 글도 함께 보면 원인 분리가 쉬워집니다.
대표 증상 4: 1px 틈(seam), 경계 깨짐, 깜빡임
합성 레이어 경계에서 1px seam이 생기는 경우가 있습니다. 특히 다음 조합에서 빈도가 올라갑니다.
- fractional pixel(예:
translateY(0.5px)같은 소수점 이동) filter,backdrop-filter,mask,clip-path같이 합성 비용이 큰 효과contain으로 페인트 영역이 잘게 쪼개짐
해결 패턴
- transform 이동 값을 정수 픽셀로 맞추기(가능하면)
filter/backdrop-filter적용 범위를 최소화- 필요 없는 레이어 승격(
will-change) 제거 - 경계가 보이는 요소에
background-color를 명시해 블렌딩 아티팩트 줄이기
contain을 안전하게 쓰는 가이드
contain은 “성능 개선”이 목적이지만, UI 컴포넌트 구조와 충돌하면 비용이 더 커집니다. 다음 원칙을 권장합니다.
1) 오버레이/포털 UI가 있는 컨테이너에는 contain: paint를 피한다
드롭다운, 툴팁, 모달, 컨텍스트 메뉴가 컨테이너 밖으로 나가야 한다면 paint 격리는 거의 항상 문제를 만듭니다.
2) contain: layout만으로도 충분한지 먼저 확인
레이아웃 계산 최적화가 목적이라면 contain: layout 또는 contain: content로도 효과를 보는 경우가 많습니다.
3) contain: size는 skeleton/placeholder 전략과 같이 써라
contain: size는 크기 계산을 독립시키므로, 콘텐츠 크기에 따라 레이아웃이 변해야 하는 UI에는 부작용이 큽니다. 고정 높이 skeleton을 두거나, 최소/최대 크기를 명시하는 식으로 설계를 동반해야 합니다.
will-change를 안전하게 쓰는 가이드
1) 상시 적용 금지, 짧게 쓰고 지운다
will-change는 “곧 바뀐다”는 힌트입니다. 항상 바뀌지 않는 요소에 상시 적용하면 레이어가 계속 유지됩니다.
2) 대상 속성 최소화
will-change: transform, opacity 정도가 일반적입니다. will-change: contents 같은 광범위한 값은 의도치 않은 비용을 만들 수 있습니다.
3) 애니메이션은 가능한 transform과 opacity로 끝내되, 텍스트 품질을 확인한다
성능만 보고 넣었다가 텍스트가 흐려지면 UX 손해가 큽니다. 특히 본문 텍스트가 들어간 카드 전체를 transform하는 패턴은 주의하세요.
실전 디버깅 절차: “원인 CSS를 격리”하는 방법
렌더링 깨짐은 재현이 어렵고, 원인이 여러 개 겹치기 쉽습니다. 다음 순서로 범위를 좁히면 빠릅니다.
- 문제 요소에서
contain을 모두 제거해보기 - 조상까지 포함해
will-change를 모두 제거해보기 - 조상에 있는
transform/filter/backdrop-filter를 하나씩 끄기 - 스크롤 컨테이너의
overflow와 sticky 관계 확인 - 레이어 수를 줄인 뒤(불필요한 승격 제거) 다시 효과를 최소 단위로 추가
이 과정은 장애 대응의 “진단 루틴”과 유사합니다. 예를 들어 네트워크/서버 장애에서 재시도와 폴백을 설계하듯, 프런트 렌더링 문제도 원인을 단계적으로 격리해야 합니다. 접근법 자체는 비슷하니 관심 있으면 OpenAI Responses API 503 멈춤 - 재시도·폴백 설계도 참고할 만합니다.
결론: 최적화 힌트는 ‘계약’이 아니라 ‘요청’이다
contain과 will-change는 브라우저에게 최적화 여지를 주는 강력한 도구지만, 컴포넌트의 페인팅/합성/스택킹 규칙을 바꿔 버릴 수 있습니다. Chrome 렌더링 깨짐이 보이면 다음을 우선 의심하세요.
contain: paint로 인한 클리핑/스택킹 변화will-change상시 적용으로 인한 레이어 승격 및 텍스트 렌더링 변화- 스크롤 컨테이너 조상에
transform/contain이 섞이며 sticky/overlay가 흔들리는 케이스
성능 최적화는 “측정 가능한 병목”에만 국소적으로 적용하고, 오버레이/텍스트 품질/스크롤 안정성 같은 UX 핵심 경로에서는 보수적으로 접근하는 것이 장기적으로 더 안전합니다.