알림 배지를 오른쪽으로 움직였는데 1초 뒤 다시 출발점에 있다. '방금 분명 움직였는데?' 싶다면 @keyframes보다 애니메이션이 끝난 뒤 어떤 스타일을 적용할지 먼저 보자. CSS는 이동을 재생했을 뿐, 최종 위치를 기본 스타일로 저장하지는 않았다.
CSS 애니메이션이 끝나면 왜 원래 위치로 돌아갈까?
@keyframes는 애니메이션이 진행되는 동안의 값을 정의한다. 아래 예제에서 배지의 기본 위치는 translateX(0)이고, 애니메이션은 1초 동안 120px 오른쪽으로 움직인다. 기본 animation-fill-mode: none이라면 재생이 끝난 뒤에는 다시 기본 위치가 적용된다.
그림의 두 줄은 같은 시작점과 같은 120px 이동을 쓴다. 다른 것은 끝난 뒤의 처리뿐이다. forwards는 마지막에 적용된 애니메이션 값을 종료 후에도 유지한다. 이름이 비슷해도 animation-direction: forwards가 아니라 animation-fill-mode: forwards다.
animation-fill-mode: forwards를 어떻게 적용할까?
아래 HTML을 파일로 저장해 열면 두 배지가 함께 움직인다. 1초 뒤 첫째는 돌아오고 둘째는 오른쪽에 남도록 작성했다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>animation-fill-mode 비교</title>
<style>
.track { width: 240px; height: 48px; margin: 12px 0; border-bottom: 1px solid #93c5fd; }
.chip {
width: 96px;
padding: 6px 0;
text-align: center;
background: #dbeafe;
transform: translateX(0);
animation: move 1s linear 1;
}
.keep { animation-fill-mode: forwards; }
@keyframes move {
from { transform: translateX(0); }
to { transform: translateX(120px); }
}
</style>
</head>
<body>
<div class="track"><div class="chip">none</div></div>
<div class="track"><div class="chip keep">forwards</div></div>
</body>
</html>첫 줄은 animation-fill-mode를 쓰지 않아 기본값 none이 적용된다. 둘째 줄의 .keep만 forwards로 바꿨다. 애니메이션이 보이지 않는다면 @keyframes move와 animation: move ...의 이름이 같은지, 지속 시간 1s가 빠지지 않았는지부터 확인하자. 이름이 달라지거나 시간이 0s면 '끝나고 돌아오는 문제' 이전에 움직임 자체를 보기 어렵다.
forwards가 최종 위치를 영구 저장할까?
아니다. forwards는 그 애니메이션이 요소에 적용되는 동안 종료 시점의 값을 보여 준다. 애니메이션을 적용한 클래스를 제거하면 기본 스타일로 돌아간다. CSS Animations 표준은 기본값 none, 종료 후 값을 적용하는 forwards, 애니메이션을 제거한 뒤에는 그 효과가 남지 않는 경계를 구분한다.
따라서 배지가 실제로 '완료' 상태가 되어 그 위치에 있어야 한다면, 애니메이션만 상태 저장 수단으로 쓰지 않는다. 완료 시 최종 위치를 가진 상태 클래스를 적용하고 애니메이션 클래스를 제거하는 식으로 상태와 움직임을 나눈다. 반대로 잠깐 보여 주는 연출이라면 forwards만으로 충분할 수 있다.
forwards가 언제나 to 값을 뜻하는 것도 아니다. 반복 방향을 reverse나 alternate로 바꾸면 마지막 반복이 끝나는 쪽이 달라진다. 이 글의 예제는 정방향으로 한 번만 실행하므로 to의 120px이 남는다.
핵심 요약
CSS 애니메이션이 끝난 뒤 원래 자리로 돌아오는 것은 대개 기본 animation-fill-mode: none 때문이다. 같은 이동에서 forwards를 주면 마지막 적용 값이 남는다. 다만 그것은 영구적인 UI 상태가 아니다. 움직임이 아예 없다면 이름과 지속 시간을 먼저 확인하고, 완료 상태를 유지해야 한다면 별도 상태 스타일로 관리하자.

