해당 글은 김주성님의 세미나를 듣고 작성한 글 입니다.
작년에 React Hooks이 정식 릴리즈 된 이후 많은 사람들이 Hooks을 사용하고 있습니다.
Hooks를 사용하는 가장 큰 이유는 바로 컴포넌트로 부터 상태 관련 로직을 추상화 할 수 있고, 별도의 함수로 빼서 재사용이 가능하기 때문인데요.
상태관리 로직의 재사용 의 특징을 활용해 다양한 Custom Hooks를 만들어 사용 할 수 있습니다.
보통 웹에 Animation이 추가 된다면 한 곳에서만 사용하는 것이 아니라, 다양한 곳에 적용되게 되는데요. Custom Hook을 사용하여 Animation을 정의해 둔다면 쉽고 깔끔하게 Animation을 적용 할 수 있습니다.
스크롤 타이밍에 따라 Dom이 FadeIn 되는 animation은 크게 세가지 부분으로 나눌 수 있습니다.
위 세가지 포인트를 토대로 구현하는 과정을 담아보려 합니다.
위에서 나눴던 포인트 중 세번째인 “Animation 트리거 이벤트를 DOM에 지정한다.” 를 구현하기 위해서는 Ref
를 사용해야 합니다.
Ref
는 포커스, 미디어 재생 또는 애니메이션을 직접적으로 실행 시키기 위해 외부에서 DOM(또는 React Component)을 제어 할 수 있게 도와줍니다.
hooks
폴더에 useScrollFadeIn.js
라는 파일을 생성 후 useRef
를 사용하여 ref를 생성 후 return 하는 함수를 만들었습니다.
import { useRef } from 'react';
const useScrollFadeIn = () => {
const dom = useRef();
return {
ref: dom,
};
};
ref(dom)
을 바로 return
하지 않고 Obejct로 한번 감싼 이유는 component에 적용 할 때 Spread와 추후 style 등의 속성이 추가 될 예정이기 때문입니다. 😽
해당 hook을 선언 후 사용하는 방법은 아래와 같습니다.
import React from 'react';
import { useScrollFadeIn } from '@/hooks';
const Contacts = () => {
const animatedItem = useScrollFadeIn();
return (
<Wrapper>
<Title>Contacts</Title>
<Description>
Consequat interdum varius sit amet mattis vulputate enim.
</Description>
<Form {...animatedItem} />
</Wrapper>
)
}
Scroll 트리거 이벤트를 만들기 위해서는 스크롤을 감지 해야하고, 언제 반응 할지에 대한 조건이 필요합니다.
// 해당 방식은 퍼포먼스에 문제가 있습니다.
// 안티 패턴을 소개하기 위해 만들어진 예시 코드입니다.
// 아래 Intersection Observer를 사용한 코드를 참고해 주세요.
const useScrollFadeIn = () => {
const dom = useRef();
const handleScroll = useCallback(() => {
const { current } = dom;
const currentScrollPosition = window.pageYOffset;
const currentDomScrollPosition = currentScrollPosition + current.getBoundingClientRect().top - 800; // 800은 이벤트 반응 시점을 조절하기 위해 넣은 임의의 Y position 값
}, []);
useEffect(() => {
if (element.current) {
window.addEventListener('scroll', handleScroll);
};
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [handleScroll]);
return {
ref: dom,
};
};
위와 같이 직접 실시간으로 window의 세로 scroll position 값과, animation을 동작 하게 할 dom element의 position을 비교해서 동작하는 트리거를 만들 수 있지만, window.addEventListener
를 통해 직접 스크롤 이벤트를 핸들링 할 경우
getBoundingClientRect()
호출 시에 리플로우(레이아웃 전체 리렌더링) 현상 발생 (성능 저하)로 인해 퍼포먼스에 문제가 생기게 됩니다.
위의 이슈의 대안으로 Intersection Observer 을 사용하면 좋습니다.
⚠️ 위의 방식대로 하면 퍼포먼스 문제가 생긴다. Intersection Observer를 사용할 것!
const useScrollFadeIn = () => {
const dom = useRef();
const handleScroll = useCallback(([entry]) => {
const { current } = dom;
if(entry.isIntersecting) {
// 원하는 이벤트를 추가 할 것
}
}, []);
useEffect(() => {
let observer;
const { current } = dom;
if (current) {
observer = new IntersectionObserver(handleScroll, { threshold: 0.7 });
observer.observe(current);
return () => observer && observer.disconnect();
};
}, [handleScroll]);
return {
ref: dom,
};
}
IntersectionObserver
에 동작 하게 할 함수와 Observer 세팅 값들을 넘겨 줍니다.
threshold
는 number
나 Array<number>
로 정의할 수 있으며, Default 값은 0입니다.
FadeIn Animation은 css의 opacity
와 transform
그리고 transition
을 사용하여 구현 할 수 있습니다.
원리는 간단한데요. 이벤트 트리거가 발생 하기 전에는 opacity: 0
, transform: translate3d(0, 50%, 0)translate3d(0, 50%, 0)
으로 원하는 위치 아래에 숨겨 두었다가, 트리거가 발생 할 때, opacity:1
, transform: translate3d(0, 0, 0)
과 같이 제자리에 돌려두면 됩니다.
const useScrollFadeIn = () => {
const dom = useRef();
const handleScroll = useCallback(([entry]) => {
const { current } = dom;
if(entry.isIntersecting) {
current.style.transitionProperty = 'opacity transform';
current.style.transitionDuration = '1s';
current.style.transitionTimingFunction = 'cubic-bezier(0, 0, 0.2, 1)';
current.style.transitionDelay = '0s';
current.style.opacity = 1;
current.style.transform = 'translate3d(0, 0, 0)';
}
}, []);
useEffect(() => {
let observer;
const { current } = dom;
if (current) {
observer = new IntersectionObserver(handleScroll, { threshold: 0.7 });
observer.observe(current);
return () => observer && observer.disconnect();
};
}, [handleScroll]);
return {
ref: dom,
style: {
opacity: 0,
transform: 'translate3d(0, 50%, 0)',
}
};
}
위와 같이 return
시 초기에 숨겨두기 위한 style
object들을 설정합니다. 외부의 영향을 최소화 하기 위해 style은 직접 주입하며, 만약 기존에 opacity 등을 사용했다면 레이아웃이 깨질 수 있습니다.
위에 구현되어있는 FadeIn Animation의 경우 항상 아래에서 위로 동작하며, Duration
과 Delay
를 설정 해 줄 수 없습니다.
Custom 하고자 하는 설정들을 hook을 사용 할 때 Props로 넘겨주고 지정해 준다면 좀 더 세세하고 다양한 FadeIn Animation을 적용 할 수 있습니다.
import { useRef, useEffect, useCallback } from 'react';
const useScrollFadeIn = (direction = 'up', duration = 1, delay = 0) => {
const dom = useRef();
const handleDirection = (name) => {
switch (name) {
case 'up':
return 'translate3d(0, 50%, 0)';
case 'down':
return 'translate3d(0, -50%, 0)';
case 'left':
return 'translate3d(50%, 0, 0)';
case 'right':
return 'translate3d(-50%, 0, 0)';
default:
return;
};
};
const handleScroll = useCallback(
([entry]) => {
const { current } = element;
if (entry.isIntersecting) {
current.style.transitionProperty = 'all';
current.style.transitionDuration = `${duration}s`;
current.style.transitionTimingFunction = 'cubic-bezier(0, 0, 0.2, 1)';
current.style.transitionDelay = `${delay}s`;
current.style.opacity = 1;
current.style.transform = 'translate3d(0, 0, 0)';
};
},
[delay, duration],
);
useEffect(() => {
let observer;
const { current } = dom;
if (current) {
observer = new IntersectionObserver(handleScroll, { threshold: 0.7 });
observer.observe(current);
}
return () => observer && observer.disconnect();
}, [handleScroll]);
return {
ref: dom,
style: {
opacity: 0,
transform: handleDirection(direction),
},
};
};
export default useScrollFadeIn;