본문 바로가기
React

React에서 canvas를 다루면서 알아보는 Hooks

by Luke K 2022. 10. 23.

리액트에서 캔버스를 다뤄보며 React가 제공하는 hooks들의 동작을 살펴보자.

시나리오

  1. 브라우저 크기 조정을 해도 캔버스의 크기는 항상 브라우저에 꽉 차게 맞추어보자.
  2. 해당 서비스에 들어오자마자 캔버스에 포커스가 돼 클릭없이 캔버스에서 키보드 이벤트를 사용할 수 있다.

useStateuseEffect를 통해 사용자가 열고 있는 브라우저의 너비와 높이를 상태로 관리하자

  1. useState를 통해 window 객체의 innerWidthinnerHeight를 상태로 관리한다.
  const [windowSize, setWindowSize] = useState<{
    width: number;
    height: number;
}>({
    width: window.innerWidth,
    height: window.innerHeight,
});
    1. jsx, tsx의 width, height props로 상태를 넣어준다.
      브라우저의 너비와 높이를 useState를 통해 관리하면 변경시마다 렌더링이 된다.
  return (
    <canvas
      width={windowSize.width}
      height={windowSize.height}
      onKeyDown={keyDownHandler}
      tabIndex={0}
      ref={canvasRef}
    ></canvas>
  );
  1. 브라우저의 크기 변화를 감지하는 resize 이벤트에 setWindowSize이벤트를 걸어준다.
      const resizeHandler = useCallback(() => {
       setWindowSize({ width: window.innerWidth, height: window.innerHeight });
      }, []);
    useCallback을 이용하여 함수를 감싸줬는데 이는 canvas를 다루는 컴포넌트가 렌더링 될 때마다 resizeHandler를 다시 생성하는 것을 막아준다.
  2. window 객체의 resize Event Listener에 resizeHandler를 넘겨준다.
      useEffect(() => {
      window.addEventListener('resize', resizeHandler);
      return () => {
      // 중복 이벤트 방지용 클린업
      window.removeEventListener('resize', resizeHandler);
      };
    }, []);
    리액트 입장에서 addEventListener는 리액트 가상돔이 아닌 실제 돔을 건드리기에 side Effect이다.
    side Effect를 다루기 위해 useEffect를 사용했다.
    하지만 이런 문제가 있다.

    해당 컴포넌트가 렌더링 될때마다 useEffect는 동작하기 때문에 계속해서 이벤트가 걸린다.
    이는 useEffect의 return값인 cleanUp을 이용하여 마운팅이 끝나는 시점에 이벤트를 해제해서 해결할 수 있다.

여기까지 작성이 완료되면 리사이즈에 따라 캔버스의 width와 높이는 꽉찬다.

두번째로 캔버스에서 onKeyDown 을 이용하기 위해 캔버스를 focusing 시키는 부분을 구현해보자.

일단 캔버스에서 아무런 작업없이 onKeyDown 이벤트를 입력하면 onKeyDown이벤트는 발생하지 않는다.
이는 keyDown이벤트 발생의 조건 때문인데 브라우저는 쓸데없는 입력 이벤트 감지를 막기 위해 몇몇의 태그(ex: input)들만 키보드 이벤트를 허용한다.
물론 이는 직접 커스터마이징 할 수 있는데 이는 tabindex 속성을 넣어주면 가능하다.

ex) keyDown Event가 안 걸어지는 div

<div class="modal" onKeyDown={keyDownHandler}></div>
아무설정이 안된 div, canvas 등의 태그는 tabindex가 -1이다.
input등의 대화형 태그는 기본 tabindex가 0이다.

ex) keyDown Event가 걸어지는 div(리액트 jsx문법에서는 tabindex가 아니라 tabIndex이다. html은 tabindex)

<div tabIndex={0} class="modal" onKeyDown={keyDownHandler}></div>

이처럼 내가 사용할 캔버스에도 tabIndex 속성을 주자.

  return (
    <canvas
      width={windowSize.width}
      height={windowSize.height}
      onKeyDown={keyDownHandler}
      tabIndex={0}
      ref={canvasRef}
    ></canvas>
  );

이제 canvas element에 focus를 시켜주어 화면이 보이자마자 키보드 이벤트를 걸 수 있게끔 만들어보자.

    1. canvas의 ref를 가지고 컨트롤하자.
const canvasRef = useRef<HTMLCanvasElement>(null);

useRef를 사용한 것을 볼 수 있는데 이는 canvas element가 가지고 있는 focus메서드를 이용하기 위해서이다.
document.querySelector를 사용하지 않고 useRef로 가져오는 이유는 react가 가상돔을 기반으로 움직이기 때문이다.
실제 DOM을 건드리는 것은 react가 제공하는 함수형 컴포넌트의 JSX문법과 일치시켜 사용할 수 있다.
그리고 state와는 달리 ref값이 변경된다고 렌더링을 시키지 않는다.
useRef로 가져온 canvas element는 canvasRef.current의 들어가 있다.\

 

2. 이제 사용자가 사이트에 처음 들어오거나 새로고침할 때 canvasRef.current.focus()가 동작하게 해보자.

useEffect(() => {
 const currentRef = canvasRef.current;
 if (currentRef !== null) currentRef.focus();
});

useEffect의 두번째 인자인 deps영역에 아무것도 넣어주지 않으면 컴포넌트가 처음 렌더링 되는 시점에만 실행된다.
useEffect내의 첫번째 인자로 함수만 넣어주면 상태변화 시점이 아닌, 정확히 렌더링 처음될 때 focus가 작동한다.
이 코드들을 전부 합치면 항상 캔버스는 브라우저에 꽉 찬다.웹에 처음 들어오거나 새로고침하는 상황에 바로 캔버스에 키보드 이벤트를 사용할 수 있다.

여기까지 완성한 코드

import React, {
  useCallback,
  useRef,
  KeyboardEvent,
  useEffect,
  useState,
} from 'react';
import { KEYCODE } from '../../constants/canvas';

const Canvas = (): JSX.Element => {
  // windowSize는 나중에 전역상태로 관리하기 위해
  const [windowSize, setWindowSize] = useState<{
    width: number;
    height: number;
  }>({
    width: window.innerWidth,
    height: window.innerHeight,
  });
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    window.addEventListener('resize', resizeHandler);
    return () => {
      // 중복 이벤트 방지용 클린업
      window.removeEventListener('resize', resizeHandler);
    };
  }, []);

  useEffect(() => {
    const currentRef = canvasRef.current;
    if (currentRef !== null) currentRef.focus();
  });

  const resizeHandler = useCallback(() => {
    setWindowSize({ width: window.innerWidth, height: window.innerHeight });
  }, []);

  const keyDownHandler = useCallback((e: KeyboardEvent<HTMLCanvasElement>) => {
    switch (e.keyCode) {
      case KEYCODE.UP:
        console.log('up');
        break;
      case KEYCODE.DOWN:
        console.log('down');
        break;
      case KEYCODE.LEFT:
        console.log('left');
        break;
      case KEYCODE.RIGHT:
        console.log('right');
        break;
      default:
        break;
    }
  }, []);

  return (
    <canvas
      width={windowSize.width}
      height={windowSize.height}
      onKeyDown={keyDownHandler}
      tabIndex={0}
      ref={canvasRef}
    ></canvas>
  );
};

export default Canvas;

마무리

리액트 hooks는 너무 잘 추상화돼있다.
프론트에 로직이 많은 시스템을 리액트와 함께 개발한다면 이것저것 hooks를 사용할 수 밖에 없다.
하지만 잘 알지 못하는 hooks의 사용은 독이 될 수 있다고 생각했다.
그래서 구현 사항을 만났을 때 제일 적절한 hook의 사용방식을 채택할 수 있도록 공부해둘 필요가 있어 정리해보았다.
앞으로도 hook을 사용할 때 계속 고민해보며 상황에 더 적합한 hook의 사용을 고려해야겠다.
개발하며 얻은 hook 사용의 팁이 생기면 계속 정리해서 추가하거나 글을 작성해볼 것이다.

댓글