React 에서 Class 형태 Canvas 사용하기

ecof·2022년 7월 19일
0

국비지원 학원에서 팀프로젝트를 진행하면서 커스텀 쇼핑몰을 주제로 삼게 되었는데, 커스텀 기능을 구현하면서 canvas를 사용하게 되었다. 이전에 Interactive Developer (https://www.youtube.com/c/cmiscm) 님의 강의를 보면서 조금 따라해본게 전부인 canvas를 바닐라 JS도 아닌 React에 적용하려니 심히 삐걱거렸다.

게다가 강의에서 사용하는건 class를 이용해 canvas를 구성하고 조작하는 방법으로 react + canvas 를 검색했을때 나오는 여러 참고자료들과 달라 곤혹은 더해졌지만, 어떻게든 적용에 성공했다. (사실 맞는 방법인지 모름)

검색하면 나오는 React + Canvas 형태

import {createRef, useEffect} from "react";

const CanvasComp = () => {
  	const canvasRef = createRef(null);

    const 이벤트함수 = () => {
       ...
    };

    useEffect(()=>{
    	canvas = canvasRef.current;
        ctx = canvas.getContext("2d");
      	canvas.addEventListener('원하는 이벤트', 이벤트함수, false);
        return(()=>{
        	canvas.removeEventListener('위에 썼던 이벤트', 이벤트함수);
        })
    }, [])

	return (
    	<canvas ref={canvasRef} />
    )
}

export default CanvasComp;

canvas 태그에 ref 를 걸어 참고할 수 있도록 만들고 useEffect 에서 canvas의 ctx나 event를 사용해 canvas를 초기화 하여 사용하는 형태이다. 또한 useEffect 에 return으로 페이지가 언마운트 될때 이벤트가 제거 되도록 만들어 리렌더링 될때 중첩되지 않도록 하는 형태다. useEffect 에서 빈 배열을 전해주어 최초 렌더링시에 실행 되도록 만들었으며 canvas 내부에서 props 나 변화하는 state를 사용하려면 배열 안에 해당 이름을 넣어주면 된다. 아마도. 이 형태는 사용해보지 않아 정확하진 않음.

사용한 React + Class형 Canvas 형태

import {createRef, useEffect} from "react";

const CanvasComp = () => {
  	const canvasRef = createRef(null);

    class App {
    	constructor() {
            this.canvas = canvasRef.current;
      		this.ctx = this.canvas.getContext("2d");

      		this.이벤트헨들러 = this.이벤트함수.bind(this);
          	this.canvas.addEventListener("원하는 이벤트", this.이벤트헨들러, false);

      		this.animateHandler = window.requestAnimationFrame(this.animate.bind(this));
    	}

      	this.animate () {
          	this.ctx.clearRect(0, 0, 캔바스넓이, 캔바스높이);
          	// 캔바스 애니메이션 작동할 것들
          	this.animateHandler2 = window.requestAnimationFrame(this.animate.bind(this));
        }

    	this.remove () {
    		cancelAnimationFrame(this.animateHandler);
      		cancelAnimationFrame(this.animateHandler2);
      		this.canvas.removeEventListener("원하는 이벤트", this.이벤트헨들러);
  		}
    }

    useEffect(()=>{
    	const Canvas = new App()
        return () => {
      		Canvas.remove();
    	};
    }, [])

	return (
    	<canvas ref={canvasRef} />
    )
}

export default CanvasComp;

canvas에 ref를 걸어 참고하는건 동일하지만 ctx나 event를 제어하는 부분은 class의 constructor 내부에서 작성한다. 기존 Interactive Developer님의 강의에서는 class내부함수를 addEventListener에 직접 전해주는 형태였지만 remove를 위해 따로 this로 할당해주고 this 로 작성된 부분을 동일하게 사용하기 위해 bind를 걸어줬다.
requestAnimationFrame 으로 인해 계속 실행이 되는 animate 함수에서는 clearRect 로 캔버스를 지워주면서 그 밑에 작성될 캔버스 내용들을 작동시켜 애니메이션 처럼 보여준다.
이벤트의 중첩방지는 class 내에 remove 함수로 작성하고 useEffect 에서 사용하기 위해 class 선언과 함께 변수에 할당하고 return 내에서 할당된 변수로 class에서 접근하여 remove 함수를 실행해 이벤트를 제거하는 형태다.
위와 마찬가지로 최초 렌더링시에 실행되도록 빈 배열을 전해주었고 변화하는 props나 state를 class 내부에서 사용하고 싶을 경우 배열내에 props 나 state를 전해주어 변화시에 재 렌더링 되도록 해주면 된다. 그렇게 할 경우 한가지 문제점은 canvas 내부에서 작동하는 요소가, 생성되는 갯수든 크기든 위치좌표든 초기 상태로 리셋되는것인데 그건 canvas 내부 요소들의 상태를 저장하는 state를 작성해 요소의 변화가 끝나면 setState로 저장하는 형식을 사용하면 방지할 수 있다.


프로젝트를 진행하며 기존 방식과 class 방식을 두고 여러번 고민하고 또 사용해보고 여러번을 뒤엎으며 사실상 기존 방식은 찍어먹어본 수준에 지나지 않아 제대로된 비교가 진행되었다고 할 수는 없지만 배운게 도둑질이라고 canvas를 사용해본 일이 class 형태밖에 없어 함수로 처리하는 기존방식 보다 class 형태를 적용한 형태가 나에겐 훨씬 쓰기 편했다.
이벤트나 requestAnimationFrame을 useEffect 내에서 처리하는것이 cavas의 작동 순서를 파악하는데 어려웠고 직관적으로 보이지가 않았기 때문에 canvas 에서 처리해야 하는 요소가 많아질 경우 어떤식으로 대응을 해야 하는지 감이 오지 않았기 때문이라고 생각한다. 그 때문인지 기존 방식에 requestAnimationFrame를 적용하지 못했다.
또한 canvas 에서 제어해야 하는 요소가 많아지게 된다면 각 요소별로 class 로 분리하여 관리할 수 있고 각 class는 따로 js 파일로 분리하여 관리할 수 있으니 관리가 더욱 용이한 장점도 들 수 있는 것 같다.

profile
프론트엔드 개발 지망

0개의 댓글