좋은 기회로(?) 리액트로 캔버스 애니메이션을 구현해보았는데
조금 마이너한 기술이라고 생각되어 정리를 망설였지만
게더타운 등의 서비스를 보면
캔버스 애니메이션 사용법을 정리해두고 공유하는게 좋겠다고 생각하여 글을 작성하게 되었습니다.
키보드 입력값에 따라 요리조리 움직이고 모습도 변화하는 귀여운 마자용😝에 대한 예제입니다.
😝 배포 링크 😝
😝 깃허브 링크 😝
리액트에서 정말 중요한 상태관리, 컴포넌트 분해 등의 개념보다
requestAnimationFrame으로 화면을 리페인트하는 로직을 더 중시하는 예제입니다!
예제에서 많이 사용된 useRef
의 사용은 지양되고 있으니
재미로 읽으시고, 추후에 더 좋은 방법들을 찾아보시면 좋을 것 같습니다!
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
의 사용법은 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
처럼 재귀적으로 호출되어야 한다.
위와 같은 동작을 하는 마자용이 있는 캔버스를 만들기 위한 동작들을 정리해보았다.
(설계할 때 데이터보다 동작을 우선으로 하라는 가르침을 받았기 때문!)
위 동작들을 수행하기 위해 필요한 변수는
이며, 필요한 함수는
가 될 것이다.
변수 선언 및 렌더할 부분 정의
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에게 냅다 렌더링을 맡기니 안 좋은 코드를 짜도 부드럽게 돌아가니까 안주하게 됩니다..
나중에 게더타운처럼 소켓 등등에서도 신경쓸 게 많은 서비스를 만들게 된다면 더 최적화된 방법을 알아봐야겠지요!
이 예제가 캔버스를 처음 배우시거나 리액트+캔버스로 뭔가 만들고 싶으신 분들께 도움이 되었으면 좋겠습니다 ㅎㅎ
역시나 자바스크립트를 잘하시는군요