리액트로 애니메이션 띄우기 (feat. Canvas & RequestAnimationFrame)
글을 시작하며
😝 글 작성 동기
좋은 기회로(?) 리액트로 캔버스 애니메이션을 구현해보았는데
조금 마이너한 기술이라고 생각되어 정리를 망설였지만
게더타운 등의 서비스를 보면
캔버스 애니메이션 사용법을 정리해두고 공유하는게 좋겠다고 생각하여 글을 작성하게 되었습니다.
😝 미리 보는 결과물
키보드 입력값에 따라 요리조리 움직이고 모습도 변화하는 귀여운 마자용😝에 대한 예제입니다.
😝 배포 링크 😝
😝 깃허브 링크 😝
😝 주의 사항
리액트에서 정말 중요한 상태관리, 컴포넌트 분해 등의 개념보다
requestAnimationFrame으로 화면을 리페인트하는 로직을 더 중시하는 예제입니다!
예제에서 많이 사용된 useRef
의 사용은 지양되고 있으니
재미로 읽으시고, 추후에 더 좋은 방법들을 찾아보시면 좋을 것 같습니다!
RequestAnimationFrame이란?
The
window.requestAnimationFrame()
method tells the browser that you wish to perform an animation and requests that the browser calls a specified function to update an animation before the next repaint. The method takes a callback as an argument to be invoked before the repaint.
window.requestAnimationFrame()은 다음 리페인트(색, 위치 등이 계산된 요소들을 브라우저에 그리는 과정) 전에 애니메이션을 업데이트하는 함수를 호출하는 메소드이다.
setInterval()
을 사용하여 요소의 top, left 값을 바꾸어주는 방법도 있겠지만,
setInterval 함수의 경우
- 디스플레이 갱신 전에 캔버스를 여러 번 고치더라도 디스플레이 갱신 바로 직전의 캔버스 상태만 적용이 되기 때문에, 프레임 손실이 발생할 수도 있으며
- 호출 스택을 깊어지게 하고
- 브라우저의 다른 탭이 선택된 경우와 같이 실제 화면을 다시 그릴 필요가 없는 경우에도 화면을 다시 그리기 때문에
이러한 단점들을 보완할 RequestAnimationFrame을 사용해야 한다.
requestAnimationFrame
의 사용법은 setTimeout
을 setInterval
처럼 사용할 때와 비슷하다.
아래 예제들은 모던 자바스크립트 튜토리얼에서 가져온 setInterval
과 setTimeout
의 예제와
func()
의 동작이 화면에 보이는 요소들을 바꾸는 애니메이션 작업이라고 생각했을 때
requestAnimationFrame
으로 똑같은 작업을 했을 때의 예제 코드이다.
setInterval
let i = 1;
setInterval(function() {
func(i++);
}, 100);
setTimeout
let i = 1;
setTimeout(function run() {
func(i++);
setTimeout(run, 100);
}, 100);
requestAnimationFrame
let i = 1;
function run() {
func(i++);
window.requestAnimationFrame(run)
}
window.requestAnimationFrame(run)
requestAnimationFrame
도 setTimeout
처럼 재귀적으로 호출되어야 한다.
코드를 작성해보자!
😝 작성 전 설계 및 구상
위와 같은 동작을 하는 마자용이 있는 캔버스를 만들기 위한 동작들을 정리해보았다.
(설계할 때 데이터보다 동작을 우선으로 하라는 가르침을 받았기 때문!)
- 어떤 키보드든 눌리면 마자용 움짤을 재생한다
- 마자용 이미지 인덱스를 늘려주는 방법으로 구현
- 상하좌우 키보드가 눌리면 그 방향대로 마자용이 움직인다
- 포지션 값을 갖고 있다가 방향대로 좌표 값을 늘려주는 방법으로 구현
- 캔버스 너비, 높이보다 초과될 경우 못 움직이게 제한
위 동작들을 수행하기 위해 필요한 변수는
- 마자용 이미지 인덱스
- 눌려 있는 키 값 (눌려 있지 않을 경우 null)
- 마자용의 좌표값
이며, 필요한 함수는
- (가장 중요) 캔버스에 렌더링하는 함수
- keydown 이벤트를 처리하는 함수
- 포지션 변수를 관리하는 함수
가 될 것이다.
😝 코드
변수 선언 및 렌더할 부분 정의
const canvasRef = useRef(null);
const requestAnimationRef = useRef(null);
const positionRef = useRef({ x: 0, y: 0 });
const [pressedKey, setPressedKey] = useState(null);
const [currentFrame, setCurrentFrame] = useState(0);
...
return (
<canvas
ref={canvasRef}
style={{
backgroundImage: `url(${background})`,
backgroundSize: "cover",
overflow: "hidden",
backgroundRepeat: "no-repeat",
backgroundPosition: "center",
}}
></canvas>
);
캔버스에다가 뭔가 그리기 위하여 canvas를 변수로 들고 있게끔 하기 위해 canvasRef를 선언하고 그를 canvas 태그 안에 넣어준다.
useEffect()에서 초기화하기
useEffect(() => {
window.addEventListener("keydown", (e) => {
e.preventDefault();
setPressedKey(e.keyCode);
});
window.addEventListener("keyup", () => setPressedKey(null));
requestAnimationRef.current = requestAnimationFrame(render);
return () => {
cancelAnimationFrame(requestAnimationRef.current);
};
});
keyDown과 keyUp 이벤트를 정의해주고, requestAnimationFrame을 호출하여 requestAnimationRef에 넣어준다.
useEffect의 리턴 함수로는 cancelAnimationFrame을 넣어주어 브라우저의 메모리 릭을 막는다.
render()함수로 마자용 띄우기
const render = () => {
const canvas = canvasRef.current;
const context = canvas.getContext("2d");
const wobbuffetsImage = new Image();
wobbuffetsImage.src = wobbuffetsArray[currentFrame];
wobbuffetsImage.onload = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
context.drawImage(
wobbuffetsImage,
positionRef.current.x,
positionRef.current.y
);
};
handleKey();
requestAnimationRef.current = requestAnimationFrame(render);
};
아무래도 매 프레임마다 마자용의 이미지를 변화시켜주어야 하다 보니,
이미지 초기화 또한 매 프레임마다 하게 되었다.
onload() 콜백 안에 마자용을 그리는 함수를 넣어주지 않으면 자주 끊기는 현상이 발생한다.
handleKey() 함수와 move()함수로 마자용 위치 조정하기
const handleKey = () => {
switch (pressedKey) {
case MAP_CONSTANTS.KEY_LEFT:
move({ x: -1 * MAP_CONSTANTS.SPEED, y: 0 });
return;
case MAP_CONSTANTS.KEY_DOWN:
move({ x: 0, y: -1 * MAP_CONSTANTS.SPEED });
return;
case MAP_CONSTANTS.KEY_RIGHT:
move({ x: MAP_CONSTANTS.SPEED, y: 0 });
return;
case MAP_CONSTANTS.KEY_UP:
move({ x: 0, y: MAP_CONSTANTS.SPEED });
return;
case null:
return;
default:
move({ x: 0, y: 0 });
return;
}
};
키값에 따라 이동하는 방향을 바꾸어줬다.
아무 키도 눌려있지 않으면 움직임을 주지 않았고,
상하좌우 키를 제외한 키가 눌려 있어도 움직임은 주었다.
const move = ({ x, y }) => {
const newX = positionRef.current.x + x;
const newY = positionRef.current.y + y;
if (newX < 0 || newX > canvasRef.current.width - MAP_CONSTANTS.IMG_WIDTH)
return;
if (newY < 0 || newY > canvasRef.current.height - MAP_CONSTANTS.IMG_HEIGHT)
return;
positionRef.current = { x: newX, y: newY };
setCurrentFrame((prev) =>
prev < MAP_CONSTANTS.FRAMES_LENGTH ? prev + 1 : 0
);
};
positionRef을 관리해주고
현재 마자용의 이미지 프레임을 관리해준다.
프레임 길이보다 벗어나면 냅다 0을 주었다.
결론
아직 손 볼 곳이 많은 코드입니다.
GPU에게 냅다 렌더링을 맡기니 안 좋은 코드를 짜도 부드럽게 돌아가니까 안주하게 됩니다..
나중에 게더타운처럼 소켓 등등에서도 신경쓸 게 많은 서비스를 만들게 된다면 더 최적화된 방법을 알아봐야겠지요!
이 예제가 캔버스를 처음 배우시거나 리액트+캔버스로 뭔가 만들고 싶으신 분들께 도움이 되었으면 좋겠습니다 ㅎㅎ
Author And Source
이 문제에 관하여(리액트로 애니메이션 띄우기 (feat. Canvas & RequestAnimationFrame)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@1106laura/react-canvas-requestanimationframe저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)