오늘은 블로그 첫화면이 너무 심심해서 interactive developer 김종민씨의 강의를 들으며 wave 애니메이션을 추가해보았다.
requestAnimationFrame 함수는 WebAPI의 비동기 함수이며, CSS transition으로 처리하기 어려운 애니메이션이나 HTML5의 Canvas, SVG 등의 애니메이션 구현을 위해 사용되는 함수다.
requestAnimationFrame 함수는 브라우저에 애니메이션을 수행하고 싶다고 알리고, 브라우저가 다음 repaint를 하기 전에 애니메이션을 업데이트 하기 위해 지정된 함수를 호출하도록 요청한다.
window.requestAnimationFrame(callback);
매개변수는 callback 하나이며 이는 다음 repaint를 위해 애니메이션을 업데이트해야 할 때 호출할 함수다. 이 callback에는 DOMHighResTimeStamp 타입의 하나의 인수가 전달되고, 이 값은 performance.now()의 반환값과 유사하다.
Date.now()와 같은 다른 타이밍 데이터와 달리 performance.now()가 반환하는 DOMHighResTimeStamp의 단위는 1 밀리초로 제한되지 않고, 시간을 최대 마이크로 초 정밀도의 부동소수점 숫자로 나타낸다. 즉, 더욱 정밀한 측정이 필요할 때 사용된다.
long integer 값을 반환하며, 이는 콜백 목록의 항목을 고유하게 식별하기 위한 request id다.
requestAnimationFrame 콜백함수가 인자로 받는 DOMHighResTimeStamp 타입의 time을 매 프레임마다 출력한다.
import { useEffect, useRef, useState } from "react";
export default function Waves() {
const requestRef = useRef<number>(0);
const [time, setTime] = useState(0);
const animate = (t: DOMHighResTimeStamp) => {
// 여기에 원하는 애니메이션 수행 코드 작성
setTime(t);
requestRef.current = requestAnimationFrame(animate);
};
useEffect(() => {
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current);
}, []);
return <p>{time}</p>;
}
useRef는 .current 프로퍼티로 전달된 인자(initialValue)로 초기화된 변경 가능한 ref 객체를 반환한다.
ref는 render 메서드에서 생성된 DOM 노드나 React element에 접근하는 방법을 제공한다.
일반적인 리액트의 자식 컴포넌트는 부모 컴포넌트로부터 props를 통해 데이터를 전달 받는다. 즉, 단방향으로 데이터를 전달받아 props가 변경되면 컴포넌트를 리랜더링한다.
하지만 가끔은 이러한 단방향 데이터 플로우에서 벗어나 직접적으로 자식으로 수정해야 하는 경우도 있다. 수정할 자식은 React 컴포넌트의 인스턴스일 수도, DOM 엘리먼트 일 수도 있는데, 이 때 ref를 통해 해당 자식들에 접근할 수 있다. (단, 함수 컴포넌트는 인스턴스가 없기 때문에 ref 어트리뷰트를 사용할 수 없다. 함수 컴포넌트의 ref가 필요한 경우 forwardRef를 사용한다.)
따라서 내가 useRef를 사용한 경우는 보통 DOM 엘리먼트에 직접 접근하고 싶은 경우가 대부분이였다. 아래 예시가 일반적인 유스케이스이다.
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
(ref를 사용한다면 .current 프로퍼티는 항상 존재하므로 inputEl?.current와 같이 사용하지 않아도 된다. -> 이건 그냥 내 실수 였어서.., inputEl.current?.focus()는 필요한 경우가 있을 수 있음)
이게 오늘 TIL의 핵심이다. "ref" 객체는 current 프로퍼티가 변경할 수 있고 어떤 값이든 보유할 수 있는 일반 컨테이너이다. 이는 class의 인스턴스 프로퍼티와 유사하다.
따라서 이런식으로 DOM element가 아닌 타이머 id를 저장할 때 사용되기도 한다.
function Timer() {
const intervalRef = useRef();
useEffect(() => {
const id = setInterval(() => {
// ...
});
intervalRef.current = id;
return () => {
clearInterval(intervalRef.current);
};
});
// ...
}
https://stackoverflow.com/questions/53090432/react-hooks-right-way-to-clear-timeouts-and-intervals
만약 타이머를 같은 컴포넌트 내에서 시작하고 종료시킨다면 let
키워드를 사용해도 무관하지만 다른 컴포넌트에서 종료시킨다면 ref를 사용해야한다.
function Image(props) {
// ⚠️ IntersectionObserver는 모든 렌더링에서 생성됩니다
const ref = useRef(new IntersectionObserver(onIntersect));
// ...
}
위 예제처럼 useRef에 초기값을 지정해주면 매 랜더링 마다 IntersectionObserver가 생성된다.
따라서 아래 처럼 지연초기화를 통해 값을 지정해 주는 것이 좋다.
function Image(props) {
const ref = useRef(null);
// ✅ IntersectionObserver는 한 번 느리게 생성됩니다
function getObserver() {
if (ref.current === null) {
ref.current = new IntersectionObserver(onIntersect);
}
return ref.current;
}
// 필요할 때 getObserver()를 호출해주세요
// ...
}
https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame
https://css-tricks.com/using-requestanimationframe-with-react-hooks/
https://ko.reactjs.org/docs/hooks-reference.html#useref
https://ko.reactjs.org/docs/refs-and-the-dom.html
https://ko.reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily