CSS 트랜지션이 레이아웃 이동을 일으키는 이유
레이아웃 이동을 일으키는 CSS 트랜지션을 찾아 제거하는 방법을 알아봅니다.
CSS 트랜지션이 레이아웃 이동을 일으키는 이유
CSS 트랜지션으로 인한 Cumulative Layout Shifts는 대개 페이지의 로딩 단계 초기에 발생합니다. 스타일시트나 스크립트가 뒤늦게 스타일을 변경하면 width나 height의 트랜지션이 크기 변화를 애니메이션으로 처리하면서 아래쪽 콘텐츠가 함께 밀려납니다. 이러한 이동은 모든 페이지 조회에서 발생하는 것은 아닙니다. 그래서 디버깅하기 어렵습니다.
2026년 3월 Arjen Karel 최종 검토
Table of Contents!
transition: all이 유발하는 문제
현장에서 가장 자주 발견하는 원인은 transition: all입니다. 어디에나 쓰이고 있습니다. 2022 Web Almanac의 CSS 챕터에 따르면 웹 페이지의 53%가 이 속성을 사용하며, 이는 다른 어떤 transition 속성보다 높은 수치입니다. 그 결과 width와 height까지 애니메이션이 적용되어 요소 주변의 콘텐츠를 움직이게 만듭니다.
CSS 트랜지션은 속성, 지속 시간, 타이밍 함수, 지연 시간의 네 가지 요소로 구성됩니다. 단축 속성은 다음과 같습니다:
/* property | duration | timing-function | delay */
transition: margin-right 4s ease-in-out 1s; 개발자는 호버 색상만 서서히 바뀌도록 의도했으나, 요소에 transition: all .3s ease를 적용하면서 문제가 시작됩니다.
트랜지션은 브라우저가 요소에 대해 처음 계산한 스타일에는 절대 실행되지 않습니다. 그 스타일이 나중에 변경될 때만 실행됩니다. 그리고 페이지 로딩 중에는 이런 일이 수시로 일어납니다. 스타일시트가 뒤늦게 도착하거나, 스크립트가 내비게이션 메뉴에 클래스를 추가하는 경우입니다. transition: all이 설정되어 있으면 브라우저는 크기 변화까지 애니메이션으로 처리합니다. 결국 화면 상단(above the fold)의 메뉴가 300밀리초 동안 제자리를 찾아 커지면서 그 아래의 모든 요소를 아래로 밀어냅니다. 로딩 중에만 발생하는 레이아웃 이동입니다. 직접 재현하기는 정말 어렵습니다.
레이아웃 이동을 일으키는 CSS 속성은 무엇인가?
모든 CSS 트랜지션이 레이아웃 이동을 일으키는 것은 아닙니다. 요소의 크기나 위치를 바꾸는 속성만 브라우저의 레이아웃을 다시 실행시키고, 오직 레이아웃만이 주변 콘텐츠를 움직입니다. Google의 연구에 따르면 레이아웃을 유발하는 CSS 속성에 애니메이션을 적용한 페이지는 CLS를 통과할 확률이 15% 낮습니다. margin과 border-width는 더 심각합니다. 이 두 속성에 애니메이션을 적용한 페이지는 불량한 CLS를 기록할 확률이 일반 페이지보다 거의 두 배나 높습니다.
다음 속성들은 모두 레이아웃 재계산을 유발합니다. 따라서 이 중 어느 하나에라도 트랜지션을 적용하면 요소 자체나 주변 콘텐츠의 위치가 바뀔 수 있습니다: width, height, margin, padding, top, left, right, bottom, border-width, font-size.
CLS에 안전한 속성: transform과 opacity는 컴포지터에서 실행될 수 있습니다. filter와 clip-path는 기껏해야 리페인트만 유발합니다. 두 조합 모두 레이아웃을 트리거하지 않습니다.
페인트만 실행되므로 CLS에 안전한 속성: background-color, color, box-shadow, outline.
핵심은 바로 transform과 opacity입니다. transform은 레이아웃에서 요소의 위치를 바꾸지 않으므로, CLS는 이를 레이아웃 이동으로 집계하지 않습니다. 요소를 이동해야 합니까? top과 left 대신 transform: translate()를 사용하세요. 크기를 키우거나 줄이려면 width와 height 대신 transform: scale()을 사용하세요.
호버 동작도 예외는 아닙니다. CLS는 클릭, 탭 또는 키 입력 후 500ms 이내에 발생하는 이동은 무시합니다. 하지만 요소 위로 마우스를 올리는 것은 이러한 입력에 해당하지 않습니다. 따라서 width에 걸린 호버 트랜지션은 CLS에 그대로 모두 반영됩니다.
아래 동영상은 로딩 단계에서 트랜지션이 레이아웃 이동을 일으키는 모습을 보여줍니다. 현장에서 늘 목격하는 현상입니다. 원인을 찾아내려면 면밀한 조사가 필요합니다.
CSS 트랜지션 찾기 및 해결
CSS 트랜지션으로 인한 모든 레이아웃 이동을 찾으려면, 먼저 페이지의 모든 트랜지션을 나열하고 어떤 트랜지션이 레이아웃에 영향을 미치는 속성에 애니메이션을 적용하는지 확인해야 합니다. 그런 다음 해당 트랜지션을 비활성화하거나 수정한 뒤, CLS가 사라지는지 측정합니다.
Core Web Vitals 팁: 이러한 이동은 페이지 로딩 초기에 발생하며, 방문할 때마다 항상 나타나지는 않습니다. 모바일 기기를 에뮬레이트하고 네트워크 속도를 제한(throttle)한 뒤 캐시를 비활성화하세요. 로딩 속도가 느려지면 뒤늦게 도착하는 스타일시트나 스크립트보다 먼저 브라우저가 페이지를 페인트하므로, 레이아웃 이동 현상이 훨씬 더 자주 나타납니다.
1단계: CSS 트랜지션 찾기
CSS 트랜지션은 직접 수작업으로 찾을 수도 있습니다. 모든 스타일시트를 열고 'transition'이라는 단어를 검색하는 방식입니다. 10분쯤 걸릴 것입니다. 더 빠른 방법이 있습니다. 콘솔에 이 스니펫을 붙여넣고 Enter 키를 누르세요.
(() => {
let nodeTable = [];
let nodeArray = [];
// Get the name of the node
function getName(node) {
const name = node.nodeName;
return node.nodeType === 1
? name.toLowerCase()
: name.toUpperCase().replace(/^#/, '');
}
// Get the selector
const getSelector = (node) => {
let sel = '';
try {
while (node && node.nodeType !== 9) {
const el = node;
const part = el.id
? '#' + el.id
: getName(el) +
(el.classList &&
el.classList.value &&
el.classList.value.trim() &&
el.classList.value.trim().length
? '.' + el.classList.value.trim().replace(/\s+/g, '.')
: '');
if (sel.length + part.length > (100) - 1) return sel || part;
sel = sel ? part + '>' + sel : part;
if (el.id) break;
node = el.parentNode;
}
} catch (err) {
// Do nothing...
}
return sel;
};
const getNodesWithTransition = (node) => {
// Get the computed style
let cs = window.getComputedStyle(node);
let tp = cs['transition-property'];
let td = cs['transition-duration'];
// If there is a transition, add it to the table
if (tp !== '' && tp !== 'none' && td != '0s') {
nodeTable.push({ selector: getSelector(node), transition: cs['transition'] });
nodeArray.push(node);
}
// Recursively call this function for each child node
for (let i = 0; i < node.children.length; i++) {
getNodesWithTransition(node.children[i]);
}
}
// find all transitions
getNodesWithTransition(document.body);
// Display the results in the console
console.log('%cReadable table of selectors and their transitions', 'color: red; font-weight: bold;');
console.table(nodeTable);
console.log('%cNodeList for you to inspect (harder to read but more info)', 'color: red; font-weight: bold;');
console.log(nodeArray);
// styles to temporarity override the transitions
let selectors = nodeTable.map((item) => item.selector).join(', ');
console.log('%cSpecific CSS to disable all transitions on this page', 'color: red; font-weight: bold;');
console.log(`<style>${selectors}{transition-property: none !important;}</style>`);
console.log('%cGlobal CSS to disable all transitions on this page (not suggested on production)', 'color: red; font-weight: bold;');
console.log(`<style>*{transition-property: none !important;}</style>`);
})() 이 스니펫은 페이지의 모든 트랜지션과 해당 트랜지션이 적용된 요소를 표 형태로 출력합니다.

이제 transition 열을 확인하세요. 먼저 all로 표시된 모든 행부터 살펴봅니다. width와 height를 포함해 애니메이션이 가능한 모든 속성이 대상이기 때문입니다. 그다음 width나 height가 직접 명시된 행을 찾으세요. margin과 padding도 콘텐츠를 움직이며, top과 left 역시 마찬가지입니다.
2단계: CSS 트랜지션 수정하기
스니펫은 이러한 트랜지션을 끄기 위한 CSS도 함께 출력합니다. 빠른 테스트를 원한다면 한 줄로 모든 트랜지션을 비활성화하는 간단한 방법을 쓸 수 있습니다:
<style>*{transition-property: none !important;}</style>실제 운영 사이트에서는 좀 더 섬세한 접근이 필요합니다. 선택자별로 불필요한 transition 속성만 골라 제거해야 합니다. 예를 들어 #button{transition: all .2s}를 #button{transition: background-color .2s}로 변경합니다.
3단계: 레이아웃 이동 변화 측정하기

트랜지션 모범 사례
- 정확한 속성 이름을 지정하세요.
transition: all대신transition: background-color .2s ease처럼 작성하면 의도치 않은 속성이 레이아웃을 이동시키는 일이 없습니다. - transform과 opacity에 애니메이션을 적용하세요. 브라우저는 두 속성 모두 컴포지터에서 실행할 수 있으며, 둘 다 레이아웃을 트리거하지 않습니다. 요소를 이동할 때는
transform: translate()를, 크기를 조절할 때는transform: scale()을 사용하세요. 페이드 효과에는 opacity를 사용합니다. 이는 Google이 권장하는 방식이기도 합니다. - 트랜지션이 적용되는 요소에는 트랜지션 전후 모두 명시적인 width와 height, 또는 aspect-ratio를 부여하여 주변 콘텐츠가 움직이지 않도록 고정하세요. 자세한 내용은 자동 크기 조절 이미지로 인한 레이아웃 이동 해결하기를 참고하세요.
will-change는 신중하게 사용하세요. 이 속성은 요소를 별도의 컴포지터 레이어로 승격시켜 애니메이션을 준비하도록 브라우저에 지시합니다. 하지만 대가가 따릅니다.will-change: transform을 사용하면 요소에 새로운 쌓임 맥락(stacking context)이 생성되고 위치가 지정된 하위 요소의 포함 블록(containing block)이 되며, 추가된 레이어마다 GPU 메모리를 소모합니다. 애니메이션이 시작되기 직전에 JavaScript로 설정하고, 애니메이션이 끝나면 제거하세요. 스타일시트에 영구적으로 남겨두지 마세요.
여전히 많은 사이트가 이를 잘못 처리하고 있습니다. 2025 Web Almanac에 따르면 모바일 페이지의 40%에서 비합성(non-composited) 애니메이션이 발견되었습니다. CLS는 모바일 기준 통과율이 81%로 가장 높은 Core Web Vital 지표입니다. 사이트가 통과하지 못한 19%에 속한다면, 아직 확인하지 않은 원인은 바로 CSS 트랜지션일 수 있습니다.