requestAnimationFrame의 callback을 update하는 hook이다.
Raf => RequestAnimationFrame의 약자
function Demo() {
const [state, setState] = useRafState({ x: 0, y: 0 });
useMount(() => {
const onMouseMove = (event: MouseEvent) => {
setState({ x: event.clientX, y: event.clientY });
};
const onTouchMove = (event: TouchEvent) => {
setState({
x: event.changedTouches[0].clientX,
y: event.changedTouches[0].clientY,
});
};
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("touchmove", onTouchMove);
return () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("touchmove", onTouchMove);
};
});
return <pre>{JSON.stringify(state, null, 2)}</pre>;
};
import type { Dispatch, SetStateAction } from "react";
import { useCallback, useRef, useState } from "react";
import { useUnmount } from "../useUnmount";
export const useRafState = <S>(
initialState: S | (() => S),
): readonly [S, Dispatch<SetStateAction<S>>] => {
const frame = useRef(0);
const [state, setState] = useState(initialState);
const setRafState = useCallback((value: S | ((prevState: S) => S)) => {
cancelAnimationFrame(frame.current);
frame.current = requestAnimationFrame(() => {
setState(value);
});
}, []);
useUnmount(() => {
cancelAnimationFrame(frame.current);
});
return [state, setRafState] as const;
};
여기서 requestAnimationFrame이란, window 인터페이스에 있는 JS내장함수이다. 보통 애니메이션을 구현 할 때 css의 animation, transition, transform 속성을 통해 구현 할 수도 있지만, 복잡한 상호작용을 구현하기 위해 JS를 사용할 때는 해당 함수를 사용함
간단하고 규칙적인 애니메이션은 CSS로만 요소의 좌표값이나 스타일 크기를 변화시키고, 세밀한 조작이 필요한 애니메이션은 자바스크립트로 스타일 속성을 변경 시키는 편.
하지만 => JS로 스타일 속성을 변화시키는 방법은 CSS보다 성능적으로 좋지 않다. => 이를 최적화하는 기법이 존재함.
애니메이션이란게 결국 => 여러 FRAME들이 빠른시간안에 화면이 전환되는 ㅁ형태
FPS(FRAME PER SECOND)를 보통 단위로 사용하는데 => 보통 인간의 눈은 1초에 60번 장면이 넘어가야 부드럽다고 느낀다고 함
현대 기기들은 시각적인 효과를 위해 초당 60번 화면을 다시 그리도록 기본적으로 설계된다. 이를 60fps 혹은 60hz 라고 불린다.
브라우저 렌더링은 여러 단계에 걸쳐서 이뤄진다. (HTML => DOM, CSS => CSSOM , => LAYOUT 계산, 이를 화면에 그려내는 PAINT 작업까지 )..
웹 페이지에서 부드러운 애니메이션 효과를 주기 위해서는 인간에 눈에 최적화된 60FPS단위로 설계해야함(즉 초당 60개의 FRAME을 렌더링해야함.) => 16.666ms 간격으로 하나의 프레임을 생성해야한다.
setInterval, setTimeout 함수로도 구현 할 수 있지만, 이는 시간내에 동작을 할뿐, 프레임을 신경쓰지 않는 단점이 있다.
const performAnimation = () => {
/* 스타일 조정 스크립트 */
}
// 1초에 60번 무한 반복
setInterval(performAnimation, 1000 / 60)
자바스크립트 실행에 의해 리플로우가 일어나 위에서 본 브라우저 렌더링 단계인 레이아웃 - 페인트 - 합성 과정이 다시 일어나게 되는데, 그러면 프레임이 생성되지 못하고 누락되어 버려 1 프레임이 깎여버리는 현상이 나타나게 됨.
JS는 하나의 콜스택을 가지기 때문에, callback이 호출되는 시점에서 task queue에 진행중인 작업이 존재하게 된다면 => queue가 비어질때까지 기다린 후 setTimeout의 callback을 호출하는 dealy현상이 발생한다, 즉 애니메이션이 버벅거리게 되는 것!!
requestAnimationFrame 함수는 시스템이 프레임을 그릴 준비가 되면 애니메이션 프레임을 호출하여 애니메이션 웹페이지를 보다 원활하고 효율적으로 생성할 수 있도록 해줌
실제 화면이 갱신되어 표시되는 주기에 따라 함수를 호출해주기 때문에 자바스크립트가 프레임 시작 시 실행되도록 보장해주어 위와 같은 밀림 현상을 방지한다.
백그라운드 동작 중지 : setTimeout 같은 경우 다른 탭화면을 보거나 브라우저가 최소화 되어있을때에도 계속해서 타이머가 돌아서 콜백을 호출하기 때문에 리소스가 낭비됨, requestAnimationFrame는 페이지가 비활성화된 상태라면 화면을 그리는 것을 중단함.
디스플레이 주사율에 맞게 호출 횟수 결정 : 웹브라우저는 디스플레이 주사율을 따름 => 만일 144hz 주사율 고사양 모니터일 경우 rAF 역시 1초에 144번 호출되게 됨.
task queue가 아닌 animation frame이라는 별도의 queue에서 처리하게 됨, task queue보다 우선순위가 높다
이벤트 루프가 비동기 작업을 처리하는 우선순위는 Microtask Queue -> Animation Frames -> Task Queue 순이다.
이벤트 루프는 Microtask Queue나 Animation Frames를 방문할 때는, 큐 안에 있는 모든 작업들을 수행하지만, Task Queue를 방문할 때는 한 번에 하나의 작업만 call stack으로 전달하고 다른 Queue를 순회한다. (이렇기 때문에 일정주기마다 동일하게 FRAME을 그려내는게 가능한듯)
cancelAnimationFrame => requestAnimationFrame을 중지하는 함수 (clearInterval처럼..)
const rocket = document.querySelector('.rocket');
const value = document.querySelector('.value');
let yPos = 0;
let rafId;
// 콜백 함수
const render = () => {
yPos += 2; // y 좌표 증가
rocket.style.transform = `translateY(${-yPos}px)`; // 로켓을 위로 올리기
value.innerHTML = yPos; // 카운터 표시
// 만약 로켓 위치가 일정 y좌표값일 경우 requestAnimationFrame 종료
if (yPos >= 500) {
cancelAnimationFrame(rafId);
return;
}
rafId = requestAnimationFrame(render); // rAF 반복 호출
}
requestAnimationFrame(render); // 애니메이션 시작
function Demo() {
const isSupported = useSupported(() => "EyeDropper" in window);
return (
<div>
<p>
window.EyeDropper is {isSupported ? "supported" : "unsupported"} in your
browser
</p>
</div>
);
};
result : window.EyeDropper is supported in your browser
document.getSelection 을 바탕으로 user의 text Selection을 tracking 하는 hook
Selection는 사용자, 캐럿의 현재 위치에 의해 선택된 텍스트의 범위를 나타냄.
사용자가 마우스를 통해 드래그하거나, 키보드를 통해 선택한 텍스트의 범위를 나타냄
브라우저에서는 사용자가 선택한 텍스트에 대한 처리를 지원하기 위해 Selection API를 지원
selection검사 또는 조작을 위해 객체를 얻으려면 document.getSelection()을 호출합니다.
사용자는 왼쪽에서 오른쪽 또는 오른쪽에서 왼쪽을 선택 할 수 있습니다.(방향성 존재)
데스크탑 마우스로 선택하면 마우스 버튼을 누른 위치에 앵커가 배치되고 마우스 버튼을 놓은 위치에 포커스가 배치됩니다.
function Demo() {
const selection = useTextSelection();
return (
<div style={{ padding: 40 }}>
<p>
Select some text here or anywhere on the page and it will be displayed
below
</p>
<div>Selected text: {selection?.toString()}</div>
</div>
);
};
function Demo() {
const [value, setValue] = useState<string>();
const throttledValue = useThrottle(value, 500);
return (
<div>
<input
value={value}
onChange={e => setValue(e.target.value)}
placeholder="Typed value"
style={{ width: 280 }}
/>
<p style={{ marginTop: 16 }}>throttledValue: {throttledValue}</p>
</div>
);
};
render(<Demo/>)
lodash throttle => 내부적으로 debounce를 이용하고 있네
option을 바꿔서 사용하는듯.
function throttle(func, wait, options) {
let leading = true;
let trailing = true;
if (typeof func !== 'function') {
throw new TypeError('Expected a function');
}
if (isObject(options)) {
leading = 'leading' in options ? !!options.leading : leading;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}
return debounce(func, wait, {
leading,
trailing,
maxWait: wait,
});
}
export default throttle;
ref)
https://developer.mozilla.org/ko/docs/Web/API/window/requestAnimationFrame
requestAnimationFrame : https://inpa.tistory.com/entry/%F0%9F%8C%90-requestAnimationFrame-%EA%B0%80%EC%9D%B4%EB%93%9C
JS 호출스택, 이벤트루프 : https://iamsjy17.github.io/javascript/2019/07/20/how-to-works-js.html
getSelection : https://developer.mozilla.org/en-US/docs/Web/API/Document/getSelection