[React] Framer Motion

찐새·2022년 8월 18일
0

React

목록 보기
10/21
post-thumbnail

Framer Motion

  • 컴포넌트의 애니메이션을 손쉽게 구성하도록 도와주는 매우 어썸한 라이브러리

Install

  • npm i framer-motion
  • react-create-app ver.5 부터 정식 지원한다.
  • react-create-app ver.4 을 사용하여 호환되지 않는 에러 발생 시 npm i @craco/craco --save를 설치해 필요한 모듈을 오버라이드한다.
    • CRACOCreate React App Configuration Override의 약자로, craco.config.js을 추가해 필요한 모듈 상세를 사용자 지정으로 사용할 수 있다.
    • https://github.com/dilanx/craco

// craco.config.js

module.exports = {
  webpack: {
    configure: {
      module: {
        rules: [
          {
            type: "javascript/auto",
            test: /\.mjs$/,
            include: /node_modules/,
          },
        ],
      },
    },
  },
};

Usage

import { motion } from "framer-motion";
import { useState } from "react";

function App() {
  const [toggleOpa, setToggleOpa] = useState(false);
  return (
    <div>
      <motion.div
        animate={{ opacity: toggleOpa ? 1 : 0 }}
        style={{ width: 200, height: 200, backgroundColor: "red" }}
      >
        Hi!
      </motion.div>
      <button onClick={() => setToggleOpa((prev) => !prev)}>
        Are you visible?
      </button>
    </div>
  );
}

export default App;
  • <motion.{element}>를 만든 후 속성을 추가한다.

  • styled components와 결합하여 사용할 수 있다.

import styled from "styled-components";
import { motion } from "framer-motion";

const Box = styled(motion.div)`
  width: 200px;
  height: 200px;
  background-color: white;
  border-radius: 10px;
`;

function App() {
  return (
    <div>
      <Box animate={{ opacity: toggleOpa ? 1 : 0 }} />
      <button onClick={() => setToggleOpa((prev) => !prev)}>
        Are you visible?
      </button>
    </div>
  );
}

export default App;

properties

  • 초깃값은 initial로 설정한다.
<motion.div initial={{ scale: 0 }} />

https://www.framer.com/docs/component/###initial

  • 애니메이션 효과는 animate로 설정한다.
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} />

https://www.framer.com/docs/component/###animate

  • 한 상태에서 다른 상태로 값이 움직이는 방식은 transition으로 설정한다.
<motion.div
  initial={{ scale: 0 }}
  animate={{ scale: 1 }}
  transition={{ type: "spring" }}
/>
  • 두 개 이상의 애니메이션의 transition을 따로 설정할 수 있다.
const vars = {
  start: { opacity: 0, scale: 1 },
  end: {
    opacity: 1,
    scale: 1.5,
    transition: {
      default: { duration: 2 },
      scale: { duration: 2, delay: 2 },
    },
  },
};

const App = () => {
  return (
    <motion.div
      style={{ width: 200, height: 200, backgroundColor: "white" }}
      variants={vars}
      initial="start"
      animate="end"
    />
  );
};

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

  • variants는 다른 여러 애니메이션을 하나의 애니메이션으로 사용할 수 있게 한다.
const vars = {
  start:{ scale: 0 },
  end:{
    scale: 1,
    transition={
       type: "spring"
    }
  }
}

<motion.div variants={vars} initial="start" animate="end" />

https://www.framer.com/docs/introduction/##variants

  • 자식 요소를 가진 variants도 설정할 수 있다.
  • 부모 variantsinitial(start)animate(end) 속성은 자동으로 자식 요소에게 상속된다,
    • 자식 variants 속성명을 같게 하면 컴포넌트에 적지 않아도 된다.
  • 여러 자식 요소의 애니메이션 delay 등 시간을 다르게 설정하고 싶다면 부모 variantsend:{transition}staggerChildren을 추가한다.
const parentVars = {
  start:{ scale: 0 },
  end:{
    scale: 1,
    transition={
       type: "spring"
       staggerChildren: 0.2,
    }
  }
}

const childrenVars {
  start: { opacity: 0 },
  end: { opacity: 1 },
}

<motion.div variants={parentVars} initial="start" animate="end">
  {[1, 2, 3, 4].map((v) => (
    <motion.div key={v} variants={circleVariants} />
  ))}
</motion.div>

https://www.framer.com/docs/transition/#orchestration

  • mouseEvent와 섞을 때는 간단히 while~~ 속성으로 사용할 수 있다.
<motion.div
  whileHover={{ scale: 1.5, rotateZ: 90 }}
  whileTap={{ borderRadius: "100px", scale: 1 }}
/>
  • whileHover는 마우스을 hover했을 때, whileTap은 마우스를 클릭하고 있을 때 동작한다.

  • variants와 결합하여 사용할 수 있다.

const vars = {
  hover: { scale: 1.5, rotateZ: 90 },
  click: { borderRadius: "100px", scale: 1 },
}

<motion.div variants={vars} whileHover="hover" whileTap="click" />

https://www.framer.com/docs/gestures/#hover

  • drag 속성을 추가하면 요소를 드래그할 수 있다.
  • whileDrag로 드래그하는 동안 애니메이션을 추가할 수 있다.
    • 색상은 rbga값으로 넣어야 애니메이션이 적용된다.
<motion.div drag whileDrag={{ backgroundColor: "rgba(255, 159, 67, 1.0)" }} />
  • dragxy등으로 축을 제약할 수 있다.
<motion.div drag="x" />
<motion.div drag="y" />
  • drag 제약을 사용자 지정으로 설정하려면 dragConstraints로 설정한다.
<motion.div
  drag
  dragConstraints={{ top: -50, bottom: 50, left: -50, right: 50 }}
/>
  • 지정한 영역을 넘어서면 제약한 공간 안쪽으로 돌아온다.
  • 넘어섰을 경우 제자리로 오게 하려면 dragSnapToOrigin을 추가한다.
<motion.div
  drag
  dragSnapToOrigin
  dragConstraints={{ top: -50, bottom: 50, left: -50, right: 50 }}
/>

https://www.framer.com/docs/gestures/#drag

  • motion이 움직이는 값을 useMotionValue로 추적할 수 있다.
function App() {
  const x = useMotionValue(0);
  console.log(x);
  return (
    <>
      <motion.div drag="x" dragSnapToOrigin style={{ x }} />
    </>
  );
}

export default App;
  • 하지만 console.log(x)는 아무것도 반환하지 않는다.
    • motionValueReact State가 아니기 때문에 re-render하지 않기 때문이다.
    • useEffect를 통해 확인할 수 있다.
function App() {
  const x = useMotionValue(0);
  useEffect(() => {
    x.onChange(() => console.log(x.get()));
  }, [x]);
  return (
    <>
      <motion.div drag="x" dragSnapToOrigin style={{ x }} />
    </>
  );
}

export default App;
  • x의 설정은 x.set()으로 한다.

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

  • useTransformmotionValue에 따른 옵션 설정이 가능한 기능이다.
    • 연결된 축의 값을 원하는 다른 값으로 변환해 반환한다.
    • 인자는 (, 축의 값[], 변환할 값[])을 받는다.
const x = useMotionValue(0);
const tmpScale = useTransform(x, [-300, 0, 300], [2, 1, 0.1]);

<motion.div drag="x" dragSnapToOrigin style={{ x, scale: tmpScale }} />;

https://www.framer.com/docs/use-transform/

  • useScroll은 스크롤의 위치를 반환한다.
    • scrollX || scrollY는 스크롤의 픽셀 좌표를 나타내고, scrollXProgress || scrollYProgress는 현재 스크롤이 위치한 퍼센티지를 나타낸다.
const { scrollY, scrollYProgress } = useScroll();
useEffect(() => {
  scrollY.onChange(() => {
    console.log(scrollY.get(), scrollYProgress.get());
    // 198 0.31118314424635335
  });
}, [scrollY]);

https://www.framer.com/docs/use-scroll/

  • 상태에 따른 애니메이션을 넣을 때는 <AnimatePresence>을 사용한다.
  • <AnimatePresence> 안에는 조건 분기가 존재해야 한다.
    • 내부에 조건에 따라 사라지는 요소가 있는지 판단하기 때문이다.
  • initialanimate 입력은 다른 것과 같고, 분기 판단 속성으로 exit을 추가한다.
function App() {
  const [visible, setVisible] = useState(false);
  const onToggle = () => setVisible((prev) => !prev);
  return (
    <>
      <button onClick={onToggle}>Click</button>
      <AnimatePresence>
        {visible ? (
          <Box
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
          />
        ) : null}
      </AnimatePresence>
    </>
  );
}

https://www.framer.com/docs/animate-presence/

  • 분기에 따른 variants 조정은 custom 속성을 사용한다.
  • custom을 사용할 때는 variants의 속성이
const vars = {
  init: (direction) => ({
    opacity: 0,
    x: direction ? 300 : -300,
  }),
  animate: {
    opacity: 1,
  },
  exit: (direction) => ({
    opacity: 0,
    x: direction ? -300 : 300,
  }),
};

function App() {
  const [visible, setVisible] = useState(false);
  const onToggle = () => setVisible((prev) => !prev);
  return (
    <>
      <button onClick={onToggle}>Click</button>
      <AnimatePresence>
        <Box initial="init" animate="animate" exit="exit" />
      </AnimatePresence>
    </>
  );
}
  • layout 속성은 layout이 바뀔 때 애니메이션을 제공한다.
const Box = styled(motion.div)`
  width: 400px;
  height: 400px;
  background-color: white;
  border-radius: 20px;
  display: flex;
  justify-content: flex-start;
  align-items: flex-start;
`;

const Circle = styled(motion.div)`
  background-color: black;
  height: 100px;
  width: 100px;
  border-radius: 50px;
`;

function App() {
  const [clicked, setClicked] = useState(false);
  const toggleClicked = () => setClicked((prev) => !prev);
  return (
    <div onClick={toggleClicked}>
      <Box
        style={{
          justifyContent: clicked ? "center" : "flex-start",
          alignItems: clicked ? "center" : "flex-start",
        }}
      >
        <Circle layout />
      </Box>
    </div>
  );
}

export default App;
  • layoutId가 같으면 다른 컴포넌트더라도 같은 레이아웃을 사용하는 것처럼 애니메이션을 적용할 수 있다.
const Box = styled(motion.div)`
  width: 400px;
  height: 400px;
  background-color: white;
  border-radius: 20px;
  display: flex;
  justify-content: center;
  align-items: center;
`;

const Circle = styled(motion.div)`
  background-color: black;
  height: 100px;
  width: 100px;
  border-radius: 50px;
`;

function App() {
  const [clicked, setClicked] = useState(false);
  const toggleClicked = () => setClicked((prev) => !prev);
  return (
    <div onClick={toggleClicked}>
      <Box>{!clicked ? <Circle layoutId="circle" /> : null}</Box>
      <Box>{clicked ? <Circle layoutId="circle" /> : null}</Box>
    </div>
  );
}

export default App;

https://www.framer.com/docs/animate-shared-layout/#syncing-layout-animations

  • 커스텀 무한 애니메이션도 가능하다.
const vars = {
  normal: {
    opacity: 0,
  },
  infinite: {
    opacity: [0, 1, 0],
    transition: {
      repeat: Infinity,
    },
  },
};

function App() {
  const [visible, setVisible] = useState(false);
  const onToggle = () => setVisible((prev) => !prev);
  return (
    <>
      <button onClick={onToggle}>Click</button>
      <AnimatePresence>
        <Box initial="normal" whileHover="infinite" />
      </AnimatePresence>
    </>
  );
}
  • 효과에 배열을 넣으면 해당 값을 반복한다.
  • transitionrepeat은 정해진 값만큼 반복하는데, 그 값으로 javascript 내장값인 Infinity를 주면 무한히 반복한다.

참고

노마드 코더 - React JS 마스터클래스
Framer Motion Docs

profile
프론트엔드 개발자가 되고 싶다

0개의 댓글