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

타입스크립트 애니메이션과 Framer Motion 타입 정의 완전 정복 가이드
타입 안전한 애니메이션 코드, 이제 진짜 제대로 써보자 🎯
TypeScript로 React 프로젝트 하다 보면 Framer Motion 쓸 일이 진짜 많잖아요. 근데 막상 쓰려고 하면 타입 에러가 줄줄이 나오면서 멘탈이 탈탈 털리는 경험... 다들 한 번씩은 해봤을 거예요 ㅋㅋㅋ
이 글에서는 Framer Motion의 타입 시스템을 완전히 이해하고, TypeScript와 함께 쓸 때 진짜 유용한 패턴들을 싹 다 정리해드릴게요. 단순히 "이렇게 쓰면 돼요" 수준이 아니라, 왜 이렇게 설계됐는지까지 파고들 거예요. 🔥
Framer Motion은 React 전용 애니메이션 라이브러리예요. 근데 그냥 평범한 라이브러리가 아니라, 선언적(declarative) 방식으로 애니메이션을 작성할 수 있게 해주는 진짜 강력한 도구예요.
TypeScript 지원도 처음부터 빌트인으로 들어가 있어서, @types/framer-motion 같은 별도 패키지 없이도 바로 타입 추론이 됩니다. 이게 진짜 큰 장점이에요 ㅋㅋ
npm install framer-motion또는
yarn add framer-motionTypeScript 프로젝트라면 추가 타입 패키지 없이 바로 사용 가능! 🎉
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로 타입이 정의되어 있어요. 이 타입들을 제대로 이해하면 자동완성도 잘 되고, 실수도 줄어들고, 코드 퀄리티가 확 올라가요 ㄹㅇ.
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;
// ... 더 많은 속성들
}
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 타입은 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>
);
TargetAndTransition 타입이에요. 그래서 CSS 속성 이름을 camelCase로 써야 하고, 일부 CSS 속성은 지원 안 될 수도 있어요. 예를 들어 background-color가 아니라 backgroundColor로 써야 해요!
애니메이션에서 "어떻게 움직이냐"를 결정하는 게 바로 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, // 감속 시간 상수
};
spring: 물리 기반. 자연스럽고 탄력 있는 움직임. UI 인터랙션에 최고.
inertia: 드래그 후 자연스럽게 멈추는 효과. 스크롤이나 드래그 UI에 사용.
부모 컴포넌트에서 자식들의 애니메이션을 순서대로 실행하고 싶을 때 쓰는 속성들이에요. 이게 진짜 꿀기능이에요 ㄹㅇ!
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>
);
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>
);
};
controls.start() - 애니메이션 시작 (Promise 반환, await 가능!)controls.stop() - 현재 애니메이션 즉시 중지controls.set() - 애니메이션 없이 즉시 상태 변경controls.mount() - 컴포넌트 마운트 시 애니메이션 트리거
스크롤 기반 애니메이션에서 진짜 많이 쓰는 패턴이에요. 요소가 뷰포트에 들어올 때 애니메이션을 실행하는 거죠!
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>
);
};
이 두 훅은 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>
);
};
스크롤 위치에 따라 애니메이션을 만들 때 쓰는 훅이에요. 진행 바나 패럴랙스 효과 만들 때 진짜 유용해요!
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>
);
};
실무에서 진짜 많이 쓰는 패턴이에요. 커스텀 컴포넌트를 만들 때 Framer 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>
);
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이 제대로 작동합니다!
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 | 동작 방식 | 사용 케이스 |
|---|---|---|
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>
);
};
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>
);
};
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>
);
};
이건 진짜 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={true} - 위치와 크기 모두 애니메이션layout="position" - 위치만 애니메이션 (크기 변화 무시)layout="size" - 크기만 애니메이션 (위치 변화 무시)layout="preserve-aspect" - 비율 유지하며 크기 애니메이션
자, 이제 실제로 프로젝트에서 바로 쓸 수 있는 패턴들을 정리해볼게요. 재능넷 같은 플랫폼에서 UI를 만들 때도 이런 패턴들이 진짜 많이 쓰여요 ㅋㅋ
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>
);
};
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>
);
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>
);
Framer Motion 쓸 때 성능 이슈가 생기는 경우가 있어요. TypeScript 타입을 제대로 활용하면 이런 문제를 미리 방지할 수 있어요!
x, y, scale, rotate는 GPU 가속이 되는 transform 속성이에요. left, top, width, height보다 훨씬 성능이 좋아요. TypeScript 자동완성으로 이 속성들을 쉽게 찾을 수 있어요!
빠른 이벤트(마우스 이동, 스크롤)에서는
useState 대신 useMotionValue를 써야 해요. MotionValue는 변경 시 리렌더링을 유발하지 않거든요!
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" />
);
};
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';
Framer Motion + TypeScript 쓰다 보면 진짜 자주 마주치는 에러들이 있어요. 미리 알아두면 삽질 시간을 엄청 줄일 수 있어요 ㅋㅋㅋ
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',
};
// 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}
/>
);
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] // 출력 범위 (타입 자동 추론)
);
package.json에서 버전 확인하고, 공식 문서의 버전별 마이그레이션 가이드를 꼭 참고하세요! 재능넷에서 프리랜서 개발자를 구할 때도 Framer Motion 버전 경험을 확인하는 게 좋아요 ㅋㅋ
자, 여기까지 TypeScript와 Framer Motion의 타입 정의를 완전히 파헤쳐봤어요. 처음엔 복잡해 보이지만 핵심 타입들만 제대로 이해하면 진짜 강력한 도구가 됩니다 ㅋㅋ
재능넷에서 프론트엔드 개발 관련 재능을 거래하거나 배울 때도, Framer Motion + TypeScript 조합은 요즘 진짜 핫한 스킬이에요. 포트폴리오에 넣으면 확실히 눈에 띄는 기술이랍니다! 💪
Variants 상태 기반 애니메이션 정의
Transition 타이밍과 물리 설정
AnimationControls 명령형 제어 (useAnimation 반환값)
MotionValue<T> 반응형 값 (리렌더링 없음)
HTMLMotionProps<T> 커스텀 컴포넌트 타입 확장
PanInfo 드래그 이벤트 정보
TargetAndTransition 목표 애니메이션 상태
2. 실제 프로젝트에 작은 애니메이션부터 적용해보기
3. useMotionValue + useTransform 조합으로 인터랙티브 UI 만들기
4. AnimatePresence로 페이지 전환 애니메이션 구현하기
5. LazyMotion으로 번들 최적화 적용해보기
관련 키워드
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.