01 — Transition (상태 변화의 보간)
한 줄 답:
transition은 **“속성이 새 값으로 바뀌는 순간, 그 사이를 시간으로 채우라”**는 수동적·반응적 명령이다.@keyframes없이 *상태(state)*가 바뀔 때만 동작하며, 2024년transition-behavior: allow-discrete로 마침내display:none ↔ block같은 이산적 속성도 보간 가능해졌다.
Why — 왜 transition이 따로 있는가
CSS는 원래 선언적 정적 스타일 언어다. hover:false이던 :hover:true가 되는 순간, 색이 즉시 바뀌는 것이 기본 동작이다. 그런데 사용자 인지 연구에 따르면:
- 150~300ms 지연이 있는 변화는 연결된 사건으로 인지된다 (“내가 호버한 결과 색이 바뀜”).
- 즉시 변화는 *깜빡임(flicker)*으로 인지된다 (“뭐가 바뀐 거지?”).
- 500ms 이상은 느린 시스템으로 인지된다 (“불편하다”).
즉 transition은 **“기능”이 아니라 “인지적 인과관계의 시각화”**다. 그래서 CSS는 모든 속성을 보간 가능 속성으로 정의하고, 개발자가 언제·얼마나·어떻게 보간할지만 선언하면 되도록 만들었다.
How — 어떻게 동작하는가
1) 보간의 트리거 조건 (4가지 모두)
브라우저가 transition을 발동하는 조건은 AND 4개다.
- 어떤 속성
P의 computed value가 변했다 (A → B). - 그 속성이 transition-property 목록에 포함된다 (또는
all). - transition-duration > 0.
- 그 속성이 interpolable property다 — 즉 어떻게 보간할지가 명세에 정의됨.
조건 4번이 핵심이다. color: red → blue는 RGB 공간에서 보간 가능. display: none → block은 이산적이라 보간 불가능 → 2024년 이전까지는 불가능했다 (§ allow-discrete 참조).
2) 4개 longhand × shorthand 매핑
.btn {
transition-property: background-color, transform;
transition-duration: 200ms, 300ms;
transition-timing-function: ease-out, cubic-bezier(.2,.8,.2,1);
transition-delay: 0s, 100ms;
/* 위 4줄 = 아래 1줄 */
transition: background-color 200ms ease-out 0s,
transform 300ms cubic-bezier(.2,.8,.2,1) 100ms;
}각 콤마 그룹이 하나의 보간 정의. 길이가 다르면 짧은 쪽이 cyclic하게 반복된다 (예: property 3개, duration 1개 → 모두 같은 duration).
3) timing-function = 시간의 곡선
| 키워드 | cubic-bezier 등가 | 체감 |
|---|---|---|
linear | cubic-bezier(0,0,1,1) | 기계적, 거리 변화에 부적합 |
ease | cubic-bezier(.25,.1,.25,1) | 기본값, 자연스럽지만 모호 |
ease-in | cubic-bezier(.42,0,1,1) | 천천히 출발 → 빠르게 (퇴장) |
ease-out | cubic-bezier(0,0,.58,1) | 빠르게 출발 → 천천히 (등장) |
ease-in-out | cubic-bezier(.42,0,.58,1) | 양 끝이 느림 (왕복) |
cubic-bezier(.2,.8,.2,1) | 커스텀 | Material Design “standard easing” |
steps(4, end) | 계단형 | 스프라이트 애니메이션 |
디자인 BP: 등장(enter)은 ease-out, 퇴장(leave)은 ease-in, 위치 이동은 ease-in-out. Material/HIG/Fluent 공통 원칙.
4) linear() — 2023년 추가된 곡선 정의
CSS Easing Functions Level 2의 linear()로 임의의 다단계 곡선을 정의 가능 — 스프링·바운스 시뮬레이션에 쓰인다.
.bounce {
/* 4개 점을 직선으로 잇는 곡선 */
transition-timing-function: linear(0, 0.5 25%, 0.9 50%, 1);
}What — 구체 스펙
transition-property
- 한 속성 이름:
transition-property: opacity - 여러 개:
transition-property: opacity, transform - 전부:
transition-property: all(⚠️ 부작용 가능) - 없음:
transition-property: none
transition-duration / delay
- 단위:
s또는ms.0.3s=300ms. - 음수 duration은 무효. 음수 delay는 시작 시점을 과거로 당긴다 (이미 진행된 상태에서 시작).
transition-behavior: allow-discrete
2024년 Baseline에 진입한 게임 체인저 속성.
.popover {
display: none;
opacity: 0;
transition: opacity 200ms, display 200ms allow-discrete;
}
.popover.open {
display: block;
opacity: 1;
}기본값 normal은 이산 속성을 보간 안 함. allow-discrete로 바꾸면:
display: none → block같은 이산 변화도 시작점에서 즉시 점프, 나머지 보간 속성과 동기화된다.- 즉
display:none인 요소를display:block으로 보이게 하면서opacity 0→1로 페이드인 가능.
@starting-style — 첫 등장 시의 시작값
문제: display:none이던 요소는 transition의 “시작값” 자체가 존재하지 않는다 (브라우저가 처음 렌더할 때 already opacity:1). 해결:
.popover {
opacity: 1;
transition: opacity 200ms;
@starting-style {
opacity: 0; /* "처음 렌더링되는 순간"의 값 */
}
}이 둘(allow-discrete + @starting-style)이 합쳐져서, 처음으로 modal·popover·toast의 enter/exit 트랜지션이 JS 없이 가능해졌다 (Chrome 117+/Safari 17.5+).
transition은 어떤 속성에 쓰나
자세한 메커니즘은 04-rendering-perf에서 다룬다.
What-if — 잘못 쓰면
1) transition: all의 부작용
* { transition: all 200ms; } /* ❌ */- 모든 속성이 보간 대상이 된다 → 의도치 않은 트랜지션 (예:
display가 아니라background이지만 함께 200ms 지연). - DevTools에서 inspect할 때 트랜지션이 계속 깜빡거려 디버깅이 안 된다.
- 성능: 모든 layout-trigger 속성이 transition되니 jank 폭탄.
BP: 항상 명시적 속성 리스트를 쓴다.
2) width 트랜지션의 jank
.card {
width: 200px;
transition: width 300ms;
}
.card:hover { width: 240px; } /* ❌ 매 프레임 Layout 재계산 */해결: transform: scaleX(1.2)로 대체. 시각적으로 동등하면서 composite-only.
단, scale은 자식 요소도 함께 늘어진다. 자식을 그대로 두려면:
.card {
transform: scaleX(1);
transition: transform 300ms;
}
.card > * {
transform: scaleX(1); /* 자식이 부모의 inverse */
}이게 § FLIP 기법의 원형이다.
3) :hover 트랜지션이 모바일에서 들러붙음
iOS Safari에서 첫 탭은 :hover 상태로 들어가고, 두 번째 탭에서 클릭으로 해석된다. → 트랜지션이 끝나도 hover 상태가 유지되어 원래 색으로 안 돌아옴.
해결: @media (hover: hover)로 호버 가능한 디바이스만 타겟.
@media (hover: hover) {
.btn:hover { background: blue; }
}4) JS로 클래스 추가 직후 transition 안 먹는 문제
el.style.display = 'block';
el.classList.add('open'); /* ❌ 같은 프레임에 둘 다 → no transition */브라우저는 같은 프레임 내 변화를 한 번에 적용한다 (transition 발동 안 함). 해결책 3가지:
@starting-style사용 (가장 깔끔)requestAnimationFrame두 번- CSS-only로
:popover-open같은 상태 셀렉터 활용 (Popover API)
5) transition-delay가 해제 시점에도 적용됨
.tooltip {
opacity: 0;
transition: opacity 200ms 500ms; /* 500ms 후 등장 */
}
.tooltip.show { opacity: 1; }.show를 제거하면 500ms 기다린 후 사라진다. 의도와 다를 수 있음. 해결:
.tooltip {
opacity: 0;
transition: opacity 200ms; /* 사라질 때는 즉시 */
}
.tooltip.show {
opacity: 1;
transition-delay: 500ms; /* 나타날 때만 delay */
}Insight — 한 단락 이야기
“transition은 상태 변화의 부작용으로 발동된다.”
2007년 WebKit이
-webkit-transition을 처음 제안했을 때, 동료 Hyatt가 이렇게 적었다: “애니메이션은 두 종류다. 사용자가 트리거하는 것(transition)과 페이지가 트리거하는 것(animation). 둘은 다른 멘탈 모델이 필요하다.” 이 분리는 결정적이었다 —transition은 *반응적(reactive)*이라 hover·focus·class change 같은 *상태 머신의 변(edge)*에서 발동한다. 그래서transition은 상태가 없는 페이지에선 의미가 없다. 반대로animation은 시간 자체가 트리거다. 2024년allow-discrete와@starting-style이 추가되면서 *“상태 변화의 부작용”*이라는 transition의 본질은 그대로 두고, 이산 속성도 그 부작용에 포함되도록 확장했다 — 15년 만의 가장 큰 의미적 확장이다.
요약 + Mermaid
- transition은 상태 변화의 자동 보간, animation은 자율적 키프레임 — 멘탈 모델이 다르다.
- 4 longhand:
property/duration/timing-function/delay. - 보간 가능한 속성에서만 작동, composite-only 속성(transform·opacity)이 가장 안전.
transition: all은 안티패턴, 명시적 리스트로.- 2024년
transition-behavior: allow-discrete+@starting-style로display:none ↔ block트랜지션 가능 (Popover/Modal의 JS 의존성 큰 폭 감소). - 등장은
ease-out, 퇴장은ease-in, 이동은ease-in-out.
다음: 02-animation — 시간 자체가 트리거인 키프레임 애니메이션.