콘텐츠 대표 이미지 - 타입스크립트 애니메이션과 Framer Motion 타입 정의 완전 정복 가이드
TypeScript × Framer Motion

타입스크립트 애니메이션과 Framer Motion 타입 정의 완전 정복 가이드

타입 안전한 애니메이션 코드, 이제 진짜 제대로 써보자 🎯

야 근데 Framer Motion 쓸 때 타입 에러 왜 이렇게 많이 나냐 ㅠㅠ

ㅋㅋㅋ 맞아 처음엔 진짜 헷갈리지... 근데 타입 정의 제대로 알면 오히려 개꿀이야
진짜? 어떻게?

지금부터 알려줄게 ㅋㅋ 따라와봐

TypeScript로 React 프로젝트 하다 보면 Framer Motion 쓸 일이 진짜 많잖아요. 근데 막상 쓰려고 하면 타입 에러가 줄줄이 나오면서 멘탈이 탈탈 털리는 경험... 다들 한 번씩은 해봤을 거예요 ㅋㅋㅋ

이 글에서는 Framer Motion의 타입 시스템을 완전히 이해하고, TypeScript와 함께 쓸 때 진짜 유용한 패턴들을 싹 다 정리해드릴게요. 단순히 "이렇게 쓰면 돼요" 수준이 아니라, 왜 이렇게 설계됐는지까지 파고들 거예요. 🔥


Framer Motion + TypeScript 구조도 Framer Motion MotionProps 애니메이션 속성 타입 Variants 상태 기반 애니메이션 AnimationControls 명령형 제어 타입 TargetAndTransition 목표 상태 타입 useAnimation 훅 기반 제어 useMotionValue 반응형 값 추적 TypeScript와 함께 타입 안전한 애니메이션 구현

🎬 1. Framer Motion이 뭔지부터 빠르게 짚고 가자

Framer Motion은 React 전용 애니메이션 라이브러리예요. 근데 그냥 평범한 라이브러리가 아니라, 선언적(declarative) 방식으로 애니메이션을 작성할 수 있게 해주는 진짜 강력한 도구예요.

TypeScript 지원도 처음부터 빌트인으로 들어가 있어서, @types/framer-motion 같은 별도 패키지 없이도 바로 타입 추론이 됩니다. 이게 진짜 큰 장점이에요 ㅋㅋ

📦 설치 방법 npm install framer-motion
또는
yarn add framer-motion

TypeScript 프로젝트라면 추가 타입 패키지 없이 바로 사용 가능! 🎉

Framer Motion의 핵심은 motion 컴포넌트예요. HTML 태그 앞에 motion.을 붙이면 그 태그가 애니메이션 가능한 컴포넌트로 변신합니다.

import { motion } from 'framer-motion';

// 기본 사용법
const MyComponent = () => {
  return (
    <motion.div
      initial={{ opacity: 0, y: -20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5 }}
    >
      안녕하세요! 나는 애니메이션 div야 ✨
    </motion.div>
  );
};

여기서 initial, animate, transition이 모두 TypeScript로 타입이 정의되어 있어요. 이 타입들을 제대로 이해하면 자동완성도 잘 되고, 실수도 줄어들고, 코드 퀄리티가 확 올라가요 ㄹㅇ.


🔍 2. Framer Motion 핵심 타입 정의 완전 분석
📌 MotionProps - 모든 motion 컴포넌트의 기본 타입

MotionProps는 Framer Motion에서 가장 중요한 타입이에요. 모든 motion.* 컴포넌트가 이 타입을 기반으로 props를 받아요.

import { MotionProps } from 'framer-motion';

// MotionProps의 주요 속성들 (실제 타입 정의 기반)
interface MotionProps {
  initial?: boolean | Target | VariantLabels;
  animate?: AnimationControls | TargetAndTransition | VariantLabels | boolean;
  exit?: TargetAndTransition | VariantLabels;
  transition?: Transition;
  variants?: Variants;
  whileHover?: VariantLabels | TargetAndTransition;
  whileTap?: VariantLabels | TargetAndTransition;
  whileFocus?: VariantLabels | TargetAndTransition;
  whileDrag?: VariantLabels | TargetAndTransition;
  whileInView?: VariantLabels | TargetAndTransition;
  // ... 더 많은 속성들
}
💡 꿀팁! MotionProps를 직접 import해서 커스텀 컴포넌트에 타입을 붙여줄 수 있어요. 이렇게 하면 motion 컴포넌트를 래핑할 때 타입 안전성을 유지할 수 있답니다!
🎯 TargetAndTransition - 목표 상태 타입

TargetAndTransition은 애니메이션의 목표 상태를 정의하는 타입이에요. animate prop에 객체를 직접 넣을 때 이 타입이 사용됩니다.

import { TargetAndTransition } from 'framer-motion';

// TargetAndTransition 타입 활용
const animateState: TargetAndTransition = {
  opacity: 1,
  x: 100,
  scale: 1.2,
  rotate: 45,
  backgroundColor: '#667eea',
  transition: {
    duration: 0.5,
    ease: 'easeInOut',
  },
};

// 컴포넌트에서 사용
const MyBox = () => (
  <motion.div animate={animateState}>
    박스야 움직여라! 🚀
  </motion.div>
);
🎨 Variants - 상태 기반 애니메이션의 핵심

Variants 타입은 Framer Motion에서 진짜 강력한 기능 중 하나예요. 여러 애니메이션 상태를 이름으로 정의하고, 그 이름으로 상태를 전환할 수 있게 해줘요.

import { Variants } from 'framer-motion';

// Variants 타입 정의
const boxVariants: Variants = {
  hidden: {
    opacity: 0,
    scale: 0.8,
    y: -30,
  },
  visible: {
    opacity: 1,
    scale: 1,
    y: 0,
    transition: {
      duration: 0.6,
      ease: 'easeOut',
    },
  },
  hover: {
    scale: 1.05,
    boxShadow: '0px 10px 30px rgba(102, 126, 234, 0.4)',
    transition: {
      duration: 0.2,
    },
  },
  tap: {
    scale: 0.95,
  },
};

// 사용 예시
const AnimatedBox = () => (
  <motion.div
    variants={boxVariants}
    initial="hidden"
    animate="visible"
    whileHover="hover"
    whileTap="tap"
  >
    나는 상태 기반 애니메이션 박스 🎭
  </motion.div>
);
⚠️ 주의! Variants 타입에서 각 상태의 값은 TargetAndTransition 타입이에요. 그래서 CSS 속성 이름을 camelCase로 써야 하고, 일부 CSS 속성은 지원 안 될 수도 있어요. 예를 들어 background-color가 아니라 backgroundColor로 써야 해요!

Variants 상태 전환 흐름도 hidden opacity: 0 scale: 0.8 visible opacity: 1 scale: 1 hover scale: 1.05 그림자 추가 tap scale: 0.95 눌림 효과 initial → animate whileHover whileTap exit opacity: 0 컴포넌트 제거 시 exit 기본 전환 인터랙션 전환 제거 전환

⏱️ 3. Transition 타입 - 애니메이션의 타이밍을 지배하자

애니메이션에서 "어떻게 움직이냐"를 결정하는 게 바로 Transition 타입이에요. 이게 진짜 복잡한데, 제대로 알면 엄청 다양한 효과를 낼 수 있어요 ㅋㅋ

import { Transition } from 'framer-motion';

// Transition 타입의 주요 속성들
const myTransition: Transition = {
  // 기본 타이밍
  duration: 0.5,        // 애니메이션 지속 시간 (초)
  delay: 0.2,           // 시작 지연 시간 (초)
  
  // 이징 함수
  ease: 'easeInOut',    // 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | number[]
  
  // 반복 설정
  repeat: Infinity,     // 반복 횟수 (Infinity = 무한)
  repeatType: 'loop',   // 'loop' | 'reverse' | 'mirror'
  repeatDelay: 0.5,     // 반복 사이 지연
};

// 스프링 애니메이션 (물리 기반)
const springTransition: Transition = {
  type: 'spring',
  stiffness: 300,   // 스프링 강도 (높을수록 빠름)
  damping: 20,      // 감쇠 (낮을수록 더 많이 튕김)
  mass: 1,          // 질량 (높을수록 느림)
  bounce: 0.25,     // 바운스 정도
};

// 관성 애니메이션 (드래그 후 자연스러운 감속)
const inertiaTransition: Transition = {
  type: 'inertia',
  velocity: 200,    // 초기 속도
  power: 0.8,       // 감속 강도
  timeConstant: 700, // 감속 시간 상수
};
💡 스프링 vs 트윈 vs 관성 tween: 기본값. duration 기반. 정확한 타이밍 제어 필요할 때.
spring: 물리 기반. 자연스럽고 탄력 있는 움직임. UI 인터랙션에 최고.
inertia: 드래그 후 자연스럽게 멈추는 효과. 스크롤이나 드래그 UI에 사용.
🔢 Orchestration - 자식 요소 애니메이션 조율

부모 컴포넌트에서 자식들의 애니메이션을 순서대로 실행하고 싶을 때 쓰는 속성들이에요. 이게 진짜 꿀기능이에요 ㄹㅇ!

import { Variants } from 'framer-motion';

// 부모 variants - 자식 애니메이션 조율
const containerVariants: Variants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      // 자식 요소들의 애니메이션 시작 시점 제어
      staggerChildren: 0.1,    // 각 자식 사이 지연 시간
      delayChildren: 0.3,      // 첫 번째 자식 시작 전 지연
      staggerDirection: 1,     // 1: 순방향, -1: 역방향
      when: 'beforeChildren',  // 'beforeChildren' | 'afterChildren'
    },
  },
};

// 자식 variants
const itemVariants: Variants = {
  hidden: { opacity: 0, y: 20 },
  visible: {
    opacity: 1,
    y: 0,
    transition: { duration: 0.4 },
  },
};

// 실제 사용
const StaggerList = ({ items }: { items: string[] }) => (
  <motion.ul
    variants={containerVariants}
    initial="hidden"
    animate="visible"
  >
    {items.map((item, i) => (
      <motion.li key={i} variants={itemVariants}>
        {item}
      </motion.li>
    ))}
  </motion.ul>
);

🎮 4. useAnimation 훅 - 명령형으로 애니메이션 제어하기
근데 버튼 클릭하면 애니메이션 실행하고 싶은데, 그건 어떻게 해요?

오 그거 useAnimation 쓰면 돼! 명령형으로 제어할 수 있거든 ㅋㅋ

useAnimation 훅은 AnimationControls 타입의 객체를 반환해요. 이걸 사용하면 이벤트나 조건에 따라 프로그래밍 방식으로 애니메이션을 시작/정지/재시작할 수 있어요.

import { motion, useAnimation, AnimationControls } from 'framer-motion';
import { useEffect } from 'react';

const ControlledAnimation = () => {
  // AnimationControls 타입 반환
  const controls: AnimationControls = useAnimation();

  const handleStart = async () => {
    // start() 메서드로 애니메이션 시작
    await controls.start({
      x: 200,
      opacity: 1,
      transition: { duration: 0.5 },
    });
    
    // 첫 번째 애니메이션 완료 후 두 번째 실행
    await controls.start({
      rotate: 360,
      transition: { duration: 0.8, ease: 'easeInOut' },
    });
  };

  const handleReset = () => {
    // 초기 상태로 즉시 리셋
    controls.set({ x: 0, opacity: 0.5, rotate: 0 });
  };

  const handleStop = () => {
    // 현재 애니메이션 중지
    controls.stop();
  };

  return (
    <div>
      <motion.div
        animate={controls}
        initial={{ x: 0, opacity: 0.5 }}
        style={{ width: 80, height: 80, background: '#667eea', borderRadius: 12 }}
      />
      <button onClick={handleStart}>시작!</button>
      <button onClick={handleReset}>리셋</button>
      <button onClick={handleStop}>정지</button>
    </div>
  );
};
✅ AnimationControls의 주요 메서드 controls.start() - 애니메이션 시작 (Promise 반환, await 가능!)
controls.stop() - 현재 애니메이션 즉시 중지
controls.set() - 애니메이션 없이 즉시 상태 변경
controls.mount() - 컴포넌트 마운트 시 애니메이션 트리거
🔄 useInView와 결합하기

스크롤 기반 애니메이션에서 진짜 많이 쓰는 패턴이에요. 요소가 뷰포트에 들어올 때 애니메이션을 실행하는 거죠!

import { motion, useAnimation } from 'framer-motion';
import { useInView } from 'framer-motion';
import { useEffect, useRef } from 'react';

const ScrollReveal = ({ children }: { children: React.ReactNode }) => {
  const controls = useAnimation();
  const ref = useRef(null);
  
  // useInView: 요소가 뷰포트에 들어왔는지 감지
  const isInView = useInView(ref, {
    once: true,    // 한 번만 실행
    margin: '-100px',  // 뷰포트 마진 조정
  });

  useEffect(() => {
    if (isInView) {
      controls.start('visible');
    }
  }, [isInView, controls]);

  return (
    <motion.div
      ref={ref}
      animate={controls}
      initial="hidden"
      variants={{
        hidden: { opacity: 0, y: 50 },
        visible: {
          opacity: 1,
          y: 0,
          transition: { duration: 0.6, ease: 'easeOut' },
        },
      }}
    >
      {children}
    </motion.div>
  );
};

📊 5. useMotionValue & useTransform - 반응형 값의 세계

이 두 훅은 Framer Motion에서 진짜 고급 기능이에요. 마우스 위치나 스크롤 위치 같은 값을 실시간으로 추적하고, 그 값을 다른 애니메이션 속성으로 변환할 수 있어요. 근데 TypeScript 타입이 좀 복잡해서 처음엔 헷갈릴 수 있어요 ㅋㅋ

import {
  motion,
  useMotionValue,
  useTransform,
  MotionValue,
  useSpring,
} from 'framer-motion';

const ParallaxCard = () => {
  // MotionValue<number> 타입
  const x: MotionValue<number> = useMotionValue(0);
  const y: MotionValue<number> = useMotionValue(0);

  // useTransform: 입력 범위를 출력 범위로 매핑
  // x 값 -150~150 → rotateY -30~30도로 변환
  const rotateY: MotionValue<number> = useTransform(x, [-150, 150], [-30, 30]);
  const rotateX: MotionValue<number> = useTransform(y, [-150, 150], [30, -30]);

  // useSpring: 스프링 물리 적용 (더 자연스러운 움직임)
  const springRotateY = useSpring(rotateY, { stiffness: 300, damping: 30 });
  const springRotateX = useSpring(rotateX, { stiffness: 300, damping: 30 });

  const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
    const rect = event.currentTarget.getBoundingClientRect();
    const centerX = rect.left + rect.width / 2;
    const centerY = rect.top + rect.height / 2;
    
    x.set(event.clientX - centerX);
    y.set(event.clientY - centerY);
  };

  const handleMouseLeave = () => {
    x.set(0);
    y.set(0);
  };

  return (
    <motion.div
      onMouseMove={handleMouseMove}
      onMouseLeave={handleMouseLeave}
      style={{
        rotateY: springRotateY,
        rotateX: springRotateX,
        transformPerspective: 1000,
      }}
    >
      마우스 올려봐! 3D 효과 ✨
    </motion.div>
  );
};
📜 useScroll - 스크롤 기반 애니메이션

스크롤 위치에 따라 애니메이션을 만들 때 쓰는 훅이에요. 진행 바나 패럴랙스 효과 만들 때 진짜 유용해요!

import {
  motion,
  useScroll,
  useTransform,
  useSpring,
  MotionValue,
} from 'framer-motion';
import { useRef } from 'react';

const ScrollProgress = () => {
  // scrollYProgress: 0~1 사이의 MotionValue<number>
  const { scrollYProgress }: { scrollYProgress: MotionValue<number> } = useScroll();
  
  // 스프링으로 부드럽게
  const scaleX = useSpring(scrollYProgress, {
    stiffness: 100,
    damping: 30,
    restDelta: 0.001,
  });

  return (
    <motion.div
      style={{
        scaleX,
        position: 'fixed',
        top: 0,
        left: 0,
        right: 0,
        height: 4,
        background: 'linear-gradient(90deg, #667eea, #764ba2)',
        transformOrigin: '0%',
      }}
    />
  );
};

// 특정 요소 기준 스크롤 추적
const ParallaxSection = () => {
  const ref = useRef(null);
  
  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ['start end', 'end start'],  // 뷰포트 진입/이탈 기준
  });

  const y = useTransform(scrollYProgress, [0, 1], [-100, 100]);
  const opacity = useTransform(scrollYProgress, [0, 0.3, 0.7, 1], [0, 1, 1, 0]);

  return (
    <div ref={ref} style={{ height: '100vh' }}>
      <motion.div style={{ y, opacity }}>
        패럴랙스 효과! 🌊
      </motion.div>
    </div>
  );
};

useMotionValue → useTransform → 스타일 적용 흐름 useMotionValue 마우스 X 위치 MotionValue<number> 입력값 useTransform [-150, 150] → [-30, 30] 변환값 useSpring 물리 기반 보간 stiffness, damping 최종값 style 적용 rotateY: springVal 실시간 렌더링 핵심 포인트 MotionValue는 React 상태(useState)와 달리 변경 시 리렌더링을 유발하지 않아요! DOM을 직접 업데이트하기 때문에 성능이 훨씬 뛰어납니다. 마우스 이동처럼 빠른 이벤트에서 특히 효과적! 🚀

🛠️ 6. 커스텀 컴포넌트에 Framer Motion 타입 적용하기

실무에서 진짜 많이 쓰는 패턴이에요. 커스텀 컴포넌트를 만들 때 Framer Motion의 타입을 제대로 활용하면 재사용성도 높아지고 타입 안전성도 확보할 수 있어요 ㅋㅋ

🎁 motion 컴포넌트 래핑하기
import { motion, MotionProps, HTMLMotionProps } from 'framer-motion';
import { forwardRef } from 'react';

// HTMLMotionProps를 활용한 커스텀 버튼 컴포넌트
// HTMLMotionProps<'button'> = HTML button 속성 + MotionProps
interface AnimatedButtonProps extends HTMLMotionProps<'button'> {
  variant?: 'primary' | 'secondary' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
}

const AnimatedButton = forwardRef<HTMLButtonElement, AnimatedButtonProps>(
  ({ variant = 'primary', size = 'md', children, ...motionProps }, ref) => {
    const baseStyle = {
      primary: { background: '#667eea', color: '#fff' },
      secondary: { background: '#ede9fe', color: '#4a3f8f' },
      ghost: { background: 'transparent', color: '#667eea' },
    }[variant];

    return (
      <motion.button
        ref={ref}
        whileHover={{ scale: 1.03 }}
        whileTap={{ scale: 0.97 }}
        transition={{ type: 'spring', stiffness: 400, damping: 25 }}
        style={baseStyle}
        {...motionProps}  // 나머지 motion props 전달
      >
        {children}
      </motion.button>
    );
  }
);

AnimatedButton.displayName = 'AnimatedButton';

// 사용 예시 - 모든 motion props 사용 가능!
const App = () => (
  <AnimatedButton
    variant="primary"
    onClick={() => console.log('클릭!')}
    animate={{ opacity: 1 }}
    initial={{ opacity: 0 }}
  >
    클릭해봐 ✨
  </AnimatedButton>
);
🎭 motion.create()로 커스텀 motion 컴포넌트 만들기

Framer Motion v11부터는 motion.create() 함수를 사용해서 커스텀 컴포넌트를 motion 컴포넌트로 만들 수 있어요. 이게 진짜 유용한 기능이에요!

import { motion } from 'framer-motion';

// 커스텀 컴포넌트
interface CardProps {
  title: string;
  description: string;
  className?: string;
}

const Card = ({ title, description, className }: CardProps) => (
  <div className={className}>
    <h3>{title}</h3>
    <p>{description}</p>
  </div>
);

// motion.create()로 motion 컴포넌트로 변환
// 이렇게 하면 Card 컴포넌트에 모든 motion props 사용 가능!
const MotionCard = motion.create(Card);

// 사용 예시
const App = () => (
  <MotionCard
    title="안녕하세요"
    description="나는 motion 카드예요"
    initial={{ opacity: 0, y: 20 }}
    animate={{ opacity: 1, y: 0 }}
    whileHover={{ scale: 1.02 }}
    transition={{ duration: 0.4 }}
  />
);
⚠️ 주의사항! motion.create()를 사용할 때 커스텀 컴포넌트는 반드시 ref를 DOM 요소로 전달해야 해요. forwardRef를 사용하거나, 내부에서 DOM 요소에 ref를 연결해야 Framer Motion이 제대로 작동합니다!

👻 7. AnimatePresence - 컴포넌트 마운트/언마운트 애니메이션

React에서 컴포넌트가 DOM에서 제거될 때 애니메이션을 주는 건 원래 진짜 어려운 작업이에요. 근데 Framer Motion의 AnimatePresence를 쓰면 이게 엄청 쉬워져요 ㄹㅇ ㅋㅋ

import { motion, AnimatePresence } from 'framer-motion';
import { useState } from 'react';

// AnimatePresenceProps 타입
interface AnimatePresenceProps {
  children: React.ReactNode;
  mode?: 'sync' | 'wait' | 'popLayout';
  initial?: boolean;
  onExitComplete?: () => void;
  custom?: any;
  presenceAffectsLayout?: boolean;
}

// 기본 사용 예시
const ToggleComponent = () => {
  const [isVisible, setIsVisible] = useState(true);

  return (
    <div>
      <button onClick={() => setIsVisible(!isVisible)}>
        {isVisible ? '숨기기' : '보이기'}
      </button>
      
      <AnimatePresence mode="wait">
        {isVisible && (
          <motion.div
            key="content"  // key 필수! AnimatePresence가 추적하는 기준
            initial={{ opacity: 0, scale: 0.9 }}
            animate={{ opacity: 1, scale: 1 }}
            exit={{ opacity: 0, scale: 0.9 }}  // 제거 시 애니메이션
            transition={{ duration: 0.3 }}
          >
            나타났다 사라졌다! 👻
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
};
🔀 mode 옵션 이해하기
mode 동작 방식 사용 케이스
sync 진입/퇴장 동시 실행 (기본값) 오버레이, 레이어 전환
wait 퇴장 완료 후 진입 시작 페이지 전환, 탭 전환
popLayout 퇴장 요소를 레이아웃에서 즉시 제거 리스트 아이템 삭제
// 페이지 전환 예시 (Next.js 스타일)
const PageTransition = ({ children, pageKey }: {
  children: React.ReactNode;
  pageKey: string;
}) => {
  const pageVariants: Variants = {
    initial: { opacity: 0, x: -20 },
    enter: { opacity: 1, x: 0 },
    exit: { opacity: 0, x: 20 },
  };

  return (
    <AnimatePresence mode="wait" onExitComplete={() => window.scrollTo(0, 0)}>
      <motion.div
        key={pageKey}
        variants={pageVariants}
        initial="initial"
        animate="enter"
        exit="exit"
        transition={{ duration: 0.4, ease: 'easeInOut' }}
      >
        {children}
      </motion.div>
    </AnimatePresence>
  );
};

🖱️ 8. 드래그 기능과 타입 정의

Framer Motion의 드래그 기능도 TypeScript 타입이 잘 정의되어 있어요. 드래그 이벤트 핸들러 타입을 제대로 알면 훨씬 편하게 쓸 수 있어요!

import {
  motion,
  PanInfo,
  DragControls,
  useDragControls,
} from 'framer-motion';

// PanInfo 타입 - 드래그 이벤트 정보
interface PanInfo {
  point: { x: number; y: number };    // 현재 포인터 위치
  delta: { x: number; y: number };    // 이전 프레임 대비 이동량
  offset: { x: number; y: number };   // 시작점 대비 총 이동량
  velocity: { x: number; y: number }; // 현재 속도
}

// 기본 드래그 컴포넌트
const DraggableCard = () => {
  const handleDragEnd = (
    event: MouseEvent | TouchEvent | PointerEvent,
    info: PanInfo
  ) => {
    console.log('드래그 종료!');
    console.log('이동 거리:', info.offset);
    console.log('속도:', info.velocity);
    
    // 특정 방향으로 던졌을 때 처리
    if (Math.abs(info.velocity.x) > 500) {
      console.log('빠르게 옆으로 던졌어요!');
    }
  };

  return (
    <motion.div
      drag                          // 모든 방향 드래그
      dragConstraints={{            // 드래그 범위 제한
        left: -100,
        right: 100,
        top: -50,
        bottom: 50,
      }}
      dragElastic={0.1}             // 경계 탄성 (0~1)
      dragMomentum={true}           // 드래그 후 관성 효과
      onDragStart={(event, info) => console.log('드래그 시작!')}
      onDrag={(event, info) => console.log('드래그 중:', info.point)}
      onDragEnd={handleDragEnd}
      whileDrag={{ scale: 1.05, cursor: 'grabbing' }}
    >
      드래그해봐! 🖱️
    </motion.div>
  );
};
🎯 useDragControls - 외부에서 드래그 제어
import { motion, useDragControls, DragControls } from 'framer-motion';

const ControlledDrag = () => {
  // DragControls 타입
  const dragControls: DragControls = useDragControls();

  const startDrag = (event: React.PointerEvent) => {
    dragControls.start(event);
  };

  return (
    <div>
      {/* 이 핸들을 잡고 드래그 */}
      <div
        onPointerDown={startDrag}
        style={{ cursor: 'grab', padding: '8px', background: '#667eea' }}
      >
        여기를 잡고 드래그 ✊
      </div>
      
      {/* 실제 드래그되는 요소 */}
      <motion.div
        drag
        dragControls={dragControls}
        dragListener={false}  // 자체 드래그 리스너 비활성화
      >
        나는 핸들로만 드래그 가능해요
      </motion.div>
    </div>
  );
};

📐 9. 레이아웃 애니메이션 - layout prop의 마법

이건 진짜 Framer Motion만의 킬러 기능이에요. layout prop 하나만 추가하면 요소의 크기나 위치가 변할 때 자동으로 애니메이션이 생겨요. CSS transition으로는 구현하기 진짜 어려운 것들이 한 줄로 해결됩니다 ㅋㅋㅋ

import { motion, LayoutGroup, Reorder } from 'framer-motion';
import { useState } from 'react';

// 기본 레이아웃 애니메이션
const ExpandableCard = () => {
  const [isExpanded, setIsExpanded] = useState(false);

  return (
    <motion.div
      layout  // 이것만 추가하면 크기 변화 시 자동 애니메이션!
      onClick={() => setIsExpanded(!isExpanded)}
      style={{
        background: '#667eea',
        borderRadius: 12,
        padding: 20,
        cursor: 'pointer',
      }}
    >
      <motion.h3 layout="position">카드 제목</motion.h3>
      {isExpanded && (
        <motion.p
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
        >
          확장된 내용이에요! 클릭하면 접혀요 ✨
        </motion.p>
      )}
    </motion.div>
  );
};

// Reorder - 드래그로 순서 변경
const ReorderList = () => {
  const [items, setItems] = useState(['아이템 1', '아이템 2', '아이템 3', '아이템 4']);

  return (
    <Reorder.Group
      axis="y"
      values={items}
      onReorder={setItems}
    >
      {items.map((item) => (
        <Reorder.Item key={item} value={item}>
          {item} ↕️
        </Reorder.Item>
      ))}
    </Reorder.Group>
  );
};
💡 layout prop 값들 layout={true} - 위치와 크기 모두 애니메이션
layout="position" - 위치만 애니메이션 (크기 변화 무시)
layout="size" - 크기만 애니메이션 (위치 변화 무시)
layout="preserve-aspect" - 비율 유지하며 크기 애니메이션

🚀 10. 실전에서 바로 쓰는 TypeScript + Framer Motion 패턴

자, 이제 실제로 프로젝트에서 바로 쓸 수 있는 패턴들을 정리해볼게요. 재능넷 같은 플랫폼에서 UI를 만들 때도 이런 패턴들이 진짜 많이 쓰여요 ㅋㅋ

🎪 패턴 1: 타입 안전한 애니메이션 훅 만들기
import {
  useAnimation,
  AnimationControls,
  Variants,
  TargetAndTransition,
} from 'framer-motion';
import { useEffect } from 'react';

// 재사용 가능한 애니메이션 훅
interface UseAnimateOnMountOptions {
  delay?: number;
  duration?: number;
  from?: TargetAndTransition;
  to?: TargetAndTransition;
}

const useAnimateOnMount = ({
  delay = 0,
  duration = 0.5,
  from = { opacity: 0, y: 20 },
  to = { opacity: 1, y: 0 },
}: UseAnimateOnMountOptions = {}) => {
  const controls: AnimationControls = useAnimation();

  useEffect(() => {
    const timer = setTimeout(() => {
      controls.start({
        ...to,
        transition: { duration },
      });
    }, delay * 1000);

    return () => clearTimeout(timer);
  }, [controls, delay, duration]);

  return {
    controls,
    initial: from,
    animate: controls,
  };
};

// 사용 예시
const FadeInCard = ({ delay }: { delay?: number }) => {
  const { controls, initial, animate } = useAnimateOnMount({
    delay,
    from: { opacity: 0, scale: 0.95 },
    to: { opacity: 1, scale: 1 },
  });

  return (
    <motion.div
      initial={initial}
      animate={animate}
    >
      나는 마운트될 때 페이드인 돼요 ✨
    </motion.div>
  );
};
🎯 패턴 2: 제네릭을 활용한 애니메이션 래퍼
import { motion, HTMLMotionProps, Variants } from 'framer-motion';
import { ElementType, ComponentPropsWithoutRef } from 'react';

// 제네릭 애니메이션 래퍼 컴포넌트
type AnimatedProps<T extends ElementType> = {
  as?: T;
  animationType?: 'fadeIn' | 'slideUp' | 'scaleIn' | 'slideLeft';
  delay?: number;
} & Omit<ComponentPropsWithoutRef<T>, 'ref'>;

const animationVariants: Record<string, Variants> = {
  fadeIn: {
    hidden: { opacity: 0 },
    visible: { opacity: 1 },
  },
  slideUp: {
    hidden: { opacity: 0, y: 30 },
    visible: { opacity: 1, y: 0 },
  },
  scaleIn: {
    hidden: { opacity: 0, scale: 0.8 },
    visible: { opacity: 1, scale: 1 },
  },
  slideLeft: {
    hidden: { opacity: 0, x: -30 },
    visible: { opacity: 1, x: 0 },
  },
};

function Animated<T extends ElementType = 'div'>({
  as,
  animationType = 'fadeIn',
  delay = 0,
  children,
  ...props
}: AnimatedProps<T>) {
  const Component = motion[as as keyof typeof motion] || motion.div;
  const variants = animationVariants[animationType];

  return (
    <Component
      variants={variants}
      initial="hidden"
      animate="visible"
      transition={{ duration: 0.5, delay }}
      {...props}
    >
      {children}
    </Component>
  );
}

// 사용 예시
const App = () => (
  <div>
    <Animated animationType="slideUp" delay={0}>
      첫 번째 요소
    </Animated>
    <Animated animationType="slideUp" delay={0.1}>
      두 번째 요소
    </Animated>
    <Animated as="section" animationType="scaleIn" delay={0.2}>
      세 번째 요소 (section 태그)
    </Animated>
  </div>
);
🌊 패턴 3: 스크롤 기반 카운터 애니메이션
import {
  motion,
  useMotionValue,
  useTransform,
  useInView,
  animate,
  MotionValue,
} from 'framer-motion';
import { useEffect, useRef } from 'react';

interface CounterProps {
  from?: number;
  to: number;
  duration?: number;
  suffix?: string;
}

const AnimatedCounter = ({
  from = 0,
  to,
  duration = 2,
  suffix = '',
}: CounterProps) => {
  const count: MotionValue<number> = useMotionValue(from);
  const ref = useRef(null);
  const isInView = useInView(ref, { once: true });

  // 숫자를 정수로 변환
  const rounded = useTransform(count, (latest) => Math.round(latest));

  useEffect(() => {
    if (isInView) {
      // animate 함수로 MotionValue 직접 애니메이션
      const controls = animate(count, to, {
        duration,
        ease: 'easeOut',
      });
      
      return controls.stop;
    }
  }, [isInView, count, to, duration]);

  return (
    <span ref={ref}>
      <motion.span>{rounded}</motion.span>
      {suffix}
    </span>
  );
};

// 사용 예시
const Stats = () => (
  <div>
    <AnimatedCounter to={1234} suffix="+" />  {/* 1234+ */}
    <AnimatedCounter to={98} suffix="%" />    {/* 98% */}
    <AnimatedCounter to={50000} />             {/* 50000 */}
  </div>
);

⚡ 11. 성능 최적화 - 타입과 함께 올바르게 쓰기

Framer Motion 쓸 때 성능 이슈가 생기는 경우가 있어요. TypeScript 타입을 제대로 활용하면 이런 문제를 미리 방지할 수 있어요!

1
transform 속성 우선 사용
x, y, scale, rotate는 GPU 가속이 되는 transform 속성이에요. left, top, width, height보다 훨씬 성능이 좋아요. TypeScript 자동완성으로 이 속성들을 쉽게 찾을 수 있어요!
2
MotionValue로 리렌더링 방지
빠른 이벤트(마우스 이동, 스크롤)에서는 useState 대신 useMotionValue를 써야 해요. MotionValue는 변경 시 리렌더링을 유발하지 않거든요!
3
Variants 객체 컴포넌트 외부에 정의
Variants 객체를 컴포넌트 내부에 정의하면 매 렌더링마다 새 객체가 생성돼요. 컴포넌트 외부에 정의하거나 useMemo를 사용하세요!
import { motion, Variants } from 'framer-motion';
import { useMemo } from 'react';

// ❌ 나쁜 예 - 매 렌더링마다 새 객체 생성
const BadComponent = ({ color }: { color: string }) => {
  return (
    <motion.div
      animate={{ backgroundColor: color }}
      variants={{  // 렌더링마다 새 객체!
        hidden: { opacity: 0 },
        visible: { opacity: 1 },
      }}
    />
  );
};

// ✅ 좋은 예 1 - 컴포넌트 외부에 정의
const cardVariants: Variants = {
  hidden: { opacity: 0, y: 20 },
  visible: { opacity: 1, y: 0 },
};

const GoodComponent1 = () => (
  <motion.div variants={cardVariants} initial="hidden" animate="visible" />
);

// ✅ 좋은 예 2 - 동적 값이 필요할 때 useMemo 사용
const GoodComponent2 = ({ delay }: { delay: number }) => {
  const variants: Variants = useMemo(() => ({
    hidden: { opacity: 0, y: 20 },
    visible: {
      opacity: 1,
      y: 0,
      transition: { delay },  // delay가 변할 때만 새 객체 생성
    },
  }), [delay]);

  return (
    <motion.div variants={variants} initial="hidden" animate="visible" />
  );
};
🔧 LazyMotion으로 번들 크기 줄이기

Framer Motion의 전체 기능을 다 쓰지 않는다면 LazyMotion으로 번들 크기를 줄일 수 있어요. 이것도 TypeScript 타입이 잘 정의되어 있어요!

import { LazyMotion, domAnimation, m } from 'framer-motion';

// LazyMotion으로 필요한 기능만 로드
// domAnimation: 기본 DOM 애니메이션 기능 (약 18kb)
// domMax: 드래그, 레이아웃 포함 전체 기능 (약 32kb)

const App = () => (
  <LazyMotion features={domAnimation}>
    {/* motion 대신 m 사용 */}
    <m.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
    >
      번들 크기 최적화! 📦
    </m.div>
  </LazyMotion>
);

// 동적 import로 더 최적화
import { LazyMotion, m } from 'framer-motion';

const loadFeatures = () =>
  import('./features').then((res) => res.default);

// features.ts
export { domMax as default } from 'framer-motion';

🐛 12. 자주 발생하는 타입 에러와 해결법

Framer Motion + TypeScript 쓰다 보면 진짜 자주 마주치는 에러들이 있어요. 미리 알아두면 삽질 시간을 엄청 줄일 수 있어요 ㅋㅋㅋ

⚠️ 에러 1: CSS 변수 타입 에러 animate={{ '--custom-color': '#667eea' }} 이렇게 CSS 변수를 쓰면 타입 에러가 나요!
// ❌ 에러 발생
<motion.div animate={{ '--custom-color': '#667eea' }} />

// ✅ 해결법: as any 또는 타입 단언
<motion.div
  animate={{ '--custom-color': '#667eea' } as any}
/>

// 더 나은 해결법: 커스텀 타입 정의
import { TargetAndTransition } from 'framer-motion';

type CSSVariables = {
  [key: `--${string}`]: string | number;
};

type ExtendedTarget = TargetAndTransition & CSSVariables;

const animateWithCSSVar: ExtendedTarget = {
  opacity: 1,
  '--custom-color': '#667eea',
};
⚠️ 에러 2: SVG 요소 타입 에러 SVG 요소에 motion을 쓸 때 타입이 맞지 않는 경우가 있어요.
// SVGMotionProps 사용
import { motion, SVGMotionProps } from 'framer-motion';

interface AnimatedCircleProps extends SVGMotionProps<SVGCircleElement> {
  color?: string;
}

const AnimatedCircle = ({ color = '#667eea', ...props }: AnimatedCircleProps) => (
  <motion.circle
    cx="50"
    cy="50"
    r="40"
    fill={color}
    initial={{ scale: 0 }}
    animate={{ scale: 1 }}
    {...props}
  />
);
⚠️ 에러 3: useTransform 콜백 타입
import { useMotionValue, useTransform, MotionValue } from 'framer-motion';

const x: MotionValue<number> = useMotionValue(0);

// ❌ 에러: 타입 추론 실패할 수 있음
const color = useTransform(x, (value) => {
  return value > 0 ? 'red' : 'blue';
});

// ✅ 명시적 타입 지정
const color: MotionValue<string> = useTransform(
  x,
  (value: number): string => {
    return value > 0 ? '#ef4444' : '#667eea';
  }
);

// 배열 형태 useTransform
const opacity: MotionValue<number> = useTransform(
  x,
  [-100, 0, 100],  // 입력 범위
  [0, 1, 0]        // 출력 범위 (타입 자동 추론)
);
💡 Framer Motion 버전 확인! Framer Motion은 버전마다 API가 꽤 많이 바뀌어요. v10, v11에서 특히 많이 변경됐어요. package.json에서 버전 확인하고, 공식 문서의 버전별 마이그레이션 가이드를 꼭 참고하세요! 재능넷에서 프리랜서 개발자를 구할 때도 Framer Motion 버전 경험을 확인하는 게 좋아요 ㅋㅋ

TypeScript + Framer Motion 핵심 타입 치트시트 MotionProps initial, animate, exit transition, variants whileHover, whileTap whileDrag, whileInView 모든 motion 컴포넌트 기본 Variants { [key: string]: TargetAndTransition } staggerChildren delayChildren 상태 기반 애니메이션 Transition type: tween|spring|inertia duration, delay, ease stiffness, damping repeat, repeatType 타이밍 제어 AnimationControls controls.start() controls.stop() controls.set() useAnimation() 반환값 명령형 제어 MotionValue<T> useMotionValue(초기값) useTransform(값, 입력, 출력) useSpring(값, 옵션) useScroll() → scrollYProgress 리렌더링 없는 반응형 값 HTMLMotionProps<T> HTMLMotionProps<'div'> HTMLMotionProps<'button'> SVGMotionProps<SVGElement> 커스텀 컴포넌트 타입 확장 HTML + Motion 타입 결합 PanInfo (드래그 이벤트) point: {x, y} - 현재 위치 delta: {x, y} - 프레임 이동량 offset: {x, y} - 총 이동량 velocity: {x, y} - 현재 속도 onDrag, onDragEnd 핸들러 성능 최적화 핵심 원칙 transform 속성 우선 사용 MotionValue로 리렌더링 방지 Variants 외부 정의 or useMemo GPU 가속 활용 빠른 이벤트에서 필수 불필요한 객체 생성 방지

🎓 13. 마무리 - 이것만 기억하자!

자, 여기까지 TypeScript와 Framer Motion의 타입 정의를 완전히 파헤쳐봤어요. 처음엔 복잡해 보이지만 핵심 타입들만 제대로 이해하면 진짜 강력한 도구가 됩니다 ㅋㅋ

재능넷에서 프론트엔드 개발 관련 재능을 거래하거나 배울 때도, Framer Motion + TypeScript 조합은 요즘 진짜 핫한 스킬이에요. 포트폴리오에 넣으면 확실히 눈에 띄는 기술이랍니다! 💪

🔑 핵심 타입 요약 MotionProps 모든 motion 컴포넌트의 기본 타입
Variants 상태 기반 애니메이션 정의
Transition 타이밍과 물리 설정
AnimationControls 명령형 제어 (useAnimation 반환값)
MotionValue<T> 반응형 값 (리렌더링 없음)
HTMLMotionProps<T> 커스텀 컴포넌트 타입 확장
PanInfo 드래그 이벤트 정보
TargetAndTransition 목표 애니메이션 상태
오 이제 Framer Motion 타입 에러 안 무서워! ㅋㅋㅋ

ㅋㅋㅋ 맞아! 타입 알면 자동완성도 잘 되고 진짜 편해
근데 공식 문서도 봐야겠지?

당연하지~ framer.com/motion 가면 최신 API 다 있어. 버전 업데이트 자주 되니까 꼭 확인해!
✅ 다음 단계로 추천하는 것들 1. Framer Motion 공식 문서 (framer.com/motion) 정독
2. 실제 프로젝트에 작은 애니메이션부터 적용해보기
3. useMotionValue + useTransform 조합으로 인터랙티브 UI 만들기
4. AnimatePresence로 페이지 전환 애니메이션 구현하기
5. LazyMotion으로 번들 최적화 적용해보기
댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0
댓글을 불러오는 중입니다.

아직 댓글이 없습니다.