React JS 마스터클래스(ANIMATIONS ) - 2

짜스의 하루 ·2024년 6월 24일

useTransform

useTransform를 이용해서 다양한 motion을 적용해보자!

1 . x 축을 중심으로 360도를 회전하는 motion을 만들어보자.

const rotateZ = useTransform(x, [-800, 800], [-360, 360]) 은 x 값이 -800에서 800으로 변할 때, rotateZ 값이 -360도에서 360도로 변하게 한다.
--> 즉, x 값이 -800일 때 rotateZ는 -360도, x 값이 800일 때 rotateZ는 360도가 된다고 이해하면 된다.

2 . 상자가 이동할 때마다 배경화면도 변경해보자!
x 값이 -800에서 800으로 변화할 때, gradient의 값이 처음에서부터 두번째 값 사이에서 전환된다.

여기서도 마찬가지로 상수값이 background이기 때문에
styled = {{background : background}}로 적지 않고 styled = {{background }} 로 적어도 적용이 된다!


useViewportScroll

이번에는 스크롤을 했을 때, 상자의 크기가 커지는 것을 만들어보려고 한다!
이때는 useViewportScroll 을 사용해야 한다!

사용자가 브라우저 창(뷰포트)을 스크롤할 때 발생하는 스크롤 위치를 추적하는 데 사용된다.
이를 통해 스크롤 위치에 따라 애니메이션을 조정하거나 다양한 스크롤 기반 인터랙션을 구현할 수 있다.

useViewportScroll은 두 개의 객체를 반환한다:

  • scrollX: 수평 스크롤 위치를 추적한다.
  • scrollY: 수직 스크롤 위치를 추적한다.
  • scrollXProgress: 전체 수평 스크롤 진행률(0에서 1 사이)을 나타낸다.
  • scrollYProgress: 전체 수직 스크롤 진행률(0에서 1 사이)을 나타낸다.

그리고 이를
const { scrollX, scrollY, scrollXProgress, scrollYProgress } = useViewportScroll(); 로 사용한다!

useViewportScroll

  • 목적: 현재 뷰포트(브라우저 창)의 스크롤 상태를 추적한다.
  • scrollYProgress: 수직 스크롤의 진행률을 0(페이지 상단)에서 1(페이지 하단)까지 반환한다.

useTransform

  • 목적: 첫 번째 배열의 범위([0, 1])를 두 번째 배열의 범위([1, 2])로 매핑한다.
  • scrollYProgress를 기반으로 변환: 스크롤 진행률에 따라 요소의 scale 값이 1에서 2로 변화한다.

보너스 스크롤바 만들어보기!

  • scrollYProgress가 0일 때 (scrollY가 페이지 맨 위에 있을 때) width는 '0%'이고
  • scrollYProgress가 1일 때 (scrollY가 페이지 맨 아래에 있을 때) width는 '100%'이다.

이 속성을 style ={{ width }} 에 주면서, width 값은 스크롤 위치에 따라 자동으로 '0%'에서 '100%' 사이로 변화하게 된다.
위에 잘 보면 스크롤 내리자 빨간색 선이 꽉 찼다가 스크롤을 올리자 스크롤이 다시 줄어드는 것을 확인할 수 있다!


SVG Animation

에어비엔비 로고를 animation을 줘보려고 한다!
웹사이트에 들어가다보면 이렇게 좀 멋스럽게 (?) 로고가 생겨난다거나, 움직인다거나 하는 것을 볼 수 있었는데, 에어비엔비 로고로 만들어보러고 한다

 <Svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
          <path 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>

이 상태로 불러오게 되면,

정말 이상한(?) 로고가 나타나게 된다.

<motion.path
            initial={{ pathLength: 0, fill: 'rgba(255,255,255,0' }}
            animate={{
              pathLength: 1,
              fill: 'rgba(255,255,255,1',
              transition: { duration: 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"
          />

이렇게 motion.path 로 Framer Motion을 사용가능하게 제공해 준 다음,
initial: 초기 상태를 설정

  • pathLength: 0: 애니메이션 시작 시 경로의 길이가 0이다.
  • fill: 'rgba(255,255,255,0)': 초기 채우기 색상은 투명하게 설정된다.

animate: 애니메이션 효과를 설정

  • pathLength: 1: 경로의 길이가 애니메이션 동안 1로 증가한다.
  • fill: 'rgba(255,255,255,1)': 채우기 색상이 애니메이션 동안 투명에서 흰색(rgba(255,255,255,1))으로 변경된다.
  • transition: { duration: 2 }: 애니메이션의 지속 시간을 2초로 설정한다.

이렇게 설정하게 되면, transition: { duration: 2 } 는 모든 애니메이션이 지속 되는 시간이 2초로 설정되기 때문에, pathLength 따로, fill 따로 진행되지 않는다.

그럼 어떻게 해야할까?

<motion.path
            initial={{ pathLength: 0, fill: 'rgba(255,255,255,0' }}
            animate={{
              pathLength: 1,
              fill: 'rgba(255,255,255,1',
            }}
            transition={{
              default: { duration: 5 },
              fill: { duration: 2, delay: 5 },
            }}
            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"
          />

transition 속성 : 애니메이션의 지속 시간과 지연 시간을 설정한다.

  • default: { duration: 5 } : 기본적인 애니메이션의 지속 시간을 5초로 설정
  • fill: { duration: 2, delay: 5 } : fill 속성에 대한 애니메이션의 지속 시간을 2초로 설정하고, 5초 후에 시작되도록 지연 시간(delay)을 5초로 설정한다.

즉, 기본적인 애니메이션의 지속 시간은 5초로 설정하지만, fill은 제외이다.
fill 속성은 fill: { duration: 2, delay: 5 } 로 따로 지정을 해두었기 때문이다!

--> fill을 제외한 난머지 애니메이션은 지속시간이 5초로 설정되고, fill은 5초 이후에, 즉 나머지 애니메이션이 진행된 후, 실행이 된다!


요렇게 variants 속성을 이용해 코드를 깔끔하게 정리할 수도 있다!

이번엔 구글 로고도 한번 animation을 줘보도록 하겠다!

다른 속성들은 이전에 적용했던 것과 똑같지만.
rotateZ : 360 속성을 주어서 회전을 할 수 있도록 해보았다

생각보다 더 이쁜 느낌이다 호호


AnimatePresence

framer-motion 라이브러리에서 제공하는 컴포넌트로, React의 컴포넌트가 DOM에서 추가되거나 제거될 때 애니메이션 효과를 쉽게 구현할 수 있도록 도와준다. 보통 요소가 마운트(추가)되거나 언마운트(제거)될 때 애니메이션을 추가하는 데 사용한다.


우선 간단하게 버튼을 눌렀을 때, <Box/> 컴포넌트가 열릴 수 있도록 코드를 작성해 두었다.
지금은 어떠한 애니메이션을 적용하지 않아서 정말 딱딱하게 열렸다가 닫혔다가 한다.

그럼 여기서 AnimatePresence를 적용하려면 어떻게 해야할까?

<AnimatePresence> {showing && <Box />}</AnimatePresence> 이렇게 animation을 적용할 부분에 <AnimatePresence>로 감싸주어야 한다.

<AnimatePresence> 로 감싸면: showing 상태에 따라 <Box /> 컴포넌트가 추가되거나 제거될 때, 해당 영역에서 애니메이션을 가능하게 해준다.

variants의 속성을 사용하여 initial, animate, exit의 속성들을 모두 정의해두고, 적용시켰다.
여기서 exit 은 사라질 때 적용할 속성인 것이다.

즉, 등장할 때, initial -> animate , 사라질 때, animate -> leaving의 효과로 변경된다고 생각하면 된다.
기존에 만들었던 창들보다 확실히 역동적이고 animation이 잘 보이는 것을 알 수 있다 !


Slider


이번에는 버튼을 눌렀을 때 슬라이더 형태로 box가 생겨나고 사라지는 것을 만들어보자!


먼저 코드를 간단하게 살펴보자면.
map 함수는 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 배열의 각 요소 i에 대해:
visiable === i일 때, 즉 현재 상태와 상자의 번호가 일치할 때만 <Box> 컴포넌트를 렌더링한다.
<Box> 컴포넌트는 variants, initial, animate, exit props로 애니메이션 설정을 받아 애니메이션 효과를 적용한다.

이후 , onClick 이벤트에 nextPlease함수를 지정해주었으며. nextPlease 함수는 visiable 값을 1씩 증가시키게 된다.
만약 visiable이 10이면, 더 이상 증가하지 않고 10으로 유지된다. 이 함수는 버튼 클릭 시 호출된다.

variants로 설정한 애니메이션을 살펴보자

invisiable

  • x: 800: 컴포넌트가 x축 방향으로 800 픽셀 오른쪽으로 이동
  • opacity: 0: 불투명도가 0으로 설정되어 컴포넌트가 완전히 투명
  • scale: 0: 크기가 0으로 축소

visiavle

  • x: 0: x축 방향으로 원래 위치(0, 0)로 이동
  • opacity: 1: 불투명도가 1로 설정되어 컴포넌트가 완전히 보인다.
  • scale: 1: 크기가 원래 크기(1)로 확장된다.
  • transition: { duration: 1 }: 이 상태로 전환될 때의 애니메이션 지속 시간을 1초로 설정한다.

exit

  • x: -800: x축 방향으로 800 픽셀 왼쪽으로 이동
  • opacity: 0: 불투명도가 0으로 설정되어 컴포넌트가 다시 완전히 투명
  • scale: 0: 크기가 0으로 축소된다
  • transition: { duration: 1 }: 이 상태로 전환될 때의 애니메이션 지속 시간을 1초로 설정한다.

varients로 설정을 주어서
<Box variants={box} initial="invisiable" animate="visiavle" exit="exit"/> 로 지정해 두었다!


이번에는 반대로 돌아가는 버튼을 생성해볼 예정이다.
여기서 생각해볼 수 있는 점이 ( 그럼 invisiable의 x의 값과, exit의 x의 값만 변경하면 되는데!)

맞다! 이떄 사용할 수 있는게 custom 이라는 속성이다!

framer-motion에서 custom 속성은 사용자 정의 데이터를 애니메이션에 전달하여 각 애니메이션 인스턴스마다 서로 다른 애니메이션 동작을 적용할 수 있게 하는 기능을 의믜한다.


새로 생성된 상태관리는 const [back, setBack] = useState(false)
---> back은 상자가 이전으로 이동(true)하거나 다음으로 이동(false)하는 방향을 관리.

  • 애니메이션 방향 결정: custom을 사용하여 back 상태에 따라 상자의 애니메이션 방향을 다르게 설정한다. back이 true이면 상자가 왼쪽에서 등장하여 오른쪽으로 사라지며, false이면 오른쪽에서 등장하여 왼쪽으로 사라지게 된다.

  • AnimatePresence와 motion: AnimatePresence의 custom 속성을 통해 전달된 back 값을 사용하여 motion.div의 애니메이션 방향을 동적으로 변경한다.

여기서 back이란 ?


여기서 정의한 back과 동일하다!

useState로 상태 관리:
App 컴포넌트에서 const [back, setBack] = useState(false);를 통해 back이라는 상태를 정의하고, 기본값을 false로 설정한다.

custom 속성에 상태 전달:
AnimatePresence의 custom={back}으로 back 상태를 전달한다.
이 custom 값은 AnimatePresence의 자식 컴포넌트로 전달된다.

Box 컴포넌트에서 custom 사용:
Box 컴포넌트(motion.div 확장)에 custom={back}으로 back 값을 전달한다.
이 custom 값은 variants의 콜백 함수에서 접근할 수 있다.

variants의 함수에서 custom 접근:
variants 객체의 entry와 exit 함수는 첫 번째 인수로 custom 속성을 통해 전달된 back 값을 받는다.

이런식으로 이어진다고 생각하면 된다.

entry와 exit 함수의 back 매개변수는 custom 속성을 통해 전달된 back 상태이다.
entry: (back) => ({ x: back ? -800 : 800, ... }) : back이 true일 때 -800으로 이동하여 상자가 왼쪽에서 등장하고, false일 때 800으로 이동하여 오른쪽에서 등장한다.

exit: (back) => ({ x: back ? 800 : -800, ... }) : back이 true일 때 800으로 이동하여 상자가 오른쪽으로 사라지고, false일 때 -800으로 이동하여 왼쪽으로 사라진다.

즉, 오른쪽에서 등장하면 왼쪽으로 사라지고 왼쪽에서 등장하면 오른쪽으로 사라지는 애니메이션을 생성한 것과 같다.


Syncing layout animations

<Box/> 컴포넌트에 <Circle/> 컴포넌트가 있다고 가정해보자

이제, 화면을 클릭했을 때, 번갈아가면서 원이 생기도록 해볼 것이다
(생기고-사라지고, 사라지고-생기고)이런 느낌!

정말 ~~~ 간단하게
<Box/> 컴포넌트를 감싸고 있는 wrapper에 onClick 이벤트를 주었으며 이는 현재 clicked 상태를 반전시키는 함수 (true면 false로, false면 true로).

Wrapper 내부에 두 개의 Box 컴포넌트가 있으며, 각각의 Box 컴포넌트는 Circle을 조건부로 렌더링하게 되는데,

  • 첫 번째 Box는 clicked가 false일 때 Circle을 렌더링한다.
  • 두 번째 Box는 clicked가 true일 때 Circle을 렌더링한다.

요렇게 이루어지고 있다.

여기서 중요하게 생각해야 할 점은 --> 두개의 <Circle/>컴포넌트는 각각 다른 컴포넌트이다.

<Circle/> 컴포넌트에 layoutId 속성 추가

Circle 컴포넌트는 layoutId를 공유하게 되는데, Framer Motion의 AnimateSharedLayout 기능을 활용하여 애니메이션이 매끄럽게 동작하도록 한다.

layoutId 속성 덕분에 Framer Motion은 두 Circle이 동일한 요소임을 인식하고 자연스러운 애니메이션을 적용하게 된다.

이전에는 각각 다른 컴포넌트로 인식해 사라졌다 생겼다만 반복했다면, layoutId를 적용해 Circle 컴포넌트가 동일한 컴포넌트임을 인식하고, 두 원이 같은 원임을 인식하고 있다

layoutId 없이 Circle 컴포넌트를 전환하는 경우, React는 Circle을 각각의 상태 (clicked의 true/false)에 따라 렌더링하거나 제거하게 된다.
이 경우, React는 첫 번째 Circle과 두 번째 Circle을 완전히 다른 컴포넌트로 인식하게 되면서, 이로 인해 상태 전환 시 Circle이 사라지고, 다시 생겨나는 것처럼 보이게 된다.

layoutId 적용 후 --> layoutId를 적용하면, Circle 컴포넌트가 동일한 컴포넌트임을 인식하게 된다. 두 Box에서 나타나는 Circle은 서로 다른 인스턴스가 아닌, 동일한 layoutId를 가진 하나의 컴포넌트로 간주된다. 이로 인해 애니메이션 효과가 두 컴포넌트 사이를 부드럽게 전환하는 것처럼 보이게 된다.


움직이는 layout만들어보기


layout 속성을 이용해서 요런 움직이는 layout 을 만들어보자!

먼저, <Box/> 컴포넌트에 Grid 속성을 적용해주었다.

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;
  }
`;

grid-template-columns: repeat(3, 1fr);:
그리드 컨테이너를 3개의 동일한 너비의 열로 설정한다.
1fr은 사용 가능한 공간의 일부를 의미하며, 각 열이 그리드 컨테이너의 너비를 균등하게 차지하게 된다.

grid-column: span 2;:
지정된 요소가 2개의 열을 차지하도록 설정한다.
--> 첫 번째 자식 요소와 마지막 자식 요소는 각각 2개의 열을 차지한다!

AnimatePresence:
조건부 렌더링된 자식 요소(Overlay)의 진입 및 퇴장 애니메이션을 관리한다.
clicked가 true일 때만 Overlay를 렌더링한다.

Overlay:
배경 색상이 애니메이션된다.
--> initial, animate, exit 을 통해 배경 색상이 투명에서 어두운 회색(rgba(0,0,0,0.7))으로 변하게 된다.
이 컴포넌트는 Box를 자식으로 가지며, Box는 layoutId가 "layout"로 설정되어 있어 기존 Box와 애니메이션 트랜지션이 된다.

코드에서 사용된 주요 framer-motion 기능:
1. layoutId:

  • 동일한 layoutId를 가진 요소들 간의 위치와 크기 변경을 애니메이션으로 연결.
  • Box의 layoutId가 동일하여 그리드의 Box와 오버레이의 Box가 자연스럽게 애니메이션됨.

2 . AnimatePresence:

  • 자식 요소의 추가 및 제거 시 애니메이션을 적용.
  • Overlay가 진입하거나 퇴장할 때 배경색 애니메이션을 관리.

3 . initial, animate, exit:

  • Overlay의 배경색이 진입(initial → animate) 및 퇴장(exit) 시 애니메이션.

애니메이션 흐름:

  • 초기 상태: clicked가 false이고, 오버레이가 없으며 그리드에 4개의 Box가 배치.
  • 클릭 시: clicked가 true로 설정
    --> Overlay가 애니메이션을 통해 배경색을 rgba(0,0,0,0)에서 rgba(0,0,0,0.7)로 변경하며, 첫 번째 Box가 그리드에서 오버레이로 이동하여 확대됨.
  • 다시 클릭 시: clicked가 false로 변경.
    --> Overlay의 배경색이 투명(rgba(0,0,0,0))으로 애니메이션되며 사라지고, Box는 다시 원래 위치로 애니메이션을 통해 돌아간다.

로 이루어지게 된다!

그럼, 다른 Box들도 layout기능이 가능하도록 기능을 줘보겠다!

const [id, setId] = useState<null | number>(null); :

  • id: 현재 클릭된 Box의 ID를 저장하는 상태 --> 초깃값은 null
  • setId: 클릭된 Box의 ID를 설정하거나, 오버레이를 닫을 때 null로 설정한다.
<Grid>
  {[1, 2, 3, 4].map((n) => (
    <Box onClick={() => setId(n)} key={n} layoutId={n + ''} />
  ))}
</Grid>
  • Grid는 4개의 동일한 열로 구성된 그리드 레이아웃을 정의한다.
  • map 함수: [1, 2, 3, 4] 배열을 순회하여 Box 컴포넌트를 생성한다.
  • Box의 onClick 이벤트: 클릭 시 해당 Box의 ID(n)를 setId로 설정한다.
  • layoutId={n + ''}: framer-motion의 layoutId로 애니메이션 연속성을 유지한다.

애니메이션 흐름:

  • 초기 상태:
    그리드에 4개의 Box가 배치되어 있으며, id는 null이므로 오버레이는 표시되지 않는다.

  • Box 클릭 시:
    클릭된 Box의 ID가 setId를 통해 설정된다.
    AnimatePresence에 의해 Overlay가 표시된다.
    배경색 애니메이션: 투명(rgba(0,0,0,0))에서 반투명 검정(rgba(0,0,0,0.7))으로 변경.
    클릭된 Box가 오버레이로 이동하고 크기가 조정된다.

  • Overlay 클릭 시:
    setId(null)이 호출되어 id가 null로 설정된다
    AnimatePresence에 의해 Overlay가 사라된다
    배경색 애니메이션: 반투명 검정(rgba(0,0,0,0.7))에서 투명(rgba(0,0,0,0))으로 변경.
    Box는 원래의 그리드 위치로 돌아간다.

결과를 확인해보자!
업로드중..
이렇게 <Box/> 컴포넌트와 overlay에 속한 <Box/> 컴포넌트가 layoutId로 연결되어 애니메이션이 이루어지는 것을 확인할 수 있다!

profile
2024. 01. 02 ~ 백앤드 공부 시작, 2024. 04.01 ~ 프론트 공부 시작

0개의 댓글