SVG와 캔버스 기초
DOM으로 다루는 벡터 SVG와 픽셀을 그리는 Canvas를 비교하고 2D 컨텍스트로 간단한 애니메이션을 만듭니다.
지금까지 HTML로 페이지의 구조를 만들고 텍스트, 링크, 이미지, 비디오와 오디오를 넣는 방법을 익혔습니다. 이 글에서는 웹 표준만으로 그래픽을 직접 그리는 두 방법을 비교합니다. 도형을 HTML처럼 요소로 선언하는 벡터 방식의 SVG(Scalable Vector Graphics)와, 자바스크립트로 픽셀을 그리는 Canvas입니다. 두 기술은 다이어그램, 차트, 게임, 애니메이션처럼 웹의 여러 시각 요소를 만드는 바탕이 됩니다.
SVG & CANVAS
SVG는 도형 노드, Canvas는 그려진 픽셀을 유지합니다
같은 원을 그려도 개별 객체의 상태·이벤트·접근성 정보를 관리하는 방식이 다릅니다.
SVG · 도형 노드를 유지
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="20" />
</svg>원에 해당하는 DOM 노드가 남습니다. 속성·CSS·이벤트로 개별 도형을 다룰 수 있습니다. 보조 기술에 전달할 이름·의미와 조작은 목적에 맞게 지정합니다.
Canvas · 그린 픽셀을 유지
ctx.beginPath();
ctx.arc(50, 50, 20, 0, Math.PI * 2);
ctx.fill();그리기 호출 뒤 비트맵의 픽셀이 남습니다. 원의 위치·선택 상태·클릭 판정은 앱이 관리합니다. 원마다 DOM 노드가 자동 생성되지 않습니다.
무엇을 개별적으로 다루는가?
도형별 스타일·이벤트·선택을 주로 다루면 SVG를 검토합니다. 픽셀 읽기·이미지 처리·빈번한 대량 그리기라면 Canvas를 검토합니다. 실제 성능은 작업량과 구현을 측정합니다.
크기와 접근성은 별도로 설계
SVG의 viewBox는 좌표와 확대를 연결합니다. Canvas는 CSS 표시 크기와 버퍼 크기를 DPR에 맞춰 구분합니다. Canvas 객체의 설명·조작에는 의미 있는 HTML 대안 등이 필요합니다.
두 모델이 모두 필요한 경우 Canvas 화면 위에 HTML/SVG 조작 요소를 조합할 수 있습니다. SVG도 이름·초점·조작을 자동으로 모두 해결하지 않으며, Canvas도 항상 전체 화면을 다시 그려야 하는 것은 아닙니다.
위 그림은 같은 원을 그릴 때 SVG와 Canvas가 각각 무엇을 남기는지 보여 줍니다. SVG에서는 circle 요소가 DOM 노드로 남아, cx, cy, r 같은 속성을 바꾸면 브라우저가 화면을 다시 그리고 CSS와 이벤트도 그 노드에 연결됩니다. Canvas에서는 원의 위치 같은 상태를 앱의 변수가 가지고 있고, arc()와 fill()을 호출하면 비트맵에 픽셀만 기록됩니다. 원에 해당하는 요소는 생기지 않으므로, 원을 클릭했는지 판단하거나 다시 그리는 일은 앱이 직접 합니다.
그림 아래의 설명처럼 두 경로는 성능의 순위가 아닙니다. 도형마다 스타일, 이벤트, 선택을 다뤄야 하면 SVG를, 픽셀을 읽거나 이미지를 처리하거나 많은 도형을 자주 다시 그려야 하면 Canvas를 검토하며, 실제 성능은 작업량에 따라 측정해 봅니다. 크기와 접근성도 따로 설계해야 합니다. SVG는 viewBox로 좌표와 확대를 연결하고, Canvas는 화면의 표시 크기와 내부 비트맵의 크기를 구분하며, 그린 내용을 설명할 HTML 대안을 따로 제공해야 합니다.
SVG: 확장 가능한 벡터 그래픽
SVG는 벡터 그래픽을 웹에 표현하는 XML 기반의 마크업 언어입니다. 벡터 그래픽은 그림을 점, 선, 면 같은 수학적인 도형으로 정의하는 방식으로, 픽셀의 색을 하나하나 저장하는 JPG나 PNG 같은 비트맵 그래픽과 다릅니다.
SVG에는 다음과 같은 특징이 있습니다.
- 확대해도 선명합니다: 도형을 수학적으로 정의하므로 표시 크기에 맞춰 다시 그립니다. 그래서 여러 해상도의 화면에서 선명하게 보여야 하는 로고, 아이콘, 다이어그램에 알맞습니다. 다만 SVG 안에 넣은 사진 같은 비트맵 이미지까지 선명해지는 것은 아닙니다.
- 파일이 작습니다: 복잡하지 않은 그래픽은 같은 그림의 비트맵 이미지보다 파일이 훨씬 작습니다.
- 고치기 쉽습니다: 텍스트로 된 코드라서 편집기에서 바로 고칠 수 있고, CSS와 자바스크립트로 색, 크기, 위치를 바꾸거나 애니메이션을 줄 수 있습니다.
- 내용을 설명할 수 있습니다:
<title>이나<desc>요소로 그래픽에 이름과 설명을 붙여 보조 기술에 전할 수 있고, SVG 안의 텍스트는 검색 엔진도 읽을 수 있습니다.
SVG 기본 문법
SVG는 HTML 문서 안에 <svg> 요소로 직접 넣거나, .svg 파일로 저장해 <img>나 CSS의 background-image로 불러올 수 있습니다. 여기서는 HTML 안에 직접 넣는 방식을 살펴봅니다. <svg> 요소가 그림을 그릴 영역이 되며, width와 height로 크기를 정합니다.
<svg width="200" height="200">
</svg>SVG의 좌표는 영역의 왼쪽 위가 (0, 0)이고, 오른쪽으로 갈수록 x가, 아래로 갈수록 y가 커집니다. 이 좌표계 위에 기본 도형을 그립니다. <rect>는 직사각형으로, 왼쪽 위 모서리의 x, y와 width, height로 위치와 크기를 정하고, fill은 안쪽 색, stroke는 테두리 색, stroke-width는 테두리 두께입니다.
<svg width="200" height="100">
<rect x="10" y="10" width="80" height="80"
fill="blue" stroke="black" stroke-width="3" />
</svg>이 사각형은 (10, 10)에서 시작하는 한 변 80의 정사각형입니다. 테두리는 도형의 윤곽선을 중심으로 안쪽과 바깥쪽에 절반씩 그려지므로, 두께 3인 테두리의 절반은 파란 면을 덮습니다.
<circle>은 원으로, cx와 cy가 중심의 좌표, r이 반지름입니다. 다음 원은 중심이 (50, 50)이고 반지름이 40이므로, 앞의 정사각형에 꼭 맞게 들어가는 크기입니다.
<svg width="200" height="100">
<circle cx="50" cy="50" r="40" fill="red" stroke="purple" stroke-width="2" />
</svg><line>은 선으로, x1, y1에서 x2, y2까지 그립니다. 선에는 안쪽이 없으므로 stroke를 지정해야 보입니다.
<svg width="200" height="100">
<line x1="10" y1="10" x2="190" y2="90" stroke="green" stroke-width="4" />
</svg><polygon>은 다각형으로, points 속성에 꼭짓점의 좌표를 x,y 쌍으로 공백을 두고 나열합니다. 다음 코드는 세 꼭짓점을 가진 삼각형입니다.
<svg width="200" height="200">
<polygon points="100,10 190,90 10,90" fill="orange" stroke="brown" stroke-width="2" />
</svg>마지막 꼭짓점 10,90에서 첫 꼭짓점 100,10으로 돌아가는 변은 따로 적지 않아도 자동으로 이어져 도형이 닫힙니다.
<text>는 글자를 그리며, x와 y는 글자의 시작 위치이고 y는 글자의 기준선(baseline) 높이입니다.
<svg width="200" height="100">
<text x="10" y="50" fill="darkblue" font-size="20">Hello SVG!</text>
</svg>그래서 y="50"은 글자 윗부분이 아니라 글자가 올라앉는 선의 높이이고, 글자 대부분은 그보다 위쪽에 그려집니다. y를 0으로 두면 글자가 영역 위로 벗어나 보이지 않으므로, 글자 크기 이상의 y를 주어야 합니다.
CSS와 자바스크립트로 SVG 다루기
SVG 도형은 DOM 요소이므로 HTML 요소처럼 CSS로 스타일을 주고 자바스크립트로 조작할 수 있습니다. 다음 예에서는 원에 class와 id를 붙입니다. <style>에서는 SVG 속성 이름과 같은 fill, stroke를 CSS 속성으로 쓰고, <script>에서는 id로 원을 찾아 클릭 이벤트를 연결합니다.
<style>
.my-circle {
fill: #ffd700; /* 금색 */
stroke: #cc0000;
stroke-width: 5;
transition: fill 0.3s ease; /* 호버 시 색상 변화 애니메이션 */
}
.my-circle:hover {
fill: #ff4500; /* 오렌지색 */
}
</style>
<svg width="100" height="100">
<circle cx="50" cy="50" r="40" class="my-circle" id="interactiveCircle" />
</svg>
<script>
const circle = document.getElementById('interactiveCircle');
circle.addEventListener('click', () => {
alert('SVG 원을 클릭했습니다!');
circle.setAttribute('r', '50'); // 반지름 변경
});
</script>이 원은 처음에 금색으로 채워지고, 마우스를 올리면 0.3초에 걸쳐 주황색으로 바뀝니다. 클릭하면 안내창이 뜬 뒤 setAttribute()로 반지름이 50으로 바뀌어, 지름 100인 원이 100×100 영역을 꽉 채웁니다. 테두리 두께 5의 바깥쪽 절반은 영역 밖으로 나가 잘립니다. 이렇게 SVG는 도형이 문서 안에 요소로 남아 있으므로, 도형 하나하나를 골라 바꿀 수 있습니다.
더 깊이: img로 넣은 SVG는 왜 바꿀 수 없을까
앞의 예제처럼 도형을 CSS와 자바스크립트로 다룰 수 있는 것은 SVG를 HTML 안에 직접 넣었기 때문입니다. 같은 SVG를 <img src="icon.svg">나 CSS의 background-image로 불러오면, 브라우저는 그 파일을 하나의 그림으로 그릴 뿐 도형을 페이지의 DOM에 넣지 않습니다. 그래서 페이지의 querySelector()로 그 안의 circle을 찾을 수 없고, 페이지의 :hover 규칙도 도형마다 따로 적용되지 않습니다. 색을 상태에 따라 바꿔야 하는 아이콘이라면 인라인으로 넣거나, 상태마다 다른 파일을 쓰는 식으로 정해야 합니다.
이미지로 쓰는 SVG에는 보안을 위한 제한도 걸릴 수 있습니다. MDN의 이미지로 쓰는 SVG 설명에 따르면 브라우저는 이때 SVG 안의 자바스크립트를 실행하지 않고, 바깥의 이미지나 스타일 시트 같은 외부 자원을 불러오지 않는 제한을 둘 수 있습니다. 외부 자원은 data: URL로 파일 안에 넣어 둔 경우에만 쓸 수 있습니다. 반대로 SVG 파일을 브라우저에서 직접 열거나 <iframe>, <object>로 넣으면 이 제한이 적용되지 않습니다.
이 차이는 사용자가 올린 SVG를 다룰 때 중요합니다. SVG에는 <script> 요소와 onclick 같은 이벤트 속성을 넣을 수 있으므로, 믿을 수 없는 SVG의 내용을 HTML에 그대로 끼워 넣으면 검사하지 않은 HTML을 넣는 것과 같은 위험이 생깁니다. 이런 파일을 인라인으로 넣지 말고 <img>로 보여 주는 쪽을 먼저 검토하는 이유입니다.
Canvas: 비트맵 그래픽
Canvas는 <canvas> 요소와 자바스크립트 API로 픽셀 기반의 2D 그래픽을 그리는 기술입니다. 이름처럼 그림을 그리는 도화지와 같아서, 한번 그린 내용은 픽셀로만 남습니다.
Canvas에는 다음과 같은 특징이 있습니다.
- 픽셀 단위로 다룹니다: 이미지 필터나 게임 화면처럼 픽셀 단위로 세밀하게 그리거나 읽어야 할 때 알맞습니다.
- 자바스크립트로 그립니다: 모든 그리기를 코드로 하므로, 상태에 따라 그림을 자유롭게 바꿀 수 있습니다.
- 반복해서 다시 그립니다: 많은 도형이나 픽셀을 매 순간 다시 그리는 장면에 씁니다. 그렇다고 항상 SVG보다 빠른 것은 아니며, 실제 성능은 해상도, 그리는 양, 브라우저에 따라 달라집니다.
Canvas는 화면에 객체를 쌓아 두는 방식이 아니라, 앱이 가진 현재 상태로 픽셀을 그리고, 상태가 바뀌면 다시 그리는 방식으로 이해하면 쉽습니다.
Canvas 기본 문법
<canvas> 요소는 그림을 그릴 영역을 정하며, width와 height 속성으로 크기를 정합니다. 두 속성을 생략하면 기본 크기인 너비 300, 높이 150이 쓰입니다. 다음 예의 id는 자바스크립트에서 이 Canvas를 찾을 때 쓰고, style의 테두리는 영역이 어디까지인지 보이게 할 뿐 그리기와는 관계가 없습니다.
<canvas id="myCanvas" width="400" height="200" style="border:1px solid #000;"></canvas><canvas> 요소 자체는 비어 있고, 그리는 일은 모두 자바스크립트로 합니다. width와 height 속성은 그리기 좌표와 내부 비트맵의 크기를 정합니다. CSS로 표시 크기를 다르게 지정하면, 내부 비트맵이 그 크기에 맞춰 늘어나거나 줄어들어 보이며, 늘어날 때는 흐려집니다.
자바스크립트로 그리는 순서는 세 단계입니다. 먼저 document.getElementById()로 <canvas> 요소를 가져오고, getContext('2d')로 2D 그리기용 렌더링 컨텍스트 객체를 얻은 뒤, 이 컨텍스트의 메서드로 도형, 텍스트, 이미지를 그립니다.
<canvas id="myDrawingCanvas" width="300" height="150" style="border:2px solid #ccc;"></canvas>
<script>
const canvas = document.getElementById('myDrawingCanvas');
const ctx = canvas.getContext('2d'); // 2D 렌더링 컨텍스트 가져오기
// 1. 사각형 그리기
ctx.fillStyle = 'green'; // 채우기 색상 설정
ctx.fillRect(10, 10, 100, 50); // (x, y, width, height)
// 2. 원 그리기
ctx.beginPath(); // 새로운 경로 시작
ctx.arc(180, 75, 40, 0, 2 * Math.PI); // (x, y, 반지름, 시작각도, 끝각도)
ctx.fillStyle = 'orange';
ctx.fill(); // 채우기
ctx.lineWidth = 3; // 선 두께
ctx.strokeStyle = 'red'; // 선 색상
ctx.stroke(); // 선 그리기
// 3. 텍스트 그리기
ctx.font = '24px Arial';
ctx.fillStyle = 'darkblue';
ctx.textAlign = 'center';
ctx.fillText('Hello Canvas!', canvas.width / 2, 130); // (텍스트, x, y)
</script>이 코드는 왼쪽 위 (10, 10)에서 시작하는 100×50 크기의 초록 사각형, 중심이 (180, 75)이고 반지름이 40인 주황 원과 그 둘레의 빨간 테두리, 가운데 정렬된 “Hello Canvas!” 글자를 차례로 그립니다. fillStyle이나 lineWidth 같은 설정은 컨텍스트의 상태로 남아 다음 그리기에도 쓰이므로, 그리기 전에 필요한 색과 두께를 다시 정합니다. 그려진 뒤의 원은 요소가 아니라 픽셀이므로, 원의 색을 바꾸려면 상태를 바꿔 다시 그려야 합니다.
SVG와 Canvas 고르기
두 기술의 차이는 다음과 같습니다.
| 특징 | SVG | Canvas |
|---|---|---|
| 방식 | 벡터, 도형을 요소로 선언 | 비트맵, 자바스크립트로 픽셀을 그림 |
| 조작 | 도형마다 DOM 요소가 있어 CSS와 자바스크립트로 따로 조작 | 전체가 하나의 비트맵이며 앱이 상태를 관리하고 다시 그림 |
| 확대 | 다시 그려 선명함 | 내부 해상도보다 크게 보이면 흐려짐 |
| 성능 | 요소 수와 갱신 방식에 따라 측정 필요 | 그리는 양, 해상도, 갱신 방식에 따라 측정 필요 |
| 주요 용도 | 로고, 아이콘, 다이어그램, 일러스트, 인터랙티브 차트 | 게임, 이미지 편집, 많은 데이터의 시각화 |
| 접근성 | 이름과 설명을 붙일 수 있으나 따로 설계해야 함 | 대체 내용과 조작 방법을 따로 제공해야 함 |
| 파일 크기 | 단순한 그래픽은 작고 복잡하면 커짐 | 해상도와 내용에 따라 다름 |
SVG는 확대해도 선명해야 하는 로고, 아이콘, 지도에, 데이터에 따라 도형마다 색이나 크기를 바꾸는 차트와 다이어그램에, CSS와 자바스크립트로 상태를 바꾸는 간단한 애니메이션에, 검색 엔진이나 스크린 리더가 그래픽의 내용을 알아야 할 때 고릅니다. Canvas는 이미지 필터나 사진 편집처럼 픽셀을 세밀하게 다뤄야 할 때, 프레임을 빠르게 바꾸는 게임이나 복잡한 애니메이션에, 수천 개의 점이 생겼다 사라지는 대규모 데이터 시각화처럼 그린 결과만 중요하고 도형마다 DOM으로 다룰 필요가 없을 때 고릅니다.
실습: SVG 아이콘과 Canvas 애니메이션
두 기술의 차이를 직접 확인해 봅니다. web-dev-practice 폴더에 graphics_playground.html을 만들고 다음 코드를 씁니다. 자바스크립트의 변수, 함수, 조건이 낯설다면 5장의 변수, 데이터 타입, 연산자, 조건문과 반복문, 함수와 스코프를 먼저 참고하세요. 여기서는 원의 위치 x, 한 번 갱신할 때의 이동량 dx, 반지름 radius만 구분하면 따라올 수 있습니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SVG와 Canvas 기초 실습</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 20px;
background-color: #f8f8f8;
color: #333;
text-align: center;
}
.container {
max-width: 900px;
margin: 20px auto;
background-color: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
display: flex;
flex-wrap: wrap;
justify-content: space-around;
gap: 30px;
}
section {
flex: 1 1 45%; /* 유연하게 공간 차지, 최소 너비 45% */
min-width: 350px;
padding: 20px;
border: 1px solid #eee;
border-radius: 8px;
background-color: #fafafa;
}
h1 {
color: #007bff;
margin-bottom: 40px;
}
h2 {
color: #0056b3;
margin-top: 0;
}
svg {
display: block;
margin: 20px auto;
border: 1px solid #ccc;
background-color: #fff;
border-radius: 5px;
}
.svg-icon-example {
display: inline-block; /* 아이콘들이 한 줄에 있도록 */
margin: 10px;
padding: 10px;
border: 1px solid #eee;
border-radius: 5px;
transition: transform 0.2s ease-out;
}
.svg-icon-example:hover {
transform: scale(1.1);
box-shadow: 0 0 8px rgba(0,0,0,0.1);
}
.svg-icon-example svg {
border: none; /* 개별 아이콘은 보더 제거 */
margin: 0;
}
canvas {
border: 1px solid #ccc;
background-color: #fff;
border-radius: 5px;
display: block;
margin: 20px auto;
}
button {
padding: 10px 20px;
background-color: #28a745;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1em;
margin-top: 15px;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #218838;
}
</style>
</head>
<body>
<h1>SVG와 Canvas 기초 실습</h1>
<div class="container">
<section>
<h2>SVG 아이콘 예시 (벡터 그래픽)</h2>
<p>SVG는 확대해도 깨지지 않는 선명한 벡터 이미지를 제공합니다.</p>
<div class="svg-icon-example" title="홈 아이콘">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
</div>
<div class="svg-icon-example" title="사용자 아이콘">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
</div>
<div class="svg-icon-example" title="메일 아이콘">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
<polyline points="22,6 12,13 2,6"></polyline>
</svg>
</div>
<h3>간단한 SVG 도형 그리기</h3>
<svg width="200" height="150">
<rect x="20" y="20" width="60" height="60" fill="#ff7f50" stroke="#cd5b45" stroke-width="3">
<title>주황색 사각형</title>
</rect>
<circle cx="150" cy="75" r="50" fill="#6a5acd" stroke="#483d8b" stroke-width="3">
<title>보라색 원</title>
</circle>
<text x="100" y="140" text-anchor="middle" font-size="18" fill="#333">SVG 도형들</text>
</svg>
</section>
<section>
<h2>Canvas 애니메이션 예시 (비트맵 그래픽)</h2>
<p>Canvas는 자바스크립트로 픽셀 단위의 동적인 그림을 그립니다.</p>
<canvas id="animatedCanvas" width="350" height="200"></canvas>
<button id="startButton">애니메이션 시작/정지</button>
<script>
const canvas = document.getElementById('animatedCanvas');
const ctx = canvas.getContext('2d');
const startButton = document.getElementById('startButton');
let animationFrameId; // requestAnimationFrame ID
let x = 50;
let dx = 2; // x 방향 속도
let radius = 20;
function drawCircle() {
ctx.clearRect(0, 0, canvas.width, canvas.height); // 캔버스 지우기
ctx.beginPath();
ctx.arc(x, canvas.height / 2, radius, 0, Math.PI * 2);
ctx.fillStyle = 'blue';
ctx.fill();
ctx.closePath();
}
function animate() {
drawCircle();
x += dx;
// 벽에 부딪히면 방향 반전
if (x + radius > canvas.width || x - radius < 0) {
dx = -dx;
}
animationFrameId = requestAnimationFrame(animate); // 다음 프레임 요청
}
let isAnimating = false;
startButton.addEventListener('click', () => {
if (isAnimating) {
cancelAnimationFrame(animationFrameId); // 애니메이션 정지
startButton.textContent = '애니메이션 시작';
} else {
animate(); // 애니메이션 시작
startButton.textContent = '애니메이션 정지';
}
isAnimating = !isAnimating;
});
// 페이지 로드 시 초기 상태 그리기
drawCircle();
</script>
</section>
</div>
</body>
</html>Live Server로 파일을 열면 왼쪽 영역에 홈, 사용자, 메일 아이콘과 주황 사각형, 보라 원, “SVG 도형들” 글자가 보입니다. 브라우저를 확대해도 아이콘과 도형의 가장자리는 선명하게 유지됩니다. 아이콘의 viewBox="0 0 24 24"는 24×24 좌표로 그린 도형을 width="40" height="40" 크기에 맞춰 늘려 보여 주며, stroke="currentColor"는 테두리 색을 주변 글자색에서 가져옵니다.
오른쪽 Canvas에는 처음에 중심이 (50, 100)이고 반지름이 20인 파란 원이 멈춰 있고, 버튼에는 애니메이션 시작/정지라고 적혀 있습니다. 버튼을 한 번 누르면 원이 오른쪽으로 움직이기 시작하고 버튼 글자가 애니메이션 정지로 바뀌며, 다시 누르면 멈추고 애니메이션 시작으로 바뀝니다. 세 번째로 누르면 처음 위치로 돌아가지 않고, 멈췄던 x에서 다시 움직입니다.
animate()는 현재 x로 원을 그린 뒤 x += dx로 다음 위치를 정하고, 원의 가장자리인 x + radius나 x - radius가 벽을 넘으면 dx의 부호를 바꿔 방향을 뒤집습니다. 마지막으로 requestAnimationFrame()으로 다음 화면 갱신 때 자신을 다시 호출하게 예약합니다. 그래서 dx = 2는 초당 2px이 아니라 호출 한 번마다 2px이며, 초당 60번 갱신하는 화면에서는 초당 120px, 120번 갱신하는 화면에서는 초당 240px로 움직입니다. 화면과 상관없이 같은 속도로 움직이게 하려면 지난 호출부터 흐른 시간을 이동량에 곱해야 합니다.
좁은 화면에서도 보이게 하기
이 코드는 넓은 화면을 기준으로 만들었습니다. section의 최소 너비 350px에 좌우 안쪽 여백 40px과 테두리 2px이 더해져 영역 하나에 392px이 필요하므로, 320px이나 375px 너비의 휴대폰 화면에서는 페이지가 가로로 넘칩니다. 다음 CSS를 같은 파일의 </style> 바로 앞에 더하면, 화면이 600px 이하일 때 두 영역을 세로로 쌓고 최소 너비를 풀며, Canvas의 표시 크기만 공간에 맞춰 줄입니다.
@media (max-width: 600px) {
body {
margin: 16px;
}
.container {
padding: 12px;
gap: 20px;
}
.container > section {
box-sizing: border-box;
flex-basis: 100%;
min-width: 0;
max-width: 100%;
padding: 12px;
}
#animatedCanvas {
box-sizing: border-box;
max-width: 100%;
height: auto;
}
}canvas의 width="350" height="200" 속성과 자바스크립트는 그대로 둡니다. 내부 좌표계는 여전히 350×200이고, CSS가 그 비트맵을 같은 비율로 작게 보여 줄 뿐이므로 원의 중심 (50, 100), 반지름 20, 벽에서 방향을 바꾸는 조건이 모두 그대로입니다. 200×150인 SVG 도형 영역과 40×40 아이콘은 좁은 화면에도 들어가므로 원래 크기로 둡니다. 화면이 작아졌다고 canvas.width를 바꾸면 그린 내용이 지워지고 좌표의 기준까지 달라지므로 주의합니다.
개발자 도구의 반응형 화면을 320px과 375px로 바꿔, 두 영역이 세로로 보이고 페이지에 가로 스크롤이 없는지 확인합니다. 700px로 넓히면 이 조건이 꺼지고 원래 배치로 돌아갑니다.
정리
- SVG는 도형을 DOM 요소로 선언하는 벡터 그래픽으로, 확대해도 선명하고 도형마다 CSS와 자바스크립트로 조작할 수 있습니다.
- SVG의 좌표는 왼쪽 위가
(0, 0)이며,rect,circle,line,polygon,text같은 기본 도형으로 그림을 그립니다. - Canvas는
getContext('2d')로 얻은 컨텍스트로 픽셀을 그리며, 그린 뒤에는 도형이 요소로 남지 않아 앱이 상태를 관리하고 다시 그립니다. - Canvas의
width와height속성은 내부 좌표와 비트맵 크기를, CSS는 표시 크기를 정합니다. requestAnimationFrame()은 화면 갱신마다 함수를 호출하므로, 호출마다 정한 이동량은 화면의 갱신 빈도에 따라 초당 속도가 달라집니다.- 도형마다 다루려면 SVG를, 픽셀을 다루거나 많이 다시 그려야 하면 Canvas를 검토하고, 성능은 측정해 판단합니다.
연습 문제
1. 벽에 닿은 원
폭 350, 반지름 20인 Canvas에서 x = 330, dx = 2일 때 animate()가 한 번 실행됩니다. 이번에 그린 원의 중심, 다음 x, 바뀐 dx를 차례로 구해 보세요. 다음 호출에서 원이 벽을 살짝 넘어 보이는 이유는 무엇일까요?
해설 보기
이번 호출은 먼저 중심 330에 원을 그립니다. 이어서 x가 332가 되고, 332 + 20 = 352가 350보다 크므로 dx는 -2가 됩니다.
다음 호출은 방향만 바뀐 상태에서 먼저 중심 332에 원을 그리므로, 원의 오른쪽 끝이 352가 되어 벽보다 2px 밖으로 나갑니다. 방향을 바꾸는 것과 위치를 벽 안으로 되돌리는 것은 다른 일입니다. 원을 항상 안쪽에 두려면 방향을 바꿀 때 x를 radius 이상, canvas.width - radius 이하로 함께 보정합니다.
2. 화면이 작아지면 이동량도 줄여야 할까
좁은 화면용 CSS로 Canvas가 화면에 작게 보이게 되었습니다. 원이 화면에서 움직이는 속도를 예전처럼 보이게 하려고 dx = 2를 dx = 1로 바꿔야 할까요?
해설 보기
바꿀 필요가 없습니다. 원은 여전히 350×200의 내부 좌표계에서 호출마다 2단위씩 움직이고, CSS는 그 비트맵 전체를 같은 비율로 줄여 보여 줍니다. 그래서 원의 이동 거리와 Canvas의 너비가 함께 줄어, 원이 벽에서 벽까지 가는 데 걸리는 시간은 넓은 화면과 같습니다.
dx를 1로 바꾸면 오히려 벽 사이를 오가는 시간이 두 배로 늘어납니다.
3. Canvas의 원을 클릭하기
SVG 예제처럼 Canvas의 원을 클릭했을 때 안내창을 띄우려고 합니다. 원에 직접 addEventListener()를 붙일 수 있을까요? 어떻게 만들어야 할까요?
해설 보기
붙일 수 없습니다. Canvas에 그린 원은 DOM 요소가 아니라 픽셀이므로, 이벤트를 붙일 대상이 없습니다.
대신 canvas 요소 전체에 클릭 리스너를 붙이고, 클릭한 위치가 원 안인지 직접 계산합니다. 클릭 좌표를 Canvas의 내부 좌표로 바꾼 뒤, 원의 중심 (x, canvas.height / 2)까지의 거리가 radius 이하이면 원을 클릭한 것으로 판단합니다. 키보드 사용자를 위해서는 같은 기능을 하는 HTML 버튼을 따로 제공합니다.
이것으로 2장을 마칩니다. 2장에서는 HTML 문서의 구조와 문법, 텍스트와 링크, 이미지, 목록과 표와 폼, 시맨틱 HTML과 접근성, HTML5의 새 요소와 Web API, SVG와 Canvas를 살펴봤습니다. 다음 장의 CSS 기본 문법과 선택자부터는 이 구조에 모양을 입히는 CSS를 다룹니다.