[React] canvas로 그림판 구현하기

멋진감자·2024년 4월 30일

React

목록 보기
1/3
post-thumbnail

오늘 해볼 것은 canvas 태그를 이용해 간단한 그림판 구현하기다.
크게는 펜과 지우개 버튼을 두고,
그 안에 크기 조정, 펜 색깔 변경, 모두 지우기 기능을 추가했다.

canvas

HTML <canvas> 태그는 주로 그래픽 콘텐츠를 그릴 때 사용되는 일종의 컨테이너이다.
실제로 그림을 그리는 동작은 스크립트를 통해 구현할 수 있다.

지원하는 브라우저 및 버전은 아래와 같다.
<canvas> 요소를 지원하지 않는 브라우저의 경우 태그 내부에 들어있는 텍스트를 노출한다.

사용법

fillStyle, fillRect 등 다양한 함수를 이용해 원하는 기능을 구현해 볼 수 있겄다.

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

ctx.fillStyle = "green";
ctx.fillRect(10, 10, 150, 100);

그림판 구현하기

큼지막한 구조는 아래와 같습니다.
ReactTypescript 환경이다.

구조

const canvasRef = useRef<HTMLCanvasElement>(null);

...

return (
  <CanvasWrapper>
    <canvas ref={canvasRef} width={720} height={480} />
    {isPenModalOpen && (
      <PenModal />
    )}
    {isEraserModalOpen && (
      <EraserModal />
    )}
    <ButtonWrapper>
      <Button type="button" onClick={handlePenButtonClick} isClicked={isPenModalOpen}></Button>
      <Button type="button" onClick={handleEraserButtonClick} isClicked={isEraserModalOpen}>지우개
      </Button>
    </ButtonWrapper>
  </CanvasWrapper>
);

마우스

사용한 함수는 strokeStyle, lineWidth, beginPath, moveTo, lineTo, stroke, clearRect.

useEffect(() => {
  // Canvas 요소에 대한 참조를 가져옵니다.
  const canvas = canvasRef.current;
  // Canvas 요소가 존재하지 않으면 종료합니다.
  if (!canvas) return;

  // Canvas의 2D context를 가져옵니다.
  const ctx = canvas.getContext("2d");
  // 2D context가 존재하지 않으면 종료합니다.
  if (!ctx) return;

  // 선 색상과 선 두께를 설정합니다.
  ctx.strokeStyle = penColor;
  ctx.lineWidth = mode === "pen" ? penSize : eraserSize;

  // 그리기를 시작하는 함수입니다.
  const startDrawing = (e: MouseEvent) => {
    setIsDrawing(true);
    const { offsetX, offsetY } = e;
    ctx.beginPath();
    ctx.moveTo(offsetX, offsetY);
  };

  // 마우스 이동 중에 그리는 함수입니다.
  const onMouseMove = (e: MouseEvent) => {
    if (!isDrawing) return;
    const { offsetX, offsetY } = e;
    if (mode === "pen") {
      ctx.lineTo(offsetX, offsetY);
      ctx.stroke();
    } else {
      ctx.clearRect(offsetX, offsetY, ctx.lineWidth, ctx.lineWidth);
    }
  };

  // 그리기를 멈추는 함수입니다.
  const stopDrawing = () => {
    setIsDrawing(false);
  };

  // 마우스 이벤트 리스너를 추가합니다.
  canvas.addEventListener("mousedown", startDrawing);
  canvas.addEventListener("mousemove", onMouseMove);
  canvas.addEventListener("mouseup", stopDrawing);
  canvas.addEventListener("mouseout", stopDrawing);

  // clearAll 상태가 true이면 Canvas를 모두 지웁니다.
  if (clearAll) {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    setClearAll(false);
  }

  // 메모리 누수와 불필요한 리소스 사용을 막기 위해 이벤트 리스너를 제거합니다.
  return () => {
    canvas.removeEventListener("mousedown", startDrawing);
    canvas.removeEventListener("mousemove", onMouseMove);
    canvas.removeEventListener("mouseup", stopDrawing);
    canvas.removeEventListener("mouseout", stopDrawing);
  };
}, [isDrawing, penSize, penColor, clearAll]);

사용된 함수에 대해 간단히들 살펴보자.

strokeStyle, lineWidth

선의 색상과 두께를 설정하는 함수다.
ctx.strokeStyle = "red"; 는 선의 색상을 빨간색으로 지정하며,
ctx.lineWidth = 2; 는 선의 두께를 2px로 지정한다.

beginPath, moveTo

beginPath() 를 호출하여 새로운 선이나 도형을 그리기 전 새 경로를 만들어주고,
선을 그리기 시작할 점을 지정한다.
예를 들어, ctx.moveTo(100, 100); 은 선을 (100, 100) 위치에서 시작하도록 만든다.

lineTo, stroke

선을 그리는 끝점을 지정한다.
moveTo로 지정한 시작점부터 해당 점까지 선이 그려진다.
마우스가 이동하면서 lineTo 가 갱신되며 점이 이어지는 식이다.
지정한 점까지 실제로 선을 화면에 그리는 함수인 stroke 가 실행된다.

clearRect

지정된 사각형 영역을 지운다.
ctx.clearRect(x, y, width, height); 는 (x, y) 위치부터 width와 height 만큼의 영역을 지운다.
모두 지우기는 ctx.clearRect(0, 0, canvas.width, canvas.height); 코드로 간단히 구현할 수 있겄다.

마치며

언급한 기능 외에 ColorPicker 나 펜 질감 바꾸기 등의 기능을 추가해볼수도 있겄다.
생각한 것보다 구현에 오랜 시간을 썼는데, 간단히 정리해뒀으니 담엔 좀 더 수월할지도 ~

Reference

https://www.tcpschool.com/html-tags/canvas
https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API

profile
난멋져

0개의 댓글