GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

cubic-bezier css 이징 커브·스프링 9종 — 복붙용 애니메이션 곡선

버튼을 누르거나 카드가 나타날 때, 움직임이 일정한 속도면 딱딱해 보이고 처음엔 빠르다가 점점 느려지거나 끝에서 살짝 튕기면 자연스럽다. 이 속도 변화를 CSS 에서 숫자 네 개로 적는 방법이 cubic-bezier css 값이고, 통통 튀는 움직임은 스프링 방정식을 곡선으로 바꾼

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개는 인기순이 아니라 값을 다루는 방법이 늘어나는 순서로 놓았다. 화면에 처음 등장하는 요소가 겪는 감속 곡선 세 개(01~03)로 시작해, cubic-bezier 로는 못 만드는 물리적인 튕김 세 개(04~06)를 지나, 그 값을 코드에서 재사용하는 두 가지 방법(07·08)을 거쳐, 마지막으로 넷을 한 화면에 놓고 눈으로 비교하는 데모(09)로 끝난다. 곡선을 실제 화면에 얹은 결과가 궁금하면 벤토 그리드 호버 애니메이션이 같은 값으로 움직이고, 컴포넌트 쪽에서 같은 곡선을 재사용하는 방법은 라이브러리 없는 리액트 CSS 애니메이션에 있습니다.

01ease-out-quart 곡선

css 곡선 애니메이션에서 어디에 쓰나

cubic-bezier(.25, 1, .5, 1) 을 translateX 에 물리면 처음부터 빠르게 튀어나가 관성 없이 목표 지점 근처에서 부드럽게 멈춘다.

ease-out-quart 곡선
새 창에서 열기

01 · ease-out-quart 곡선 — 측정 누적면적 3.2% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

cubic-bezier(.25, 1, .5, 1)transform

코드 읽는 법

모달이나 드로어가 열릴 때, 카드가 화면에 등장할 때 쓰기 좋다.

$duration: $dur-loop;
$easing: cubic-bezier(.25, 1, .5, 1);   // ease-out-quart

.stage.is-demo .puck {
  animation: move $duration $easing infinite alternate;
}
@keyframes move {
  from { transform: translate(0, -50%); }
  to   { transform: translate($travel, -50%); }
}

02ease-in-out-back 곡선

어디에 쓰나

cubic-bezier(.68, -.6, .32, 1.6) 은 y값이 0과 1을 벗어난다.

ease-in-out-back 곡선
새 창에서 열기

02 · ease-in-out-back 곡선 — 측정 누적면적 6.664% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

cubic-bezier오버슈트transform

코드의 요점

그래서 출발 전 살짝 뒤로 당겼다가, 도착 뒤 목표를 넘었다 되돌아오는 두 번의 예비동작이 생긴다. 토스트·배지가 튀어나오듯 등장할 때 쓴다.

$easing: cubic-bezier(.68, -.6, .32, 1.6);   // ease-in-out-back

.stage.is-demo .puck {
  animation: move $duration $easing infinite alternate;
}
@keyframes move {
  from { transform: translate(0, -50%); }
  to   { transform: translate($travel, -50%); }
}

03ease-out-expo 곡선

눈에 보이는 것

cubic-bezier(.16, 1, .3, 1) 은 첫 몇 프레임 만에 이동 대부분을 끝내고, 남은 구간은 육안으로 거의 안 움직일 만큼 천천히 기어가 도착을 길게 늘인다.

ease-out-expo 곡선
새 창에서 열기

03 · ease-out-expo 곡선 — 측정 누적면적 5.573% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

cubic-bezier급감속transform

동작 원리

사이드바가 훅 펼쳐지는 속도감을 강조하고 싶을 때 쓴다.

$easing: cubic-bezier(.16, 1, .3, 1);   // ease-out-expo

.stage.is-demo .puck {
  animation: move $duration $easing infinite alternate;
}
@keyframes move {
  from { transform: translate(0, -50%); }
  to   { transform: translate($travel, -50%); }
}

04linear() 바운스

어떤 상황인가

cubic-bezier 는 값이 한 번만 오르내리는 곡선이라, 공이 바닥에서 몇 번을 더 튕기는 움직임은 못 만든다. Robert Penner 의 easeOutBounce 공식을 25개 점으로 직접 표본화해 linear() 에 넣으면 값이 커지다 줄었다를 반복하는 진짜 튕기는 곡선이 나온다.

linear() 바운스
새 창에서 열기

04 · linear() 바운스 — 측정 누적면적 3.203% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

linear()표본점 25개transform

만드는 방식

장바구니에 아이콘이 담기거나 알림 배지가 새로 뜰 때 쓴다.

$easing: linear(0, .013, .053, .118, .21, .328, .473, .643, .84, .97, .875, .807, .766, .75, .761, .798, .861, .951, .973, .943, .939, .962, .995, .984, 1);

.stage.is-demo .puck {
  animation: drop $duration $easing infinite;
}
@keyframes drop {
  from { transform: translate(-50%, 0); }
  to   { transform: translate(-50%, $drop); }
}

05부드러운 스프링

무슨 일이 일어나나

감쇠비(zeta) .62·각진동수(omega) 7.0 인 감쇠 스프링 방정식 x(t) = 1 - e^(-ζωt)(cos(ω_d t) + (ζω/ω_d)sin(ω_d t)) 를 23개 점으로 표본화하면, 목표를 8% 정도만 넘겼다가 빠르게 가라앉는 값이 나온다. 감쇠비가 1에 가까울수록 오버슈트가 줄어든다.

부드러운 스프링
새 창에서 열기

05 · 부드러운 스프링 — 측정 누적면적 3.44% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

linear()감쇠 스프링transform

왜 이렇게 만들었나

설정 패널이 옆에서 미끄러져 들어오는 것처럼 평소 쓰는 UI 전환에 맞는다.

$easing: linear(0, .044, .153, .297, .452, .602, .736, .849, .937, 1.002, 1.045, 1.071, 1.082, 1.083, 1.077, 1.066, 1.053, 1.04, 1.028, 1.018, 1.009, 1.003, 1);

.stage.is-demo .puck {
  animation: move $duration $easing infinite alternate;
}
@keyframes move {
  from { transform: translate(0, -50%); }
  to   { transform: translate($travel, -50%); }
}

06탄력적인 스프링

화면에서 벌어지는 일

같은 방정식에서 감쇠비를 .35 로 낮추고 각진동수를 9.0 으로 올리면, 목표를 30% 넘게(1.307) 지나쳤다가 몇 번을 더 오가는 탄력이 뚜렷한 값이 된다.

탄력적인 스프링
새 창에서 열기

06 · 탄력적인 스프링 — 측정 누적면적 6.622% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

linear()감쇠 스프링오버슈트

구현의 핵심

좋아요 버튼을 눌렀을 때, 드래그 후 손을 놓아 카드가 튕겨 돌아올 때 쓴다.

$easing: linear(0, .055, .197, .394, .612, .824, 1.009, 1.153, 1.25, 1.3, 1.307, 1.281, 1.231, 1.167, 1.099, 1.035, .982, .942, .916, .905, .907, .917, .935, .955, .976, .995, 1);

.stage.is-demo .puck {
  animation: move $duration $easing infinite alternate;
}
@keyframes move {
  from { transform: translate(0, -50%); }
  to   { transform: translate($travel, -50%); }
}

07SCSS 이징 맵 믹스인

어떻게 보이나

편마다 곡선값을 복붙하지 않으려고, 이 편에서 쓴 값들을 $easings 맵 하나에 이름으로 묶고 @mixin ease($name) 이 map.get 으로 꺼내 쓰게 했다.

SCSS 이징 맵 믹스인
새 창에서 열기

07 · SCSS 이징 맵 믹스인 — 측정 누적면적 9.358% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

Sass map@mixinmap.get

무엇이 핵심인가

여러 컴포넌트가 같은 브랜드 이징을 공유해야 하는 디자인 시스템에서 쓰는 패턴이다.

$easings: (
  "ease-out-quart":   cubic-bezier(.25, 1, .5, 1),
  "ease-in-out-back": cubic-bezier(.68, -.6, .32, 1.6),
  "spring-gentle":    linear(0, .044, .153, .297, .452, .602, .736, .849, .937, 1.002, 1.045, 1.071, 1.082, 1.083, 1.077, 1.066, 1.053, 1.04, 1.028, 1.018, 1.009, 1.003, 1),
  "spring-snappy":    linear(0, .055, .197, .394, .612, .824, 1.009, 1.153, 1.25, 1.3, 1.307, 1.281, 1.231, 1.167, 1.099, 1.035, .982, .942, .916, .905, .907, .917, .935, .955, .976, .995, 1),
);

@mixin ease($name, $prop: animation-timing-function) {
  #{$prop}: map.get($easings, $name);
}

.stage.is-demo .puck {
  animation-name: move;
  animation-duration: $duration;
  animation-iteration-count: infinite;
  animation-direction: alternate;
  @include ease("spring-snappy");   // 맵에서 06번과 같은 값을 꺼낸다
}

08Tailwind v4 --ease 토큰

언제 쓰나

Tailwind v4 는 @theme 블록에 --ease-* 를 등록하면 이징을 유틸리티 클래스처럼 이름으로 쓸 수 있다.

Tailwind v4 --ease 토큰
새 창에서 열기

08 · Tailwind v4 --ease 토큰 — 측정 누적면적 6.524% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CSS 커스텀 프로퍼티var()Tailwind v4

짚어 둘 점

이 zip 은 외부 의존을 안 넣으려고 Tailwind 를 직접 불러오지 않지만, 같은 패턴을 :root 커스텀 프로퍼티만으로 그대로 재현했다.

$easing: var(--ease-spring);

:root {
  --ease-spring: cubic-bezier(.34, 1.56, .64, 1);
}
.stage.is-demo .puck {
  animation: move $duration $easing infinite alternate;
}

09이징 비교 데모

어떤 화면인가

quart(01)·back(02)·snappy 스프링(06)·bounce(04) 네 곡선을 같은 2초·같은 거리로 나란히 돌리면, 감속의 모양이 눈으로 바로 갈린다.

이징 비교 데모
새 창에서 열기

09 · 이징 비교 데모 — 측정 누적면적 4.24% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

cubic-bezierlinear()비교

어떻게 만들었나

값을 새로 만들지 않고 앞선 네 항목의 값을 그대로 재사용했다.

.puck--quart  { animation: move $duration cubic-bezier(.25, 1, .5, 1) infinite alternate; }
.puck--back   { animation: move $duration cubic-bezier(.68, -.6, .32, 1.6) infinite alternate; }
.puck--spring { animation: move $duration linear(0, .055, .197, .394, .612, .824, 1.009, 1.153, 1.25, 1.3, 1.307, 1.281, 1.231, 1.167, 1.099, 1.035, .982, .942, .916, .905, .907, .917, .935, .955, .976, .995, 1) infinite alternate; }
.puck--bounce {
  animation: move-once $duration linear(0, .013, .053, .118, .21, .328, .473, .643, .84, .97, .875, .807, .766, .75, .761, .798, .861, .951, .973, .943, .939, .962, .995, .984, 1) infinite;
}

어디서 깨지는가 — 세로 트랙은 가로보다 먼저 넘친다

무엇이 문제인가

cubic-bezier 사용법으로는 못 만드는 04번(linear() 바운스)은 처음엔 공이 132px 가 아니라 180px 를 떨어지게 만들었다. 480×300 데스크톱 화면에선 멀쩡했다. 문제는 이 사이트가 모바일 대응을 재는 방식이다 — 폭 320px 로 줄일 때 높이도 같은 비율(320×300÷480)로 200px 까지 같이 줄어든다. 세로로 쌓이는 트랙 180px 에 위아래 여백을 더하면 200px 를 넘겨, 실제로 렌더해서 재 보니 폰넘침: true 가 나왔다.

어떻게 피하나

곡선 하나를 제품 전체에서 돌려쓸 때 생기는 불일치는 값을 토큰으로 묶어 두는 다크 SaaS 리니어 디자인 시스템에서 시스템 단위로 다룹니다.

가로로 눕는 트랙은 폭 320px 안에서 여유가 있어 안 걸렸지만, 세로 트랙은 높이 200px 라는 더 좁은 예산을 바로 만난다. 트랙을 132px 로 줄이고 나서야 넘침이 사라졌다. zip 을 열 때 묻는 비밀번호는 wnxge56e 이고, 안내 그대로 입력하면 풀린다. 세로 방향 애니메이션을 넣을 땐 가로 기준으로만 반응형을 확인하고 넘어가기 쉬운데, 이 편처럼 모바일 뷰포트의 높이도 따로 재야 한다.

접근성

reduced-motion 에서 남는 것

9개 전부 prefers-reduced-motion: reduce 에서 animation: none 으로 떨어지고, transform 을 애니메이션의 도착 지점 값으로 고정한다 — 01·02·03·05·06·07·08번은 트랙 끝(196px 또는 188px)에, 04번은 낙하 지점(108px)에, 09번은 네 레인 모두 각자의 끝 지점에 멈춘다. 움직임이 꺼져도 정지 화면이 중간에 걸린 것처럼 보이지 않는다.

그 밖에 챙길 것

이 글 자체가 곡선의 모양 차이를 보여주는 게 목적이라 reduced-motion 에서는 그 차이를 못 느끼지만, 최종 위치 정보는 그대로 남아 레이아웃이 깨지지 않는다.

FAQ

cubic bezier transition 과 linear() 는 뭐가 다른가요?

cubic-bezier() 는 숫자 네 개로 정의되는 하나의 매끄러운 곡선이라 값이 방향을 여러 번 바꾸는 움직임(공이 몇 번을 더 튕기는 것 같은)은 표현하지 못한다. linear() 는 임의의 점을 원하는 만큼 나열해 곡선을 그리므로, 이 글의 04·05·06번처럼 물리 방정식을 직접 표본화한 값도 그대로 옮길 수 있다.

CSS 에 스프링 전용 키워드가 따로 있나요?

없다. spring() 같은 전용 이징 함수는 아직 브라우저에 표준으로 들어오지 않았다. 지금 스프링처럼 보이는 움직임을 CSS 로 내려면 이 글의 05·06번처럼 감쇠 스프링 방정식을 직접 계산해 linear() 로 표본화하는 방법이 현실적이다.

이 값을 직접 계산하지 않고 구할 방법이 있나요?

cubic-bezier() 는 이름별 프리셋을 모아둔 easings.net 같은 목록에서 값만 복사해도 되고, Chrome·Firefox 개발자 도구의 애니메이션 패널은 곡선을 눈으로 조정하면서 값을 바로 볼 수 있다. linear() 표본값은 이 글의 04~06번처럼 파이썬이나 자바스크립트로 방정식을 직접 풀어 뽑는 편이 물리적으로 정확하다. 두 함수의 문법은 MDN 의 easing-function 문서 에 정리돼 있다. 다른 CSS 애니메이션은 사이트 검색 에서 찾아볼 수 있고, 이 블로그가 데모를 만드는 방식은 소개 페이지 에 있다. 스프링 느낌만 빠르게 가져다 쓰고 싶으면 CSS 통통 튀는 애니메이션이 곡선을 이미 입혀 둔 예제입니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.