[Framer Motion] React 모션 라이브러리

강경서·2023년 11월 21일
0
post-thumbnail

Intro

인터렉티브한 애플리케이션은 사용자의 경험에 큰 도움이 됩니다. 흥미롭게 반응하는 UI는 사용자가 애플리케이션에 머무는 시간을 늘려주고 제공하는 서비스는 더욱 신뢰성 있게 보입니다. 이는 경쟁하고 있는 타 애플리케이션과는 차별화된 기능이 될 것입니다.


Framer Motion

framer-motionReact에서 애니메이션과 제스쳐를 쉽게 다룰 수 있도록 해주는 라이브러리입니다. css를 이용해 힘들게 만들었던 애니메이션을 framer-motion은 아주 손쉽게 만들 수 있습니다.


motion

framer-motion을 사용하기 위해서는 motion을 통해서 엘리먼트를 생성해야 합니다.

import { motion } from 'framer-motion'

funtion App() {
	return <motion.div />
}

styled-components와 같은 라이브러리를 사용하고 있더라고 motion을 이용해서 엘리먼트를 사용하면 framer-motion을 사용할 수 있습니다.

const Box = styled(motion.div)``

Animation

motion컴포넌트의 prop을 통해 애니메이션을 실행할 수 있습니다.

initial

초기 속성을 지정하는 prop입니다. boolean값인 false값으로 설정한다면 애니메이션이 비활성화됩니다.

<motion.div initial={{ opacity: 0 }} />

animate

애니메이션으로 지정할 값을 설정하는 prop입니다.

<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />

transition

애니메이션의 transtion을 지정하는 prop입니다.

<motion.div
	initial={{ scale: 0 }}
    animate={{ scale: 1 }}
    transition={{ type: "spring", bounce: 0.5, delay: 1 }}
/>

variants

variants를 사용하여 코드를 정돈할 수 있습니다. 객체형태로 구성되어 motion컴포넌트의 prop을 이름별로 구분하여 사용할 수 있으며, motion컴포넌트의 variants prop에 해당 객체로 지정 후 각각의 prop에 구분된 이름을 넣어 사용할 수 있습니다. 상위 컴포넌트와 하위 컴포넌트의 variants의 구성 요소의 이름이 같다면 상위 컴포넌트에만 prop의 이름을 지정해도 하위 컴포넌트의 애니메이션이 실행됩니다.

const boxVariants = {
  start: { opacity: 1, scale: 0 },
  end: {
    opacity: 1,
    scale: 1,
  },
};

const circleVariants = {
  start: { opacity: 0, y: 20 },
  end: {
    opacity: 1,
    y: 0,
  },
};

function App() {
  return (
    <Wrapper>
      <Box
        transition={{ type: "spring", bounce: 0.5, delay: 1 }}
        initial={{ scale: 0 }}
        animate={{ scale: 1, rotateZ: 360 }}
      />
      <Box variants={boxVariants} initial="start" animate="end">
        <Circle variants={circleVariants} />
        <Circle variants={circleVariants} />
        <Circle variants={circleVariants} />
        <Circle variants={circleVariants} />
      </Box>
    </Wrapper>
  );
}

상위 컴포넌트 variantstransition에서 자식들의 애니메이션을 조정할 수 있습니다.
delayChildren를 통해 자식 컴포넌트의 애니메이션을 지연시킬 수 있으며, staggerChildren를 통해 자식들간의 애니메이션 간격을 조정할 수 있습니다.

const boxVariants = {
  start: { opacity: 1, scale: 0 },
  end: {
    opacity: 1,
    scale: 1,
    transition: {
      delayChildren: 0.3,
      staggerChildren: 0.2,
    },
  },
};

Gestures

마우스 상태에 따른 이벤트에 반응하는 애니메이션을 실행할 수 있습니다.

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

drag

drag prop을 입력하는 것만으로 엘리먼트를 마우스 드레그로 이동시킬 수 있습니다.

<motion.div drag />
// x축만 드레그 가능
<motion.div drag="x" />
// y축만 드레그 가능
<motion.div drag="y" />

dragconstraints를 통해 드레그 구역을 제한할 수 있습니다.

<motion.div
  drag="x"
  dragConstraints={{ left: 0, right: 300 }}
/>
// useRef를 활용하면 부모 컴포넌트 내에서만 드레그 할 수 있게 만들 수 있습니다.
function App() {
  const biggerBoxRef = useRef(null);
  return (
      <BiggerBox ref={biggerBoxRef}>
        <Box
          drag
          // 드레그 후 다시 돌아오게 합니다.
		  dragSnapToOrigin
          // 드레그 강도를 조절합니다.
          dragElastic={0}
          dragConstraints={biggerBoxRef}
        />
      </BiggerBox>
  );
}

MotionValue

MotionValue는 애니메이션 내의 수치를 트래킹할때 사용합니다. useMotionValue를 통해 상태를 생성할 수 있습니다. Motionvalue는 업데이트시 리엑트 렌더링 싸이클을 발동하지 않습니다. useMotionValueEvent를 통해서 상태의 변화를 확인할 수 있습니다. 또한 useTransform를 사용하면 Motionvalue값에 따라 새로운 값을 출력할 수 있습니다.
그 외에도 useScroll, useTime등을 이용하여 다양한 Motionvalue를 이용할 수 있습니다.

function App() {
  const x = useMotionValue(0);
  const { scrollY, scrollYProgress } = useScroll();
  const rotate = useTransform(x, [-800, 800], [360, -360]);
  const gradient = useTransform(
    x,
    [-800, 0, 800],
    [
      "linear-gradient(135deg, rgb(0,210,238), rgb(0,83,238))",
      "linear-gradient(135deg, rgb(238,0,153), rgb(221,0,238))",
      "linear-gradient(135deg, rgb(0,238,155), rgb(238,178,0))",
    ]
  );
  
  const scale = useTransform(scrollYProgress, [0, 1], [1, 5]);
  useMotionValueEvent(x, "change", () => {
    console.log(gradient.get());
  });
  
  useMotionValueEvent(scrollY, "change", () => {
    console.log(scrollY.get(), scrollYProgress.get());
  });
  
  return (
    <Wrapper style={{ background: gradient }}>
      <Box style={{ x, rotate, scale }} drag="x" dragSnapToOrigin />
    </Wrapper>
  );
}

SVG

svg, path 엘리먼트로 이루어진 SVG를 pathLength, fill css속성을 사용한다면 손쉽게 애니메이트할 수 있습니다.

const svgVariants = {
  start: { pathLength: 0, opacity: 0 },
  end: {
    pathLength: 1,
    opacity: 1,
    transition: { default: { duration: 5 }, opacity: { duration: 3 } },
  },
};

function App() {
  return (
    <Wrapper>
      <motion.svg
        xmlns="http://www.w3.org/2000/svg"
        fill="none"
        viewBox="0 0 24 24"
        stroke-width="1.5"
        stroke="currentColor"
        class="w-6 h-6"
      >
        <motion.path
          variants={svgVariants}
          initial="start"
          animate="end"
          stroke="white"
          strokeWidth={1}
          stroke-linecap="round"
          stroke-linejoin="round"
        />
      </motion.svg>
    </Wrapper>
  );
}

Animatepresence

Animatepresence 컴포넌트는 React에서 사라지는 컴포넌트를 애니메이트할 수 있습니다.
Animatepresence 컴포넌트 내부에 조건문에 따른 Motion 컴포넌트가 존재한다면 exit prop을 이용해 사라지는 애니메이트가 가능합니다. 이외에도 Animatepresence 컴포넌트에 mode와 같은 속성을 사용하여 애니메이트가 가능합니다.

const boxVariants = {
  initial: { opacity: 0, scale: 0, rotateZ: 45 },
  visible: { opacity: 1, scale: 1, rotateZ: 0 },
  leaving: { opacity: 0, scale: 0, y: 100 },
};

function App() {
  const [isShowing, setIsShowing] = useState(false);
  return (
    <Wrapper>
      <button onClick={() => setIsShowing((pre) => !pre)}>click</button>
      <AnimatePresence>
        {isShowing && (
          <motion.div
            variants={boxVariants}
            initial="initial"
            animate="visible"
            exit="leaving"
          />
        )}
      </AnimatePresence>
    </Wrapper>
  );
}

custom

customvariant 객체에 데이터를 보내주는 속성입니다. variant 객체는 함수 형태로 custom을 받아와 사용할 수 있습니다. Animatepresence 컴포넌트를 사용한다면 Animatepresence 컴포넌트 속성에도 cutom값을 넣어주어야 합니다.

const boxVariants = {
  entry: (isBack) => ({
    opacity: 0,
    scale: 0,
    x: isBack ? -300 : 300,
    rotateZ: isBack ? -10 : 10,
  }),
  center: {
    opacity: 1,
    scale: 1,
    x: 0,
    rotateZ: 0,
    transition: { duration: 0.3 },
  },
  exit: (isBack) => ({
    opacity: 0,
    scale: 0,
    x: isBack ? 300 : -300,
    rotateZ: isBack ? 10 : -10,
    transition: { duration: 0.3 },
  }),
};

function App() {
  const [visible, setVisible] = useState(1);
  const [isBack, setIsBack] = useState(false);
  const nextVisible = () => {
    setVisible((prev) => (prev === 10 ? 10 : prev + 1));
    setIsBack(false);
  };
  const preVisible = () => {
    setVisible((prev) => (prev === 1 ? 1 : prev - 1));
    setIsBack(true);
  };
  return (
    <Wrapper>
      <AnimatePresence mode="wait" custom={isBack}>
        <Box
          key={visible}
          custom={isBack}
          variants={boxVariants}
          initial="entry"
          animate="center"
          exit="exit"
        >
          {visible}
        </Box>
      </AnimatePresence>
      <button onClick={preVisible}>pre</button>
      <button onClick={nextVisible}>next</button>
    </Wrapper>
  );
}

layout

layout은 css의 변화에 애니메이션을 부여합니다. 단순이 Motion 컴포넌트에 layout속성을 넣으면 css의 변화를 애니메이트가 가능합니다. 더불어 layoutId를 사용한다면 서로 다른 Motion 컴포넌트를 서로를 공유하여 애니메이트가 가능합니다.

function App() {
  const [isClicked, setIsClicked] = useState(false);
  const toggleClick = () => setIsClicked((prev) => !prev);
  return (
    <Wrapper onClick={toggleClick}>
      <motion.div>
        {isClicked && (
          <Circle layoutId="circle" style={{ borderRadius: "50%", scale: 2 }} />
        )}
      </motion.div>
      <motion.div>
        {!isClicked && <Circle layoutId="circle" style={{ borderRadius: 0 }} />}
      </motion.div>
    </Wrapper>
  );
}

📝 후기

Framer Motion은 정말 간편하게 멋있는 애니메이션을 만들어 줍니다. 글을 작성하다보니 작성한 내용뿐만 아니라 더욱 다양한 기능을 제공하는 해당 라이브러리를 좀 더 사용해보고 싶었습니다. 물론 라이브러리를 완벽히 이해하고 사용하려면 바닐라 상태에서의 css 또한 완벽히 이해해야 한다는 생각이 들었습니다.


🧾 Reference

profile
기록하고 배우고 시도하고

0개의 댓글