Skip to main content
ENKO

모션

모션은 무엇이 바뀌었고 무엇이 어디로 갔는지 보여 준다. Senco의 애니메이션은 모두 목적이 있어야 하고, 장식으로 쓰지 않는다.

모션을 엄격하게 제한하는 이유가 하나 더 있다. Senco 사용자는 불안정한 연결에서 중급 안드로이드 기기를 쓰는데, 이런 환경에서 가장 먼저 무너지는 게 애니메이션이다. 300ms 전환이 8fps로 떨어지면, 사용자가 송금을 확인하는 바로 그 순간 앱이 멈춘 것처럼 보인다. 모션은 한정된 예산이고, 그 예산을 어디에 쓸지 이 페이지에서 정한다.

지속 시간 토큰

지속 시간 토큰은 애니메이션이 얼마나 이어지는지 정한다. 작고 자주 일어나는 상호작용에는 짧은 시간을, 크고 드문 전환에는 긴 시간을 쓴다.

TokenValueUsage
--ds-duration-instant50ms버튼 누름, 토글, 체크박스
--ds-duration-fast100ms드롭다운 열림, 툴팁 표시, Hover 상태
--ds-duration-normal180ms패널 슬라이드, 토스트 등장·퇴장, 아코디언
--ds-duration-slow300ms페이지 전환, 모달 열기·닫기
--ds-duration-slower500ms스켈레톤 페이드, 복잡한 전환

--ds-transition은 기본 조합인 180ms cubic-bezier(0.4, 0, 0.2, 1)을 묶은 토큰이다. 컴포넌트에 다른 값을 쓸 이유가 없으면 이걸 쓴다.

지속 시간 고르기

  • 작은 요소는 더 빠르게 움직인다. 체크박스 토글은 즉각 반응하는 것처럼 느껴져야 한다.
  • 큰 표면은 눈이 변화를 따라갈 수 있도록 시간을 길게 준다.
  • 자주 일어나는 동작(예: Hover)은 굼떠 보이지 않도록 짧은 시간을 쓴다.
  • 드문 동작(예: 페이지 전환)은 조금 느리고 표현력 있는 타이밍을 써도 된다.

기준은 간단하다. 지속 시간은 요소가 얼마나 중요한지가 아니라 얼마나 움직이는지에 비례한다. 중요한 버튼이라도 4px만 움직이면 50ms다.

300ms 상한

Senco에서는 스켈레톤 페이드를 빼고 300ms보다 오래 움직이는 것이 없다. 400ms 정도를 넘으면 전환이 피드백이 아니라 지연으로 느껴지기 시작하고, 송금 앱에서 사용자는 애니메이션이 느린 건지 네트워크가 느린 건지 구분하지 못한다. 애매하면 불확실하게 느껴지는 것보다 차라리 툭 끊기는 편이 낫다.

--ds-duration-slower(500ms)는 스켈레톤 크로스페이드에만 쓴다. 여기서는 느린 게 목적이다. 스켈레톤이 콘텐츠로 순간 바뀌면 화면이 번쩍인다.

Do / Don't
Do

Subtle 150ms ease on hover. Functional feedback without distraction.

Don't

Flashy 2s bouncing animation. Distracting, inaccessible, and unprofessional.

Duration tokens
duration-instant
70msButton press, toggle, checkbox
duration-fast
150msDropdown, tooltip, hover state
duration-moderate
240msPanel slide, toast, accordion
duration-slow
400msPage transition, modal, skeleton

이징 곡선

이징 곡선은 애니메이션이 어떻게 가속하고 감속하는지 정한다. Senco는 곡선 네 가지를 쓴다.

TokenValueUsage
--ds-ease-defaultcubic-bezier(0.4, 0, 0.2, 1)대부분의 전환. 상태가 바뀌며 움직이는 요소
--ds-ease-outcubic-bezier(0, 0, 0.2, 1)나타나는 요소(등장). 감속하며 자리를 잡는다
--ds-ease-incubic-bezier(0.4, 0, 1, 1)사라지는 요소(퇴장). 가속하며 빠져나간다
--ds-ease-bouncecubic-bezier(0.34, 1.56, 0.64, 1)경쾌한 피드백(성공 체크 표시, 토글이 딸깍 맞물리는 동작)

곡선별 용도

  • Standard가 기본값이다. 요소가 DOM에 새로 들어오거나 빠지지 않는 상태 변화에는 모두 이걸 쓴다.
  • Entrance는 모달 열림, 토스트 등장, 드롭다운 펼침에 쓴다.
  • Exit는 모달 닫힘, 토스트 닫힘, 드롭다운 접힘에 쓴다.

등장이 감속하는 건 요소가 막 도착하는 중이라 눈이 따라잡아야 하기 때문이다. 퇴장이 가속하는 건 사용자가 이미 그 요소를 봤고, 더 머물러 봐야 주의만 뺏기 때문이다.

bounce를 쓸 수 있는 곳

--ds-ease-bounce는 목표 지점을 살짝 지나쳤다가 돌아온다. 돈을 다루는 앱에서 이런 움직임은 가벼워 보인다. 쓸 수 있는 곳은 딱 두 군데다.

AllowedNot allowed
완료된 송금의 성공 체크 표시모든 에러, 경고, 실패 상태
토글과 스위치가 맞물리는 동작모달, 시트, 페이지 전환

제품에서 작은 축하를 해도 되는 순간은 송금이 완료될 때뿐이다. 그 밖의 곳에서 bounce를 쓰면 금융 인터페이스가 장난스러워 보인다.

Easing curves
Standard
Toggle switch
cubic-bezier(0.2, 0, 0.38, 0.9)
Entrance
Dot wave loading
cubic-bezier(0, 0, 0.38, 0.9)
Exit
Card fade-out
cubic-bezier(0.2, 0, 1, 0.9)

동작 줄이기

OS에서 prefers-reduced-motion을 켠 사용자에게는 불편을 줄 수 있는 애니메이션을 보여 주지 않는다. Senco는 이 설정을 전역으로 따른다.

컴포넌트마다 따로 처리하지 않고, Platform 계층에 블록 하나를 둔다.

@media (prefers-reduced-motion: reduce) {
  .ds-layout *,
  .ds-layout *::before,
  .ds-layout *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

한곳에서 처리해 두면, 내년에 누가 새 컴포넌트를 만들더라도 이 규칙을 몰라도 자동으로 지켜진다. 지속 시간을 0이 아니라 0.01ms로 줄이는 건 transitionend와 animationend 리스너가 계속 실행되게 하려는 것이다. 0으로 두면 애니메이션이 끝나기를 기다리는 컴포넌트가 아무 경고 없이 망가진다.

단순화할 대상

CategoryDefaultReduced motion
전환(불투명도, 색)평소대로 움직인다즉시 바뀐다
슬라이드·transform 애니메이션이징을 넣어 움직인다불투명도 페이드로 바꾼다
로딩 스피너1s linear로 계속 회전회전을 멈춘다. 글리프는 화면에 남고, 상태는 라벨이 알려 준다
자동 재생 캐러셀슬라이드가 넘어가며 움직인다멈추고 수동 컨트롤을 보여 준다
상태 타임라인 펄스활성 점이 깜빡인다점이 멈춰 있다. 상태는 색, 아이콘, 라벨이 알려 준다

위 Platform 블록은 스피너도 예외로 두지 않는다. 0.01ms에 animation-iteration-count: 1이면 모든 회전이 멈춘다. 의도한 것이다. 처리 방식은 하나로 두고, 컴포넌트별 예외는 만들지 않는다. 그래도 대기 상태가 제 정보를 전하는 건 시스템의 모든 스피너에 텍스트 라벨이 붙어 있기 때문이다(Inline loading 참고). 멈춘 글리프와 "Checking recipient details"라는 라벨만으로도 돌던 스피너와 똑같은 내용이 전달된다. 동작을 줄인다는 건 움직임을 줄인다는 뜻이지, 정보를 줄인다는 뜻이 아니다.

전역 블록은 최소한의 조치다

Platform 규칙으로 전환은 꺼지지만, 모션으로만 정보를 전하는 컴포넌트는 이 사용자들에게 여전히 제대로 동작하지 않는다. 움직이는 상태에는 반드시 움직이지 않는 표현이 함께 있어야 한다. 상태 색을 색, 아이콘, 텍스트로 함께 표시하는 규칙과 같은 원칙이다.

Reduced motion
Full Motion
Confirm transfer?$250.00 CAD to Adaeze O.
Cancel
Confirm
Reduced Motion
Confirm transfer?$250.00 CAD to Adaeze O.
Cancel
Confirm
Left: Full animation with slide, scale, and opacity transitions. Right: Only opacity changes — no spatial movement. This is what users with prefers-reduced-motion: reduce will see.

디자인과 접근성

  • 다듬기보다 목적. 모든 애니메이션은 "사용자가 무엇을 이해하는 데 도움이 되는가?"에 답할 수 있어야 한다.
  • 애니메이션만으로 피드백하지 않는다. 모션으로 상태 변화를 알린다면, 애니메이션이 끝난 뒤에도 남는 시각적 변화(색, 아이콘, 텍스트)를 함께 준다.
  • 사용자 설정을 따른다. prefers-reduced-motion은 항상 따르고, 절대 무시하지 않는다.
  • 성능 예산. 움직이는 요소는 transform과 opacity로, 상태 피드백은 background-color, border-color, color, box-shadow로 애니메이션한다. width, height, top, left, margin, padding은 애니메이션하지 않는다. 프레임마다 레이아웃을 다시 계산하게 만든다. 색과 그림자는 요소를 움직이지 않으니 레이아웃 계산이 일어나지 않는다. 문서에 있는 버튼, 카드, 입력의 상태 변화도 모두 이 속성들을 쓴다.
  • 저사양 기기에서 테스트한다. MacBook Pro에서 매끄러운 애니메이션도 Senco의 주 사용자층이 쓰는 저가 안드로이드 폰에서는 끊길 수 있다.
  • 1초에 3회 넘게 깜빡이지 않는다 (WCAG 2.3.1). 지금 시스템에는 여기에 가까운 것도 없고, 앞으로도 추가하지 않는다.
  • 모션이 입력을 막지 않는다. 등장 애니메이션이 끝나기 전에도 주요 액션을 탭할 수 있어야 한다.

권장과 금지

DoDon't
Animate transform, opacity, colour, and shadowAnimate height to expand an accordion
Use bounce on a completed transferUse bounce on a failed one
Pair every spinner with a text label, so a static one still communicatesShip a bare spinner that becomes meaningless once it stops
Keep transitions under 300msAdd a 600ms "premium" page transition
Pair a state animation with a persistent visual changeRely on a flash to communicate that a value updated

알려진 공백

ItemStatus
모션 토큰 범위지속 시간과 이징은 토큰으로 만듦. 지연(delay)과 순차 등장(stagger) 토큰은 없음
저사양 기기 테스트요구사항으로만 적혀 있음. 테스트 기기 목록이나 프레임 레이트 예산은 문서화 안 됨
forced-colors와의 상호작용동작 줄이기와 함께 테스트 안 함