모션 디자인 원칙: easing과 duration을 정하는 기준

모달이 하나 뜬다. 그런데 뭔가 답답하다. 화면 아래에서 부드럽게 올라오는 게 아니라, 일정한 속도로 쓱 올라와서 딱 멈춘다. 색도 깔끔하고 여백도 잘 잡혀 있는데, 이상하게 싼 느낌이 든다. 반대로 어떤 모달은 뜨는 것만 봐도 잘 만들었다는 인상을 준다. 두 모달의 차이는 대개 두 가지 값에서 갈린다. 얼마나 오래 움직이는가, 그리고 어떤 속도 곡선으로 움직이는가.

이 두 값을 디자이너들은 자주 감으로 찍는다. Framer나 CSS에서 지속시간(duration) 필드에 300을 넣고, 이징(easing)은 그냥 ease로 둔다. 화면 대부분에 같은 값을 박아버린다. 문제는 이렇게 찍은 값이 맞을 때도 있고 틀릴 때도 있는데, 왜 맞고 왜 틀린지를 설명하지 못한다는 데 있다. 이 글은 그 두 값을 감이 아니라 원리로 정하는 방법에 대한 글이다.

지속시간: 하나의 숫자가 아니라 범위

먼저 지속시간부터 보자. 가장 흔한 실수는 모든 애니메이션에 같은 값을 쓰는 거다. 버튼 색이 바뀌는 것도 300ms, 모달이 화면을 가로질러 올라오는 것도 300ms. 이러면 하나는 굼뜨고 하나는 급하다.

지속시간을 정하는 기준은 하나다. 얼마나 먼 거리를, 얼마나 큰 요소가 움직이는가. 물리 세계를 떠올리면 쉽다. 손끝으로 각설탕 하나를 툭 미는 것과, 책상 전체를 밀어 옮기는 것은 걸리는 시간이 다르다. 화면 위에서도 똑같다. 짧게 움직이는 작은 요소는 빨리 끝나야 하고, 화면을 크게 가로지르는 큰 요소는 그만큼 시간이 걸려야 자연스럽다.

그래서 하나의 숫자가 아니라 범위로 감을 잡는 게 맞다. 대략 100ms에서 500ms 사이다. 이 범위의 양 끝을 예로 붙여보자.

  • 버튼을 눌렀을 때 색이 살짝 어두워지는 정도의 작은 상태 변화. 이동이랄 게 거의 없다. 100ms 근처면 충분하다. 여기에 300ms를 주면 손끝의 반응이 굼떠서 답답하다.
  • 화면 아래에서 올라오는 전체 화면 모달. 이동 거리가 화면 높이만큼 크다. 300ms에서 400ms 정도는 줘야 갑자기 튀어나온 느낌이 안 든다.
  • 작은 토스트 알림이 모서리에서 살짝 나타나는 정도. 이동 거리가 짧으니 200ms 안쪽.

100ms보다 짧으면 전환 과정이 눈에 안 걸리고 그냥 순간이동처럼 느껴진다. 500ms를 넘기면 이번엔 기다리게 된다. 특히 사용자가 방금 버튼을 눌러서 결과를 기다리는 상황이라면, 조금만 길어도 제품이 느리다는 인상을 준다. 지나치게 긴 지속시간은 정성 들인 것처럼 보이지만 실제로는 사용자를 붙잡아두는 것에 가깝다.

Material Design의 모션 가이드도 같은 원리를 값으로 옮겨놓은 것에 가깝다. 큰 화면 전환에는 더 긴 시간을, 작은 요소의 상태 변화에는 더 짧은 시간을 배정한다. 이 표를 그대로 외울 필요는 없다. 이동 거리가 크면 길게, 작으면 짧게라는 원리만 손에 익히면 표는 그 원리의 결과로 읽힌다.

이징: 현실의 물체는 등속으로 움직이지 않는다

지속시간이 얼마나 오래인가라면, 이징은 그 시간 안에서 속도가 어떻게 변하는가다. 여기서부터가 모션의 인상을 실제로 가른다.

현실의 물체를 떠올려보자. 굴러가던 공은 갑자기 멈추지 않는다. 서서히 느려지다 선다. 정지해 있던 물체도 갑자기 최고 속도가 되지 않는다. 천천히 붙었다가 빨라진다. 우리 눈은 이 가속과 감속에 평생 익숙해져 있다. 그래서 화면 위의 요소가 처음부터 끝까지 똑같은 속도로 움직이면, 머리로는 설명 못 해도 몸이 어색하다고 느낀다. 이게 등속(linear)이 대개 어색한 이유다.

linear는 시작 속도와 끝 속도가 같다. 기계가 움직이는 것처럼 보인다. 그래서 모달이 등장하는데 linear를 쓰면, 부드럽게 자리를 잡는 게 아니라 벽에 딱 부딪혀 멈추는 것처럼 보인다. 앞에서 말한 답답하고 싼 느낌의 정체가 대개 이거다. (예외는 있다. 로딩 스피너처럼 끝없이 도는 회전이나, 일정한 속도로 계속 반복되는 배경 애니메이션처럼 시작과 끝이 따로 없는 모션에는 linear가 오히려 맞다.)

그럼 언제 어떤 곡선을 쓰는가. 기준은 요소가 지금 무엇을 하고 있는가다. 세 가지로 나뉜다.

요소가 화면에 들어올 때는 감속을 쓴다. ease-out이다. 빠르게 들어와서 부드럽게 자리를 잡는다. 사용자의 주의가 그 요소로 옮겨가야 하니, 처음엔 빠르게 눈길을 끌고 끝에서 천천히 안착하는 게 자연스럽다. 아래에서 올라오는 모달, 화면에 나타나는 카드가 여기 해당한다.

요소가 화면에서 나갈 때는 가속을 쓴다. ease-in이다. 천천히 출발해서 빠르게 사라진다. 이미 사용자의 관심에서 벗어난 요소이니, 끝에서 미적거릴 이유가 없다. 닫히는 모달, 삭제되어 날아가는 항목이 여기 해당한다.

요소가 화면 안에서 자리를 옮길 때는 가속과 감속을 둘 다 쓴다. 표준 곡선인 ease-in-out이다. 천천히 출발해 중간에 빨라졌다가 다시 천천히 멈춘다. 시작점도 끝점도 화면 안에 있으니 양쪽 다 부드럽게 처리한다. 탭 사이를 미끄러지는 인디케이터, 위치를 재배치하는 리스트 항목이 여기 해당한다.

정리하면 이렇다.

  • 진입: 감속(ease-out)
  • 이탈: 가속(ease-in)
  • 이동: 표준(ease-in-out)

애플의 휴먼 인터페이스 가이드라인(HIG)도 이 감각을 강조한다. 값을 표로 주기보다 물리적으로 자연스러운 움직임, 즉 현실 세계의 관성과 무게감을 닮은 모션을 권한다. 결국 두 회사가 서로 다른 방식으로 같은 말을 하고 있다. 현실의 물체가 움직이는 방식을 모사하라는 것이다.

cubic-bezier로 곡선을 직접 쥐기

CSS의 ease-out 같은 키워드는 편하지만, 곡선을 세밀하게 조정할 수는 없다. 곡선을 직접 쥐고 싶으면 cubic-bezier()를 쓴다. 네 개의 숫자가 두 개의 제어점을 정의하고, 그 제어점이 속도 곡선의 모양을 결정한다.

Material Design이 표준으로 제시하는 곡선을 예로 보자. 진입에 쓰는 감속 곡선은 이렇게 쓴다.

/* 진입: 감속 — 빠르게 들어와 부드럽게 안착 */
.modal-enter {
  transition: transform 300ms cubic-bezier(0.0, 0.0, 0.2, 1);
}

/* 이탈: 가속 — 천천히 출발해 빠르게 사라짐 */
.modal-exit {
  transition: transform 200ms cubic-bezier(0.4, 0.0, 1, 1);
}

/* 이동: 표준 — 양끝을 부드럽게 */
.tab-indicator {
  transition: transform 250ms cubic-bezier(0.4, 0.0, 0.2, 1);
}

숫자를 하나하나 외울 필요는 없다. 핵심은 곡선이 어느 쪽 끝에서 천천히 움직이느냐다. 감속 곡선은 도착 부근에서 속도를 줄여 부드럽게 안착하고, 가속 곡선은 출발 부근을 느리게 떠나 끝에서 빨라진다. 네 숫자는 두 제어점의 위치이고, 그 제어점이 곡선을 어느 쪽으로 얼마나 당길지를 정한다. 곡선 편집기(대표적으로 easings.net이나 브라우저 개발자 도구의 베지어 편집기)에서 점을 끌어보면 이 관계가 눈에 바로 들어온다. 참고로 앞에서 키워드로 쓴 ease-in-out은 이 표준 곡선을 좌우 대칭으로 근사한 것이라, 값이 완전히 같지는 않고 감각만 비슷하다.

버튼 상태 변화처럼 짧은 모션은 키워드로 충분하다. 아래처럼 간단히 쓴다.

/* 작은 상태 변화: 짧게, 감속 */
.button {
  transition: background-color 100ms ease-out;
}

접근성: 모션을 꺼야 하는 사람들

여기까지가 모션을 어떻게 만드는가였다면, 마지막 하나는 모션을 언제 꺼야 하는가다. 이걸 빼먹으면 앞의 원리를 아무리 잘 지켜도 반쪽이다.

어떤 사용자는 화면의 움직임에 민감하게 반응한다. 전정기관이 예민한 사람에게는 크게 미끄러지거나 확대되는 모션이 어지럼증이나 메스꺼움을 유발할 수 있다. 그래서 운영체제에는 모션 줄이기 설정이 있고, 브라우저는 이 설정을 prefers-reduced-motion 미디어 쿼리로 전달한다. 디자이너가 할 일은 이 신호가 켜져 있을 때 큰 모션을 걷어내는 거다.

주의할 점 하나. 모션을 끈다는 게 전환을 0으로 만들어서 화면이 툭툭 끊기게 하라는 뜻은 아니다. 위치를 크게 이동시키는 모션은 제거하되, 투명도만 짧게 바꾸는 정도의 은은한 전환은 남겨두는 편이 낫다. 이동은 없애고 페이드만 남기는 식이다.

/* 기본: 아래에서 올라오는 모달 */
.modal {
  transition: transform 300ms cubic-bezier(0.0, 0.0, 0.2, 1);
}

@media (prefers-reduced-motion: reduce) {
  /* 큰 이동은 걷어내고, 은은한 페이드만 남긴다 */
  .modal {
    transition: opacity 120ms ease-out;
    transform: none;
  }
}

이건 예외 케이스를 챙기는 배려의 문제이자, 애초에 모션의 목적이 무엇인가를 되묻는 문제이기도 하다. 모션은 사용자의 이해를 돕기 위한 것이지 과시를 위한 것이 아니다. 모션이 사라져도 화면의 논리가 무너지지 않는다면, 그 모션은 처음부터 잘 설계된 거다.

정리

세 가지만 손에 익히면 된다. 첫째, 지속시간은 하나의 숫자가 아니다. 이동 거리가 크면 길게, 작은 요소는 짧게, 대략 100ms에서 500ms 사이에서 정한다. 둘째, 이징은 현실 물리를 모사한다. 진입은 감속, 이탈은 가속, 이동은 표준 곡선이고, 시작과 끝이 있는 모션에 linear는 대개 어색하다. 셋째, prefers-reduced-motion으로 모션에 민감한 사용자를 배려한다.

이 세 가지를 알고 나면, 모든 화면에 300ms를 박거나 모달에 linear를 쓰는 실수는 저절로 사라진다. 값을 찍는 게 아니라 이유를 대고 값을 고르게 되기 때문이다. 다음에 모달이 답답하게 느껴지면, 감으로 숫자를 바꾸지 말고 물어보자. 이 요소는 지금 들어오는가 나가는가, 그리고 얼마나 먼 거리를 움직이는가.