본문 바로가기

개발자 강화/프론트엔드

[개발] framer motion (motion.div) 사용 경험

 

입사 후 framer motion과 싸우다가 대학 개발 동아리방에 남겼던 혼잣말

지금도 막 잘 아는 건 아니지만 7개월의 경험 속에서 나오는 뭔가뭔가...를 정리해 봄

 

framer motion이란 무엇인가?

https://motion.dev/docs/react

 

Motion for React — Install & first React animation | Motion

Learn Motion for React animation library: Install, animate HTML and SVG elements with spring animations, staggering effects. Complete guide with examples.

motion.dev

 

React(혹은 JavaScript) 기반 ui에서 요소가 움직이고 반응하는 애니메이션을 쉽게 만들어주는 라이브러리

 

ui 요소가 나타나고, 사라지고, 움직이고, 반응하는 동작 구현

- css로만 처리하기에는 상태 변화와 애니메이션을 연결하기 복잡함

 

framer motion 애니메이션을 선언적으로 설정할 수 있게 도움

- 컴포넌트의 상태 변화(state/props), 사용자 제스처(hover/tap/drag), 레이아웃 변화(layout shift) 등

 

'언제 어떻게 바뀌면 이렇게 움직여라'를 짧고 직관적인 코드로 표현할 수 있음

 


 

famer motion을 선택한 이유

framer motion 역시 rive와 마찬가지로 내가 입사했을 때는 이미 도입이 되어있었음

그래서 히스토리를 찾아봄

 

디자이너님의 ui 리뉴얼에 대한 문서를 발견함

리라이팅을 하면서 ui 개선도 필요했는데, 이때 고려가 필요한 항목은 아래와 같았음

- 통일성 / 확장성

- 터치감 살리기 (framer motion/animate.css/animista 중에서 고민한 흔적)

- 모션 json으로 교체

 

아마 모션 json으로 교체는 기존에 gif로 되어있던 애니메이션에 대한 고민이었을 것임 (단순 재생)

gif는 lottie에 비해 용량이 큰 편이라 최적화를 계속 고민하고 계셨던 것 같음

 

터치감 살리기는 유저 인터렉션에 관련된 영역을 말하는 것 같음

animate.css와 animista는 미리 정의된 css 애니메이션을 불러와 재생하는 방식

한 번 실행되는 요소(fade in/out 같은 동작)에는 적합하지만,

state 변화나 사용자 행동에 따라 다르게 움직이는 인터렉션에는 제한됨

 

아마 이런 이유 때문에 framer motion을 선택한 것으로 추정...

 


 

farmer motion을 사용하는 방법?

 

1. framer motion을 설치한다

본인 레포에서 사용하는 패키지 관리 도구 명령어로 'framer-motion'을 설치

 

2. motion 컴포넌트로 감싸기

framer motion의 핵심 기능

기존의 html 태그(div, button 등)에 모션 기능을 추가할 때 앞에 'motion.'을 추가

 

자세한 예시는 아래 예시 코드 섹션에서 다루겠음

 

3. 애니메이션 속성 지정하기

animate, inital, exit, transition 등의 props를 사용

요소가 어떻게 나타나고, 움직이고, 사라질지 정의함

 

4. variants 상태로 관리

variants로 상태별 모션을 미리 정의해두면 유지보수가 쉬움

'hidden', 'visible', 'exit' 같은 상태를 만들어두고, 상태 전환만 바꾸면 됨

 

5. 컴포넌트 등장/퇴장 제어 - AnimatePresence

React 컴포넌트가 DOM에서 사라질 때(exit)도 애니메이션이 실행되도록 도와주는 컴포넌트

 

React는 기본적으로 컴포넌트를 즉시 unmount 하므로, 사라질 때 애니메이션이 실행되지 않음

AnimatePresence는 unmount 직전에 exit 애니메이션을 먼저 실행시켜준 뒤 제거함

 

나도 초반에 종종 했던 실수이지만 AnimatePresence로 감싸지 않고 exit만 쓰면 fade out 애니메이션이 동작하지 않음

여러 개의 motion 컴포넌트가 같은 조건부 렌더링에 묶여있으면 key를 지정하지 않아 트랜지션이 꼬일 수 있음

 

6. 인터랙션 추가하기

사용자 행동(hover, tag, drag 등)에 따라 반응하는 모션을 줌

터치감 있는 ui를 만들 때 유용함

 

7. 고급 기능

useMotionValue, useTransform, layout 등

상태변화나 위치 이동에도 자연스러운 모션을 구현할 수 있음

인터랙티브 ux를 만들 수 있는 기능

 


framer motion 실제 케이스

framer motion을 직접 사용하면서 딥다이브 했던 내용 기록

 

modal을 노출 시키는 경우 - stiffness, damping에 대한 분석

이미 만들어진 컴포넌트를 수정해서 modal이 닫힐 때 화면에서 '좀 더 빠르게' 사라졌으면 좋겠다는 요구를 받음

 

spring(탄성) 타입의 트랜지션에서 stifness와 damping을 사용해 물리적 동작을 제어할 수 있음

 <AnimatePresence>
 // 중략
  <motion.div
    animate='visible'
    initial='hidden'
    role='dialog'
    transition={{
      damping: 40,
      stiffness: 400,
      type: 'spring',
    }}
    variants={{
      hidden: { opacity: 0 },
      visible: { opacity: 1 },
    }}
    exit='hidden'
  >
// 중략
  </motion.div>
// 중략
</AnimatePresence>

 

코드에 대한 설명

variant로 hidden과 visible이라는 이름의 상태를 정의함

- visible: opacity 0->1 (fade in, 서서히 나타남)

- hidden: opacity 1->0 (fade out, 서서히 사라짐)

 

모달이 처음 나타날 때 안보이는 상태에서 서서히 나타나도록 구현

  initial='hidden': 컴포넌트가 처음 마운트 될 때 hidden variant 상태로 시작

  animate='visible': 마운트 후 visible variant 상태로 애니메이션

 

모달이 사라질 때 서서히 사라지도록 구현

exit='hidden' & motion.div를 AnimatePresence로 감쌈

- 언마운트 될 때 다시 hidden variant 상태로 페이드 아웃

 

  stiffness (강성도) damping (감쇠력)
정의 - 스프링의 탄성 강도를 조절함
- 값이 클수록 더 빠르게 반응하고, 강하게 튕김
- 낮으면 느슨하게 움직이고 반응이 느려짐
- 얼마나 빠르게 진동을 멈출지 
- 값이 작으면 더 오래 흔들리고 튕김이 많고, 크면 빠르게 멈춤
작을 때 효과  느리고 부드럽게 이동 많이 튕김 (진동 큼)
클 때 효과 빠르고 강하게 튀는  금방 멈춤 (진동 작음)

 

요구사항: 사용자가 '빠르게' 느끼게 해주세요

- 빠르게 동작 완료 지점에 도달하려면: 스프링 탄성을 높임 (stiffness 값을 키움)

- 덜 튕기려면: 감쇠값을 키움 (damping 값을 키움)

 

pm의 요구사항은 '서서히 나타나고 사라지는 모달이 확! 확! 나타나고 사라졌으면 좋겠다!'라는 요구였음

그럼 stiffness 값을 조정하는게 맞다고 판단

 

따라서 damping 값은 유지한채로 stiffness 값을 950으로 올려서 마무리했다.

(여러 값으로 테스트를 해서 pm님께 확인을 요청한 결과 만족하는 결과를 내기 위해서는 sitffness 값 조정이 필요함을 알게 됨)

 

누를 때마다 띠용~거리는 버튼 만들기 - 바운스 애니메이션

<motion.button
  id={id}
  type='button'
  className={className}
  onClick={disabled ? onDisabledClick : onClick}
  whileTap={disabled ? undefined : { scale: 0.96 }}
  animate={isBounce ? 'bounce' : 'normal'}
  variants={{
    bounce: {
      scale: [1, 1.08, 1],
      transition: {
        duration: 0.6,
        repeat: Infinity,
        ease: 'easeInOut',
      },
    },
    normal: { 
      scale: 1,
      transition: { duration: 0.2 }
    },
  }}
  style={{
    willChange: isBounce ? 'transform' : 'auto',
  }}
>

 

코드에 대한 설명

 

1. varaint 설정

- bounce: scale 1 -> 1.08 ->1 / 원래 크기 -> 8% 확대 -> 원래 크기로 복귀

- normal: scale 1 / 정지 상태(누르지 않았을 때), 크기 변화 없음

 

2. bounce animation 적용 - 조건부로 바운스 효과 활성화

- animate={isBounce ? 'bounce' : 'normal'} / isBounce가 true일 때만 바운스 상태로 애니메이션

- transition.repeat: Infinity / 무한반복으로 계속 튕김 (한번 튕기고 끝나는게 아니라 duration 동안 띠용용 하고 반복)

- transition.duration: 0.6 / 0.6초마다 한 사이클 완성

 

3. 클릭 피드백 구현

- whileTap={{scale: 0.96}} / 버튼을 누르는 순간 96% 크기로 축소

- disabled 일 때는 효과 없음 (undefined 처리)

 

4. 불필요한 리렌더링 방지 - 성능 최적화

- willChange: isBounce ?  'transform' : 'auto' / 바운스 중일 때만 브라우저에게 transform 변경 예고

- 애니메이션이 없을 때는 최적화 해제로 메모리 절약

 

ui 리뉴얼에서 유저의 '클릭감'을 살리기 위해 터치 이벤트에 대한 반응이 필요했고, 버튼에 바운스를 넣게 됨

그 외에도 클릭할 때마다 'haptic'을 넣어 진동 피드백도 같이 느껴지도록 함

 

두 개의 요소를 움직여 동작 만들기

방망이와 절구 움직이기

// 방망이 애니메이션
  <motion.div
    initial={{ rotate: 20 }}
    animate={
      isStickAnimating
        ? {
            rotate: [20, -35, 20],
            scale: [1, 1.1, 1],
          }
        : { rotate: 20, scale: 1 }
    }
    transition={{
      duration: 0.2,
      ease: 'easeInOut',
      times: [0, 0.5, 1],
    }}
  >
    <StickIcon />
  </motion.div>

 

  

코드 설명
  1. 애니메이션 재실행 트리거
    - animate prop 변경 감지

    - isStickAnimating 상태가 바뀔 때마다 framer-motion이 자동으로 새 애니메이션 시작
    - 컴포넌트 재마운트 없이 효율적으로 반복 실행
  2. 막대 휘두르기 동작 구현
    - rotate: [20, -35, 20] / 기본 각도(20°) → 휘두르기(-35°) → 원위치(20°)
    - scale: [1, 1.1, 1] / 타격 순간 크기 확대로 임팩트 강조
  3. 타이밍 제어
    - duration: 0.2 / 빠른 휘두르기 동작 (0.2초)
    - times: [0, 0.5, 1] / 각 키프레임 타이밍 명시 (시작-중간-끝)

 

 // 절구 애니메이션
  <motion.div
    animate={
      isTteokBouncing
        ? {
            scale: [1, 1.2, 0.9, 1.1, 1],  // 찌그러졌다 튀어오르는 효과
            rotate: [0, -3, 3, -2, 0],      // 좌우 흔들림
          }
        : {}
    }
    transition={{
      duration: 0.4,  // 막대보다 0.2초 길게
      ease: 'easeInOut',
    }}
  >
    <TteokIcon />
  </motion.div>

 

코드 설명
  1. 물리적 타격감 표현
    - scale: [1, 1.2, 0.9, 1.1, 1] / 맞는 순간 커지고(1.2) → 찌그러지고(0.9) → 튀어오르고(1.1) → 안정화(1)
    - 실제 절구를 칠 때의 탄성을 5단계로 표현
  2. 좌우 흔들림 효과
    - rotate: [0, -3, 3, -2, 0] / 타격 충격으로 좌우로 흔들리다 안정화
    - 미세한 각도 변화로 자연스러움 추가
  3. 막대보다 긴 애니메이션 시간
    - duration: 0.4 / 막대(0.2초)보다 2배 길게
    - 막대가 절구를 친 후 절구가 반응하는 시간 확보


구현 포인트: 방망이 -> 절구 순차적으로 애니메이션 트리거

  1. 애니메이션 흐름
  클릭 → 막대 휘두르기(0.2초) → 떡 찌그러짐(0.4초)

  2. 순차적 실행
  - 부모 컴포넌트에서 isStickAnimating → isTteokBouncing 순차 제어
  - 막대가 떡에 닿는 타이밍을 고려한 딜레이 설정

  3. 자연스러운 물리감
  - 떡의 5단계 scale 변화로 탄성 표현
  - 막대와 떡의 타이밍 차이로 인과관계 명확화


추가적인 고민

당시 구현에서는 animationKey 값을 부모 컴포넌트에서 증가시키며 애니메이션이 재실행되도록 만들었음

 

  1. 초기값: animationKey = 0 (49번째 줄)
  2. 클릭할 때마다: animationKey가 1씩 증가 (0 → 1 → 2 → 3...)
  3. 애니메이션 재실행: React의 key prop이 변경되면 컴포넌트가 재마운트되면서 애니메이션이 처음부터 다시 실행됨

 

그런데 이번 블로그를 쓰면서 생각해보니, 재마운트 방식은 일반적으로 좋은 패턴이 아님! (성능 면에서 마이너스)

클릭할 때마다 기존 컴포넌트가 언마운트되고, 새 컴포넌트가 마운트되고, DOM이 재생성되고, 이벤트 리스너가 재등록 됨

 

framer-motion은 animate prop 변경만으로도 애니메이션을 재실행하므로 불필요한 성능 비용을 피할 수 있음

지금은 코드를 날려버려서 key를 지웠을 때 정상동작하는지 테스트를 할 수 없어서, 다음에 유사한 동작 구현 시 테스트해볼 예정.

 


 

framer motion을 좀 더 쉽게 쓰는 법 (feat. figma mcp)

1. cursor mcp에 figma를 연결한다

figma

- 좌상단 내 프로필을 클릭한다

- settings를 클릭한다

- security 탭을 클릭한다

- personal access tokens 섹션에서 'Generate new token'을 클릭한다

- read 관련 권한만 수락해서(혹시 cursor가 figma를 수정한다면 디자이너님께서 슬퍼할 것) 토큰을 만든다

 

cursor

- cursor 우상단에 톱니바퀴 모양을 누른다

- 좌측 메뉴에서 Tools & Integration을 클릭한다

- '+ New MCP Server'를 클릭한다

{
  "mcpServers": {
    "Framelink Figma MCP": {
      "command": "npx",
      "args": [
        "-y",
        "figma-developer-mcp",
        "--figma-api-key=본인 키 붙여넣기",
        "--stdio"
      ]
    }
  }
}

- 위 코드에서 본인 키를 figma에서 방금 생성한 키로 붙여넣는다

 

2. 기본적인 ui를 직접 구현한다

예시) 두 개의 요소를 움직여 동작 만들기 - 이 상황을 가정해보자

- 배경 이미지 삽입

- 절구 이미지 삽입 후 적절한 위치에 배치

- 막대 이미지 삽입 후 적절한 위치에 배치

- 버튼 컴포넌트 import 후 적절한 위치에 배치

 

3. figma 링크를 cursor에 주입한다

figma에서 내가 구현하고자 하는 요소를 copy link to selection로 복사하고, cusor 입력창에 붙여넣는다

 

4. cursor에 추가 커맨드를 입력한다

예시) 두 개의 요소를 움직여 동작 만들기 - 이 상황을 가정해보자

(figma 특정 요소 link)
이런 ui를 만들건데, 내가 기본 틀은 미리 작성해놨어.
이제 버튼을 누르면 stick(막대)이 tteok(절구)를 칠거야.
막대가 절구를 때리고 나서, 절구가 띠용~하는 효과(바운스)가 있었으면 좋겠어 (motion.div 사용)

 

5. 4번의 결과물을 바탕으로 직접 수정

- 수정 -> 디자이너 피드백 -> 수정 -> 디자이너 피드백 -> ... -> 완성!

 

이렇게 하면 완전 처음부터 다 구현하는 것보다 시간을 많이 줄일 수 있다

rive에 비해 framer motion은 cursor가 잘 만들어내는 편이다

 

타이핑 애니메이션, 순차적으로 요소가 나타나며 바운싱 효과도 동시에 실행하는 애니메이션 등등

프롬프트 만으로도 꽤 완성도 높은 결과물을 뱉어냄

 

개인적으로 framer motion 관련 코드 생성은 claude code보다 cursor에서 괜찮은 결과를 봤음

 

그러나, 프롬프트로 만들기만 하고 맥락을 직접 파악하지 않으면,

여느 ai가 짠 코드가 그렇듯 유지보수가 불가능한 코드가 됨

 

cursor 결과물을 뱉어내면 꼭 맥락 파악을 해서 수정할 수 있는 코드로 만들기!

 


또 실무 하면서 얻는 인사이트가 있다면 후속글로 가져오겠습니당~

 

2025.10.21. 00:15

 

728x90