Next.js(React.js)에서 Canvas 사용법 with Typescript

여는 글

최근 Next.js에서 Canvas를 사용할 일이 있었습니다.

기존에 바닐라 자바스크립트에서는 공부를 했었지만, Next.js로는 해본적이 없었고, 따로 정리된 블로그가 딱히 없었습니다.

그래서 최근에 사용하면서 가장 사용하기 좋았던 방식을 한번 공유해 보도록 하겠습니다.


Canvas란?

Canvas는 HTML태그중 하나로 브라우저에서 도형 혹은 이미지들을 렌더링할 수 있는 요소중 하나입니다. 주로 게임 혹은 그래픽적인 요소를 브라우저에서 표현할 때 사용합니다.


프로젝트 셋팅

터미널에서 프로젝트를 타입스크립트로 생성합니다.

yarn create next-app canvas_example --typescript
---
✔ Would you like to use ESLint with this project? … Yes
✔ Would you like to use `src/` directory with this project? … No
✔ Would you like to use experimental `app/` directory with this project? … No
✔ What import alias would you like configured? … @/*

pages/index.tsx

import { NextPage } from "next";

const Home: NextPage = () => {
  return (
    <div>테스트</div>
  )
};

export default Home;

styles/globals.css

body {
	margin: 0;
}

Canvas 컴포넌트 생성 및 적용

캔버스 태그를 가지는 컴포넌트를 생성하고 적용해보도록 합시다.

components/Canvas/index.tsx

const Canvas: React.FC = () => {
	return <canvas width={500} height={500} />;
};

export default Canvas;

pages/index.tsx

import Canvas from "@/components/Canvas";
import { NextPage } from "next";

const Home: NextPage = () => {
    return <Canvas />;
};

export default Home;

그러면 아래 사진처럼 canvas가 생성 된 것을 확인할 수 있습니다.


Canvas에 사각형 그리기

우리는 canvas를 생성하는 컴포넌트를 만들었습니다.

하지만, canvas만 만든다고 해서 canvas를 사용할 수 있는것은 아닙니다.

그래서 컨텍스트를 받고 그 컨텍스트를 이용해 작은 사각형 하나를 만들어 보도록 하겠습니다.

components/Canvas/index.tsx

import React, { RefObject, useEffect, useRef, useState } from "react";

const Canvas: React.FC = () => {
	const canvasRef: RefObject<HTMLCanvasElement> =
		useRef<HTMLCanvasElement>(null);
	const [ctx, setCtx] = useState<CanvasRenderingContext2D | null>(null);

	// 컨텍스트 셋팅
	useEffect(() => {
		if (canvasRef?.current) {
			const canvas = canvasRef.current;
			const context = canvas.getContext("2d");

			setCtx(context);
		}
	}, [canvasRef]);

	useEffect(() => {
		let requestAnimationId: number;

		// 애니메이션 처리
		const onAnimation = () => {
			if (ctx) {
				ctx.fillRect(20, 20, 30, 30);
			}
			requestAnimationId = window.requestAnimationFrame(onAnimation);
		};

		// 리퀘스트 애니메이션 초기화
		requestAnimationId = window.requestAnimationFrame(onAnimation);

		return () => {
			// 기존 리퀘스트 애니메이션 캔슬
			window.cancelAnimationFrame(requestAnimationId);
		};
	}, [ctx]);

	return <canvas ref={canvasRef} width={500} height={500} />;
};

export default Canvas;

코드가 길긴 하지만, 바닐라 자바스크립트에서 canvas를 사용해 보셨고, 리액트를 사용해 보셨다면 충분히 이해가 갈 코드입니다.

실행해보면 결과는 아래와 같습니다.


Custom Hook을 사용하여 비즈니스 로직 분리

우리는 canvas를 사용하여 사각형을 그렸습니다.

하지만, 컴포넌트 안에서 그래픽을 만드는 것은 재사용성을 버리겠다는 뜻입니다.

따라서 커스텀 훅을 통해서 비즈니스 로직을 분리하도록 하겠습니다.

hooks/Canvas/useCanvas.tsx

import { RefObject, useEffect, useRef, useState } from "react";

interface InitialProps {
	animate: (ctx: CanvasRenderingContext2D) => void;
}

function useCanvas({ animate }: InitialProps) {
	const canvasRef: RefObject<HTMLCanvasElement> =
		useRef<HTMLCanvasElement>(null);
	const [ctx, setCtx] = useState<CanvasRenderingContext2D | null>(null);

	// 컨텍스트 셋팅
	useEffect(() => {
		if (canvasRef?.current) {
			const canvas = canvasRef.current;
			const context = canvas.getContext("2d");

			setCtx(context);
		}
	}, []);

	// 애니메이션 처리
	useEffect(() => {
		let requestId: number;

		const RequestAnimation = (ctx: CanvasRenderingContext2D | null) => () => {
			if (ctx) {
				animate(ctx);
			}
			// 애니메이션 콜백 반복
			requestId = window.requestAnimationFrame(RequestAnimation(ctx));
		};

		// 애니메이션 초기화
		requestId = window.requestAnimationFrame(RequestAnimation(ctx));

		return () => {
			window.cancelAnimationFrame(requestId);
		};
	}, [ctx, animate]);

	return {
		canvasRef,
	};
}

export default useCanvas;

components/Canvas/index.tsx

import React, { RefObject } from "react";

interface CanvasProps {
	canvasRef: RefObject<HTMLCanvasElement>;
}

const Canvas: React.FC<CanvasProps> = ({ canvasRef }) => {
	return <canvas ref={canvasRef} width={500} height={500} />;
};

export default Canvas;

pages/index.tsx

import { NextPage } from "next";
import Canvas from "@/components/Canvas";
import useCanvas from "@/hooks/Canvas/useCanvas";

const Home: NextPage = () => {
	// 애니메이션 처리 함수
	const onAnimation = (ctx: CanvasRenderingContext2D) => {
		ctx.fillRect(20, 20, 30, 30);
	};

	const { canvasRef } = useCanvas({
		animate: onAnimation,
	});

	return <Canvas canvasRef={canvasRef} />;
};

export default Home;

이제 컴포넌트가 아닌, Canvas 컴포넌트를 사용하는 곳에서도 애니메이션 핸들링이 가능해졌습니다.


최종코드

pages/index.tsx

import { NextPage } from "next";
import Canvas from "@/components/Canvas";
import useCanvas from "@/hooks/Canvas/useCanvas";

const Home: NextPage = () => {
	// 애니메이션 처리 함수
	const onAnimation = (ctx: CanvasRenderingContext2D) => {
		ctx.fillRect(20, 20, 30, 30);
	};

	const { canvasRef } = useCanvas({
		animate: onAnimation,
	});

	return <Canvas canvasRef={canvasRef} />;
};

export default Home;

hooks/Canvas/useCanvas.tsx

import { RefObject, useEffect, useRef, useState } from "react";

interface InitialProps {
	animate: (ctx: CanvasRenderingContext2D) => void;
}

function useCanvas({ animate }: InitialProps) {
	const canvasRef: RefObject<HTMLCanvasElement> =
		useRef<HTMLCanvasElement>(null);
	const [ctx, setCtx] = useState<CanvasRenderingContext2D | null>(null);

	// 컨텍스트 셋팅
	useEffect(() => {
		if (canvasRef?.current) {
			const canvas = canvasRef.current;
			const context = canvas.getContext("2d");

			setCtx(context);
		}
	}, []);

	// 애니메이션 처리
	useEffect(() => {
		let requestId: number;

		const RequestAnimation = (ctx: CanvasRenderingContext2D | null) => () => {
			if (ctx) {
				animate(ctx);
			}
			// 애니메이션 콜백 반복
			requestId = window.requestAnimationFrame(RequestAnimation(ctx));
		};

		// 애니메이션 초기화
		requestId = window.requestAnimationFrame(RequestAnimation(ctx));

		return () => {
			window.cancelAnimationFrame(requestId);
		};
	}, [ctx, animate]);

	return {
		canvasRef,
	};
}

export default useCanvas;

components/Canvas/index.tsx

import React, { RefObject } from "react";

interface CanvasProps {
	canvasRef: RefObject<HTMLCanvasElement>;
}

const Canvas: React.FC<CanvasProps> = ({ canvasRef }) => {
	return <canvas ref={canvasRef} width={500} height={500} />;
};

export default Canvas;

styles/globals.css

body {
	margin: 0;
}

이 글을 공유하기

댓글