💻 Frontend

Scroll 위치에 따른 애니메이션 제작하기

category
💻 Frontend
이번 Official gsm 프로젝트를 제작하면서 많은 애니메이션을 제작하게 되었습니다. 그중에 스크롤 할 때 애니메이션을 적용시키는 걸 제작하였는데 어떻게 제작하였는지 그 과정을 적어보도록 하겠습니다.
 

1. 특정 요소의 스크롤 값 구하기

먼저 스크롤 될 때마다 애니메이션을 구현하기 위해선 자식 요소를 감싸고 있는 부모 요소의 스크롤 값을 구해야 합니다.
저희 프로젝트에서는 window scroll 값을 구하는 custom hooks이 있어서 그 코드를 참고하여 custom hooks을 제작하였습니다.
notion image
코드블록
// hooks/useGetScrollHeight import type { RefObject } from 'react'; import { useEffect, useState } from 'react'; const useGetScrollHeight = (ref: RefObject<HTMLElement>) => { const [scrollTop, setScrollTop] = useState<number | undefined>(0); const handleScroll = () => { setScrollTop(ref?.current?.scrollTop); }; useEffect(() => { ref?.current?.addEventListener('scroll', handleScroll, { passive: true }); return () => { // eslint-disable-next-line react-hooks/exhaustive-deps ref?.current?.removeEventListener('scroll', handleScroll); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [ref]); return scrollTop; }; export default useGetScrollHeight;
위 코드는 ref를 인자로 받고 해당 ref의 스크롤 값을 return 해주는 hooks 입니다. 코드를 더 자세히 설명하겠습니다.
 

1-1. throttling

throttling이란 이용하면 발생되는 이벤트 중간에 지연을 발생시키는 것입니다. 출처
저희가 스크롤 할 때는 마우스 휠을 한번 툭 만지죠? 하지만 웹 사이트에선 그동안 많은 일이 일어납니다. 해당 hooks를 실행하면 스크롤 될 때마다 계속 페이지도 렌더링 될 것입니다. 그래서 throttling를 적용한 것이죠.
0.3초마다 throttling 상태를 변경시켜 이벤트 지연을 시켰습니다.
notion image
 

1-2. passive option

passive option은 이벤트 리스너 옵션 중 하나로 스크롤 성능을 최적화하는 역할입니다. 하지만 이 옵션은 default 옵션이 아닌데요. 어떠한 이유일까요?
passive를 true로 설정하면 개발자가 명시적으로 preventDefault를 호출할 수 없게 되므로 스크롤 동작에 대한 제어가 제한될 수 있습니다. 하지만 제가 작성하는 코드는 preventDefault를 호출할 필요가 없어 스크롤 연산 성능 향상을 위해 passive를 true로 설정하였습니다.
notion image
 
 
그래서 최종 코드는 다음과 같습니다!
notion image
코드블록
import type { RefObject } from 'react'; import { useEffect, useState } from 'react'; const useGetScrollHeight = (ref: RefObject<HTMLElement>) => { const [scrollTop, setScrollTop] = useState<number | undefined>(0); let throttling = false; const handleScroll = () => { if (throttling) return; throttling = true; setTimeout(() => { setScrollTop(ref?.current?.scrollTop); throttling = false; }, 300); }; useEffect(() => { ref?.current?.addEventListener('scroll', handleScroll, { passive: true }); return () => { // eslint-disable-next-line react-hooks/exhaustive-deps ref?.current?.removeEventListener('scroll', handleScroll); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [ref]); return scrollTop; }; export default useGetScrollHeight;

2. 스크롤 영역 만들기

css에서 스크롤 되게 하는 방법은 크게 2가지가 있습니다.
  1. height를 100vh보다 더 큰 height 값을 지정하기
  1. overflow: scroll을 지정하기
제가 제작한 애니메이션은 100vh보다 높이가 작기 때문에 2번의 방법으로 스크롤 영역을 만들었습니다.
notion image
모든 레이아웃을 감싸는 부모 영역에 overflow scroll 속성을 지정합니다. 그리고 바로 아래 Scroll 영역에 부모 영역보다 더 긴 height를 지정해 주면 스크롤을 발생시킬 수 있습니다.
 

2-1. ref 연결하기

아까 1번에서 제작한 useGetScrollHeight hooks를 사용해서 스크롤 값을 구할 수 있습니다.
notion image
여기서 알아야 할 점은 scrollHeight값의 범위는 스크롤 요소 높이 - ref 요소 높이 입니다. 해당 예제에서 scrollHeight의 범위는 1300 - 600 = 700으로 0 ~ 700 입니다.
 

3. 애니메이션을 구현해 보자!

스크롤 높이를 구하고 스크롤 영역을 만들었다면 이제는 계산의 단계입니다. scroll 값의 범위를 계산하고, 범위마다 스타일을 지정해 주는 것이 애니메이션을 제작하는 것이죠.

3-1. 요소가 부모 가운데 오면 애니메이션

위의 영상같이 가운데 영역으로 오게 되면 하이라이팅 되는 애니메이션을 구현해 봅시다.
notion image
해당 부모의 높이는 600, 스크롤 높이는 1300. 스크롤 범위는 0 ~ 700입니다.
notion image
스크롤 범위에서 해당 요소가 가운데로 오는 범위는 스크롤 범위 / 자식 아이템 개수입니다. 그래서 범위는 700 / 4 = 175입니다.
다음과 같이 구간을 정할 수 있습니다.
1: 0 ~ 175
2: 175 ~ 350
3: 350 ~ 525
4: 525 ~ 700
따라서 그 범위를 계산해 주는 변수를 만든 후 ,
notion image
해당 변수값에 따라 스타일링을 변경시켜 주면 다음과 같습니다.
notion image
이렇게 코드를 작성하면 해당 애니메이션을 구현할 수 있습니다!
 

 

3-2. 요소는 고정하고, 스크롤 시 UI 변경

위 영상같이 요소는 고정 시키되, 스크롤 시 UI가 변경되는 애니메이션을 제작해 보겠습니다.
notion image
이러한 애니메이션은 apple 사이트에서 많이 보실 수 있을 겁니다.
 
해당 애니메이션을 구현하기 위해서는 position sticky 속성을 지정해주면 됩니다.
notion image
고정해야 할 요소에 sticky 속성을 지정해 주고, 부모 요소에 ref를 연결해주면 됩니다. 그리고 요소는 고정되어 있지만 스크롤 현상은 일어나고 있기 때문에 고정되는 것 처럼 보이게 스크롤바를 지워야 합니다.
notion image
이제 기본 스타일 세팅은 다 마쳤으니 js 코드로 애니메이션을 구현하겠습니다.
먼저 위의 코드에서 스크롤 범위는 2600 - 1100으로 1,500입니다. 아이템 개수는 총 3개이니 1500 / 3으로 요소마다 하이라이팅 범위는 500입니다.
1: 0 ~ 500
2: 500 ~ 1,000
3: 1,000 ~ 1,500
해당 범위 값에 도달했을 때 스타일링 변동을 주는 코드를 작성하면 해당 애니메이션을 구현하실 수 있습니다!
notion image
 

마무리

keyframes이나 animation 속성으로 애니메이션을 구현하는 것이 아닌 js코드로 애니메이션을 구현할 수 있다는 것이 흥미로웠습니다!
 

관련 코드