안녕하세요! 프론트엔드 개발자 취업을 위해 오늘도 열심히 달리고 계시는군요! 웹 프로필이나 독후감 사이트 같은 멋진 포트폴리오를 만들 때, UI 요소들이 딱딱하게 끊기며 변하는 것보다 스르륵 부드럽게 움직이면 훨씬 완성도 높은 웹사이트처럼 보이겠죠?
이번에 가져오신 문서는 바로 그런 '부드러운 전환'을 담당하는 'CSS 트랜지션(CSS transitions)'의 핵심 가이드입니다. React나 Next.js 환경에서 컴포넌트 단위로 개발할 때, 상태 변화에 따른 부드러운 애니메이션을 주기 위해 반드시 알아야 하는 필수 지식이기도 합니다. 면접에서도 자주 등장하는 개념이니, 공식 문서의 내용을 하나도 빠짐없이 제 실무 팁과 함께 구어체로 알기 쉽게 설명해 드릴게요!
CSS 트랜지션(CSS transitions)은 CSS 속성이 변경될 때 애니메이션 속도를 제어할 수 있는 방법을 제공합니다. 속성 변경 사항이 즉시 적용되게 하는 대신, 일정 시간(period of time)에 걸쳐 속성 변화가 일어나도록 만들 수 있습니다. 예를 들어, 어떤 요소의 색상을 흰색에서 검은색으로 변경하면 보통은 그 변화가 즉각적으로 일어납니다. 하지만 CSS 트랜지션을 활성화하면, 가속도 곡선(acceleration curve)을 따르는 시간 간격에 맞춰 변화가 일어납니다. 물론 이 모든 과정은 여러분의 마음대로 커스터마이징할 수 있습니다.
두 가지 상태 사이를 전환하는 애니메이션은 종종 암시적 트랜지션(implicit transitions)이라고 불립니다. 왜냐하면 시작 상태와 최종 상태 사이의 중간 상태들을 브라우저가 암시적으로 알아서 정의하고 그려주기 때문입니다.

CSS 트랜지션을 사용하면 어떤 속성들을 애니메이션 할 것인지(명시적으로 나열하여), 애니메이션이 언제 시작될 것인지(지연 시간(delay) 설정), 트랜지션이 얼마나 오래 지속될 것인지(지속 시간(duration) 설정), 그리고 트랜지션이 어떤 방식으로 실행될 것인지(타이밍 함수(easing function) 정의, 예: 일정한 속도로 진행할지, 혹은 처음엔 빠르고 끝엔 느리게 진행할지 등)를 직접 결정할 수 있습니다.
웹 제작자는 어떤 속성을 어떤 방식으로 애니메이션 할지 직접 정의할 수 있습니다. 이를 통해 아주 복잡한 트랜지션을 만들어낼 수 있죠. 하지만 일부 속성들은 애니메이션을 적용하는 것 자체가 의미가 없기 때문에 애니메이션을 적용할 수 없습니다(not animatable).
참고 (Note):
auto값은 꽤 복잡한 케이스입니다. CSS 명세에서는auto값에서 시작하거나auto값으로 끝나는 애니메이션을 권장하지 않습니다. Gecko 기반 브라우저 같은 일부 사용자 에이전트(브라우저)는 이 요구사항을 엄격하게 구현하지만, WebKit 기반 브라우저 같은 곳들은 덜 엄격합니다. 따라서auto값에 애니메이션을 사용하는 것은 브라우저와 그 버전에 따라 예측 불가능한 결과를 초래할 수 있으므로 피하는 것이 좋습니다.
💡 강사의 실무 팁 1:
height: auto애니메이션의 비밀
"아코디언 메뉴나 드롭다운 메뉴를 만들 때, 닫혀있을 땐height: 0이었다가 열리면 안의 내용물만큼 쫙 펼쳐지게height: auto로 부드럽게 트랜지션을 주고 싶어요!"
프론트엔드 개발을 하다 보면 100% 마주치게 되는 고민입니다. 하지만 공식 문서의 경고처럼 CSS는0에서auto로 넘어가는 중간값을 계산하지 못합니다! 실무에서는 이를 해결하기 위해max-height를 아주 큰 값으로 설정해서 애니메이션 하거나, CSS Grid의1fr을 활용하거나, 또는 자바스크립트로 요소의scrollHeight를 직접 계산해서px값으로 넣어주는 트릭을 사용합니다. 기술 면접에서도 종종 물어보는 주제니 꼭 기억해 두세요!
CSS 트랜지션은 단축 속성인 transition을 사용하여 제어합니다. 이 방법은 트랜지션을 구성하는 가장 좋은 방법입니다. 파라미터들의 싱크가 어긋나는 것을 방지해 주어, CSS 디버깅에 많은 시간을 낭비하는 답답한 상황을 줄여주기 때문입니다.
트랜지션을 구성하는 개별 요소들은 다음의 하위 속성들을 통해 제어할 수 있습니다:
transition-propertytransition-durationtransition-timing-functiontransition-delaytransition 단축 속성 문법은 다음과 같이 작성합니다:
transition: <property> <duration> <timing-function> <delay>;
이 예제는 사용자가 요소 위에 마우스를 올린(hover) 후 2초의 지연 시간을 거친 뒤, 4초 동안 글꼴 크기(font size)가 변경되는 트랜지션을 수행합니다:
#delay {
font-size: 14px;
transition-property: font-size;
transition-duration: 4s;
transition-delay: 2s;
}
#delay:hover {
font-size: 36px;
}
<body>
<p>
아래 박스는 width, height, background-color, rotate에 대한 트랜지션을 결합한 것입니다. 박스 위에 마우스를 올려 애니메이션이 어떻게 작동하는지 확인해 보세요.
</p>
<div class="box">Sample</div>
</body>
.box {
border-style: solid;
border-width: 1px;
display: block;
width: 100px;
height: 100px;
background-color: blue;
transition:
width 2s,
height 2s,
background-color 2s,
rotate 2s;
}
.box:hover {
background-color: #ffcccc;
width: 200px;
height: 200px;
rotate: 180deg;
}
만약 어떤 하위 속성의 값 목록이 다른 것들보다 짧다면, 개수를 맞추기 위해 그 값들이 반복(repeated)됩니다. 예를 들면 다음과 같습니다:
div {
transition-property: opacity, left, top, height;
transition-duration: 3s, 5s;
}
위 코드는 내부적으로 아래와 같이 처리됩니다:
div {
transition-property: opacity, left, top, height;
transition-duration: 3s, 5s, 3s, 5s; /* 3s, 5s가 부족한 개수만큼 반복되었습니다! */
}
마찬가지로, 다른 속성의 값 목록이 transition-property에 나열된 속성 개수보다 더 길다면, 초과한 값들은 무시되고 잘려나갑니다(truncated). 다음 CSS를 살펴보세요:
div {
transition-property: opacity, left;
transition-duration: 3s, 5s, 2s, 1s;
}
이 코드는 다음과 같이 해석됩니다:
div {
transition-property: opacity, left;
transition-duration: 3s, 5s; /* 속성이 두 개뿐이므로 뒤에 적힌 2s, 1s는 무시됩니다. */
}
CSS의 아주 흔한 사용 사례 중 하나는 사용자가 마우스 커서를 메뉴 아이템 위에 올렸을 때(hover) 해당 항목을 강조 표시(highlight)하는 것입니다. 트랜지션을 사용하면 이 효과를 훨씬 더 매력적으로 만들기 쉽습니다.
먼저 HTML로 메뉴를 구성해 보겠습니다:
<nav>
<a href="#">Home</a>
<a href="#">About</a>
<a href="#">Contact Us</a>
<a href="#">Links</a>
</nav>
그런 다음 메뉴의 디자인(look and feel)을 구현하기 위한 CSS를 작성합니다:
nav {
display: flex;
gap: 0.5rem;
}
a {
flex: 1;
background-color: #333333;
color: white;
border: 1px solid;
padding: 0.5rem;
text-align: center;
text-decoration: none;
transition: all 0.5s ease-out; /* 배경색, 글자색이 변할 때 0.5초 동안 서서히 변하도록 설정합니다 */
}
a:hover,
a:focus {
background-color: white;
color: #333333;
}
이 CSS는 메뉴의 외관을 설정하며, 요소가 :hover 상태이거나 :focus 상태가 되었을 때 배경색과 텍스트 색상이 모두 변경되도록 만들어 줍니다.
display와 content-visibility 트랜지션하기 (Transitioning display and content-visibility)이 예제는 display 속성과 content-visibility 속성에 어떻게 트랜지션을 적용할 수 있는지 보여줍니다. 이 동작은 예를 들어 DOM에서 display: none으로 컨테이너를 아예 제거하고 싶지만, 뚝딱 사라지게 냅두지 않고 opacity를 통해 서서히 페이드아웃(fade out) 되며 사라지게 하는 진입/퇴장(entry/exit) 애니메이션을 만들 때 매우 유용합니다.
💡 강사의 실무 팁 2: 이거 진짜 최신 CSS의 혁명입니다!
예전에는 모달(Modal) 창을 닫을 때 부드럽게 사라지게 하려면 자바스크립트의setTimeout을 써서 투명도 애니메이션이 끝날 때까지 기다렸다가display: none을 먹이거나,framer-motion같은 무거운 라이브러리를 써야 했어요. 하지만 이제 최신 CSS 문법을 활용하면 오직 CSS만으로 모달 창의 페이드아웃 효과를 완벽하게 구현할 수 있습니다!
최신 브라우저들은 display와 content-visibility를 불연속 애니메이션 유형 (discrete animation type)의 한 변형으로 취급하여 트랜지션을 지원합니다. 일반적으로 불연속 애니메이션이란, 애니메이션 시간이 50% 진행되었을 때 두 값 사이를 휙 하고 전환(flip)하는 것을 뜻합니다.
하지만 예외가 있습니다. 바로 display: none이나 content-visibility: hidden 상태로(또는 그 상태로부터) 애니메이션을 할 때입니다. 이 경우 브라우저는 전환되는 콘텐츠가 애니메이션 지속 시간 내내 화면에 보이도록(보장하기 위해) 값의 전환 시점을 조정합니다.
예를 들면 다음과 같습니다:
display 값을 none에서 block(또는 다른 보이게 하는 값)으로 전환할 때: 애니메이션 진행률 0% 시점에 값이 즉시 block으로 전환되어, 애니메이션 내내 요소가 화면에 보이게 됩니다.display 값을 block에서 none으로 전환할 때: 애니메이션 진행률 100% 시점(맨 마지막)에 값이 none으로 전환되어, 투명해지는 애니메이션이 진행되는 내내 요소가 화면에 보이도록 유지해 줍니다.이 속성들에 트랜지션을 적용하려면, 반드시 트랜지션 설정에 transition-behavior: allow-discrete 속성을 추가해야 합니다. 이 설정이 있어야만 display와 content-visibility의 트랜지션이 활성화됩니다.
또한 display에 트랜지션을 적용할 때는, 요소가 DOM에 처음 나타날 때(예를 들어 display가 none에서 다른 상태로 변경되며 첫 스타일 업데이트를 받을 때) 시작할 기준점을 제공하기 위해 반드시 @starting-style 블록이 필요합니다. 이는 예기치 않은 동작을 방지하기 위함입니다. 기본적으로 CSS 트랜지션은 요소가 DOM에 처음 나타나는 그 첫 번째 스타일 업데이트 시점에는 트리거되지 않기 때문입니다.
반면 content-visibility 애니메이션은 @starting-style 블록에 시작값을 지정할 필요가 없습니다. content-visibility는 display처럼 DOM에서 요소를 아예 렌더링 파이프라인에서 숨기는 것이 아니라, 그저 콘텐츠 부분의 렌더링만 건너뛰기(skip) 때문입니다.
이 HTML에는 두 개의 <p> 요소 사이에 display: none에서 block으로 애니메이션 될 <div>가 하나 들어있습니다.
<p>
화면 아무 곳이나 클릭하거나 아무 키나 눌러서 아래 <code><div></code>의 숨김/표시 상태를 전환해 보세요.
</p>
<div>
이 <code><div></code> 요소는
<code>display: none; opacity: 0</code> 상태와
<code>display: block; opacity: 1</code> 상태 사이를 부드럽게 전환(transition)합니다. 멋지죠?
</div>
<p>
이건 아래쪽 단락입니다. 위쪽 <code><div></code> 요소에 실제로 <code>display: none;</code>이 적용되었다가 해제되는 것을 보여주기 위한 단락입니다. 만약 투명도(<code>opacity</code>)만 조절되었다면, <code>div</code>가 투명할 때도 이 단락 위쪽에 항상 빈 공간(자리)을 차지하고 있었을 것입니다.
</p>
html {
height: 100vh;
}
div {
font-size: 1.6rem;
padding: 20px;
border: 3px solid red;
border-radius: 20px;
width: 480px;
display: none; /* 기본 상태는 숨김 */
opacity: 0;
transition:
opacity 1s,
display 1s allow-discrete;
/* 다음과 같이 단축해서 쓸 수도 있습니다:
transition: all 1s allow-discrete; */
}
.showing {
opacity: 1;
display: block; /* 클래스가 붙으면 보여줍니다 */
}
/* display가 none에서 block으로 바뀔 때 투명도가 0에서 시작하도록 명시합니다 */
@starting-style {
.showing {
opacity: 0;
}
}
@starting-style 블록이 트랜지션의 '시작 스타일'을 지정하는 데 사용된 점, 그리고 트랜지션 목록에 display 속성이 포함되어 있으며 그 옆에 allow-discrete 키워드가 함께 설정된 점에 주목하세요!
마지막으로, 사용자의 클릭이나 키보드 입력에 맞춰 showing 클래스를 토글(toggle)하여 트랜지션을 유발하는 간단한 자바스크립트를 추가합니다.
const divElem = document.querySelector("div");
const htmlElem = document.querySelector(":root");
htmlElem.addEventListener("click", showHide);
document.addEventListener("keydown", showHide);
function showHide() {
divElem.classList.toggle("showing");
}
참고 (Note):
다음 상황 직후에 트랜지션을 사용하려고 할 때는 주의가 필요합니다:
- 자바스크립트의
.appendChild()를 사용해 DOM에 방금 막 요소를 추가했을 때- 요소의
display: none;속성을 막 제거했을 때이런 상황에서는 브라우저가 "요소가 초기 상태를 거친 적이 없고, 태어날 때부터 최종 상태였다"고 취급해 버려서 애니메이션이 스킵될 수 있습니다. 이 한계를 극복하는 고전적인 방법 중 하나는, 트랜지션을 주고 싶은 CSS 속성을 변경하기 직전에
setTimeout()을 사용해 몇 밀리초(milliseconds) 정도 아주 약간의 시차를 두는 것입니다.
자바스크립트 코드(로직) 자체를 수정하지 않고도 동작을 훨씬 부드럽게 보이도록 만들고 싶을 때 트랜지션은 아주 훌륭한 도구가 됩니다. 다음 예제를 살펴보세요.
<p>공을 움직이려면 아무 곳이나 클릭하세요</p>
<div id="foo" class="ball"></div>
// 클릭한 위치로 공을 이동시킵니다:
const f = document.getElementById("foo");
document.addEventListener("click", (ev) => {
f.style.transform = `translateY(${ev.clientY - 25}px)`;
f.style.transform += `translateX(${ev.clientX - 25}px)`;
});
만약 CSS 트랜지션이 없다면 클릭할 때마다 공이 그 위치로 순간이동하겠죠. 하지만 CSS를 사용하면 이 자바스크립트의 동작을 아주 부드럽게 덧칠할 수 있습니다. 요소에 트랜지션만 추가해 주면 모든 변화가 부드럽게 일어납니다:
.ball {
border-radius: 25px;
width: 50px;
height: 50px;
background: #cc0000;
position: absolute;
top: 0;
left: 0;
transition: transform 1s; /* 마법의 한 줄! JS로 위치를 바꿔도 1초에 걸쳐 부드럽게 날아갑니다 */
}
자바스크립트에서 애니메이션 실행이 끝났다는 것을 감지하고 싶다면 transitionend 이벤트를 사용할 수 있습니다. 이 이벤트의 핸들러에 전달되는 매개변수는 TransitionEvent 객체인데, 일반적인 Event 객체 외에 두 가지 유용한 프로퍼티가 추가되어 있습니다:
propertyNameelapsedTimetransition-delay에 설정된 지연 시간의 영향을 받지 않습니다(실제 움직인 시간만 잼).여느 때처럼 addEventListener() 메서드를 사용해 이 이벤트를 모니터링할 수 있습니다:
el.addEventListener("transitionend", updateTransition);
💡 강사의 실무 팁 3: React에서의 트랜지션 종료 이벤트
React 환경에서 개발하실 때 이 기능을 쓰고 싶다면, JSX 태그 안에onTransitionEnd={핸들러함수}를 달아주시면 됩니다! 슬라이드 메뉴나 툴팁이 화면에서 완전히 사라진 직후에 특정 상태(State)를 변경하고 싶을 때 이 이벤트가 아주 제격입니다.
트랜지션이 '시작'되는 시점을 감지하고 싶다면, 같은 방식으로 transitionrun(지연 시간이 시작되기 전, 즉 트랜지션이 '발동'될 때 발생)과 transitionstart(지연 시간이 모두 끝나고 진짜로 움직이기 시작할 때 발생) 이벤트를 사용하면 됩니다:
el.addEventListener("transitionrun", signalStart);
el.addEventListener("transitionstart", signalStart);
참고 (Note):
만약 요소가 도중에display: none이 되어버리거나 애니메이션 중이던 속성값이 엎어져버려서 트랜지션이 완료되기 전에 강제로 중단(aborted)된다면,transitionend이벤트는 발생하지 않습니다.
| Specification (명세) |
|---|
| CSS Transitions Module Level 1 |
TransitionEvent 인터페이스 및 transitionend 이벤트MDN 향상에 도움 주기 (Help improve MDN)
어떠셨나요? CSS만으로도 상태를 부드럽게 이어주는 트랜지션의 강력함이 느껴지시나요? 특히 allow-discrete와 @starting-style 같은 최신 문법들은 모던 웹 개발자라면 반드시 챙겨가야 할 강력한 무기입니다.
포트폴리오 작업을 하시면서 책 소개 카드의 크기를 키우거나, 설명 모달 창을 띄울 때 이 트랜지션 기법들을 꼭 한번 적용해 보세요! 혹시 CSS 키프레임 애니메이션(Keyframes)과 트랜지션 중 어느 걸 써야 할지 고민되거나 헷갈리는 부분이 있으신가요?