2024. 5. 26. 20:37ㆍReact
Canvas로 그림 그리기 기능 구현: 이슈와 해결 과정
Canvas를 활용해서 그림 그리는 기능을 구현해 보았습니다. 이 과정에서 발생했던 이슈들을 정리해보고자 합니다.
📍 Canvas란 무엇인가?
Canvas는 HTML5의 일부로 도입된 강력한 그래픽 인터페이스입니다. HTML 문서에 <canvas> 태그를 사용하여 그래픽 영역을 만들 수 있으며, 이 영역에서 JavaScript를 사용해 동적으로 그림을 그리거나 애니메이션을 생성할 수 있습니다. Canvas는 래스터 그래픽을 다루기 위한 도구로, 픽셀 단위로 그래픽을 조작하는 데 매우 유용합니다.
래스터 그래픽
래스터 그래픽은 픽셀, 즉 작은 점들의 집합으로 이루어진 이미지 형식을 말합니다. 각 픽셀은 고유한 색상 정보를 가지고 있으며, 이 픽셀들이 모여 전체 이미지를 형성합니다. 일반적인 사진 파일 형식인 JPEG, PNG, GIF 등이 래스터 그래픽의 예입니다. 래스터 이미지의 해상도는 포함된 픽셀의 수에 의해 정해지므로, 이미지를 확대할 경우 픽셀이 확대되어 이미지가 깨지는 현상(픽셀화)이 발생할 수 있습니다.
픽셀 단위 그래픽 조작
캔버스는 다음과 같은 다양한 그래픽 작업을 픽셀 단위로 수행할 수 있습니다.
1. 도형 그리기: 선, 원, 사각형 등 기본적인 도형을 그릴 수 있습니다.
2. 이미지 그리기 및 편집: 이미지를 캔버스에 그린 후, 이미지의 특정 부분을 편집하거나 필터를 적용할 수 있습니다.
3. 텍스트 렌더링: 다양한 스타일의 텍스트를 캔버스에 그릴 수 있습니다.
4. 애니메이션 생성: JavaScript와 결합하여 복잡한 애니메이션을 구현할 수 있습니다.
5. 데이터 시각화: 차트, 그래프 등 데이터를 시각적으로 표현할 수 있습니다.
기본 예제와 사용 방법
<canvas>는 HTML 페이지 위에 그려지는 직사각형 영역이며, 기본적으로는 테두리도 없고 내용도 없습니다. id 속성을 통해 Canvas 요소를 식별하고, width와 height 속성을 통해 크기를 지정합니다. width 및 height 속성을 지정하지 않으면 캔버스의 처음 너비는 300 픽셀이고 높이는 150 픽셀입니다.
Canvas에 그래픽을 그리기 위해서는 JavaScript의 getContext 메서드를 사용하여 2D 또는 3D 컨텍스트를 가져와야 합니다. <canvas> 요소는 getContext() 메서드를 이용해서, 랜더링 컨텍스트와 컨텍스트의 그리기 함수들을 사용할 수 있습니다. 2D 그래픽을 그리는 예제는 다음과 같습니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas Drawing Example</title>
<style>
#myCanvas {
border: 1px solid black;
}
</style>
</head>
<body>
<h1>Canvas Drawing Example</h1>
<canvas id="myCanvas" width="500" height="500"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
const context = canvas.getContext('2d');
// 빨간색 직사각형 그리기
context.fillStyle = 'red';
context.fillRect(50, 50, 150, 100);
// 검은색 테두리 그리기
context.strokeStyle = 'black';
context.strokeRect(50, 50, 150, 100);
</script>
</body>
</html>
✅ 고해상도 디스플레이에서 선명한 그래픽 유지하기
기본 예제대로 구현 시 고해상도 디스플레이에서 그래픽이 흐릿하게 보이는 문제가 있었습니다. 이 문제를 해결하기 위해서는 Canvas의 실제 해상도를 디바이스 픽셀 비율(Device Pixel Ratio)에 맞춰 조정해야 합니다.
해결 방법
- Canvas의 실제 크기를 디바이스 픽셀 비율에 맞춰 설정
- 설명: 고해상도 디스플레이에서는 더 많은 픽셀을 사용하여 그래픽을 그려야 합니다.
- 방법: canvas.width와 canvas.height 속성을 디바이스 픽셀 비율에 맞게 설정합니다.
- CSS 스타일로 사용자가 화면에서 보게 되는 요소의 크기를 설정하여 비율 유지
- 설명: 브라우저에서 캔버스가 화면에 표시되는 크기를 설정합니다. 이 크기는 사용자에게 보이는 크기입니다.
- 방법: canvas.style.width와 canvas.style.height 속성을 사용하여 CSS 픽셀 단위로 캔버스의 크기를 설정합니다.
- 렌더링 컨텍스트를 스케일링하여 고해상도에 맞게 조정
- 설명: 렌더링 된 그래픽이 디바이스 픽셀 비율에 맞게 조정되어 선명하게 보이도록 합니다.
- 방법: context.scale(dpr, dpr)을 사용하여 캔버스의 렌더링 컨텍스트를 스케일링합니다.
예제 코드
import React, { useRef, useEffect } from 'react';
const CanvasComponent: React.FC = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const initializeCanvas = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const context = canvas.getContext('2d');
if (!context) return;
const dpr = window.devicePixelRatio || 1;
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
canvas.style.width = `${window.innerWidth}px`;
canvas.style.height = `${window.innerHeight}px`;
context.scale(dpr, dpr);
drawContent(context);
};
const drawContent = (context: CanvasRenderingContext2D) => {
context.fillStyle = 'blue';
context.fillRect(50, 50, 200, 150);
context.fillStyle = 'white';
context.font = '30px Arial';
context.fillText('Canvas Example', 60, 100);
};
useEffect(() => {
initializeCanvas();
window.addEventListener('resize', initializeCanvas);
return () => {
window.removeEventListener('resize', initializeCanvas);
};
}, []);
return (
<canvas ref={canvasRef} className="w-full h-full block" />
);
};
export default CanvasComponent;
고해상도 디스플레이에서도 선명한 그래픽을 유지하기 위해서는 캔버스의 실제 크기와 CSS 크기를 적절히 설정하는 것이 중요합니다. 실제 크기(렌더링 크기)는 캔버스 내부에서 사용하는 픽셀 수를 의미하고, CSS 크기(표시 크기)는 화면에 표시되는 크기를 의미합니다. 이 두 가지를 함께 설정함으로써, 디스플레이의 해상도에 맞춰 선명한 그래픽을 유지하면서도, 사용자가 의도한 크기로 화면에 표시할 수 있습니다.
✅ 화면 크기 변경 시 Canvas 크기 조정 및 그래픽 다시 그리기
이슈
기본적으로 Canvas 요소의 가로, 세로 크기값을 지정해 두었기 때문에 화면 크기가 변경되더라도 캔버스의 크기는 변경되지 않고, UI가 깨지는 현상이 있었습니다. 따라서 resize 이벤트 발생 시 캔버스 크기를 조정하고, 전체 경로를 초기화하며 변경된 비율에 맞게 다시 그려줘야 합니다. 이를 위해 전체 경로를 저장해 둘 필요가 있습니다. 그러나 이때 resize 이벤트가 불필요하게 많이 호출될 수 있습니다. 이는 성능 저하를 유발할 수 있으며, 효율적이지 않습니다.
해결 방법
resize 이벤트를 최적화하는 방법 중 하나는 디바운스(Debounce) 기법을 적용하는 것입니다. debounce 기법은 특정 이벤트가 연속해서 발생할 때, 이벤트 발생이 멈춘 후 일정 시간이 지나면 한 번만 처리되도록 하는 방법입니다. 즉, 이벤트가 계속 발생하는 동안에는 처리가 지연되다가, 마지막 이벤트가 발생한 후 일정 시간 동안 추가 이벤트가 발생하지 않으면 그때 한 번만 실행됩니다.
예제 코드
// utils/debounce.ts
export const debounce = <T extends (...args: any[]) => void>(callback: T, delay: number) => {
let timer: ReturnType<typeof setTimeout>;
return (...args: Parameters<T>): void => {
clearTimeout(timer);
timer = setTimeout(() => callback(...args), delay);
};
};
// components/CanvasComponent.tsx
import React, { useRef, useEffect, useState } from 'react';
import { Path } from '../types/Path';
import { debounce } from '../utils/debounce';
useEffect(() => {
initializeCanvas();
const debouncedResize = debounce(initializeCanvas, 300);
window.addEventListener('resize', debouncedResize);
return () => {
window.removeEventListener('resize', debouncedResize);
};
}, [paths]);
- resize() 이벤트가 발생하면 debounce 함수를 사용하여 불필요한 호출을 줄입니다.
- 캔버스를 초기화하고, 저장된 경로를 다시 그립니다.
✅ 결과 화면
🎨 그림 그리는 화면
🎨 목록 화면
참고
'React' 카테고리의 다른 글
함수형 컴포넌트의 구조와 동작 방식(feat. 클로저) (1) | 2024.12.07 |
---|---|
왜 리액트인가? (2) | 2024.11.20 |