AnimateSharedLayout | Framer for Developers

 

AnimateSharedLayout | Framer for Developers

Animate layout changes across, and between, multiple components.

www.framer.com

https://github.com/framer/motion

 

GitHub - framer/motion: Open source, production-ready animation and gesture library for React

Open source, production-ready animation and gesture library for React - GitHub - framer/motion: Open source, production-ready animation and gesture library for React

github.com

애니메이션을 쉽게 사용하게 해주는 라이브러리이며 디자이너가 개발할 때 활용하는 Framer라는 곳에서 개발하였습니다.


기본문법

import React from 'react';
import styled from "styled-components"
import {GlobalStyle} from "./styles/global-styles"
import {motion } from "framer-motion"

function App() {
  return (
    <>
      <GlobalStyle/>
      <Wrapper>
        <Box
          transition={{delay:0.5, type:"spring", }} 
          initial={{scale:0}}  
          animate={{scale:1, rotateZ:360}}/>
      </Wrapper>
    </>
  );
}

const Wrapper  = styled.div`
  width: 100vw;
  height: 100vh;
  background-color: black;
  display: flex;
  justify-content: center;
  align-items: center;
`

const Box = styled(motion.div)` // Framer-motion과 styled컴포넌트를 사용하는 방법
  width: 200px;
  height: 200px;
  background-color: red;
  border-radius: 15px;
  box-shadow: 0 2px 3px rgba(0,0,0,0.1), 0 10px 20px rgba(0,0,0,0.6);
`

export default App;

기본적으로 <motion.div animate={{ x: 0 }} /> 이러한 구조를 가지고 있는데

제가 세팅한 환경에서 styled-components와 같이 쓰기 위해서는 단순히 motion.컴포넌트명 으로 사용할 순 없습니다.

그래서 이것을 사용하기 위해서는 기존에 컴포넌트로 선언하는 부분에서 motion 을 사용할 것을 명시해야 합니다.

const Box = styled(motion.div)` // Framer-motion과 styled컴포넌트를 사용하는 방법
 ...
`

const Box = styled.div` //motion.div로 쓰지 않으면 사용할 수 없다
	...
`

기본적인 props들은 아래 공식 문서에서 활용할 수 있습니다.

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

 

Documentation | Framer for Developers

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

www.framer.com


Variants

variants는 기본적으로 코드를 깔끔하게 하기위해 Object로 묶어 변수형식을 사용할 수 있습니다

  • 기본형식
<Box
    transition={{delay:0.5, type:"spring", }} 
    initial={{scale:0}}  
    animate={{scale:1, rotateZ:360}}
/>
  • variants
const BoxVars = { 
  start : {
    opacity:0,
    scale:0.5
  },
  end : {
    scale:1,
    opacity:1,
    transition: {
      type:"spring",
      delay:0.5,
    }
  }
}

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

훨씬 더 간결해지고 재사용 할 수 있는 코드로 바뀌었습니다.

또한 variants의 장점은 이걸로 끝나지 않는데 위와 같이 Box에서 자식요소가 있을 때 코드를 훨씬 더 간결하게 표현할 수 있게 됩니다.

const BoxVars = { 
  start : {
    opacity:0,
    scale:0.5
  },
  end : {
    scale:1,
    opacity:1,
    transition: {
      type:"spring",
      delay:0.5,
      duration:0.5,
      bounce:0.5,
      delayChildren:1,
      staggerChildren:0.5
    }
  }
}

const CircleVars = {
  start : {
    opacity:0,
    y:10

  },
  end: {
    opacity:1,
    y:0,
  },
}

<Box variants={BoxVars} initial="start" animate="end"> 
	<Circle variants={CircleVars}/>
	<Circle variants={CircleVars}/>
	<Circle variants={CircleVars}/>
	<Circle variants={CircleVars}/>
</Box>

위 코드에서 기본적으로 부모요소에 variants가 있을 때 Framer-Motion은 default값을 initial과 animate의 이름을 자식에게 복사해서 붙여넣어줍니다

 

<Circle initial="start" animate="end"/> 이런 구조를 가지게 되며 자식요소에서 variants를 쓸 때 intial="start"와 animate="end"를 또 적어서 전달해줄 필요는 없게 되지만 위 선언한 CircleVars 에서의 Object 키 값은 일치해야 합니다.( 위에선 “start”와 “end”로 작성함)

 

이처럼 자식요소를 감지하는 기능 때문에 부모요소에서 자식에게 명령을 내릴 수 있는데 delayChildren , staggerChildren 이 있습니다.


delayChildren : 말 그대로 자식요소(전체)에게 delay를 적용시키는 것입니다.

staggerChildren : 자식요소(각각 개별)에 각 순서에 따라 지연시간을 적용시킵니다

 

ex) 첫 번째 <Circle/> 에게 0.5.. 두 번째 <Circle/> 에서 0.5 * 2... 순차적으로 적용시켜주는 명령어 입니다.

 

이렇게 variants는 자식요소를 갖게 되었을 때 더 간결하고 쉽게 사용할 수 있으며 동작은

https://www.framer.com/motion/ 에서 Examples를 통해서 확인할 수 있습니다.

 

Production-Ready Animation Library for React | Framer Motion

Framer Motion is a production-ready React animation and gesture library.

www.framer.com


Drag

Drag는 말그대로 Drag할 수 있게 만들어줍니다

<Box drag/> //그냥 drag만 붙이면 사용가능
<Box drag="x"/> //x축으로 drag가능
<Box drag="y"/> //y축으로 drag가능
  • Drag를 사용한 하나의 예시
const constraintsRef = useRef<HTMLDivElement>(null)

return (
  <Wrapper ref={constraintsRef}>
    <Box
      drag
      dragConstraints={constraintsRef}
    />
  </Wrapper>
)

const Wrapper = styled(motion.div)`
  width: 600px;
  height: 500px;
  background-color: rgba(255,255,255,0.2);
  border-radius: 40px;
  display: flex;
  justify-content: center;
  align-items: center;
  overflow: hidden;
`

const Box = styled(motion.div)` // Framer-motion과 styled컴포넌트를 사용하는 방법
  width: 200px;
  height: 200px;
  background-color: rgba(255,255,255,0.2);
  border-radius: 40px;
  /* display: grid;
  grid-template-columns: repeat(2, 1fr); */
  box-shadow: 0 2px 3px rgba(0,0,0,0.1), 0 10px 20px rgba(0,0,0,0.6);
`
<Box 
    drag 
    dragSnapToOrigin 
    dragElastic={0}
    dragConstraints={biggerBoxRef} 
    variants={BoxVariants} 
    whileHover={"hover"} 
    whileDrag={"drag"} 
    whileTap={"click"}
/>

dragSnapToOrigin :원 상태로 복귀

dragElastic:탄성.. 마우스로 끌어당기는게 무거워진다

dragConstraints: 부모에 크기만큼 drag를 할 수 있게 만들어준다(부모를 useRef로 지정해야 함)

whileHover : hover했을 때

whileDrag :drag중일 때

whileTap : 클릭했을 때

→ 이것들은 그냥 참고로만 알아두자 공식문서에 다 나와있다.


useMotionValue, useTransform

const xDrag = useMotionValue(0); //  ex) xDrag는 x값을 받아오기 위한 MotionValue이다

<Box style={{ x:xDrag, rotateZ, scale}}  drag="x" dragSnapToOrigin/>

기본적으로 useMotionValue는 hooks처럼 상태가 변경되었을 때 리렌더링 되지 않는다.

값을 받아오더라도 갱신이 되지 않는다고 이해하면 된다. 만약 값(애니메이션에 대한)을 받아올 때 갱신이 발생한다면 상당한 자원낭비를 할 수 있게 되므로 갱신되지 않아야 한다.

(React.memo)

 

위 코드에서는 x축으로 drag될 때 x축 위치를 받아오는 xDrag변수가 있다.

이것을 사용하기 위해서 useTransform을 사용함

const rotateZ = useTransform(xDrag, [-800, 800], [-360, 360]);

useTransForm은 useMotionValue에서 값을 받아온 뒤 입력한 범위값에 따라 그에 맞는 범위의 출력값을 얻게 해준다

  • (한가지 값)을 받아와서 (우리가 확인해줬으면 하는 입력값 범위에 따라서) (그에 맞는 범위의 출력값)을 얻을 수 있게 해준다
const xDrag = useMotionValue(0); //이 x는 x값을 받아오기 위한 MotionValue이다
const rotateZ = useTransform(xDrag, [-800, 800], [-360, 360]);

<Box style={{ x:xDrag, rotateZ}}  drag="x" dragSnapToOrigin/>

현재 x값을 xDrag에 받아온 뒤 useTransform에서 필요한 3가지 값들인 확인해줬으면 하는 값, 그 값의 범위 , 그에 맞는 출력값을 입력하면 된다. (입력값 범위와 출력값의 범위 갯수는 같아야한다, 배열의 원소 갯수)

 

위 코드를 해석해보면 x축에서 xDrag가 -800, 800 값의 범위만큼 움직일 때 -360부터 360까지의 값으로 바꿔서 출력해달라는 것과 같고 이것을 rotateZ로 사용하였으니 Box를 x축으로 입력한 범위 만큼 drag할 때마다 출력값만큼 회전, 역회전한다는 것을 알 수 있다

 

말로 풀어쓰니 상당히 어렵다..

아무튼 useTransform은 어떠한 값을 받아서 움직일 때 지정한 입력범위만큼의 값을 출력범위로 나눠 출력시켜 준다.



useViewportScroll

const {scrollY, scrollYProgress} = useViewportScroll();
const scale = useTransform(scrollYProgress, [0, 1], [1,5])

<Box style={{ scale }}/>

useViewportScroll() 은 스크롤 값을 반환해 주는데 scrollY는 픽셀단위, progress는 퍼센트로 반환을 해준다.

위 코드는 스크롤에 따라 값이 커지고 작아지는 효과를 가진다.


Path

Framer-motion에 있는 Path는 svg이미지에 애니메이션을 추가해준다

import styled from 'styled-components'
import { motion, useMotionValue, useTransform, useViewportScroll } from "framer-motion"

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

const svg = {
  start: {
     pathLength:0, fill: "rgba(255,255,255,0)"
  },
  end : {
    pathLength:1,
    fill: "rgba(255,255,255,1)",
    transition:{
      default: {duration:5},
      fill:{duration:2, delay:3}
    }
  }
}

function SVG() {
    return (
        
        
            
        
    )
}

export default SVG

기본적으로 https://fontawesome.com/ 에 가서 개발자도구를 열어보면 svg이미지를 가져올 수 있다.

그리고 styled-components로 svg에 CSS를 적용할 수 있고 애니메이션을 구현하기 위해

<svg></svg> 안에 path.motion 으로 만들어 주어야 한다. 그리고 path에 animation을 적용시키면 된다

 

path : 2가지 CSS를 적용시켰는데 path 자체에 fill을 넣는다면 내부 색깔이 바뀐다.

stroke,stroke-Width : 적용시킨다면 border 효과처럼 내부의 라인들에 색깔과 굵기가 바뀌게 된다

pathLength : svg이미지의 0~1의 비율을 가지고 svg 전체에 일정비율로 채우는 효과를 가진다

//variants

const svg = {
  start: {
     pathLength:0, fill: "rgba(255,255,255,0)"
  },
  end : {
    pathLength:1,
    fill: "rgba(255,255,255,1)",
    transition:{
      default: {duration:5},
      fill:{duration:2, delay:3}
    }
  }
}

transition효과를 줄 때 바로 duration 같은 option값을 주게 된다면 전체에 적용되는 default로 적용이 된다. 하지만 transition에 default를 준다면 각 animation Effect 마다 각각의 transition 옵션 값들을 줄 수 가 있다


AnimatePresence

컴포넌트가 사라질 때 애니메이션 동작을 지시하는 Framer-motion에 기능이다

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

//AnimatePresence : 컴포넌트가 사라질 때 애니메이션 동작을 지시

const Box = styled(motion.div)` // Framer-motion과 styled컴포넌트를 사용하는 방법
  width: 200px;
  height: 200px;
  background-color: rgba(255,255,255,0.2);
  border-radius: 40px;
  /* display: grid;
  grid-template-columns: repeat(2, 1fr); */
  box-shadow: 0 2px 3px rgba(0,0,0,0.1), 0 10px 20px rgba(0,0,0,0.6);
  position: absolute;
  top:10px;
`

const BoxVariants = {
    //AnimatePresence variants는 조금다르다
    initial : {
        opacitiy:0,
        scale:0,
    },
    visiable : {
        opacity:1,
        scale:1,
        rotateZ:360,
    },
    exit : {
        opacity:0,
        scale:0,
        y:100,
    }
}

function AnimatePresenceEx() {

    const [showing, setShowing] = useState(false);
    const toggleShowing = () => setShowing((prev) => !prev)
  
    return (
        //AnimatePresence의 한가지 규칙은 visible 상태여야 한다.
        //children으로 조건문이 있어야 한다
        <>
            <AnimatePresence>
                {showing ? <Box variants={BoxVariants} initial="initial" animate="visiable" exit="exit"/> : null}
            </AnimatePresence>
            {/* {showing ?  <AnimatePresence><Box/></AnimatePresence> : null} 
                --> 이렇게 만들면 안된다. 보여야함 
                왜냐하면 </AnimatePresence>는 안쪽에 사라지는 것을 감지하고 그것을 animate시켜준다
            */}
            <button onClick={toggleShowing}>Click??</button>
        </>

    )
}

export default AnimatePresenceEx

AnimatePresence의 한가지 규칙은 visible 상태여야 한다. 왜냐하면 내부에서 사라지는 Component를 감지하여 animation을 적용시켜주기 때문에 사라지는 Component에 붙어서는 안된다.

<AnimatePresence>
 {showing ? <Box variants={BoxVariants} initial="initial" animate="visiable" exit="exit"/> : null}
</AnimatePresence>
//이러한 구조가 필요하다

{showing ?  <AnimatePresence><Box/></AnimatePresence> : null} // 불가능

두 번째로는 children으로 조건문이 있어야 한다. 위 이유처럼 사라지는 컴포넌트를 감지하고 적용시켜줄 예정이기 때문에 필요하다.

const BoxVariants = {
    //AnimatePresence variants는 조금다르다
    initial : {
        opacitiy:0,
        scale:0,
    },
    animate: {
        opacity:1,
        scale:1,
        rotateZ:360,
    },
    exit : {
        opacity:0,
        scale:0,
        y:100,
    }
}

마지막으로 계속 사용했던 variants는 initial 과 animate 에 필요한 object 였었는데 하나가 더 있다.

<Box variants={BoxVariants} initial="initial" animate="animate" exit="exit"/>

exit으로 component가 종료될 때 실행되는 효과를 가진다

그래서 현재 목차 맨 위 코드를 보면 버튼을 누르면 사라졌다 나오는 Effect를 가지는데 여기서 initial , animate , exit 효과를 거치며 동작한다고 볼 수 있다.

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

//AnimatePresence : 컴포넌트가 사라질 때 애니메이션 동작을 지시

const Box = styled(motion.div)` // Framer-motion과 styled컴포넌트를 사용하는 방법
  width: 200px;
  height: 200px;
  background-color: rgba(255,255,255,0.2);
  border-radius: 40px;
  position: absolute;
  top:100px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 20px;
`

const BoxVariants = {
    //AnimatePresence variants는 조금다르다
    initial : {
        opacitiy:0,
        scale:0,
    },
    animate : {
        opacity:1,
        scale:1,
        rotateZ:360,
    },
    exit : {
        opacity:0,
        scale:0,
        y:100,
    }
}

const BoxSlide = { // 이 Box는 slide를 만들기 위한 Box
    entry : (back:boolean) => ({
        x: back ? -500 : 500,
        opacity:0,
        scale:0
    }),
    center : {
        x:0,
        opacity:1,
        scale:1,
        transition: {
            duration:0.3
        }
    },
    exit : (back:boolean) => ({
        x: back ? 500 : -500,
        opacity:0,
        scale:0,
        transition: {
            duration:0.3
        }
    })
}

function AnimatePresenceEx() {

    const [showing, setShowing] = useState(false);
    const toggleShowing = () => setShowing((prev) => !prev)
    const [visible, setVisible] = useState(1);
    const [back, setBack] = useState(false);
    const nextPlease = () => {
        setBack(false)
        setVisible(prev=> prev === 10 ? 1: prev+1)
    };
    const prevPlease = () => {
        setBack(true)
        setVisible(prev=> prev === 1 ? 10 : prev-1)
    }

  
    return (
        //AnimatePresence의 한가지 규칙은 visible 상태여야 한다.
        //children으로 조건문이 있어야 한다
        <>
            <AnimatePresence exitBeforeEnter custom={back}>
                <Box custom={back} variants={BoxSlide} initial="entry" animate="center" exit={"exit"} key={visible}>{visible}</Box>
            </AnimatePresence>
            {/* {showing ?  <AnimatePresence><Box/></AnimatePresence> : null} 
                --> 이렇게 만들면 안된다. 보여야함 
                왜냐하면 </AnimatePresence>는 안쪽에 사라지는 것을 감지하고 그것을 animate시켜준다
            */}
            <button onClick={nextPlease}>next</button>
            <button onClick={prevPlease}>prev</button>
        </>

    )
}

export default AnimatePresenceEx

next, prev를 누르면 넘어가는 간단한 슬라이드가 있는데 이것을 보면 이전에 작성했던 initial , animate ,exit 순서를 따라가게 되는걸 확인할 수 있다.

const [visible, setVisible] = useState(1);

const nextPlease = () => {
 setBack(false)
 setVisible(prev=> prev === 10 ? 1: prev+1)
};
const prevPlease = () => {
	setBack(true)
	setVisible(prev=> prev === 1 ? 10 : prev-1)
}

const BoxSlide = { // 이 Box는 slide를 만들기 위한 Box
    entry : {
        x: back ? -500 : 500,
        opacity:0,
        scale:0
    },
    center : {
        x:0,
        opacity:1,
        scale:1,
        transition: {
            duration:0.3
        }
    },
    exit : {
        x: back ? 500 : -500,
        opacity:0,
        scale:0,
        transition: {
            duration:0.3
        }
    }
}

<AnimatePresence >
 <Box variants={BoxSlide} initial="entry" animate="center" exit={"exit"} key={visible}>{visible}</Box>
</AnimatePresence >

<button onClick={nextPlease}>next</button>
<button onClick={prevPlease}>prev</button>

하지만 문제가 있는데 next와 prev가 날아오는 방향이 같아진다.

 

이게 무슨말이냐면 슬라이드 next를 누르면 오른쪽에서 왼쪽으로 넘어온다고 할 때, prev를 누르면 왼쪽에서 오른쪽으로 넘어와야 한다. 하지만 여기서는 구분이 없다보니 같은 방향으로 날라오게 되고 상당히 어색하다는 것을 알 수 있다.

 

이럴 때 우리는 조건을 걸어서 구분을 하게 되는데, 그럴 때 사용하는 게 custom 이다.

const [back, setBack] = useState(false);

<AnimatePresence custom={back}>
 <Box custom={back} variants={BoxSlide} initial="entry" animate="center" exit={"exit"} key={visible}>{visible}</Box>
</AnimatePresence >

custom은 AnimatePresence 에도 적어줘야 하고 Box에도 전달할 props를 적어줘야 한다. 여기서는 뒤로 움직이는 것을 확인할 수 있게 back이라는 변수를 사용하였다.

 

그 다음 variants로 가서 각각의 initial , animate ,exit 들이 object를 반환하는 함수로 만들어 줘야 한다

const BoxSlide = { // 이 Box는 slide를 만들기 위한 Box
    entry : (back:boolean) => ({
        x: back ? -500 : 500,
        opacity:0,
        scale:0
    }),
    center : {
        x:0,
        opacity:1,
        scale:1,
        transition: {
            duration:0.3
        }
    },
    exit : (back:boolean) => ({
        x: back ? 500 : -500,
        opacity:0,
        scale:0,
        transition: {
            duration:0.3
        }
    })
}
    entry : (back:boolean) => {
			return {
        x: back ? -500 : 500,
        opacity:0,
        scale:0
			}
    },
// 바로 괄호로 감싸주게 되면 return을 적지않아도 된다!

    entry : (back:boolean) => ({
        x: back ? -500 : 500,
        opacity:0,
        scale:0
    }),

Object를 반환하는 각각의 variants 들을 설정했다면 custom props를 전달받아 variants 내부에서 사용할 수 있게 된다!

다수의 props들을 전달받을 수 있는지 확인해보고 싶다 ⇒ 배열로 전달하게 되면 multiprops를 쓸 수 있다


https://github.com/framer/motion

 

GitHub - framer/motion: Open source, production-ready animation and gesture library for React

Open source, production-ready animation and gesture library for React - GitHub - framer/motion: Open source, production-ready animation and gesture library for React

github.com

 

'TIL > 개념정리' 카테고리의 다른 글

swagger가 필요한 이유와 정리  (0) 2022.07.19
API 쉽게 이해하기  (0) 2022.07.19
@keyframes - animation 사용법  (0) 2022.07.14
styled-components 문법 정리  (0) 2022.07.13
File upload 취약점  (0) 2022.07.12
복사했습니다!