코린이의 개발 일지

[React] 리액트에서 Canvas API로 애니메이션 구현하기 본문

웹 (web)/프론트엔드

[React] 리액트에서 Canvas API로 애니메이션 구현하기

폴라민 2022. 10. 21. 14:37
반응형

window.addeventListener를 써보자

맨처음에 이 방법으로 keyDown이벤트를 발생시켜 보았다. 리액트스럽게 훅으로만 대부분을 해결해서 코드를 짜는게 목표였지만 onKeyDown으로 도저히 작동을 안해서 (내가 잘못쓰고 있는 것였다.) 그냥 직접 이벤트 리스너를 붙여봤다. (리액트에서 권유하는 방법은 아니니 안쓰는 편이 좋다)

근데 문제가 발생했다.

이벤트 리스너를 렌더링 할 때마다 달아주면 리스너가 너무 많이 달리니까 useEffect안에 넣어뒀는데 그것도 문제였다.

리스너는 딱 한번만 호출되어야하는데 useEffect가 여러번 호출되면서 이벤트 리스너가 너무 많이 붙여지는게 문제였다.

해결한 코드는 다음과 같다.

import React, { RefObject, useRef, useEffect, useState } from 'react';
import { CanvasWrapper } from './style/AppStyle';

const boxInfo: { x: number; y: number; color: string } = {
  x: 50,
  y: 50,
  color: 'blue',
};

const prevPosition: { x: number; y: number } = {
  x: 100,
  y: 100,
};

const move: number = 10;

function App(): JSX.Element {
  const canvasRef: RefObject<HTMLCanvasElement> =
    useRef<HTMLCanvasElement>(null); // 현재 태그
  const [position, setPosition] = useState({ x: 100, y: 100 });
  const [ctx, setCtx] = useState<CanvasRenderingContext2D | null>(null);
  let raf: number;

  function draw(): void {
    if (ctx !== null) {
      // console.log(position);
      ctx.clearRect(prevPosition.x, prevPosition.y, boxInfo.x, boxInfo.y);
      ctx.fillStyle = boxInfo.color;
      ctx.fillRect(position.x, position.y, boxInfo.x, boxInfo.y);
      raf = window.requestAnimationFrame(draw);
    }
  }
  const handleKeyDown = (e: KeyboardEvent): void => {
    switch (e.key) {
      case 'ArrowRight':
        prevPosition.x = position.x;
        prevPosition.y = position.y;
        setPosition({ x: position.x + move, y: position.y });
        break;
      case '37':
        prevPosition.x = position.x;
        prevPosition.y = position.y;
        setPosition({ x: position.x - move, y: position.y });
        break;
      case 'ArrowUp':
        prevPosition.x = position.x;
        prevPosition.y = position.y;
        setPosition({ x: position.x, y: position.y + move });
        break;
      case 'ArrowDown':
        prevPosition.x = position.x;
        prevPosition.y = position.y;
        setPosition({ x: position.x, y: position.y - move });
        break;
      default:
        break;
    }
    raf = window.requestAnimationFrame(draw);
  };
  useEffect(() => {
    console.log('in useEffect');
    const Canvas = canvasRef.current as HTMLCanvasElement;
    setCtx(Canvas.getContext('2d'));
    window.addEventListener('keydown', handleKeyDown);

    if (ctx !== null) {
      ctx.fillStyle = 'blue';
      ctx.fillRect(position.x, position.y, boxInfo.x, boxInfo.y);
    }
    return () => {
      window.removeEventListener('keydown', handleKeyDown);
      window.cancelAnimationFrame(raf);
    };
  }, [ctx, position]);
  return (
    <>
      <CanvasWrapper>
        <canvas id="canvas" width="1500" height="1000" ref={canvasRef}></canvas>
      </CanvasWrapper>
    </>
  );
}

export default App;

useEffect에 리턴 값을 주면 언마운트 될때 그 값들이 실행된다.

return 값으로 이벤트리스너를 제거해주니 정상적으로 동작하였다. 허나 앞서 말했듯 이 방법은 직접 돔을 건드리는 거라 그렇게 좋은 방법은 아니다.

다음은 onKeyDown 으로 해결한 코드이다.

import React, {
  RefObject,
  useRef,
  useEffect,
  useState,
  KeyboardEvent,
} from 'react';
import { CanvasWrapper } from './style/AppStyle';

const boxInfo: { x: number; y: number; color: string } = {
  x: 50,
  y: 50,
  color: 'blue',
};

const prevPosition: { x: number; y: number } = {
  x: 100,
  y: 100,
};

const move: number = 10;

function App(): JSX.Element {
  const canvasRef: RefObject<HTMLCanvasElement> =
    useRef<HTMLCanvasElement>(null); // 현재 태그
  const [position, setPosition] = useState({ x: 100, y: 100 });
  const [ctx, setCtx] = useState<CanvasRenderingContext2D | null>(null);
  let raf: number;
  const keyDownEvent = (e: KeyboardEvent<HTMLDivElement>): void => {
    console.log('이벤트 전', position);
    console.log('이벤트 전', e.key);

    switch (e.key) {
      case 'ArrowRight':
        prevPosition.x = position.x;
        prevPosition.y = position.y;
        setPosition({ x: position.x + move, y: position.y });
        console.log('이벤트 중', position);
        break;
      case '37':
        prevPosition.x = position.x;
        prevPosition.y = position.y;
        setPosition({ x: position.x - move, y: position.y });
        break;
      case 'ArrowUp':
        prevPosition.x = position.x;
        prevPosition.y = position.y;
        setPosition({ x: position.x, y: position.y + move });
        break;
      case 'ArrowDown':
        prevPosition.x = position.x;
        prevPosition.y = position.y;
        setPosition({ x: position.x, y: position.y - move });
        break;
      default:
        break;
    }
    raf = window.requestAnimationFrame(draw);
  };

  function draw(): void {
    if (ctx !== null) {
      // console.log(position);
      ctx.clearRect(prevPosition.x, prevPosition.y, boxInfo.x, boxInfo.y);
      ctx.fillStyle = boxInfo.color;
      ctx.fillRect(position.x, position.y, boxInfo.x, boxInfo.y);
      raf = window.requestAnimationFrame(draw);
    }
  }
  useEffect(() => {
    console.log('in useEffect');
    const Canvas = canvasRef.current as HTMLCanvasElement;
    setCtx(Canvas.getContext('2d'));
    window.addEventListener('keyup', e => {
      window.cancelAnimationFrame(raf);
    });
    if (ctx !== null) {
      ctx.fillStyle = 'blue';
      ctx.fillRect(position.x, position.y, boxInfo.x, boxInfo.y);
    }
    return () => window.removeEventListener("keydown", handleKeyUp);
  }, [ctx, position]);
  return (
    <>
      <CanvasWrapper>
        <div onKeyDown={keyDownEvent} tabIndex={0}>
          <canvas
            id="canvas"
            width="1500"
            height="1000"
            ref={canvasRef}
          ></canvas>
        </div>
      </CanvasWrapper>
    </>
  );
}

export default App;

이때 tabindex라는 걸 처음 들어봤다.

이 tabindex라는 것을 알기 위해선 키보드 포커스라는 걸 알아야한다.

보통 사이트에서 칸마다 뭘 입력할때 탭으로 이동이 가능한 것을 알 수 있는데 이것은 각각의 입력 태그들이 기본적으로 키보드 포커스가 잡히게 되어있기 때문이다.

이런 것들이 가능한 element들이 있는데 대표적을 input, select, button 같은 form태그와 a 태그이다.

상호작용하지 않는 div나 span 태그와 같은 요소에도 키보드 포커스가 잡히게 하고 싶을 경우에는 tabindex를 0으로 주면 된다.

tabindex를 -1로 주면 상호작용 가능한 요소라도 포커스가 이동하지 않게 된다.

div 태그에 tabindex를 0으로 주면서 keydown 이벤트가 인식되게 된다.

반응형
Comments