• Framer-motion 라이브러리 훑어보기

    2022. 9. 8.

    by. 나나 (nykim)

    320x100

     

    프롤로그

     

    motion을 활용한 예시 (SVG Drawing, Slide, Gesture, Modal)

     

    Framer-motion은 Framer가 제공하는 리액트용 애니메이션 라이브러리입니다. 

    NomadCoders의 <React JS 마스터클래스> 를 듣던 중, framer-motion 라이브러리를 사용하는 내용이 있어 공식 문서를 슥 훑어보았는데요, 
    아니... 이것은... 애니메이션 총집합 선물세트?! 

    정말 다양한 애니메이션 모션을 제공하는 멋쟁이 라이브러리였습니다.

     

    공식 문서 보러 들어갈 때마다 구글 번역을 쓰기 귀찮아서 일부 내용만 발췌해 쫌쫌따리 가져왔습니다. 😽

     

    이 글은 framer-motion v.7 을 기준으로 작성되었습니다. (2022.09)

     

     


     

    1. Introduction 🔗

    Github (Star 15.4k)
    MIT 라이센스
    요구사항: React 17+

     

    공식 문서는 요기로 👇

    https://www.framer.com/docs/

     

    Documentation | Framer for Developers

    An open source, production-ready motion library for React on the web.

    www.framer.com

     

     

    일단은 아묻따 설치합니다!

    $ npm i framer-motion

     

    사용법은 간단합니다. 우선 라이브러리를 가져온 다음…

    import { motion } from "framer-motion"

     

    motion.div 와 같이, HTML 태그 앞에 motion 키워드를 붙여줍니다. 이렇게 motion 키워드가 붙은 요소를 motion component 라고 합니다.

    초기 상태를 initial 속성에 객체 형태로 넣고, 애니메이션 할 상태를 animate 속성에 객체 형태로 넣습니다.

     

    <motion.div
      initial={{ scale: 0 }} 
      animate={{ scale: 1, rotateZ: 360 }}
    />
    

     

     

    이런 식으로 적절한 속성을 달아주기만 하면 끝!

    animate 값이 변경되면 Motion이 자동으로 애니메이션을 적용해 줍니다 👍

     

     

     

     


     

     

    2. Animation 🔗

     

    Simple animations 🔗

     

    Transitions

    기본적으로 Motion 라이브러리는 ‘알잘딱깔센’하게 애니메이션을 만들어줍니다.

    예를 들어 x/y값 변경이나 scale이 바뀔 때는 튀는 듯한 스프링 효과를 적용하고, opacity나 color가 바뀔 때는 자연스러운 트윈 효과를 적용합니다.

    사용자는 transition 속성으로 원하는 유형의 애니메이션을 정의할 수 있습니다.

     

    <motion.div
      initial={{ opacity: 0, scale: 0.5 }}
      animate={{ opacity: 1, scale: 1 }}
      transition={{
        duration: 0.8,
        delay: 0.5,
        ease: [0, 0.71, 0.2, 1.01]
      }}
    />

     

     

     

     

    Enter animations

    motion 컴포넌트가 처음 생성될 때, animate 속성에 적용된 값이 style 또는 inital 에 정의된 값과 다르다면 animate 속성에 적용된 값으로 자동으로 애니메이션을 적용해 줍니다.

    자동으로 적용하길 원치 않는다면 inital 값을 false로 설정해 주세요.

     

    <motion.div animate={{ x: 100 }} initial={false} />
    

     

     

     

    Exit animations

    리액트에서는 컴포넌트가 트리에서 삭제될 경우 “즉시” 사라져버리기 때문에 사라지는 애니메이션을 적용하기 어렵다는 문제가 있습니다.

    하지만 Motion을 쓴다면?! AnimatePresence 컴포넌트를 사용하면 사라지는 애니메이션이 보여지는 동안 DOM에 유지되도록 할 수 있습니다.

     

    <AnimatePresence>
      {isVisible && (
        <motion.div
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
        />
      )}
    </AnimatePresence>
    

     

     

     

    Keyframes

    animate의 값을 배열로 설정하면 Motion이 각 값을 차례로 처리합니다.

    현재값을 초기 키프레임으로 사용하고 싶다면 null 값을 주면 됩니다.

    이렇게 하면 애니메이션 되는 도중에 애니메이션이 시작되더라도 전환이 자연스러워집니다.

     

    <motion.div
      animate={{
        scale: [1, 2, 2, 1, 1],
        rotate: [0, 0, 270, 270, 0],
        borderRadius: ["20%", "20%", "50%", "50%", "20%"],
      }}
    />
    

     

     

     

    일단 배열로 값을 넘기면 라이브러리가 알아서 값들을 균등하게 배치해주지만,
    정확한 타이밍을 지정하고 싶다면 transition 속성의 times를 사용합니다.

     

    <motion.div
      animate={{ scale: [1, 1.5, 1.1] }}
      transition={{ duration: 3, times: [0, 0.2, 1] }}
    />
    
    /*
     * 3초에 걸쳐 scale 애니메이션을 실행합니다.
     * [1 ⇢ 1.5]는 [0 ⇢ 0.2]에 걸쳐 실행되고,
     * [1.5 ⇢ 1.1]은 [0.2 ⇢ 1]에 걸쳐 실행됩니다.
     */
    

     

     

     

     

     

     

     

    Gesture animations 🔗

     

    hover, tap, drag, focus, inView 등의 제스처가 시작될 때 값 집합에 애니메이션을 적용할 수도 있어요!

     

    <motion.div
    	initial={{ opacity: 0.2 }}
    	whileInView={{
    	  opacity: 1,
    	  rotate: [0, 360],
    	  borderRadius: ["20%", "50%"],
    	  transition: { delay: 0.05 }
    	}}
    	whileHover={{
    	  scale: 1.2,
    	  transition: { type: "spring", stiffness: 400, damping: 10 }
    	}}
    />
    

     

     

     

     

     

     

    Variants 🔗

     

    단일 개체에 애니메이션을 설정하는 것은 쉽습니다.
    그런데 DOM 전체에 파생되는 애니메이션이나, 차례로 이뤄지는 애니메이션을 설정하고 싶을 때는 어떻게 할까요?

    그럴 때는 variants(변형)를 써봅시다! 이건 미리 정의한 애니메이션 세트라고 할 수 있어요.

     

    const variants = {
      hidden: { opacity: 0 },
      visible: { opacity: 1 }
    }
    

     

    motion 컴포넌트에 variants 속성으로 사전 정의한 내용을 넘겨줍니다.

     

    <motion.div variants={variants} />
    

     

    초기 상태 initial, 적용할 애니메이션 animate 속성을 variants 객체에 있는 속성 이름으로 지정하면 끝!

     

    <motion.h1
      initial="hidden"
      animate="visible"
      variants={variants}
    />
    
    <motion.button
      initial="hidden"
      animate="visible"
      variants={variants}
    />
    

     

     

     

    Propagation

    만약 motion 컴포넌트에 자식 요소가 있다면, 자식 요소가 자체 animate 속성을 정의하기 전까지 variants의 변화를 상속받도록 할 수 있습니다.

    쉽게 말해, variants에 정의한 속성명을 자식에게 그대로 물려줄 수 있습니다.

     

    const list = {
      hidden: { opacity: 0 },
      visible: { opacity: 1 },
    }
    
    const item = {
      hidden: { opacity: 0, y: -100 },
      visible: { opacity: 1, y: 0 },
    }
    
    return (
      <motion.ul
        initial="hidden"
        animate="visible"
        variants={list}
      >
        <motion.li variants={item} />
        <motion.li variants={item} />
        <motion.li variants={item} />
      </motion.ul>
    )
    

     

     

     

    이 경우 li에 달린 variants={item}은 initial="hidden"과 animate="visible"가 자동으로 적용됩니다.

     

     

     

     

     

    Orchestration

    위의 예제에서 볼 수 있는 것처럼, item에 달린 애니메이션은 모두 동시에 시작됩니다.
    하지만 transition에 추가적인 속성을 더해 자식 애니메이션의 실행을 조정할 수 있습니다.

     

    const list = {
      hidden: { opacity: 0 },
      visible: {
        opacity: 1,
        transition: {
          when: "beforeChildren",
          staggerChildren: 0.2
        }
      }
    };
    

     

     

     

     

     

    Dynamic variants

    함수를 정의해서 각 variant에 동적으로 애니메이션을 설정할 수도 있습니다.
    이러한 variant 함수들은 컴포넌트의 custom 속성으로 넘어오는 값을 인자로 받습니다.

     

    const variants = {
      hidden: {
        opacity: 0.2,
        y: 15
      },
      visible: i => ({
        opacity: 1,
        y: 0,
        transition: {
          delay: i * 0.2,
          duration: 1,
          repeat: Infinity,
          repeatType: "reverse"
        }
      })
    };
    
    return (
      <ul>
        {items.map((item, i) => (
          <motion.li
            key={item}
            initial="hidden"
            animate="visible"
            variants={variants}
            custom={i}
          >
            {item}
          </motion.li>
        ))}
      </ul>
      );
    

     

     

     

     

     

     

    Manual Controls 🔗

     

    대부분의 UI 인터랙션에 맞춰 착착 애니메이션이 실행되지만,
    좀더 복잡한 시퀀스를 구현하고 싶다면 useAnimationControls 훅으로 애니메이션을 수동 시작/중지할 수 있습니다.

     

    import { useEffect, useState } from "react";
    import { motion, useAnimationControls } from "framer-motion";
    
    export default function App() {
      const [show, setShow] = useState(false);
      const controls = useAnimationControls();
    
      useEffect(() => {
        if (show) {
          controls.start({ scale: 6 });
        }
      }, [controls, show]);
    
      return (
        <div className="wrap">
          <motion.h1 animate={controls}>{show ? "Wow!" : "..."}</motion.h1>
          <button onClick={() => setShow(true)}>setShow(true)</button>
        </div>
      );
    }
    

     

     

     

     

     

     

     


     

     

     

     

     

    3. Motion components 🔗

     

    motion 컴포넌트는 60fps 애니메이션에 최적화된 DOM 기본 요소입니다.

    숫자, 숫자를 포함한 문자열, 16진수 또는 RGB 색상값 등 원하는 값만 넣으세요! Motion이 알아서 다 해줍니다 👏

     

     

     

    Supported values 🔗

     

    일반적으로 같은 유형끼리(px to px) 애니메이션이 가능하지만,
    HTML의 x, y, width, height, top, left,  right,  bottom 은 다른 값 유형이더라도 자유롭게 애니메이션될 수 있습니다.

     

    <motion.div
      initial={{ x: "100%" }}
      animate={{ x: "calc(100vw - 50%)" }}
    />
    

     

     

    Transform

    CSS Transform 속성은 GPU에 의해 가속화되므로 애니메이션 하기 딱 좋은 속성이죠.

    여러 값이 있을 때 translate ⇒ scale ⇒ rotate ⇒ skew 순대로 적용되지만, 원한다면 transformTemplate 속성을 써서 사용자 정의도 가능합니다.

     

    const transform = ({ rotate, x }) => `rotate(${rotate}) translateX(${x})`
    
    <motion.div transformTemplate={transform} />
    

     

     

    CSS variables

    똑똑한 Motion은 CSS 변수값도 애니메이션 처리할 수 있습니다.

    (타입스크립트를 쓰고 있다면 as any 로 타입 처리를 해주세요!)

     

    <motion.ul
      initial={{ '--rotate': '0deg' } as any}
      animate={{ '--rotate': '360deg' } as any}
    >
      <li style={{ transform: 'rotate(var(--rotate))' }} />
    </motion.ul>
    

     

     

    SVG line drawing

    pathLength, pathSpacing, pathOffset 속성을 사용하여 SVG 패스 애니메이션을 만들 수 있습니다. 0~1 사이의 값으로 설정되며, 여기서 1은 path의 측정된 길이입니다. (그러니 0에서 1로 만들면 되겠죠!)

    패스 애니메이션은 circle, ellipse, line, path, polygon, polyline, rect 와 호환됩니다.

     

    const variants = {
      start: { pathLength: 0, fill: "rgba(255, 255, 255,0)" },
      end: { pathLength: 1, fill: "rgba(255, 255, 255, 1)" }
    };
    
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 500">
      <motion.path
        variants={variants}
        initial="start"
        animate="end"
        transition={{
          default: { duration: 1.8 },
          fill: { duration: 1, delay: 1.1 }
        }}
        d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z"
      />
    </svg>

     

     

     

     

     


     

     

     

     

    4. Layout animations 🔗

     

    Framer Motion의 강력한 강점 중 하나는 손쉽게 레이아웃 애니메이션을 설정할 수 있다는 점입니다.

     

    CSS 레이아웃은 사실 힘들고 어려운 일입니다. 예를 들어 height를 100px에서 500px로 만들고 트랜지션을 적용하면 리플로우(Reflow)가 발생하면서 성능이 저하됩니다. 게다가 트랜지션으로는 한계가 있습니다. justify-content 상태일 때 flex-start와 flex-end를 트랜지션할 수 있을까요? 음, 힘들 것 같은데요…

     

    하지만?! motion을 쓴다면 가능합니다 😎

     

    <motion.div layout />
    

     

     

     

     

    이렇게 layout 속성만 달아주세요! 그러면 리렌더링 결과로 발생하는 모든 레이아웃 변경사항을 감지해 애니메이션을 실행합니다. 다음과 같은 조합이 있을 수 있겠죠.

    • 목록 순서 변경
    • 컴포넌트 자체에 설정된 스타일 변경 (width, position 등)
    • 부모 레이아웃의 변경 (flexbox, grid 등)
    • 그밖에 다른 모든 레이아웃 변경사항

     

     

     

    Scale correction 🔗

     

    모든 레이아웃 애니메이션은 transform 속성을 사용하므로 부드럽게 구현됩니다.

    transform을 사용하는 레이아웃 애니메이션은 때때로 자식 요소가 시각적으로 왜곡되곤 하는데요, 이를 수정하기 위해 요소의 첫 번째 자식 요소에도 layout 속성을 지정할 수 있습니다.

     

    box-shadow나 border-radius 속성이 왜곡되는 경우도 있지만 돈 워리! 값이 설정되어 있는 한 motion 컴포넌트는 이를 자동으로 수정합니다. 만약 이런 값들을 애니메이션화 하지 않을 거라면 그냥 style로 지정하는 것이 편합니다.

     

    <motion.div layout style={{ borderRadius: 20 }} />
    

     

     

     

    Customising layout animations

    레이아웃 애니메이션도 transition 속성으로 커스터마이징 가능합니다.

     

    <motion.div layout transition={{ duration: 0.3 }} />
    

     

    특별히 레이아웃 애니메이션에만 트랜지션을 지정하고 싶다면 layout 속성을 콕 집어 설정해 주세요.

     

    <motion.div
      layout
      animate={{ opacity: 0.5 }}
      transition={{
        opacity: { ease: "linear" },
        layout: { duration: 0.3 }
      }}
    />
    

     

     

     

    Animating within scroll containers

    스크롤 가능한 요소 내에서 레이아웃 애니메이션을 실행하고 싶다면 layoutScroll 속성을 꼭 넣어주세요.

    그럼 라이브러리가 자식을 측정할 때 요소의 스크롤 옵셋을 고려할 수 있습니다.

     

    <motion.div layoutScroll style={{ overflow: "scroll" }}/>
    

     

     

     

    Coordinating layout animations

    컴포넌트가 리렌더링되면서 레이아웃이 변경되면 애니메이션이 자동으로 트리거됩니다.

    그런데 만약! 동시에 리렌더링 되는 일은 없지만 서로의 레이아웃에 영향을 주는 컴포넌트가 함께 있다면 어떨까요? 예를 들면 FAQ 등에서 자주 쓰는 아코디언 메뉴가 있겠죠.

     

    메뉴 하나를 클릭 해 펼치는 경우를 생각해봅시다. 클릭된 메뉴는 리렌더링 되지만 그 옆에 있는 다른 메뉴는 레이아웃 변경을 감지하지 못합니다. 이럴 때는 LayoutGroup 컴포넌트를 사용해 함께 묶어 주세요.

     

    import { LayoutGroup } from "framer-motion"
    
    function List() {
      return (
        <LayoutGroup>
          <Accordion />
          <Accordion />
        </LayoutGroup>  
      )
    }
    

     

    이렇게 그룹화된 컴포넌트 중 하나에서 레이아웃 변경이 감지되면 부가적인 리렌더링 없이 다른 모든 컴포넌트들에게도 레이아웃 애니메이션이 발생합니다.

     

     

     

     

    Shared layout animations 🔗

     

    layoutId 속성을 가진 기존 컴포넌트와 일치하는 새로운 컴포넌트가 추가될 경우, 이전 컴포넌트에서 자동으로 애니메이션이 적용됩니다.
    이를 통해 이전 컴포넌트에서 새 컴포넌트를 보여줄 때 자연스러운 처리가 가능합니다. (어메이징하죠!)

     

    <ul>
    	{tabs.map((item) => (
    	  <li
    	    key={item.label}
    	    className={item === selectedTab ? "selected" : ""}
    	    onClick={() => setSelectedTab(item)}
    	  >
    	    {`${item.icon} ${item.label}`}
    	    {item === selectedTab ? (
    	      <motion.div className="underline" layoutId="underline" />
    	    ) : null}
    	  </li>
    	))}
    </ul>
    

     

     

     

     

     

     


     

     

     

     

    5. Gestures 🔗

     

    Motion은 React에서 제공하는 기본 이벤트 리스너 세트를 확장해 제스처 애니메이션을 제공합니다.
    motion 컴포넌트에 hover, tap, pan, viewport, drag 등의 제스처 이벤트 리스너를 붙일 수 있습니다.

     

     

     

    Animation helpers 🔗

     

    motion 컴포넌트는 다양한 애니메이션 제스쳐 속성을 제공합니다:
    whileHover, whileTap, whileFocus, whileDrag, whileInView

     

    <motion.button
      whileHover={{
        scale: 1.2,
        transition: { duration: 1 },
      }}
      whileTap={{ scale: 0.9 }}
    />
    

     

     

    애니메이션할 값을 직접 입력해도 되고, variant 속성을 통해 정의된 이름으로 설정할 수도 있습니다.
    이 경우, variants에 정의된 속성명을 자식 요소에서 그대로 사용할 수 있습니다.

     

    const buttonVariants = {
      hover: { scale: 2, rotzteZ: 90 }
    };
    
    <motion.button
      whileHover="hover"
      variants={buttonVariants}
    />
    

     

     

     

     

    Hover 🔗

     

    호버 제스처는 포인터가 컴포넌트 위로 이동하거나 컴포넌트를 떠날 때를 감지합니다. onMouseEnter나 onMouseLeave 와는 다르게 실제 마우스 이벤트 결과가 있을 때만 실행됩니다.

     

    <motion.a
    	whileHover={{ scale: 1.1 }}
      onHoverStart={e => {}} // 컴포넌트 위로 포인터를 가져갈 때 실행되는 콜백 함수
      onHoverEnd={e => {}}   // 컴포넌트에서 포인터가 떠날 때 실행되는 콜백 함수
    />
    

     

     

     

     

     

    Tap 🔗

     

    탭 제스처는 포인터가 동일한 컴포넌트를 눌렀다 뗄 때를 감지합니다.
    어떤 구성요소에서 탭핑이 성공적으로 완료되면 tap 이벤트를 실행하고, 컴포넌트 외부에서 탭핑이 종료되면 tapCancel 이벤트를 실행합니다.

    만약 컴포넌트가 드래그 가능한 컴포넌트의 자식인 경우 제스처 중 포인터가 3픽셀 이상 이동하면 탭 제스처가 자동으로 취소됩니다.

     

    <motion.div
      whileTap={{ scale: 0.9 }}
    	onTap={e => {}}       // 탭 제스처가 성공적으로 종료될 때 실행되는 콜백 함수
    	onTapStart={e => {}}  // 탭 제스처가 시작될 때 실행되는 콜백 함수
    	onTapCancel={e => {}} // 탭 제스처가 컴포넌트 외부에서 끝날 때 실행되는 콜백 함수
    />
    

     

     

     

     

     

     

    Drag 🔗

     

    포인터가 구성 요소를 누르고 3픽셀 이상 이동할 때를 인식합니다. 포인터를 놓으면 제스처가 종료됩니다.

    drag="x" 를 통해 x 방향(또는 y 방향)으로만 드래그 되도록 설정할 수 있습니다.

     

    <motion.div 
    	drag
    	dragConstraints={{top:100}} // 드래그 허용 영역 (useRef hook으로 생성된 컴포넌트도 지정 가능)
    	dragSnapToOrigin            // 드래그한 요소를 놓으면 원점으로 되돌아감
    	dragElastic                 // constraints 밖으로 허용되는 움직임의 정도 (0=없음, 1=전체)
    	dragTransition={}           // 드래그 요소의 관성(inertia) 애니메이션을 지정
     />
    

     

     

     

     

     


     

     

     

     

    6. Scroll Animations 🔗

     

    Motion에는 2가지의 스크롤 애니메이션 유형이 있습니다.

    1. 스크롤 링크(Scroll-linked): 스크롤 진행 상황이 애니메이션 진행 상황과 연결됨
    2. 스크롤 트리거(Scroll-triggered): 요소가 뷰포트에 들어오거나 나갈 때 애니메이션이 트리거됨

     

     

     

    Scroll-linked animations 🔗

     

    모션 값(Motion Values)과 useScroll 훅을 사용해 구현할 수 있습니다.

     

     

    useScroll

    기본적으로 페이지 스크롤을 추적하며, 4개의 모션 값을 반환합니다.

    • scrollX, scrollY : 절대 위치값
    • scrollXProgress, scrollYProgresss: 정의된 오프셋 사이의 스크롤 값 (0~1)

     

    import { motion, useScroll } from "framer-motion"
    
    function Component() {
      const { scrollYProgress } = useScroll();
      
      return (
        <motion.div style={{ scaleX: scrollYProgress }} />  
      )
    }
    

     

     

     

     

    Scroll-triggered animations 🔗

     

    whileInView 속성 세트(& 트랜지션)를 정의하여 요소가 뷰에 있을 때 애니메이션을 적용할 수 있습니다.

     

    <motion.div
      initial={{ opacity: 0 }}
      whileInView={{ opacity: 1 }}
      viewport={} // 뷰포트가 감지되는 방식을 정의하는 뷰포트 옵션 개체
      onViewportEnter={()=>{}}  // 뷰포트에 진입할 때 호출 (단, 브라우저가 IntersectionObserver를 지원해야 함)
      onViewportLeave={()=>{}}  // 뷰포트에서 떠날 때 호출 (단, 브라우저가 IntersectionObserver를 지원해야 함)
    />
    

     

    Viewport options

    • once (boolean)
      true인 경우, 뷰포트에 들어가면 whileInView 상태가 유지됩니다. 즉, 뷰포트 콜백이 호출되지 않습니다.
    • root (RefObject<Element>)
      viewport.root와 조상 요소에 ref 객체를 넘기면, 해당 요소를 뷰포트 측정에 사용합니다.
    • margin (string)
      뷰포트 진입을 판단할 때 마진을 추가할 수 있습니다.
    • amount ("some" | "all" | number)
      요소가 쬐끔(some) 보일 때 뷰포트와 교차한다고 판단할지, 전부(all) 또는 일정한 만큼 보일 때 판단할지 정의합니다.

     

    function Component() {
      const scrollRef = useRef(null)
      
      return (
        <div ref={scrollRef}>
          <motion.div
            variants={emojiVariants}
            initial="hidden"
            whileInView="visible"
            viewport={{ root: scrollRef, once: true, amount: 0.3 }}
          />
        </div>
      )
    }
    

     

     

     

     

     

     


     

     

     

     

     

    7. Transition 🔗

     

    CSS에서도 자주 써먹는 그 속성 맞습니다! 두 값 사이에 애니메이션을 적용할 때 사용합니다.

     

    <motion.div
      animate={}
      transition={{ duration: 1 }}
    />
    

     

     

    이렇게 정의한 트랜지션은 모든 속성에 적용되지만, 값에 각각 다른 트랜지션을 적용하는 것도 가능해요.

     

    <motion.div
      transition={{
    		default: { duration: 1 },
        opacity: {
          delay: 0.3
        },
        y: {
          type: "spring",
          damping: 3,
          stiffness: 50,
          restDelta: 0.01,
          duration: 0.3
        }
      }}
    />
    

     

     

     

    Orchestration 🔗

     

    다음과 같은 속성을 통해 트랜지션을 보다 심미적으로 만들 수 있습니다.

    • delay (number)
      말그대로 n초 간의 딜레이를 적용합니다.
    • delayChildren (number)
      variants를 사용하는 경우, n초 후 자식 컴포넌트의 애니메이션이 시작됩니다.
    • staggerChildren (number)
      variants를 사용할 때, 자식 컴포넌트 간 애니메이션 간격을 설정합니다.
    • staggerDirection (number)
      스태거를 적용할 방향입니다. -1을 주면 역순이 됩니다.
    • when (false | "beforeChildren" | "afterChildren" | string)
      varaints를 사용할 때, 하위 트랜지션이 시작하기 전 또는 완료한 후에 이 트랜지션을 완료할지 정합니다.
    • repeat (number)
      트랜지션을 반복할 횟수입니다. Infinity로 설정하면 영원히 계속됩니다… ☆ 값을 설정하지 않으면 repeatType애니메이션이 반복됩니다.
    • repeatType ("loop" | "reverse" | "mirror")
      트랜지션이 반복되는 경우 어떻게 처리할지 정의합니다.

     

     

     

    Spring 🔗

     

    Motion의 좋은 점은 보다 사실적인 애니메이션을 표현해 준다는 것입니다.
    transition 타입으로 spring을 설정하면 용수철이 튀어오르는 것처럼 사실적인 물리 표현을 할 수 있습니다.

    • bounce (number)
      말그대로 얼마나 바운스바운스할지 정합니다(두근..!) 0이라면 탄력이 없고, 1은 매우 탄력적입니다. duration 기본값은 0.25로 설정됩니다.
    • damping (number)
      저항하는 반대 힘. 0으로 설정하면 스프링이 무한으로 빠운스합니다. (기본값: 10)
    • mass (number)
      질량. 값이 높을수록 움직임이 둔해집니다. (기본값: 1)
    • stiffness (number)
      값이 높을수록 더 갑작스럽게 움직입니다. (기본값: 100)
    • velocity (number)
      스프링의 초기 속도.
    • restSpeed (number)
      절대 속도가 이 값 아래로 떨어지고 델타가 restDelta보다 작으면 애니메이션을 종료합니다. (기본값: 0.01)
    • restDelta (number)
      거리(distance)가 이 값보다 작고 속도가 restSpeed 보다 작으면 애니메이션을 종료합니다. (기본값: 0.01)

     

    <motion.div
      animate={{ rotate: 180 }}
      transition={{ type: 'spring' }}
    />
    

     

     

     

     

     

     


     

     

     

     

     

    8. Motion values 🔗

     

    모든 motion 컴포넌트는 내부적으로 MotionValue를 사용해 애니메이션 값의 상태와 속도를 추적합니다.
    이는 자동으로 생성되지만 사용자가 원한다면 수동으로 생성해 motion 컴포넌트에 넣을 수 있습니다.

     

    수동으로 MotionValue를 설정하는 경우,

    • 상태를 Set/Get 할 수 있습니다.
    • 여러 컴포넌트 간 모션을 동기화할 수 있습니다.
    • useTransform 훅을 통해 MotionValue를 연결할 수 있습니다.
    • React의 렌더링 주기를 트리거하지 않고도 시각적 속성을 업데이트할 수 있습니다.
    • 업데이트를 구독(Subscribe)할 수 있습니다.

     

     

     

    Overview 🔗

     

    MotionValue는 useMotionValue 훅으로 생성할 수 있습니다. 문자열 또는 숫자로 된 초기값을 지정할 수 있습니다.

    사용자는 set() 메서드로 업데이트할 수 있고, get() 으로 값을 읽어올 수 있습니다. getVelocity() 로 초당 계산된 속도를 받아올 수도 있고요. 세부 메서드는 여기를 참고하세요.

     

    재밌는 점은 MotionValue가 바뀌더라도 컴포넌트는 리렌더링되지 않는다는 것입니다.
    <motion.div/>가 움직인다고 한들 이 컴포넌트는 다시 랜더링되지 않기 때문에 여기 붙어있는 api가 계속해서 호출된다거나 하는 불상사는 없겠죠! 👍

     

    import { motion, useMotionValue } from "framer-motion"
    
    function App() {
      const x = useMotionValue(0)
    
    	x.set(100);
    	x.get();
    	x.getVelocity();
    }
    

     

     

     

    Injecting MotionValues

    일단 MotionValue가 생성되면, 시각적 속성을 줬던 것처럼 motion component에 연결할 수 있습니다.

    HTML이라면 style 속성을 통해, SVG라면 SVG 속성을 통해 넣어주면 됩니다.

     

    const x = useMotionValue(0);
    const cx = useMotionValue(0);
    
    <motion.div style={{ x }} /> // HTML
    <motion.circle cx={cx} />    // SVG
    

     

    하나의 MotionValue를 여러 개의 컴포넌트에 넣는 것도 가능합니다. 그럼 그 MotionValue를 바라보는 모든 컴포넌트에 영향이 가겠죠!

     

     

    Responding to changes

    onChange 메서드로 MotionValue에 리스너를 추가할 수도 있습니다.
    리액트 컴포넌트 안에서 onChange를 호출할 때는 useEffect 훅으로 꼭 감싸 주세요. onChange는 unsubscribe 함수를 반환하므로 subscriber 중복을 막기 위해 useEfeect() 안에서 쓰여야 합니다.

     

    useEffect(() => x.onChange(latest => {}), [])
    

     

     

    Composing MotionValues

    useTransform 등의 훅을 써서 MotionValue값을 구성할 수 있습니다.

    • useTransform : 값 범위를 다른 값 범위로 매핑할 수 있습니다.

     

    박스를 왼쪽으로 드래그하면 scale(3)로, 오른쪽으로 드래그하면 scale(0.1)로 바뀌도록 하는 예제입니다.

     

    const x = useMotionValue(0);
    const scale = useTransform(
      x,
      // Map x from these values:
      [-400, 400],
      // Into these values:
      [3, 0.1]
    );
    
    <motion.div drag="x" style={{ x, scale }}/>
    

     

     

     

     

     

     


     

     

     

    9. Examples

     

     

     

     

    더 많은 예시는 요기로 👇

    useScroll | Framer for Developers

    AnimateSharedLayout | Framer for Developers

    Reorder | Framer for Developers

    3D: Introduction | Framer for Developers

     

     

     

     


     

     

     

    에필로그

     

    훑어봤지만 아직 볼 게 더 많이 남은 시리즈물 같은 느낌...!!

    다양한 애니메이션을 하나의 라이브러리로 구현 가능하고, 사용법도 (그렇게) 어렵지 않다는 점에서 매력적이라고 느꼈습니다.

     

    사실 팀 세미나 주제로 이녀석을 들고가서 라이브러리 도입에 대해 얘기해봤는데,
    의존성 / 유지보수성 / 전파성을 모두 고려해야하는 문제로 결국 실무에선 보수적인 태도로 보게 되더라고요.

    그렇게 다양한 모션을 필요로 하는 서비스가 아니라는 것도 있고요.

     

    framer-motion은 디자인+개발을 모두하는 만능일꾼에게는 빠른 구현이 가능하다는 점에선 좋은 선택지가 될 것 같습니다.

    motion 말고도 다른 사람들이 자주 쓰는 리액트 애니메이션 라이브러리에 뭐가 있는지는 여기서 볼 수 있어요!

     

     

     

     

     

    그나저나

    긴 스크롤을 끝까지 내리셨다니 hoxy.. 👼?

    728x90

    댓글 2

    • 프로필사진
      웨스트브룩 2022.09.22 09:19

      hoxy 다 내려서 정독 했다는 ㅠㅠ
      역시 기술을 도입한다는 것은 쉬운게 아닌 것 같습니다!

      PS:포폴에 아직도 3년차로 되어있자나요 ㅋㅋㅋ 빨리 수정하시옵소서!! ㅋㅋㅋㅋㅋ

      • 프로필사진
        나나 (nykim) 2022.09.22 19:36 신고

        안녕하세요, 웨스트브룩님!
        크으 끝까지 내려서 댓글까지 남겨주신 찐 👼...
        맞아요 이것저것 고민할 게 많은데 레퍼로 참고도 돼서 라이브러리 줍줍하는 재미도 있더라구요ㅎㅎ

        포폴은 재취준할 때 쓰고 쭉 방치해서 뜨끔하네요ㅋㅋㅋㅠㅠ
        이건 본업에 집중하느라 그렇습니당... 흐흐흐 언제 업뎃을 해야겠네요!
        댓글 남겨주셔서 감사합니다 ;D