한 걸음씩

[React] Animations 본문

React

[React] Animations

winter17 2023. 8. 14. 18:54

https://www.framer.com/motion/

 

Documentation | Framer for Developers

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

www.framer.com


▶ Basic Animation

 

import styled from 'styled-components'
import { motion } from 'framer-motion'
const Wrapper = styled.div`
  height: 100vh;
  width: 100vw;
  display: flex;
  justify-content: center;
  align-items: center;
`
// styled-components와 motion을 함께 사용하려면 다음과 같이 사용해야 한다.
const Box = styled(motion.div)`
  width: 200px;
  height: 200px;
  background-color: white;
  border-radius: 15px;
  box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.06);
`

function App() {
  return (
    <Wrapper>
      <Box
        // 초기 상태 설정 (크기 0으로 시작)
        initial={{ scale: 0 }}
        // 애니메이션 실행 (크기를 1로 키우고, 360도 회전)
        animate={{ scale: 1, rotateZ: 360 }}
        // 애니메이션 전환 설정 (스프링 타입, 0.5초 딜레이)
        transition={{ type: 'spring', delay: 0.5 }}
      />
    </Wrapper>
  )
}

export default App

 

 

▶ variants

  • variants 객체 안에는 애니메이션의 시작과 종료 상태, 그리고 해당 상태 간의 전환 효과 등이 정의된다.

 

import styled from 'styled-components'
import { motion } from 'framer-motion'

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

const Box = styled(motion.div)`
  width: 200px;
  height: 200px;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  background-color: rgba(255, 255, 255, 0.2);
  border-radius: 40px;
  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: white;
  height: 70px;
  width: 70px;
  place-self: center;
  border-radius: 35px;
  box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.06);
`
const boxVariants = {
  // 애니메이션의 시작 상태, opacity, scale이 적용되어 박스가 보이지 않고 크기가 줄어든다
  start: {
    opacity: 0,
    scale: 0.5,
  },
  // 애니메이션의 종료 상태, 박스가 원래 크기로 돌아오면서 페이드 인(opacity)효과가 적용된다.
  end: {
    scale: 1,
    opacity: 1,
    transition: { // transition속성으로 전환 효과를 정의하며,
      type: 'spring',// type은 스프링 애니메이션을 사용하고,
      duration: 0.5, // duration은 애니메이션의 지속시간을 나타낸다.
      bounce: 0.5, // bounce는 반동 효과의 강도를 조절하고,
      delayChildren: 0.5, // delayChildren은 자식 요소들에 대한 딜레이를 설정하며,
      staggerChildren: 0.2, // staggerChildren은 자식 요소들 간의 간격을 조절한다.
    },
  },
}

const circleVariants = {
  // 애니메이션의 시작 상태, 원이 보이지 않고 약간 위로 올라간다.
  start: {
    opacity: 0,
    y: 10,
  },
  // 애니메이션의 종료 상태, 원이 아래로 내려오면서 페이드 인(opacity)효과가 적용된다
  end: {
    opacity: 1,
    y: 0, // y 속성을 이용하여 위아래로 이동하는 효과를 구현한다.
  },
}

function App() {
  return (
    <Wrapper>
      <Box variants={boxVariants} initial="start" animate="end">
        <Circle variants={circleVariants} />
        <Circle variants={circleVariants} />
        <Circle variants={circleVariants} />
        <Circle variants={circleVariants} />
      </Box>
    </Wrapper>
  )
}
export default App

 

 

▶ Gesture, Drag

  • Gesture
    • 사용자의 입력(제스처)에 응답하는 것을 말한다.
    • whileHover: 요소가 마우스 커서 위에 있을 때의 동작을 정의한다.
    • whileTap: 요소가 터치(클릭)되었을 때의 동작을 정의한다.
  • Drag
    • drag속성을 사용하여 요소를 드래그 및 이동시킬 수 있다. 
    • drag: true로 설정하면 해당 요소를 드래그할 수 있다.
    • dragSnapToOrigin: 요소를 드래그한 후 원래 위치로 자동으로 돌아가게 한다.
    • dragElastic: 드래그할 때 요소의 탄성을 설정한다. 값이 클수록 느슨하게 이동하며, 작을수록 단단하게 이동한다.
    • dragConstraints: 드래그의 제약 조건을 설정한다. 

 

import styled from 'styled-components'
import { motion } from 'framer-motion'
import { useRef } from 'react'

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

const BiggerBox = styled.div`
  width: 600px;
  height: 600px;
  background-color: rgba(255, 255, 255, 0.4);
  border-radius: 40px;
  display: flex;
  justify-content: center;
  align-items: center;
`

const Box = styled(motion.div)`
  width: 200px;
  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 boxVariants = {
  hover: { rotateZ: 90 },
  click: { borderRadius: '100px' },
}

function App() {
  const biggerBoxRef = useRef<HTMLDivElement>(null)
  return (
    <Wrapper>
      <BiggerBox ref={biggerBoxRef}>
        <Box
          drag
          dragSnapToOrigin
          dragElastic={0.5}
          dragConstraints={biggerBoxRef}
          variants={boxVariants}
          whileHover="hover"
          whileTap="click"
        />
      </BiggerBox>
    </Wrapper>
  )
}

export default App

 

 

▶ MotionValues

  • 애니메이션 및 인터랙선을 위해 컴포넌트의 속성값을 동적으로 추적하고 변경할 수 있는 기능을 제공한다.
  • 애니메이션의 시작과 끝 값 사이에서 중간 값으로 자연스러운 전환을 만들거나, 사용자 입력에 반응하여 속성값을 변경할 수 있다
  • useMotionValue(initial): initial매개변수는 초기값을 나타낸다. 이 값을 동적으로 변경하면 MotionValue가 자동으로 업데이트된다.
  • useTransfrom(value, inputRange, outputRange): MotionValue의 값을 변환하여, 다른 범위의 값으로 매핑한다. value는 변환할 MotionValue이고, inputRange는 입력 범위, outputRange는 출력 범위이다. 예를 들어, useTransfrom(x, [0, 100], [0, 1])은 x값을 0에서 100까지의 범위에서 0에서 1까지의 범위로 변환한다.
  • animate: MotionValue를 사용하여 컴포넌트의 애니메이션을 제어한다. 예를 들어, animate = {{ x: 100 }}와 같이 사용하여 x속성을 100으로 애니메이션 화할 수 있다.
  • drag: MotionValue를 사용하여 드래그 가능한 컴포넌트를 만든다. drag='x'와 같이 사용하여 x축으로만 드래그 가능하도록 설정할 수 있다.

 

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

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

const Box = styled(motion.div)`
  width: 200px;
  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);
`

function App() {
  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 { scrollYProgress } = useViewportScroll()
  const scale = useTransform(scrollYProgress, [0, 1], [1, 5])
  return (
    <Wrapper style={{ background: gradient }}>
      <Box style={{ x, rotateZ, scale }} drag="x" dragSnapToOrigin />
    </Wrapper>
  )
}

export default App

 

 

▶ SVG Animation

  • SVG Animation: path 요소의 d속성 값(경로)을 변경하여 그래픽 요소를 동적으로 움직이고 변화시키는 방법을 사용한다.
  • pathLength와 fill속성을 이용하여 경로가 그려지는 동안의 색상 변화와 완료된 후의 색상 변경을 표현하는 것이다.
  • motion.path 컴포넌트가 자동으로 pathLength를 추적하고 애니메이션화하여 경로를 그릴 때마다 fill속성도 적절히 변화시키는 역할을 한다.
  • svg 객체: 애니메이션의 기작과 끝 상태를 정의한다. start상태는 path의 pathLength 값을 0으로 하여 경로를 아직 그려지지 않은 상태로, fill을 투명한 색상으로 설정한다. end 상태는 pathLength 값을 1로 하여 경로를 완전히 그린 상태로, fill을 불투명한 색상으로 설정한다. 

 

import styled from 'styled-components'
import { motion } from 'framer-motion'

const Wrapper = styled(motion.div)`
  height: 100vh;
  width: 100vw;
  display: flex;
  justify-content: center;
  align-items: center;
`
const Svg = styled.svg`
  width: 300px;
  height: 300px;
  path {
    stroke: white;
    stroke-width: 2;
  }
`
const svg = {
  start: { pathLength: 0, fill: 'rgba(255, 255, 255, 0)' },
  end: {
    fill: 'rgba(255, 255, 255, 1)',
    pathLength: 1,
  },
}
function App() {
  return (
    <Wrapper>
      <Svg
        // SVG요소가 포커스 가능한 요소로 간주되지 않도록 설정한다. 
        // 보통 SVG 그래픽은 키보드 포커스를 받을 필요가 없으므로 스크린 리더나 접근성 기술을 사용하는 사용자에게 혼란을 줄 수 있다
        // 이 속성을 사용하여 SVG가 포커스를 받지 않도록 설정하고, 웹 접근성을 향상시킬 수 있다
        focusable="false"
        // 해당 SVG 문서가 XML 네임스페이스를 정의한다는 것을 나타낸다.
        // SVG 문서에는 XML 네임스페이스를 명시해야 하며, 이 속성을 통해 해당 네임스페이스를 지정한다.
        xmlns="http://www.w3.org/2000/svg"
        // SVG 뷰박스를 정의한다. 
        // 뷰박스는 SVG의 그려질 영역을 나타내며, 좌상단 모서리 (0, 0)을 기준으로 폭과 높이를 지정한다.
        viewBox="0 0 448 512"
      >
        <motion.path
          // svg 객체의 상태를 연결
          variants={svg}
          // 애니메이션 시작 상태
          initial="start"
          // 애니메이션의 종료 상태
          animate="end"
          // 애니메이션의 전환 효과와 시간을 정의
          transition={{
            // 기본 트랜지션 설정으로, 애니메이션의 전체 지속 시간을 5초로 설정한다
            default: { duration: 5 },
            // 애니메이션을 시작한 후 3초의 지연 시간 뒤에 1초 동안 색상 변화를 적용한다.
            fill: { duration: 1, delay: 3 },
          }}
          // d 속성은 SVG 경로를 나타낸다. 이 경로를 애니메이션화하여 그래픽을 변화시킨다.
          d="M224 373.12c-25.24-31.67-40.08-59.43-45-83.18-22.55-88 112.61-88 90.06 0-5.45 24.25-20.29 52-45 83.18zm138.15 73.23c-42.06 18.31-83.67-10.88-119.3-50.47 103.9-130.07 46.11-200-18.85-200-54.92 0-85.16 46.51-73.28 100.5 6.93 29.19 25.23 62.39 54.43 99.5-32.53 36.05-60.55 52.69-85.15 54.92-50 7.43-89.11-41.06-71.3-91.09 15.1-39.16 111.72-231.18 115.87-241.56 15.75-30.07 25.56-57.4 59.38-57.4 32.34 0 43.4 25.94 60.37 59.87 36 70.62 89.35 177.48 114.84 239.09 13.17 33.07-1.37 71.29-37.01 86.64zm47-136.12C280.27 35.93 273.13 32 224 32c-45.52 0-64.87 31.67-84.66 72.79C33.18 317.1 22.89 347.19 22 349.81-3.22 419.14 48.74 480 111.63 480c21.71 0 60.61-6.06 112.37-62.4 58.68 63.78 101.26 62.4 112.37 62.4 62.89.05 114.85-60.86 89.61-130.19.02-3.89-16.82-38.9-16.82-39.58z"
        ></motion.path>
      </Svg>
    </Wrapper>
  )
}

export default App

 

 

▶ AnimatePresence

  • 컴포넌트가 등장하거나 사라질 때, 변경될 때 애니메이션을 쉽게 관리할 수 있도록 도와주는 역할을 한다.
  • 주로 컴포넌트의 등장(enter)과 사라짐(exit)시에 애니메이션을 정의하고 조작할 때 사용된다.
  • initial: 컴포넌트가 처음 나타낼 때의 애니메이션 설정을 지정한다.
  • animate: 컴포넌트가 나타날 때의 애니메이션 설정을 지정한다.
  • exit: 컴포넌트가 사라질 때의 애니메이션 설정을 지정한다.

 

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: 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 = {
  initial: {
    opacity: 0,
    scale: 0,
  },
  visible: {
    opacity: 1,
    scale: 1,
    rotateZ: 360,
  },
  leaving: {
    opacity: 0,
    scale: 0,
    y: 50,
  },
}
function App() {
  const [showing, setShowing] = useState(false)
  const toggleShowing = () => setShowing(prev => !prev)
  return (
    <Wrapper>
      {/* 버튼을 누르면 toggleShowing함수가 실행되어 showing을 true또는 false 상태로 만들어줌 */}
      <button onClick={toggleShowing}>Click</button>
      {/* AnimatePresence는 항상 조건문을 가져야하고 항상 visible상태여야 한다. */}
      <AnimatePresence>
        {/* showing이 true이면 Box컴포넌트를 렌더링하고 아니라면 null이기 때문에 Box컴포넌트가 사라짐 */}
        {showing ? (
          <Box
            variants={boxVariants}
            initial="initial"
            animate="visible"
            exit="leaving"
          />
        ) : null}
      </AnimatePresence>
    </Wrapper>
  )
}

export default App

 

 

▶ Silder 

 

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: 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;
  position: absolute;
  top: 100px;
  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);
`

const box = {
  // back argument는 Box컴포넌트의 custom prop으로부터 받는다
  entry: (back: boolean) => ({
    // next 버튼을 누르면 오른쪽에서 박스가 나타나는데 back이 false니까 500
    // prev 버튼을 누르면 왼쪽에서 박스가 나타나는데 back이 true니까 -500
    x: back ? -500 : 500,
    opacity: 0,
    scale: 0,
  }),
  center: {
    x: 0,
    opacity: 1,
    scale: 1,
    transition: {
      duration: 1,
    },
  },
  exit: (back: boolean) => ({
    // next 버튼을 누를 때 false상태인데 그럼 -500이기 떄문에 왼쪽으로 사라짐
    // prev 버튼을 누를 때 true상태인데 그럼 500이기 때문에 오른쪽으로 사라짐
    x: back ? 500 : -500,
    opacity: 0,
    scale: 0,
    transition: { duration: 0.3 },
  }),
}

function App() {
  const [visible, setVisible] = useState(1)
  const [back, setBack] = useState(false)
  const nextPlease = () => {
    setBack(false)
    setVisible(prev => (prev === 10 ? 10 : prev + 1))
  }
  const prevPlease = () => {
    setBack(true)
    setVisible(prev => (prev === 1 ? 1 : prev - 1))
  }
  return (
    <Wrapper>
      {/* 이전 애니메이션과 다음 애니메이션이 동시에 실행된다  */}
      {/* 1번 박스의 exit할 때, 거의 동시에 2번 박스가 enter하게된다 */}
      {/* 위의 과정을 방지하기 위해서 mode="wait"를 해준다. */}
      {/* 1번 박스가 완전히 exit한 후, 2번 박스가 enter할 수 있도록! */}
      <AnimatePresence mode="wait" custom={back}>
        <Box
          custom={back}
          variants={box}
          initial="entry"
          animate="center"
          exit="exit"
          // key가 바뀌면  재렌더링 된다
          key={visible}
        >
          {visible}
        </Box>
      </AnimatePresence>
      <button onClick={nextPlease}>next</button>
      <button onClick={prevPlease}>prev</button>
    </Wrapper>
  )
}

export default App

 

 

▶ Shared layout animation

 

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: center;
  align-items: center;
  justify-content: space-around;
`
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);
`

function App() {
  const [clicked, setClicked] = useState(false)
  const toggleClicked = () => setClicked(prev => !prev)
  return (
    <Wrapper onClick={toggleClicked}>
      <Box>
        {/* layoutId는 두 컴포넌트가 연결되어있음을 알려준다 */}
        {!clicked ? (
          <Circle layoutId="circle" style={{ borderRadius: 50 }} />
        ) : null}
      </Box>
      <Box>
        {clicked ? (
          <Circle layoutId="circle" style={{ borderRadius: 0, scale: 2 }} />
        ) : null}
      </Box>
    </Wrapper>
  )
}

export default App

 

 

▶ grid box with overlay

 

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

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

const Grid = styled.div`
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  width: 50vw;
  gap: 10px;
  div:first-child,
  div:last-child {
    grid-column: span 2;
  }
`
// 박스를 클릭했을 때 나타날 박스
const Box = styled(motion.div)`
  background-color: rgba(255, 255, 255, 1);
  border-radius: 40px;
  height: 200px;
  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;
`
// Overlay 컴포넌트의 배경색을 변화시키는 역할
const overlay = {
  hidden: { backgroundColor: 'rgba(0, 0, 0, 0)' }, // 투명하게
  visible: { backgroundColor: 'rgba(0, 0, 0, 0.5)' }, // 반투명한 검은색
  exit: { backgroundColor: 'rgba(0, 0, 0, 0)' }, // 투명하게
}

function App() {
  const [id, setId] = useState<null | string>(null)
  return (
    <Wrapper>
      <Grid>
        {/* layoutId는 String이어야하기 떄문에 문자열 배열을 만들어야 한다 */}
        {['1', '2', '3', '4'].map(n => (
          <Box onClick={() => setId(n)} key={n} layoutId={n} />
        ))}
      </Grid>
      <AnimatePresence>
      {/* id가 1, 2, 3, 4 중 하나일 떄, Box 컴포넌트가 먼저 오버레이되고, 그 후에 Overlay컴포넌트가 표시된다 */}
      {/* Overlay 컴포넌트의 클릭 이벤트 핸들러가 호출되면 Overlay 컴포넌트와 그 안에 있는 Box 컴포넌트가 애니메이션과 함께 사라진다 */}
        {id ? (
          <Overlay
            variants={overlay}
            {/* 클릭 동작을 통해 setId(null)을 호출하여 오버레이를 닫을 수 있다 */}
            onClick={() => setId(null)}
            initial="hidden"
            animate="visible"
            exit="exit"
          >
            {/* useState를 사용하여 id값을 가져 두 Box컴포넌트를 연결한다 */}
            <Box layoutId={id} style={{ width: 400, height: 200 }} />
          </Overlay>
        ) : null}
      </AnimatePresence>
    </Wrapper>
  )
}

export default App

id가 1, 2, 3, 4 중 하나인 경우에 클릭하면 Box가 나타나고 동시에 Overlay도 활성화된다. 

그리고 Overlay가 활성화된 상태에서 다시 클릭하면 setId(null)을 호출하여 id 값을 null로 만들어서 Box와 Overlay가 사라지게 된다.

이렇게 하면 사용자가 Box의 클릭으로 Overlay를 열고, 다시 Overlay의 클릭으로 Box와 Overlay를 닫을 수 있다.

'React' 카테고리의 다른 글

Next.js 13 기초  (0) 2023.08.21
[React] Gatsby  (0) 2023.08.16
[React] Drag and Drop  (0) 2023.08.12
[React] atom, selector를 사용한 minutes, hours 변환기  (0) 2023.08.11
[React] To Do  (0) 2023.08.11