framer-motion 사용해보기

김은서·2024년 6월 28일
0

TIL

목록 보기
51/52
post-thumbnail

framer-motion ?

framer-motion 준비

 npm i framer-motion
import { motion } from "framer-motion";

framer-motion 사용하기

// 기존 방식
<div><div>
  
  
// 태그 앞에 motion. 붙여 쓰기!
<motion.div></motion.div>

styled-component랑 같이 사용할때는?

// 기존 방식
const Box = styled.div``


// () 안에 motion. 붙여 쓰기!
const Box = styled(motion.div)``

간단한 애니메이션의 경우 animate props에서 직접 값을 설정할 수 있음!

<Box transition={{ delay: 3 }} animate={{ borderRadius: "100px" }} />

initial : 애니메이션의 초기값 지정

// scale 0에서 1로 바뀌는거! 
      <Box
        initial={{ scale: 0 }}
        animate={{ scale: 1, rotateZ: 360 }}
      />

transition : 값이 한 상태에서 다른 상태로 움직이는 방식


// teen, spring 또는 inertia를 사용할 애니메이션 유형을 정의
      <BoxSt
        transition={{ type: "spring", bounce: 0.8 }}
        initial={{ scale: 0 }}
        animate={{ scale: 1, rotateZ: 360 }}
      />

많은 transition이 있음!
framer-morion transition

Variants

  • 코드를 깔끔하게 해줌.
  • 많은 애니메이션들을 하나로 연결해줌.
// 기존 코드
      <Box
        transition={{ type: "spring", bounce: 0.8 }}
        initial={{ scale: 0 }}
        animate={{ scale: 1, rotateZ: 360 }}
      />
      
      
// Variants 사용하기

// 이름은 중요하지 않음! 원하는 이름으로 하기! 
// start, end도 마찬가지임! initial, final 등등 원하는 것으로!

const myVariants = {
 start: {},
 end: {}
}

// 코드 옮겨주기
const myVariants = {
  start: { scale: 0 },
  end: { scale: 1, rotateZ: 360, transition: { type: "spring", bounce: 0.8 } },
};

// variants props 사용하기.
// initial state(initial)랑 finishing state(animate) 지정해주기
// myVariants 오브젝트 내의 property 이름 적어주기 (start, end)

 <Box variants={myVariants} initial="start" animate="end" />

Variants 활용하기

const boxVariants = {
  start: {
    opacity: 0,
    scale: 0.5,
  },
  end: {
    scale: 1,
    opacity: 1,
    transition: {
      type: "spring",
      duration: 0.5,
      bounce: 0.5,
      delayChildren: 0.5, //딜레이 시간(초) 후에 하위 애니메이션이 시작
      staggerChildren: 0.2, //하위 컴포넌트의 애니메이션에 지속 시간(초)만큼 시차를 둘 수 있음. 4개의 서클 컴포넌트가 0.2초 간격으로 생겨남.
    },
  },
};

const circleVariants = {
  start: {
    opacity: 0,
    y: 10, //x , y는 framer motion에만 있는거
  },
  end: {
    opacity: 1,
    y: 0,
  },
};

// 부모로 부터 variant 상속됨.
// initial="start" animate="end" 를 자식까지 쓸 필요는 없음.
// start, end 이름은 꼭 같게! 그래야 부모가 자식 컴포넌트에 영향을 줄 수 있음.
      <Box variants={boxVariants} initial="start" animate="end">
        <Circle variants={circleVariants} />
        <Circle variants={circleVariants} />
        <Circle variants={circleVariants} />
        <Circle variants={circleVariants} />
      </Box>

Gestures



const boxVariants = {
//hover,click,drag 이름은 원하는걸로.
  hover: { scale: 1.5, rotateZ: 90 },
  click: { scale: 1, borderRadius: "100px" },
  drag: { backgroundColor: "rgb(46, 204, 113)", transition: { duration: 2 } },
}; 

// 컬러는 "blue" 이렇게 텍스트로 넣는것 보다는 rgb값으로 넣어주는게 좋음. 

      <Box
        variants={boxVariants}
        whileHover="hover"  //호버 제스처가 인식되는 동안 애니메이션할 속성 또는 변형 레이블
        drag //이 요소에 대해 끌기를 활성화
        whileDrag="drag" //드래그 제스처가 인식되는 동안 애니메이션할 속성 또는 변형 레이블
        whileTap="click" //컴포넌트를 누르고 있는 동안 애니메이션할 속성 또는 변형 레이블
      />
      
      
      //drag
      //특정 방향으로만 드래그하려면 "x" 또는 "y"를 설정
      //< motion.div drag="x" / >

      
      

drag 활용해보기

      <BiggerBox>
        <Box
          variants={boxVariants}
          whileHover="hover"
          drag
          dragConstraints={{ top: -200, bottom: 200, left: -200, right: 200 }} //허용된 드래그 가능 영역에 제약 조건을 적용, dragConstraints 에는 드래그 가능한 컴포넌트의 가장자리 거리를 정의
          dragSnapToOrigin // 드래그를 놓을 때, 원점으로 다시 애니메이션
          dragElastic={0.5} //외부 제약 조건에서 허용되는 이동 정도. 0 = 움직임 없음, 1 = 전체 움직임. 기본적으로 0.5로 설정
          whileDrag="drag"
          whileTap="click"
        />
      </BiggerBox>

MotionValue

  • 애니메이션 값의 상태(state)와 속도(velocity)를 추적
  • MotionValue는 React State가 아니기 때문에 Motion Value값이 바뀌어도 리랜더링이 일어나지 않음
import { motion, useMotionValue, useMotionValueEvent } from "framer-motion";

const x = useMotionValue(0);

    <Wrapper>
      <button onClick={() => x.set(200)}>Click Me</button> 
      <Box style={{ x: x }} drag="x" dragSnapToOrigin />
    </Wrapper>

// x.set(100)
// set 메서드로 업데이트할 수 있음 , 이것은 React 리렌더링을 트리거하지 않음

// x.get() // 100
// MotionValue는 문자열이나 숫자가 될 수 있음
// get 메소드로 값을 읽을 수 있음

MotionValue 활용해보기

useTransform

: useTransform 훅을 통해 MotionValues를 연결
useTransform()는 한 값 범위에서 다른 값 범위로 매핑하여 다른 MotionValue의 output을 변환하는 MotionValue를 만듬
x(Motion Value)값을 다른 output값으로 변환함
ex) x: -400 => 1

import {
  motion,
  useMotionValue,
  useMotionValueEvent,
  useTransform,
} from "framer-motion";

const x = useMotionValue(0)
// Box돌리기
    const rotateZ = useTransform(x, [-800, 800], [-360, 360]);
// 배경 색 바꾸기
  const gradient = useTransform(
    x,
    [-800, 800],
    [
      "linear-gradient(135deg, rgb(0, 210, 238), rgb(0, 83, 238))",
      "linear-gradient(135deg, rgb(0, 238, 155), rgb(238, 178, 0))",
    ]
  );
  useMotionValueEvent(x, "change", (x) => {
    console.log("x :", x);
  });


<Wrapper style={{ background: gradient }}>
  <Box style={{ x, rotateZ }} drag="x" dragSnapToOrigin />
</Wrapper> 

useScroll

: 뷰포트가 스크롤될 때 업데이트되는 MotionValues를 리턴
아래 값들은 모두 MotionValue< number >를 넘겨줌
scrollX: 실제 수평 스크롤 픽셀 ex) 500px
scrollY: 실제 수직 스크롤 픽셀 ex) 500px
scrollXProgress : 0 ~ 1 사이의 수평 스크롤
scrollYProgress : 0 ~ 1 사이의 수직 스크롤(가장 상단 0, 가장 하단 1)


  const x = useMotionValue(0);

  const rotateZ = useTransform(x, [-800, 800], [-360, 360]);
  const gradient = useTransform(
    x,
    [-800, 800],
    [
      "linear-gradient(135deg, rgb(0, 210, 238), rgb(0, 83, 238))",
      "linear-gradient(135deg, rgb(0, 238, 155), rgb(238, 178, 0))",
    ]
  );

  const { scrollY, scrollYProgress } = useScroll();

  const scale = useTransform(scrollYProgress, [0, 1], [1, 5]);
  useMotionValueEvent(scrollY, "change", (latest) => {
    console.log("scrollY : ", latest);
  });

  useMotionValueEvent(scrollYProgress, "change", (latest) => {
    console.log("scrollYProgress : ", latest);
  });


  return (
    <Wrapper style={{ background: gradient }}>
      <Box style={{ x, rotateZ, scale }} drag="x" dragSnapToOrigin />
    </Wrapper>

SVG

Line drawing

svg 엘리먼트에 'pathLength', 'pathSpacing', 'pathOffset' 속성을 사용하여 Line drawing 애니메이션을 만들 수 있음

path (SVG)

path SVG 엘리먼트는 모양을 정의하는 일반 엘리먼트
모든 기본 모양은 path 엘리먼트로 만들 수 있음
path의 속성 d는 경로의 모양을 정의함

Path

motion.path 컴포넌트는 세 가지 강력한 SVG path 속성인 pathLength, pathSpacing 및 pathOffset을 가지고 있음
수동 경로 측정이 필요 없이 모두 0과 1 사이의 값으로 설정됨

SVG 활용하기 (애니메이션 로고 만들기)

const Svg = styled.svg`
  width: 300px;
  height: 300px;
  path {
    stroke: white;
    stroke-width: 2;
  }
`;

const svgVariants = {
  start: { pathLength: 0, fill: "rgba(255,255,255,0" },
  end: {
    pathLength: 1,
    fill: "rgba(255,255,255,1",
    // transition: { duration: 3 }, // 이렇게 주면 모두 다 5초
  },
};


      <Svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
        <motion.path
          variants={svgVariants}
          initial={"start"}
          animate={"end"}
          transition={{ // 이렇게 주면 원하는 프로퍼티별로 각각 지정 가능
            default: { duration: 5 },
            fill: { duration: 2, delay: 2 },
          }}
          d="M224 373.1c-25.2-31.7-40.1-59.4-45-83.2-22.6-88 112.6-88 90.1 0-5.5 24.3-20.3 52-45 83.2zm138.2 73.2c-42.1 18.3-83.7-10.9-119.3-50.5 103.9-130.1 46.1-200-18.9-200-54.9 0-85.2 46.5-73.3 100.5 6.9 29.2 25.2 62.4 54.4 99.5-32.5 36.1-60.6 52.7-85.2 54.9-50 7.4-89.1-41.1-71.3-91.1 15.1-39.2 111.7-231.2 115.9-241.6 15.8-30.1 25.6-57.4 59.4-57.4 32.3 0 43.4 25.9 60.4 59.9 36 70.6 89.4 177.5 114.8 239.1 13.2 33.1-1.4 71.3-37 86.6zm47-136.1C280.3 35.9 273.1 32 224 32c-45.5 0-64.9 31.7-84.7 72.8C33.2 317.1 22.9 347.2 22 349.8-3.2 419.1 48.7 480 111.6 480c21.7 0 60.6-6.1 112.4-62.4 58.7 63.8 101.3 62.4 112.4 62.4 62.9 .1 114.9-60.9 89.6-130.2 0-3.9-16.8-38.9-16.8-39.6z"
        />
      </Svg>

AnimatePresence

AnimatePresence는 안쪽에서 나타나거나 사라지는게 있으면 그것을 animate 할 수 있도록 해줌.

사용 규칙

    1. visible 상태여야함.
    1. AnimatePresence의 내부에는 condition(조건문)이 있어야함.

import styled from "styled-components";
import { motion, AnimatePresence, Variants } from "framer-motion";
import { useState } from "react";


const Wrapper = styled(motion.div)`
  height: 100vh;
  width: 100vw;
  display: flex;
  justify-content: center;
  align-items: center;
`;

const Box = styled(motion.div)`
  width: 400px;
  height: 200px;
  background-color: rgba(255, 255, 255, 1);
  border-radius: 40px;
  position: absolute;
  top: 100px;
  box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.06);
`;

const boxVariants: Variants = {
  initial: {
    opacity: 0,
    scale: 0,
  },
  visible: {
    opacity: 1,
    scale: 1,
    rotate: 360,
  },
  leaving: {
    opacity: 1,
    scale: 0,
    y: 50,
  },
};

const AnimatePresenceStudy = () => {
  const [showing, setShowing] = useState(false);
  const toggleShowing = () => setShowing((prev) => !prev);


  return (
    <Wrapper>
      <button onClick={toggleShowing}>Click</button>
      <AnimatePresence> //AnimatePresence의 내부에는 condition(조건문)이 있어야함.
        {showing ? ( 
          <Box
            variants={boxVariants}
            initial="initial"
            animate="visible"
            exit="leaving"
          />
        ) : null}
      </AnimatePresence>
    </Wrapper>
  );
};


export default AnimatePresenceStudy;

AnimatePresence 활용해서 Slider 만들어보기

  • custom
    :각 애니메이션 컴포넌트에 대해 동적 variants를 다르게 적용할 때 사용할 수 있는 사용자 지정 데이터
    : variants에 데이터를 보낼 수 있게 해줌
  • custom example :
const variants = {
visible: (custom) => ({
opacity: 1,
transition: { delay: custom * 0.2 }
})
}

< motion.div custom={0} animate="visible" variants={variants} />
< motion.div custom={1} animate="visible" variants={variants} />
< motion.div custom={2} animate="visible" variants={variants} />
  • slider 만들기
import styled from "styled-components";
import { motion, AnimatePresence, Variants } from "framer-motion";
import { useState } from "react";

const Wrapper = styled(motion.div)`
  height: 100vh;
  width: 100vw;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
`;

const Box = styled(motion.div)`
  width: 400px;
  height: 200px;
  background-color: rgba(255, 255, 255, 1);
  border-radius: 40px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 28px;
  box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.06);
  position: absolute;
  top: 100px;
`;

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



const SliderStudy = () => {
  const [visible, setVisible] = useState(1);
  const [back, setBack] = useState(false);
  const prevSlider = () => {
    setBack(true);
    setVisible((prev) => (prev === 1 ? 1 : prev - 1));
  };
  const nextSlider = () => {
    setBack(false);
    setVisible((prev) => (prev === 10 ? 10 : prev + 1));
  };
  return (
    <Wrapper>
      <AnimatePresence mode="wait" custom={back}>
        <Box
          variants={boxVariants}
          custom={back} // true냐 false냐에 따라 entry, exit이 달라짐.
          initial="entry"
          animate="center"
          exit="exit"
          key={visible}
        >
          {visible}
        </Box>
      </AnimatePresence>
      <button onClick={prevSlider}>prev</button>
      <button onClick={nextSlider}>next</button>
    </Wrapper>
  );
};

export default SliderStudy;

Layout

Layout animation

true인 경우 이 컴포넌트는 레이아웃이 변경될 때 새 위치에 자동으로 애니메이션을 적용함
크기나 위치가 변경될 때 모션 컴포넌트의 레이아웃에 자동으로 애니메이션을 적용하려면 레이아웃 prop을 제공함
부모 플렉스박스 방향, 너비, 상단/오른쪽 등 레이아웃 변경의 원인이 무엇이든 상관없이 애니메이션 자체는 최대 성능을 위해 변환으로 수행됨
ex)

< motion.div layout>< /motion.div>
  • layoutId로 연결
import styled from "styled-components";
import { motion } from "framer-motion";
import { useState } from "react";

const Wrapper = styled(motion.div)`
  height: 100vh;
  width: 100vw;
  display: flex;
  justify-content: space-around;
  align-items: center;
`;

const Box = styled(motion.div)`
  width: 400px;
  height: 400px;
  background-color: rgba(255, 255, 255, 1);
  border-radius: 40px;
  display: flex;
  justify-content: center;
  align-items: center;
  box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.06);
`;

const Circle = styled(motion.div)`
  background-color: #00a5ff;
  height: 100px;
  width: 100px;
  box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.06);
`;

const LayoutStudy = () => {
  const [clicked, setClicked] = useState(false);
  const toggleClicked = () => setClicked((prev) => !prev);
  return (
    <Wrapper onClick={toggleClicked}>
      <Box>
        {!clicked ? (
          <Circle layoutId="circle" style={{ borderRadius: 50 }} />
        ) : null}
      </Box>
      <Box>
        {clicked ? (
          <Circle layoutId="circle" style={{ borderRadius: 0, scale: 2 }} />
        ) : null}
      </Box>
    </Wrapper>
  );
};
export default LayoutStudy;
  • layoutId는 string이어야함.
import styled from "styled-components";
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";

const Wrapper = styled(motion.div)`
  height: 100vh;
  width: 100vw;
  display: flex;
  justify-content: space-around;
  align-items: center;
`;

const Grid = styled.div`
  width: 50vw;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  div:first-child,
  div:last-child {
    grid-column: span 2;
  }
`;

const Box = styled(motion.div)`
  height: 200px;
  background-color: rgba(255, 255, 255, 1);
  border-radius: 40px;
  box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.06);
`;

const Overlay = styled(motion.div)`
  width: 100%;
  height: 100%;
  position: absolute;
  display: flex;
  justify-content: center;
  align-items: center;
`;


const Modal2 = () => {
  const [id, setId] = useState<null | string>(null);

  return (
    <Wrapper>
      <Grid>
        {["1", "2", "3", "4"].map((n) => (
          <Box onClick={() => setId(n)} key={n} layoutId={n} />
        ))}
      </Grid>
      <AnimatePresence>
        {id ? (
          <Overlay
            onClick={() => setId(null)} //  박스 바깥 영역 클릭하면 사라지게 만들기
            initial={{ backgroundColor: "rgba(0, 0, 0, 0)" }}
            animate={{ backgroundColor: "rgba(0, 0, 0, 0.5)" }}
            exit={{ backgroundColor: "rgba(0, 0, 0, 0)" }}
          >
            <Box layoutId={id} style={{ width: 400, height: 200 }} />
          </Overlay>
        ) : null}
      </AnimatePresence>
    </Wrapper>
  );
};

export default Modal2;

0개의 댓글