HTML5웹프로그래밍 12강 - Canvas 기본과 도형 그리기
Canvas는 픽셀 기반의 그리기 영역을 만들고 JavaScript로 도형, 그래프, 애니메이션을 표현하는 기술이다. 이 글에서는 캔버스의 좌표계와 해상도, 2D 컨텍스트, 사각형과 패스, 원호와 부채꼴, 그리기 상태와 선 스타일, 애니메이션의 기본 흐름까지 12강의 핵심을 예제 중심으로 정리한다.
1. Canvas의 개념과 활용
1.1 Canvas는 무엇을 하는가
<canvas> 요소는 웹 문서 안에 그림을 그릴 수 있는 투명한 직사각형 영역을 만든다. 요소를 선언하는 것만으로 그림이 생기지는 않는다. JavaScript가 캔버스의 그리기 컨텍스트를 얻은 뒤 픽셀을 채우거나 선을 그려야 실제 결과가 나타난다.
HTML의
<canvas>는 그릴 공간을 준비하고, JavaScript의 Canvas API는 그 공간에 실제 그림을 만든다.Canvas는 웹 게임과 애니메이션, 실시간 데이터 시각화, 이미지·동영상 편집, 사용자의 입력에 따라 달라지는 생성형 그래픽 등에 활용된다. 화면에 그려진 결과는 비트맵이므로 개별 도형이 DOM 요소로 남지 않는다는 점도 함께 기억해야 한다.
| 특징 | 의미 | 실무상 주의점 |
|---|---|---|
| 픽셀 기반 | 그리기 명령의 결과가 캔버스 비트맵에 기록된다. | 크기를 억지로 늘리면 흐릿해질 수 있다. |
| 즉시 모드 | 그린 뒤에는 도형 객체가 자동으로 보존되지 않는다. | 수정하려면 화면을 지우고 다시 그리는 방식이 일반적이다. |
| 상태 기반 | 색, 선 두께, 변환 등의 현재 설정이 다음 그리기에도 이어진다. | save()와 restore()로 범위를 관리한다. |
| JavaScript 연동 | 데이터와 사용자 입력을 실시간으로 반영할 수 있다. | 그리기 기능을 작은 함수로 나누면 관리하기 쉽다. |
2. canvas 요소, 좌표계와 해상도
2.1 요소 선언과 대체 콘텐츠
<canvas id="drawing" width="600" height="300"
role="img" aria-label="파란 사각형과 빨간 원을 그린 예시">
브라우저가 Canvas를 지원하지 않으면 표시할 설명입니다.
</canvas>
id는 JavaScript가 요소를 찾는 데 사용한다. width와 height 속성을 생략하면 내부 그리기 크기는 기본적으로 300×150픽셀이다. 시작 태그와 종료 태그 사이의 내용은 Canvas를 지원하지 않는 환경을 위한 대체 콘텐츠다. 그림의 의미가 있다면 role="img"와 aria-label도 제공해 접근성을 보완한다.
2.2 좌표계
Canvas 좌표계의 원점 (0, 0)은 왼쪽 위이다. x값은 오른쪽으로 갈수록 커지고 y값은 아래쪽으로 갈수록 커진다. 따라서 (50, 30)은 왼쪽 경계에서 50픽셀, 위쪽 경계에서 30픽셀 떨어진 위치다.
2.3 내부 크기와 CSS 표시 크기
HTML 속성의 width, height는 캔버스 비트맵의 내부 해상도를 정한다. CSS의 width, height는 페이지에서 보이는 표시 크기를 정한다. 내부 해상도는 그대로 둔 채 CSS로만 크게 보이게 하면 작은 비트맵을 확대하는 셈이므로 그림이 늘어나고 흐릿해진다.
const canvas = document.querySelector('#drawing');
const ctx = canvas.getContext('2d');
function resizeCanvas() {
const rect = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.height * dpr);
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(dpr, dpr);
draw();
}
고해상도 화면에서는 CSS 표시 크기에 기기 픽셀 비율인 devicePixelRatio를 곱해 내부 크기를 만든다. 크기를 바꾸면 기존 그림이 지워지므로 반드시 다시 그린다. 또한 scale()을 반복 호출하면 배율이 누적되므로 먼저 setTransform(1, 0, 0, 1, 0, 0)로 변환을 초기화하는 것이 안전하다.
3. 2D 컨텍스트와 사각형 그리기
3.1 표준 그리기 흐름
Canvas에서 그림을 그리는 기본 순서는 ① 요소 선택, ② 2D 컨텍스트 획득, ③ 스타일 설정, ④ 그리기 명령 실행이다. 컨텍스트는 붓과 물감, 현재 설정을 함께 가진 작업 도구라고 이해할 수 있다.
const canvas = document.querySelector('#drawing');
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'royalblue';
ctx.fillRect(30, 20, 160, 90);
getContext('2d')의 2d는 소문자로 작성한다. 복잡한 그림은 drawBackground(), drawCharacter(), drawChart()처럼 기능별 함수로 분리하면 그리는 순서와 상태의 영향을 파악하기 쉽다.
3.2 사각형 전용 메서드
| 메서드 | 동작 | 형식 |
|---|---|---|
fillRect() | 현재 채우기 색으로 채운 사각형을 즉시 그린다. | fillRect(x, y, width, height) |
strokeRect() | 현재 선 스타일로 사각형 테두리를 즉시 그린다. | strokeRect(x, y, width, height) |
clearRect() | 지정 영역의 픽셀을 투명하게 지운다. | clearRect(x, y, width, height) |
ctx.fillStyle = '#4f46e5';
ctx.fillRect(20, 20, 120, 70);
ctx.strokeStyle = '#ef4444';
ctx.lineWidth = 6;
ctx.strokeRect(170, 20, 120, 70);
ctx.clearRect(45, 35, 40, 30);
세 메서드는 모두 네 개의 인수를 받으며 별도의 패스 작성 없이 곧바로 결과를 만든다. 캔버스 전체를 지울 때는 보통 ctx.clearRect(0, 0, canvas.width, canvas.height)를 사용한다.
clearRect()와 크기 재대입의 차이clearRect()는 픽셀만 지우므로 색상, 변환 등의 상태는 유지된다. canvas.width = canvas.width처럼 크기를 다시 지정하면 그림뿐 아니라 컨텍스트 상태도 초기화되며 상대적으로 비용이 크다.4. 그리기 상태와 save·restore
4.1 Canvas는 상태 기계다
fillStyle, strokeStyle, lineWidth, globalAlpha, 그림자, 변환, 클리핑 영역 등은 현재 컨텍스트의 상태다. 한 번 바꾼 값은 다시 바꾸기 전까지 뒤의 그리기에 계속 적용된다. 이 지속성을 모르고 그리면 앞의 함수가 바꾼 색이나 변환이 다음 그림에 뜻밖의 영향을 준다.
save()는 현재 상태를 스택에 복사하고, restore()는 가장 최근에 저장한 상태를 꺼내 복원한다. 즉, 나중에 저장한 상태를 먼저 복원하는 LIFO 구조다. 중요한 점은 픽셀이나 도형 자체를 저장하는 기능이 아니라는 것이다.
function drawBadge(ctx, x, y) {
ctx.save();
ctx.translate(x, y);
ctx.fillStyle = '#f59e0b';
ctx.strokeStyle = '#7c2d12';
ctx.lineWidth = 4;
ctx.globalAlpha = 0.9;
ctx.fillRect(0, 0, 120, 60);
ctx.strokeRect(0, 0, 120, 60);
ctx.restore();
}
함수의 시작에서 저장하고 끝에서 복원하면 함수 내부의 스타일과 변환을 캡슐화할 수 있다. 중첩 저장도 가능하지만 저장과 복원의 짝이 어긋나지 않도록 관리해야 한다.
save() → 변환·스타일 설정 → 그리기 → restore() 순서를 습관화하면 서로 다른 도형의 상태가 섞이는 문제를 크게 줄일 수 있다.5. 패스로 선과 다각형 그리기
5.1 즉시 그리기와 패스 그리기
fillRect(), strokeRect(), clearRect(), fillText(), strokeText(), drawImage()는 호출 즉시 결과를 그린다. 반면 임의의 선과 곡선은 먼저 패스를 정의한 뒤 fill()이나 stroke()로 출력한다.
| 순서 | 메서드 | 역할 |
|---|---|---|
| 1 | beginPath() | 이전 패스를 비우고 새 패스를 시작한다. |
| 2 | moveTo(x, y) | 선을 그리지 않고 현재점을 이동한다. |
| 3 | lineTo(x, y) | 현재점에서 지정점까지 선분을 추가한다. |
| 4 | closePath() | 현재점과 시작점을 연결해 닫힌 도형으로 만든다. |
| 5 | fill(), stroke() | 정의한 내부 또는 윤곽선을 실제로 그린다. |
ctx.beginPath();
ctx.moveTo(150, 20);
ctx.lineTo(280, 120);
ctx.lineTo(210, 250);
ctx.lineTo(90, 250);
ctx.lineTo(20, 120);
ctx.closePath();
ctx.fillStyle = '#fde68a';
ctx.strokeStyle = '#92400e';
ctx.lineWidth = 8;
ctx.fill();
ctx.stroke();
서로 독립된 도형을 그릴 때 beginPath()를 생략하면 이전 경로가 현재 경로에 남아 뜻하지 않게 연결되거나 다시 칠해질 수 있다. 닫힌 도형은 closePath()로 마지막 점과 시작점을 연결하는 것이 깔끔하다. 채우기와 테두리를 함께 사용할 때는 fill() 후 stroke()를 실행하면 선의 안쪽 절반이 채우기에 가려지는 일을 피할 수 있다.
5.2 다각형 함수와 Path2D
function polygon(ctx, points, fill, stroke) {
if (points.length < 3) return;
ctx.save();
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
ctx.lineTo(points[i].x, points[i].y);
}
ctx.closePath();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.fill();
ctx.stroke();
ctx.restore();
}
여러 점을 배열로 받아 순서대로 연결하면 삼각형부터 복잡한 다각형까지 하나의 함수로 그릴 수 있다. 반복해서 사용할 경로는 Path2D 객체로 보관하는 방법도 있다.
const marker = new Path2D();
marker.moveTo(0, -20);
marker.lineTo(18, 16);
marker.lineTo(-18, 16);
marker.closePath();
ctx.save();
ctx.translate(80, 80);
ctx.fill(marker);
ctx.stroke(marker);
ctx.restore();
Path2D는 재사용 가능한 경로 설계도다. 새 Path2D를 만들 때 기존 경로를 복사할 수 있고 다른 경로를 추가할 수도 있다. 경로 객체를 ctx.fill(path), ctx.stroke(path)에 전달하므로 매번 현재 패스를 다시 만들 필요가 없다.
6. 원, 원호, 부채꼴과 파이차트
6.1 arc()와 라디안
arc(x, y, radius, startAngle, endAngle, counterclockwise)는 중심점, 반지름, 시작각, 끝각, 회전 방향으로 원호를 만든다. 각도 단위는 도가 아니라 라디안이다.
| 각도 | 라디안 | Canvas 위치 |
|---|---|---|
| 0° | 0 | 중심의 오른쪽, 3시 방향 |
| 90° | Math.PI / 2 | 아래쪽, 6시 방향 |
| 180° | Math.PI | 왼쪽, 9시 방향 |
| 360° | Math.PI * 2 | 한 바퀴 |
도 단위를 라디안으로 바꾸는 공식은 라디안 = 각도 × Math.PI / 180이다. 마지막 인수를 생략하거나 false로 두면 기본적으로 시계 방향이며, true이면 반시계 방향으로 경로를 만든다.
ctx.beginPath();
ctx.arc(120, 120, 80, 0, Math.PI * 2);
ctx.fillStyle = '#60a5fa';
ctx.fill();
ctx.stroke();
6.2 부채꼴과 팩맨
부채꼴은 중심점에서 시작해 원호를 그리고 다시 중심점으로 닫는다. 따라서 moveTo(중심)가 핵심이다. 이 구조에서 시작각과 끝각을 조절하면 팩맨처럼 입이 열린 원도 만들 수 있다.
const cx = 140;
const cy = 120;
const radius = 90;
const start = -30 * Math.PI / 180;
const end = 30 * Math.PI / 180;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, radius, start, end, true);
ctx.closePath();
ctx.fillStyle = '#facc15';
ctx.fill();
ctx.stroke();
6.3 파이차트의 각도 계산
파이차트에서는 모든 데이터의 합을 구한 뒤 각 항목의 비율만큼 원의 전체 각도 2π를 나눈다. 각 조각의 각도는 (값 / 전체 합) × Math.PI × 2이며, 한 조각을 그린 뒤 끝각을 다음 조각의 시작각으로 누적한다.
const values = [25, 35, 20, 20];
const colors = ['#ef4444', '#3b82f6', '#22c55e', '#f59e0b'];
const total = values.reduce((sum, value) => sum + value, 0);
let currentAngle = -Math.PI / 2;
values.forEach((value, index) => {
const sliceAngle = (value / total) * Math.PI * 2;
ctx.beginPath();
ctx.moveTo(160, 150);
ctx.arc(160, 150, 110, currentAngle, currentAngle + sliceAngle);
ctx.closePath();
ctx.fillStyle = colors[index];
ctx.fill();
ctx.stroke();
currentAngle += sliceAngle;
});
7. 색상, 선 스타일, 투명도와 애니메이션
7.1 채우기와 선의 스타일
fillStyle은 내부 채우기, strokeStyle은 윤곽선의 스타일을 정한다. CSS 색상 문자열뿐 아니라 그라디언트와 패턴도 지정할 수 있다. 선은 경로의 중심을 기준으로 안쪽과 바깥쪽에 절반씩 퍼진다.
| 속성·메서드 | 값 또는 의미 |
|---|---|
lineWidth | 선의 두께를 픽셀 단위로 지정한다. |
lineCap | 선 끝 모양: butt, round, square |
lineJoin | 선 연결점 모양: miter, round, bevel |
globalAlpha | 이후 모든 그리기에 적용되는 전체 투명도 |
setLineDash() | 선과 빈칸 길이의 반복 배열을 지정한다. |
lineDashOffset | 점선 패턴의 시작 위치를 이동한다. |
globalAlpha는 이후의 채우기와 선 등 모든 작업에 지속적으로 적용된다. 특정 색 하나만 투명하게 하려면 rgba() 색상을 쓰는 편이 영향 범위를 좁히기 쉽다. 점선을 실선으로 되돌릴 때는 setLineDash([])를 호출한다. 홀수 개 길이로 된 점선 배열은 브라우저가 같은 배열을 한 번 더 이어 짝수 패턴으로 만든다.
ctx.save();
ctx.strokeStyle = '#2563eb';
ctx.lineWidth = 8;
ctx.lineCap = 'round';
ctx.lineJoin = 'bevel';
ctx.globalAlpha = 0.75;
ctx.setLineDash([16, 8]);
ctx.lineDashOffset = 4;
ctx.stroke(path);
ctx.restore();
7.2 움직이는 점선과 requestAnimationFrame
lineDashOffset 값을 프레임마다 조금씩 바꾸면 선택 영역을 나타내는 ‘행진하는 개미’ 효과를 만들 수 있다. 애니메이션은 이전 프레임을 지우고, 상태를 갱신하고, 새 프레임을 그린 다음 다음 화면 갱신을 예약하는 순환 구조다.
let offset = 0;
let requestID;
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
offset = (offset + 1) % 24;
ctx.save();
ctx.setLineDash([12, 12]);
ctx.lineDashOffset = -offset;
ctx.strokeRect(40, 40, 220, 130);
ctx.restore();
requestID = requestAnimationFrame(animate);
}
animate();
// 중지할 때: cancelAnimationFrame(requestID);
requestAnimationFrame()은 브라우저의 화면 갱신 시점에 맞춰 콜백을 실행하므로 Canvas 애니메이션의 기본 도구다. 반환된 요청 ID를 보관하면 cancelAnimationFrame(requestID)로 반복을 중지할 수 있다.
8. 12강 핵심 정리
<canvas>는 투명한 픽셀 기반 그리기 영역이고, 실제 그림은 JavaScript의 2D 컨텍스트가 만든다.- 원점은 왼쪽 위이며 x축은 오른쪽, y축은 아래쪽으로 증가한다.
- HTML 속성은 내부 해상도, CSS는 표시 크기를 정한다. 고해상도 화면은 DPR을 반영하고 크기 변경 뒤 다시 그린다.
- 사각형 전용 메서드는 즉시 그리지만, 임의 도형은
beginPath()로 시작해 경로를 정의한 뒤 채우거나 선을 그린다. save()와restore()는 픽셀이 아니라 스타일·변환·클리핑 등의 그리기 상태를 스택으로 관리한다.arc()의 각도는 라디안이며 0은 3시 방향, 한 바퀴는2π이다.- 부채꼴은 중심으로 이동한 뒤 원호를 그리고 경로를 닫는다. 파이차트는 각 데이터 비율의 각도를 누적한다.
- 점선의 이동과
requestAnimationFrame()을 결합하면 자연스러운 Canvas 애니메이션을 만들 수 있다.
Canvas 프로그래밍의 핵심은 좌표와 경로를 정확히 정의하고, 지속되는 컨텍스트 상태를 통제하면서 매 프레임 필요한 장면을 다시 그리는 것이다.
9. 예상문제 20선
1. Canvas에서 실제 그림을 만드는 주체로 가장 알맞은 것은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ②
canvas 요소는 투명한 그리기 영역을 제공하며 실제 픽셀은 JavaScript의 2D 컨텍스트가 그린다.
2. Canvas의 기본 좌표계에 대한 설명으로 옳은 것은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ④
Canvas의 (0, 0)은 왼쪽 위에 있고 화면 아래쪽으로 갈수록 y값이 커진다.
3. width와 height 속성을 생략한 canvas의 기본 내부 크기는?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ①
canvas의 width와 height 속성 기본값은 각각 300과 150이다.
4. Canvas 그림의 접근성을 보완하는 방법으로 가장 적절한 것은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ③
시작·종료 태그 사이의 설명과 role, aria-label은 Canvas를 보지 못하는 사용자에게 의미를 전달한다.
5. 작은 내부 비트맵을 CSS로만 크게 표시했을 때 생기기 쉬운 현상은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ①
CSS 표시 크기만 키우면 낮은 해상도의 비트맵 자체를 확대하므로 선과 글자가 흐릿해질 수 있다.
6. 2차원 그리기 컨텍스트를 얻는 올바른 코드는?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ④
Canvas 2D API의 컨텍스트 식별자는 소문자 문자열 '2d'이다.
7. 고해상도 화면에서 선명한 Canvas를 만들기 위해 내부 크기에 곱하는 값은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ②
CSS 표시 크기에 DPR을 곱한 값을 내부 픽셀 크기로 사용하면 고밀도 화면의 물리 픽셀을 활용할 수 있다.
8. 반응형 크기 조절 때 scale()의 배율 누적을 막는 방법은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ③setTransform(1, 0, 0, 1, 0, 0)로 기존 변환을 초기화한 뒤 DPR 배율을 적용한다.
9. 현재 채우기 스타일로 사각형을 즉시 그리는 메서드는?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ④fillRect(x, y, width, height)는 별도의 패스 정의 없이 채운 사각형을 바로 그린다.
10. clearRect()로 캔버스를 지운 뒤에도 유지되는 것은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ②clearRect()는 지정 영역의 픽셀만 투명하게 만들고 컨텍스트의 스타일과 변환은 초기화하지 않는다.
11. ctx.save()가 저장하는 것으로 옳은 것은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ①save()는 그리기 상태를 스택에 저장하며 그려진 픽셀 자체를 저장하지 않는다.
12. save()를 여러 번 호출한 뒤 restore()를 호출할 때 복원되는 상태는?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ③
상태 저장은 스택 구조이므로 나중에 저장한 상태가 먼저 복원되는 LIFO 방식이다.
13. 새로운 삼각형 패스를 만들어 채우는 순서로 가장 알맞은 것은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ②
새 경로를 시작하고 첫 점으로 이동한 뒤 선분을 추가하고 닫아서 채우는 것이 기본 흐름이다.
14. 독립된 도형을 그릴 때 beginPath()를 호출하는 주된 이유는?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ④
현재 패스는 자동으로 사라지지 않으므로 새 도형 앞에서 beginPath()로 초기화해야 한다.
15. closePath()의 기능으로 옳은 것은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ③closePath()는 마지막 위치에서 시작점까지 선분을 추가해 닫힌 도형을 만든다.
16. arc()로 완전한 원을 그릴 때의 각도 범위는?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ①
원의 한 바퀴 360°는 라디안으로 2π이므로 시작각 0, 끝각 Math.PI * 2를 사용한다.
17. 원호를 부채꼴로 만들기 위해 먼저 추가해야 할 경로는?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ④
중심점에서 원호로 이어지는 반지름을 만든 뒤 경로를 닫아야 부채꼴 영역이 된다.
18. 채우기와 굵은 테두리를 함께 그릴 때 테두리가 덜 가려지는 순서는?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ①
선은 경로 중심에서 양쪽으로 퍼지므로 채운 뒤 선을 그려야 선의 안쪽 부분까지 온전히 보인다.
19. 점선을 다시 실선으로 되돌리는 코드로 옳은 것은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ③
빈 배열을 점선 패턴으로 지정하면 대시 구간이 없어져 다시 실선이 된다.
20. Canvas 애니메이션의 기본 반복 순서로 가장 알맞은 것은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ②
프레임마다 기존 장면을 지우고 상태를 바꾼 뒤 새 장면을 그리고 requestAnimationFrame()으로 다음 갱신을 예약한다.
댓글
댓글 쓰기