💻 Frontend
Scroll 위치에 따른 애니메이션 제작하기
category
💻 Frontend
1. 특정 요소의 스크롤 값 구하기1-1. throttling1-2. passive option2. 스크롤 영역 만들기2-1. ref 연결하기3. 애니메이션을 구현해 보자!3-1. 요소가 부모 가운데 오면 애니메이션3-2. 요소는 고정하고, 스크롤 시 UI 변경마무리관련 코드
이번 Official gsm 프로젝트를 제작하면서 많은 애니메이션을 제작하게 되었습니다. 그중에 스크롤 할 때 애니메이션을 적용시키는 걸 제작하였는데 어떻게 제작하였는지 그 과정을 적어보도록 하겠습니다.
1. 특정 요소의 스크롤 값 구하기
먼저 스크롤 될 때마다 애니메이션을 구현하기 위해선
자식 요소를 감싸고 있는 부모 요소의 스크롤 값을 구해야 합니다.
저희 프로젝트에서는 window scroll 값을 구하는 custom hooks이 있어서
그 코드를 참고하여 custom hooks을 제작하였습니다.
코드블록
// 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 상태를 변경시켜 이벤트 지연을 시켰습니다.
1-2. passive option
passive option은 이벤트 리스너 옵션 중 하나로 스크롤 성능을 최적화하는 역할입니다.
하지만 이 옵션은 default 옵션이 아닌데요. 어떠한 이유일까요?
passive를 true로 설정하면 개발자가 명시적으로
preventDefault
를 호출할 수 없게 되므로 스크롤 동작에 대한 제어가 제한될 수 있습니다. 하지만 제가 작성하는 코드는 preventDefault
를 호출할 필요가 없어 스크롤 연산 성능 향상을 위해 passive를 true로 설정하였습니다.그래서 최종 코드는 다음과 같습니다!
코드블록
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가지가 있습니다.
- height를 100vh보다 더 큰 height 값을 지정하기
- overflow: scroll을 지정하기
제가 제작한 애니메이션은 100vh보다 높이가 작기 때문에 2번의 방법으로 스크롤 영역을 만들었습니다.
모든 레이아웃을 감싸는 부모 영역에 overflow scroll 속성을 지정합니다.
그리고 바로 아래 Scroll 영역에 부모 영역보다 더 긴 height를 지정해 주면 스크롤을 발생시킬 수 있습니다.
2-1. ref 연결하기
아까 1번에서 제작한 useGetScrollHeight hooks를 사용해서 스크롤 값을 구할 수 있습니다.
여기서 알아야 할 점은 scrollHeight값의 범위는
스크롤 요소 높이 - ref 요소 높이
입니다.
해당 예제에서 scrollHeight의 범위는 1300 - 600 = 700으로 0 ~ 700 입니다.3. 애니메이션을 구현해 보자!
스크롤 높이를 구하고 스크롤 영역을 만들었다면 이제는 계산의 단계입니다. scroll 값의 범위를 계산하고, 범위마다 스타일을 지정해 주는 것이 애니메이션을 제작하는 것이죠.
3-1. 요소가 부모 가운데 오면 애니메이션
위의 영상같이 가운데 영역으로 오게 되면 하이라이팅 되는 애니메이션을 구현해 봅시다.
해당 부모의 높이는 600, 스크롤 높이는 1300. 스크롤 범위는 0 ~ 700입니다.
스크롤 범위에서 해당 요소가 가운데로 오는 범위는
스크롤 범위 / 자식 아이템 개수
입니다. 그래서 범위는 700 / 4 = 175입니다.다음과 같이 구간을 정할 수 있습니다.
1: 0 ~ 175
2: 175 ~ 350
3: 350 ~ 525
4: 525 ~ 700
따라서 그 범위를 계산해 주는 변수를 만든 후 ,
해당 변수값에 따라 스타일링을 변경시켜 주면 다음과 같습니다.
이렇게 코드를 작성하면 해당 애니메이션을 구현할 수 있습니다!
3-2. 요소는 고정하고, 스크롤 시 UI 변경
위 영상같이 요소는 고정 시키되, 스크롤 시 UI가 변경되는 애니메이션을 제작해 보겠습니다.
이러한 애니메이션은 apple 사이트에서 많이 보실 수 있을 겁니다.
해당 애니메이션을 구현하기 위해서는
position sticky
속성을 지정해주면 됩니다.고정해야 할 요소에 sticky 속성을 지정해 주고, 부모 요소에 ref를 연결해주면 됩니다. 그리고 요소는 고정되어 있지만 스크롤 현상은 일어나고 있기 때문에 고정되는 것 처럼 보이게 스크롤바를 지워야 합니다.
이제 기본 스타일 세팅은 다 마쳤으니 js 코드로 애니메이션을 구현하겠습니다.
먼저 위의 코드에서 스크롤 범위는
2600 - 1100
으로 1,500입니다.
아이템 개수는 총 3개이니 1500 / 3
으로 요소마다 하이라이팅 범위는 500입니다.1: 0 ~ 500
2: 500 ~ 1,000
3: 1,000 ~ 1,500
해당 범위 값에 도달했을 때 스타일링 변동을 주는 코드를 작성하면 해당 애니메이션을 구현하실 수 있습니다!
마무리
keyframes이나 animation 속성으로 애니메이션을 구현하는 것이 아닌 js코드로 애니메이션을 구현할 수 있다는 것이 흥미로웠습니다!