요즘IT
위시켓
새로 나온
인기요즘 작가들컬렉션
물어봐
새로 나온
인기
요즘 작가들
컬렉션
물어봐
개발
AI
IT서비스
기획
디자인
비즈니스
프로덕트
커리어
트렌드
스타트업
서비스 전체보기
위시켓요즘IT
고객 문의
02-6925-4867
10:00-18:00주말·공휴일 제외
[email protected]
요즘IT
요즘IT 소개작가 지원
기타 문의
콘텐츠 제안하기광고 상품 보기
요즘IT 슬랙봇크롬 확장 프로그램
이용약관
개인정보 처리방침
청소년보호정책
㈜위시켓
대표이사 : 박우범
서울특별시 강남구 테헤란로 211 3층 ㈜위시켓
사업자등록번호 : 209-81-57303
통신판매업신고 : 제2018-서울강남-02337 호
직업정보제공사업 신고번호 : J1200020180019
제호 : 요즘IT
발행인 : 박우범
편집인 : 노희선
청소년보호책임자 : 박우범
인터넷신문등록번호 : 서울,아54129
등록일 : 2022년 01월 23일
발행일 : 2021년 01월 10일
© 2013 Wishket Corp.
로그인
요즘IT 소개
콘텐츠 제안하기
광고 상품 보기
개발

프론트엔드에서 갑자기 물리학이 왜 나와?

스벨트전도사
12분
18시간 전
1.7K

프론트엔드 개발자로서 가장 보람찬 순간은 디자이너의 "이거 어려울 것 같은데..."라는 말에 "해볼게요"라고 대답하고 완벽히 구현해 냈을 때입니다. 저는 복잡한 인터랙션과 애니메이션을 구현할 때마다 사용자 경험이 한층 풍부해지는 것을 느끼며 성장합니다. 과거에는 이런 애니메이션을 구현하는 데 많은 시간과 노력이 필요했지만, 최근 다양한 라이브러리와 도구들의 발전으로 훨씬 수월해졌습니다.

 

그런데 애니메이션 구현을 위해 라이브러리 문서를 펼쳐보면, 이상한 용어가 눈에 들어옵니다. "spring"? 봄? 아니면 용수철? 그리고 여러 라이브러리에서 공통되는 변수명들, 이것들이 왜 웹 개발 문서에 등장하는 걸까요?

 

// Framer Motion 예시
<motion.div
  animate={{ x: 100 }}
  transition={{ type: "spring", stiffness: 100, damping: 10 }}
/>
// React Spring 예시
const springs = useSpring({
  from: { x: 0 },
  to: { x: 100 },
  config: { mass: 1, tension: 170, friction: 26 }
})
// Svelte Motion 예시
<script>
	import { Spring } from 'svelte/motion';
	let coords = new Spring({ x: 50, y: 50 }, {
		stiffness: 0.1,
		damping: 0.25
	});
	let size = new Spring(10);
</script>

 

여기서 stiffness, damping, mass, tension, friction 같은 물리학 용어들이 보이시나요? 실제 세계와 유사하게 움직이는 애니메이션은 사용자 만족도를 높이고 전환율을 개선하는 등 실질적인 비즈니스 가치를 창출합니다. 이러한 자연스러운 움직임의 비밀은 바로 ‘물리학’에 있습니다. 프론트엔드에 갑자기 물리학이 왜 나와? 싶을 수 있지만, 이 글에서는 왜 스프링 운동이 웹 애니메이션에 등장했는지를 소개하고, 구현 원리에 대해 알아보겠습니다.

 

CSS로는 안 되는 걸까?

CSS 애니메이션은 웹 개발의 기본이지만, 여러 한계점을 가지고 있습니다. 2015년 발표된 "The State of Animation in React"에서는 CSS 애니메이션의 근본적인 문제를 명확히 지적했고, 이것이 이후 프론트엔드 애니메이션의 패러다임을 바꾸는 시발점이 되었습니다.

 

<출처: Cheng Lou - The State of Animation in React at react-europe 2015>

 

CSS는 시간 기반이라 자연스럽지 않다

CSS 애니메이션은 시간(duration)을 기반으로 작동합니다. 빠른 애니메이션을 원하면 시간을 짧게, 느린 애니메이션을 원하면 시간을 길게 설정하죠. 그리고 시간 내의 값 변화를 베지어 곡선(cubic-bezier)으로 통제합니다.

 

.box {
  transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}

 

하지만 이 접근법에는 심각한 문제들이 있습니다.

 

1. 베지어 곡선의 난해함: 직관적이지 않고 원하는 효과를 정확히 얻기 위해서는 많은 시행착오가 필요합니다. 어떤 곡선이 자연스러운 움직임을 만들어낼지 예측하기 어렵습니다.

2. 거리와 시간의 불일치: 가장 치명적인 문제입니다. 값의 변화해야 하는 양(거리)에 따라 속도가 달라집니다. 시간을 짧게 설정해도 변화량이 적다면 느리게 표현되고, 변화량이 많다면 빠르게 표현됩니다.

3. 애니메이션 중단 시 부자연스러움: 진행 중인 애니메이션이 중단되거나 방향이 바뀔 때, 속도가 갑자기 변하여 부자연스러운 모션이 만들어집니다. 실제 물리 세계에서는 관성이 유지되지만, CSS에서는 이를 표현하기 어렵습니다.

 

/* 이 두 요소는 같은 시간(2초)이 걸리지만 속도 느낌이 완전히 다릅니다 */
.short-distance {
  animation: move-100px 2s ease;
}
.long-distance {
  animation: move-500px 2s ease;
}

 

특히 2번의 경우, 무한 배너(infinite banner)를 구현할 때 문제가 극명하게 드러납니다. 화면이 작은 모바일에서는 배너가 천천히 움직이고, 같은 코드가 큰 화면에서는 배너가 빠르게 질주하게 됩니다. 일관된 사용자 경험을 제공하기 위해 미디어 쿼리마다 다른 시간을 설정해야 하는 비효율이 발생합니다.

 

<출처: 작가>

 

"물리학을 적용해 보면 어떨까?"

이런 문제점을 해결하기 위해 등장한 것이 바로 "Spring" 모델입니다. Spring은 용수철의 움직임을 모방한 모델로, 실제 물리 세계의 법칙을 웹 애니메이션에 적용했습니다. React-spring 라이브러리를 시작으로, Framer Motion, PopMotion, svelte/motion 등 현대 프론트엔드의 대표적인 애니메이션 라이브러리들은 모두 이 Spring 모델을 기반으로 합니다.

 

Spring 모델은 마치 용수철에 달린 추의 움직임으로 모든 빠르기를 나타냅니다. 핵심은 세 가지 물리적 파라미터입니다.

 

1. Stiffness(탄성계수): 용수철이 얼마나 탄성이 있는지 나타냅니다. 값이 높을수록 용수철이 더 단단해져 빠르고 활발하게 움직입니다.

2. Damping(마찰계수): 실제 세계에서는 공기나 물 등에 의해 물체의 움직임과 반대 방향으로 저항하는 힘이 작용합니다. 그래서 용수철에 매달린 공은 결국 운동이 멈추게 됩니다. 값이 높을수록 빨리 안정화됩니다.

3. Mass(질량): 물체의 무게를 나타냅니다. 값이 클수록 움직임이 느려집니다. 같은 힘을 받아도 무거운 물체는 가벼운 물체보다 천천히 움직이죠.

 

// React Spring 예시
useSpring({
  from: { x: 0 },
  to: { x: 300 },
  config: {
    mass: 1,      // 질량
    tension: 170, // 탄성계수(stiffness)
    friction: 26  // 마찰계수(damping)
  }
})

 

Spring 모델의 장점들

1. 거리 독립적 일관성: 이동 거리가 달라도 같은 물리적 느낌을 유지합니다. 무한 배너가 어떤 화면 크기에서도 일관된 속도감을 제공할 수 있습니다.

2. 자연스러운 중단과 방향 전환: 애니메이션이 중간에 중단되거나 목표가 바뀌어도, 현재 속도(관성)를 유지하면서 새로운 목표로 부드럽게 전환됩니다.

3. 직관적인 조절: 물리적 개념을 바탕으로 하기 때문에, 파라미터 조정이 예측 가능합니다. "더 무겁게" 또는 "더 탄력 있게"와 같은 직관적인 사고로 원하는 효과를 얻을 수 있습니다.

 

이렇게 Spring 모델은 CSS 애니메이션의 한계를 뛰어넘어, 웹에서도 실제 세계와 같은 자연스러운 움직임을 가능케 했습니다. 이제 거의 모든 모던 프론트엔드 프레임워크에서 Spring 기반 애니메이션은 필수적인 요소가 되었습니다.

 

 

수치해석으로 이해하는 Spring 애니메이션

대학교에서 물리교육을 전공했던 저는 이런 물리 기반 애니메이션 원리를 살펴보는 것을 좋아합니다. 실제로 라이브러리 내부에서 어떻게 작동하는지 이해하면, 더 효과적으로 활용할 수 있기 때문이죠.

 

뉴턴 법칙과 운동방정식

물체는 어떤 힘을 받느냐에 따라 움직임이 달라집니다. 이 힘은 거리에 따라 달라질 수도 있고, 물체의 속도에 따라서도 달라질 수 있습니다. 뉴턴의 제2법칙(F = ma)에 따르면, 물체에 힘이 가해지면 가속도가 발생합니다. 이 힘과 물체의 여러 요소들 간의 관계를 수학적으로 표현한 것이 바로 운동방정식입니다.

 

Spring 모델에서의 운동방정식은 다음과 같습니다.

F = ma = -kx - cv

 

여기서

  • `F`는 힘
  • `m`은 질량
  • `a`는 가속도
  • `k`는 탄성계수(stiffness)
  • `x`는 평형 위치로부터의 변위
  • `c`는 마찰계수(damping)
  • `v`는 속도

 

고등학교 물리에서는 단순화된 `F = -kx`만 배우지만, 현실에서는 저항력까지 꼭 포함해야 합니다. 저항은 속도에 비례해서 반대 방향으로 작용하기 때문에 `-cv` 항이 추가됩니다.

 

수치해석으로 푸는 운동방정식

이런 미분방정식은 간단한 경우 해석적으로 풀 수도 있지만, 비선형 방정식은 정확한 해를 구하기 어려운 경우가 많습니다. 이럴 때 수치해석법이 유용합니다.

 

수치해석의 기본 아이디어는 간단합니다.

1. 시간을 아주 작은 조각(dt)으로 나눕니다.

2. 각 시간 조각 동안은 물체가 직선 등속운동을 한다고 가정합니다.

3. 각 단계마다 새로운 위치와 속도를 계산하고, 이를 다음 단계의 출발점으로 삼습니다.

4. 이 과정을 반복해 전체 움직임을 근사합니다.

 

웹에서는 `requestAnimationFrame`을 사용하여 화면 갱신 주기마다 이 계산을 수행할 수 있습니다.

 

Spring 애니메이션의 수치해석 구현

가장 단순한 형태의 수치해석 방법(오일러 방법)을 사용한 Spring 구현 코드는 다음과 같습니다.

 

// 초기 상태
let position = 0;  // 현재 위치
let velocity = 0;  // 현재 속도
const target = 100;  // 목표 위치
const stiffness = 0.1;  // 탄성계수
const damping = 0.6;  // 마찰계수
const mass = 1;  // 질량

function updateSpring(dt) {
  // 힘 계산: F = -k(x - target) - cv
  const displacement = position - target;
  const springForce = -stiffness * displacement;
  const dampingForce = -damping * velocity;
  const force = springForce + dampingForce;
  
  // 가속도 계산: a = F/m
  const acceleration = force / mass;
  
  // 속도 업데이트: v = v + a*dt
  velocity += acceleration * dt;
  
  // 위치 업데이트: x = x + v*dt
  position += velocity * dt;
  
  return position;
}

// 애니메이션 루프
let lastTime = 0;
function animate(currentTime) {
  if (lastTime > 0) {
    const dt = (currentTime - lastTime) / 1000;  // 초 단위로 변환
    const currentPosition = updateSpring(dt);
    // DOM 요소의 위치 업데이트
    element.style.transform = `translateX(${currentPosition}px)`;
  }
  lastTime = currentTime;
  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

 

이 코드는 Spring 움직임의 기본 원리를 보여줍니다.

  • 스프링은 중심(target)으로 끌어당기는 힘을 가합니다.
  • 저항이 없으면 물체는 목표 지점을 중심으로 왔다갔다 반복합니다.
  • 저항이 크면 한 번에 목표 지점에 도달하고 멈춥니다.

 

보다 정확한 수치해석: 룽게-쿠타 방법

위의 단순한 방법(오일러 방법)은 오차가 누적되어 에너지가 증가하는 문제가 있습니다. 특히 Spring 모델에서는 시간이 지날수록 진동이 점점 커지는 현상이 발생할 수 있습니다.

 

<출처: 작가>

 

이 문제를 해결하기 위해 더 정확한 수치해석 방법인 룽게-쿠타(Runge-Kutta) 방법을 사용할 수 있습니다. 이 방법은 더 복잡하지만 훨씬 안정적인 결과를 제공합니다.

 

function rungeKuttaStep(state, dt) {
  const x = state.position;
  const v = state.velocity;
  
  // Spring 방정식: a = -w^2 * x
  function acceleration(pos, vel) {
    return -stiffness * pos / mass; // -w^2 * x
  }
  
  // k1: 현재 상태에서의 도함수
  const k1 = {
    v: v,
    a: acceleration(x, v)
  };
  
  // k2: 중간점(dt/2)에서의 도함수 첫 근사
  const k2 = {
    v: v + (dt * k1.a) / 2,
    a: acceleration(
      x + (dt * k1.v) / 2,
      v + (dt * k1.a) / 2
    )
  };
  
  // k3: 중간점(dt/2)에서의 도함수 두번째 근사
  const k3 = {
    v: v + (dt * k2.a) / 2,
    a: acceleration(
      x + (dt * k2.v) / 2,
      v + (dt * k2.a) / 2
    )
  };
  
  // k4: 끝점(dt)에서의 도함수
  const k4 = {
    v: v + dt * k3.a,
    a: acceleration(
      x + k3.v * dt,
      v + dt * k3.a
    )
  };
  
  // 가중 평균으로 최종 변화량 계산
  return {
    position: x + (dt * (k1.v + 2 * k2.v + 2 * k3.v + k4.v)) / 6,
    velocity: v + (dt * (k1.a + 2 * k2.a + 2 * k3.a + k4.a)) / 6,
    time: state.time + dt
  };
}

 

룽게-쿠타 방법은 각 시간 단계에서 4번의 도함수 평가를 통해 더 정확한 다음 상태를 추정합니다. 시간 단계 사이의 중간 지점들을 활용하여 가중 평균을 계산하는 방식이죠.

 

이 방법은 오일러 방법과 달리 에너지 보존 법칙을 더 잘 유지하기 때문에, 오래 시뮬레이션을 돌려도 진폭이 부자연스럽게 증가하거나 감소하는 현상이 크게 줄어듭니다. 정확한 수치해석 방법은 실제 물리계의 특성을 더 잘 반영하기 때문에, 더 자연스러운 애니메이션 결과를 만들어냅니다.

 

궤도 운동하는 행성

이런 수치해석 원리를 응용해서 행성의 움직임을 표현할 수 있습니다. 운동방정식으로 접근하면, 현재의 속도와 위치를 보존하면서 자연스러운 애니메이션 변화를 구현할 수 있습니다.

 

실제 세계에서는 속도가 갑자기 변하지 않습니다. 속도가 시간에 따라 변화하는 이유는 힘이 작용하기 때문이며, 속도가 갑자기 변한다면 그것은 힘이 무한대라는 의미입니다. 물리 세계에서 무한대의 힘이 작용하는 상황은 존재하지 않죠.

 

이러한 물리적 직관이 우리의 무의식에 박혀있기 때문에, CSS처럼 시간 기반으로 애니메이션이 동적으로 바뀔 때 어색함을 느끼는 것입니다. 운동 방정식에 기반한 접근법은 이러한 부자연스러움을 해결하고, 사용자의 무의식적 기대에 부합하는 자연스러운 움직임을 만들어냅니다.

 

<출처: 작가>

 

 

운동방정식 기반 라이브러리 직접 써보자

운동방정식 기반의 자바스크립트 애니메이션 라이브러리를 활용하면 동적으로 변화하는 애니메이션을 자연스럽게 구현할 수 있습니다. 물리 법칙을 코드로 녹여낸 이런 라이브러리들은 개발자가 복잡한 수학적 계산을 직접 구현할 필요 없이 자연스러운 인터랙션을 만들 수 있게 해줍니다.

 

React Spring으로 구현하는 동적 인터랙션

React Spring은 React 애플리케이션에서 물리 기반 애니메이션을 쉽게 구현할 수 있게 해주는 라이브러리입니다. 적은 코드로 자연스러운 스프링 애니메이션을 구현할 수 있으며, 무엇보다 실시간으로 목푯값이 변해도 부드럽게 전환됩니다.

 

<출처: 작가>

 

내 라이브러리를 만들 때도 적용했다!

Flitter라는 렌더링 엔진 라이브러리에서 AnimatedAlign이라는 위젯을 개발했습니다. 이 위젯은 물체의 위치를 정렬이라는 속성으로 변경할 때 자동으로 트랜지션 애니메이션을 부여합니다. 사용자가 화면을 조작하면서 동적으로 바뀌는 위치에 대해서도 자연스럽게 속도와 위치를 기록해 적용됩니다.

 

<출처: 작가>

 

처음에는 운동 애니메이션을 직접 구현할지 고민했습니다. 하지만 특정 프레임워크 라이브러리라 할지라도, 코어 부분은 JavaScript로 따로 모듈화하는 이른바 "agnostic"(독립적) 구현체가 있을 거라고 생각했습니다. framer-motion과 같은 라이브러리를 타고 들어가 살펴보니, popmotion이라는 바닐라 자바스크립트 라이브러리를 발견했습니다. 이 라이브러리가 프레임워크에 독립적으로 구현되어 있어서 그대로 활용할 수 있었습니다.

 

 

왜 프론트엔드를 선택하셨나요?

프론트엔드 개발을 시작한 이유는 저마다 다르겠지만, 많은 개발자가 사용자와 직접 소통하는 인터페이스를 만드는 즐거움을 언급합니다. 저 역시 그런 이유로 이 분야에 빠져들었습니다. 앱을 사용하다 보면, 자연스럽게 움직이는 UI와 그렇지 않은 UI의 차이를 느끼게 됩니다. 메뉴가 너무 기계적으로 열리면 어색하고, 적절한 탄력으로 부드럽게 움직이면 품질이 높게 느껴지죠. 이런 차이가 사용자 경험의 질을 결정합니다.

 

물리 기반 애니메이션의 장점을 알게 된 후로는 다양한 라이브러리들을 탐색하게 되었습니다. 수려한 라이브러리들이 복잡한 물리 계산을 감추고, 직관적인 인터페이스를 제공해 우리는 사용만 하면 됩니다. 복잡한 수학적 개념을 이해하지 않아도 자연스러운 움직임을 쉽게 적용할 수 있죠.

 

이제는 프로젝트를 할 때마다 자연스러운 애니메이션을 어떻게 적용할지 고민하게 됩니다. 사용자는 애니메이션의 기술적 구현 방식을 의식하지 못합니다. 단지 "사용하기 좋다" 또는 "뭔가 부족하다"라고 느낄 뿐이죠. 하지만 그 미묘한 차이가 결국 제품의 품질과 사용자 만족도를 좌우합니다.

 

여러분이 알고 있는 애니메이션 라이브러리는 어떤 것이 있나요? 

 

©️요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.

에디터가 직접 고른 실무 인사이트 매주 목요일에 만나요.
newsletter_profile0명 뉴스레터 구독 중