스프링 애니메이션: 언제 쓰고 어떻게 조절하나

스프링 애니메이션: 언제 쓰고 어떻게 조절하나

바텀 시트를 손가락으로 반쯤 끌어 올리다 놓는다. 손끝은 위로 빠르게 움직이던 중이었다. 그런데 놓는 순간 시트가 그 속도를 이어받아 스르륵 끝까지 올라가는 게 아니라, 잠깐 멈칫하더니 처음부터 다시 부드럽게 올라간다. 뭔가 어색하다. 방금 내 손이 만든 속도는 어디로 사라졌을까.

지난 글에서 지속시간(duration)과 이징(easing) 곡선을 원리로 정하는 법을 다뤘다. 이동 거리에 맞춰 시간을 잡고, 들어올 땐 감속, 나갈 땐 가속 곡선을 쓰라는 이야기였다. 그 원리는 여전히 맞다. 다만 그건 처음부터 끝까지 정해진 대로 재생되는 모션에 대한 이야기였다. 사용자가 도중에 끼어들고, 마음을 바꾸고, 손가락으로 끌다 놓는 인터랙션으로 들어가면 그 방식이 어색해진다. 이 글은 그 지점에서 스프링(spring) 애니메이션이 왜 필요하고, 디자이너가 언제 어떻게 쓰면 되는지에 대한 글이다. 물리 수식은 꺼내지 않는다. 필요한 건 감각과 판단이지 방정식이 아니다.

이징은 정해진 시간표다

이징 곡선의 성질을 한 줄로 줄이면 이렇다. 출발하기 전에 언제 얼마나 움직일지가 이미 다 적혀 있는 시간표다. 300ms짜리 트랜지션이면, 300ms 동안 어떤 속도로 어디까지 갈지가 시작 전에 확정된다.

이 성질은 잔잔한 화면에선 장점이다. 언제 끝날지 정확히 알고, 여러 요소를 순서대로 등장시키기도 쉽다. 문제는 사용자가 이 시간표 위로 끼어들 때다.

메뉴 패널이 왼쪽에서 슬라이드로 열리는 중이라고 하자. 40%쯤 열렸을 때 사용자가 마음을 바꿔 닫기를 누른다. 이때 닫기 애니메이션이 처음부터 300ms 시간표로 재생되면, 패널은 40% 지점에서 갑자기 완전히 열린 위치로 튀었다가 닫히거나, 40%에서 닫히기 시작하는데 그 출발이 툭 끊겨 보인다. 지금 화면에서 벌어지던 움직임과 새 움직임이 매끄럽게 이어지지 않는다. 시간표에는 지금 어디까지 왔고 얼마나 빠른지를 물려받는다는 개념이 없기 때문이다.

제스처에서 이게 정점을 찍는다. 손가락으로 무언가를 끌면 그 손끝에는 매 순간 속도가 있다. 스크롤을 튕기든 시트를 밀어 올리든, 놓는 순간의 속도가 그다음 움직임을 결정해야 자연스럽다. 세게 튕겼으면 멀리, 살살 놓았으면 가까이 가서 멈추는 관성 말이다. 그런데 이징 곡선은 이 속도를 받아낼 입구가 없다. 곡선은 정해진 시간과 진행률만 알지, 지금 손가락이 얼마나 빠르게 움직이고 있었는지는 모른다. 그래서 놓는 순간 손끝의 속도는 버려지고, 미리 정해둔 시간표가 그 자리를 대신 재생한다. 글 첫머리의 바텀 시트가 멈칫한 이유가 이거다. 내가 만든 속도가 사라진 게 아니라, 시간표라는 방식이 애초에 그 속도를 담을 그릇을 갖고 있지 않았다.

스프링은 움직임을 이어받는다

스프링은 이 문제를 정반대로 푼다. 시간표를 미리 적어두는 대신, 매 순간 지금 위치와 지금 속도를 들고 목표를 향해 움직인다. 용수철에 매달린 물체를 떠올리면 된다. 목표에서 멀면 세게 당겨지고, 가까워지면 느슨해지고, 움직이던 속도는 그대로 유지한 채 목표에 가서 멎는다.

핵심은 딱 하나다. 스프링은 지금 얼마나 빠른지를 늘 기억하고 있다. 그래서 중간에 목표가 바뀌어도 가던 속도를 물려받은 채 방향만 튼다. 손가락이 놓은 속도를 그대로 이어받아 관성으로 흘러간다. 앞에서 본 어색함이 여기서 한꺼번에 풀린다. 열리다 닫혀도 매끄럽게 이어지고, 튕긴 만큼 멀리 날아간다. iOS의 상당수 인터랙션이 이 방식으로 움직이고, 웹에선 Framer Motion이나 React Spring 같은 도구가 같은 방식을 쓴다.

내부에서 물리 계산이 돌아가긴 한다. 하지만 디자이너가 그 수식을 알 필요는 없다. 자동차를 몰기 위해 엔진을 분해할 필요가 없는 것과 같다. 우리가 만질 손잡이는 훨씬 단순하다.

조절하는 건 결국 둘, 빠르기와 튐

스프링을 조절할 때 실제로 느끼게 되는 건 두 가지다.

하나는 빠르기다. 목표로 얼마나 급하게 당겨지느냐다. 빠르게 잡으면 버튼을 눌렀을 때 딱 붙는 반응이 나오고, 여유 있게 잡으면 큰 패널이 천천히 떠오르는 느낌이 된다.

다른 하나는 튐이다. 목표에 도착할 때 지나쳤다가 되돌아오길 반복하며 통통 튈지, 아니면 지나침 없이 부드럽게 딱 멎을지다. 튐이 있으면 생기 있고 장난스럽고, 없으면 차분하고 정확하다.

도구마다 이 둘을 부르는 이름이 다르다. 어떤 도구는 강성(stiffness)과 감쇠(damping)라는 두 값으로 노출하는데, 대충 강성이 빠르기를, 감쇠가 튐을 줄이는 정도를 맡는다. 요즘 도구는 아예 디자이너 말로 바꿔서 튐 정도(bounce)와 지속시간(duration) 두 손잡이로 준다. 이쪽이 훨씬 직관적이다. 튐을 0으로 두면 안 튀고, 올릴수록 통통 튄다.

// duration 하나로 정확한 시간을 못 박는 대신, 두 손잡이로 느낌을 정한다.
<motion.div
  animate={{ y: isOpen ? 0 : 300 }}
  transition={{
    type: "spring",
    bounce: 0.3,   // 튐: 0이면 안 튀고, 올릴수록 통통 튄다
    duration: 0.5, // 대략적인 체감 속도
  }}
/>

여기서 기억할 건 이름이 아니라 감각이다. 손잡이가 강성·감쇠로 되어 있든 bounce·duration으로 되어 있든, 내가 정하는 건 결국 얼마나 빠른가와 얼마나 튀는가 둘뿐이다. 이 둘만 손에 쥐면 나머지는 값을 몇 번 만져보며 눈으로 맞추면 된다.

언제 스프링을 쓰고, 언제 이징을 쓰나

이 글의 진짜 질문이 여기다. 스프링이 이징의 상위 호환은 아니다. 둘은 쓰임이 다르다.

스프링이 제값을 하는 자리는 사용자가 움직임에 개입하는 곳이다. 세 가지 신호로 알아보면 된다. 첫째, 제스처가 있는가. 손가락으로 끌고 튕기고 스와이프하는 동작이 있다면 스프링이다. 손끝의 속도를 이어받아야 자연스럽기 때문이다. 둘째, 도중에 끊기고 되돌릴 수 있는가. 열리다 닫히고, 펼쳐지다 접히는 것처럼 진행 중에 방향이 바뀔 수 있다면 스프링이다. 셋째, 물리적인 실체처럼 느껴져야 하는가. 집어 옮기는 카드, 잡아당겼다 놓는 시트처럼 손에 잡히는 물성이 필요하다면 스프링이다. 바텀 시트, 스와이프로 넘기는 카드, 당겨서 새로고침, 드래그로 옮기는 요소가 전형적인 자리다.

반대로 이징이 맞는 자리는 정해진 대로 재생되고 정확한 타이밍이 필요한 곳이다. 대표적인 게 스태거(stagger) 시퀀스다. 리스트 항목 다섯 개를 40ms씩 시차를 두고 순서대로 등장시킨다고 하자. 각 항목이 정확히 언제 끝나는지 알아야 다음 항목의 지연을 얹을 수 있다. 그런데 스프링은 끝나는 시점을 딱 잘라 알려주지 않는다. 목표에 가까워지며 스르륵 잦아들 뿐이라, 정확히 몇 밀리초에 끝난다고 약속하지 못한다. 반면 이징은 300ms면 300ms에 끝난다고 약속한다. 여러 요소를 프레임 단위로 맞춰 춤추게 하려면 이 약속이 필요하다. 사용자 입력과 무관하게 재생되는 온보딩 시퀀스, 로딩 인디케이터, 정해진 안무가 여기 속한다.

선을 한 문장으로 그으면 이렇다. 사용자가 끼어들고 손으로 만지는 인터랙션에는 스프링, 정해진 대로 재생되고 정확히 맞춰야 하는 안무에는 이징. 애프터이펙트 같은 타임라인 도구에서 넘어온 디자이너라면 끝 시점이 없는 스프링이 처음엔 통제 불능처럼 느껴질 수 있다. 그건 스프링을 잘못 쓴 게 아니라 도구를 잘못 고른 거다. 통제가 필요한 안무에는 원래 이징을 쓰면 된다.

튐은 언제 허용하고 언제 금지하나

튐은 공짜 매력이 아니다. 쓸 자리와 못 쓸 자리가 갈린다.

튐이 어울리는 건 물리적 실체처럼 다루는 오브젝트다. 집어 옮기는 카드, 잡아당겼다 놓는 시트, 통통 튀며 등장해 존재감을 주려는 플레이풀한 요소. 여기선 살짝의 튐이 생동감과 물성을 준다.

튐을 금지해야 하는 건 정보를 전달하는 움직임이다. 진행률 막대가 60%까지 차는 애니메이션에 튐이 붙으면, 막대가 65%까지 갔다가 60%로 되돌아온다. 사용자는 값이 잠깐 더 높았다고 오해한다. 숫자를 세는 카운터나 위치가 곧 의미인 지표도 마찬가지다. 지나쳤다 돌아오는 움직임 자체가 없는 정보를 만들어낸다. 이럴 땐 튐을 0으로 둬서 지나침 없이 목표에 딱 멎게 한다. 판단 기준은 단순하다. 이 움직임의 도착 지점이 곧 데이터인가. 그렇다면 튐은 거짓말이 된다.

접근성은 더 무겁게 걸린다. 크게 흔들리는 튐은 전정기관이 예민한 사용자에게 어지럼증이나 메스꺼움을 유발할 수 있다. 튐은 취향의 문제로 보이지만, 어떤 사용자에겐 신체 증상의 문제다.

운영체제의 모션 줄이기 설정은 prefers-reduced-motion 미디어 쿼리로 전달된다. 이 신호가 켜져 있으면 크게 튀거나 이동하는 모션을 걷어낸다. 주의할 점은 지난 글에서와 같다. 전환을 통째로 없애 화면이 툭툭 끊기게 하는 게 아니라, 큰 이동과 튐만 제거하고 은은한 페이드 정도는 남긴다.

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

원칙은 하나다. 모션이 사라져도 화면의 논리가 무너지지 않아야 한다. 그렇다면 그 모션은 처음부터 잘 설계된 것이다.

정리

차이 하나만 쥐면 된다. 이징은 출발 전에 다 정해진 시간표라 손끝의 속도를 못 받고, 그래서 중단과 제스처 앞에서 어색해진다. 스프링은 지금 위치와 속도를 늘 들고 다니며 목표로 움직여서, 도중에 방향이 바뀌어도 매끄럽게 이어진다. 물리 수식을 알 필요는 없다. 조절하는 건 빠르기와 튐 둘뿐이다.

그래서 판단은 이렇게 한다. 사용자가 손으로 만지고 도중에 끼어드는 인터랙션이면 스프링, 정해진 대로 정확히 맞춰야 하는 안무면 이징. 튐은 물성이 필요한 오브젝트엔 허용하고, 값이 곧 정보인 움직임과 모션에 민감한 사용자 앞에선 걷어낸다. 다음에 시트가 손끝의 속도를 못 받아 멈칫하거든, 곡선을 더 매끄럽게 다듬으려 하지 말고 물어보자. 이 움직임에 지금 필요한 건 정해진 시간표인가, 이어지는 물리인가.

이 글, 어떠셨어요?