Canvas API
고친 사람 github-actions[bot]
Canvas API 는 웹 페이지 안의 그림판에 스크립트로 그림을 그리게 해 줍니다. 선을 긋고 도형을 칠하는 함수를 브라우저가 내어 줍니다. 그린 뒤에는 도형이 아니라 픽셀만 남습니다. 점이 아주 많거나 쉬지 않고 바뀌는 그림에 씁니다.
쉽고 빠른 이해
무슨 일을 하는 물건인가 — 브라우저가 내어 주는 그리기 함수 묶음입니다. 점이 아주 많은 차트나 웹 게임의 화면을 이 함수들로 그립니다.
왜 이렇게 하나 — 점 하나마다 태그를 하나씩 만들면 점이 많을 때 페이지가 무거워집니다. 그림판 하나에 픽셀로 칠하면 점이 몇 개든 그림판의 픽셀만 기억하면 됩니다.
어떻게 도나
- 페이지에 그림판 태그를 하나 놓습니다
- 그 그림판에서 그리기 함수를 담은 객체를 꺼냅니다
- 색과 굵기를 정하고 도형을 칠합니다
- 움직이는 그림은 화면이 바뀔 때마다 지우고 다시 칠합니다
대가 — 칠한 도형은 기억되지 않습니다. 옮기려면 다시 그려야 합니다. 무엇이 클릭됐는지도 코드가 직접 계산해야 합니다. 그림 속 글자는 선택도 검색도 안 됩니다.
상세
이 절은 Canvas API 를 부르는 순서를 따라갑니다. 렌더링 컨텍스트 꺼내기 · 도형 그리기 · 움직이기 · 픽셀 만지기 순으로 갑니다. 마지막에 언제 쓰고 언제 안 쓰는지를 가릅니다.
API(Application Programming Interface, 응용 프로그램 인터페이스)는 프로그램이 다른 프로그램의 기능을 불러 쓰는 창구입니다. Canvas API 는 브라우저가 자바스크립트 코드에 내어 주는 창구 가운데 하나입니다. 그림을 그리는 함수들이 이 창구에 모여 있습니다.
그림판과 컨텍스트
그림은 HTML(HyperText Markup Language, 하이퍼텍스트 마크업 언어) 문서 안의 <canvas> 태그 위에 그립니다. 이 태그가 만드는 사각형 그림판이 canvas 요소입니다. 그림판은 가로세로 칸으로 나뉜 격자입니다. 이 격자의 칸 하나를 픽셀이라고 부릅니다.
태그만 놓으면 빈 판입니다. 그리려면 그림판에서 렌더링 컨텍스트를 꺼내야 합니다. 렌더링 컨텍스트는 그리기 함수와 지금 쓰는 색 같은 설정을 한데 담은 객체입니다. 모든 그리기는 이 객체의 메서드를 부르는 일입니다.
const c = document.querySelector("canvas");
const ctx = c.getContext("2d");
ctx.fillStyle = "red";
ctx.fillRect(10, 10, 100, 50);
getContext("2d") 는 평면 도형을 그리는 2D(2차원) 컨텍스트를 돌려줍니다. Canvas API 라고 하면 대개 이 2D 컨텍스트의 함수 묶음을 가리킵니다. 뒤의 두 줄은 칠할 색을 빨강으로 정하고 사각형 하나를 칠합니다.
같은 그림판에서 getContext("webgl") 을 부르면 WebGL 컨텍스트가 나옵니다. WebGL 은 GPU(Graphics Processing Unit, 그래픽 처리 장치)로 3D(3차원) 그림까지 그리는 별도의 API 입니다.
한 그림판에서는 한 종류의 컨텍스트만 꺼낼 수 있습니다. 이미 2D 를 꺼낸 판에 다른 종류를 청하면 null 이 돌아옵니다. 브라우저가 모르는 이름을 대도 null 입니다.
좌표
그림판의 좌표는 왼쪽 위 모서리에서 시작합니다. x 는 오른쪽으로, y 는 아래로 커집니다. 수학 시간에 배운 좌표평면과 y 의 방향이 반대입니다.
앞 코드의 fillRect(10, 10, 100, 50) 을 이 좌표로 읽어 봅니다. 왼쪽 위 모서리에서 오른쪽으로 10, 아래로 10 떨어진 점이 사각형의 왼쪽 위 꼭짓점입니다. 거기서 가로 100, 세로 50 픽셀(격자 칸)을 칠합니다.
경로로 그리는 도형
사각형이 아닌 도형은 경로로 그립니다. 경로는 칠하기 전에 그려 두는 밑그림 선입니다. 밑그림만으로는 화면에 아무것도 안 나타납니다. 선을 따라 긋거나 안을 칠해야 픽셀이 바뀝니다.
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(120, 20);
ctx.lineTo(70, 100);
ctx.closePath();
ctx.stroke();
beginPath 가 새 밑그림을 시작합니다. moveTo 는 선을 긋지 않고 시작점만 (20, 20) 으로 옮깁니다. lineTo 두 번이 오른쪽으로 한 변, 아래로 한 변을 긋습니다.
closePath 는 시작점으로 돌아와 삼각형을 닫습니다. 마지막 stroke 가 밑그림을 따라 선을 긋습니다. 선 대신 안을 칠하려면 stroke 대신 fill 을 부릅니다. 원이나 호는 arc 로 밑그림에 더합니다.
beginPath 를 빠뜨리면 흔한 버그가 납니다. 밑그림은 새로 시작하기 전까지 계속 쌓입니다. 그래서 새 도형을 그리고 stroke 를 부르면 앞서 그린 도형의 선까지 다시 그어집니다. 색을 바꿨다면 앞 도형도 새 색으로 덧칠됩니다.
그리기 상태와 save · restore
컨텍스트는 설정을 기억합니다. fillStyle 로 정한 색, lineWidth 로 정한 선 굵기, font 로 정한 글꼴은 다시 바꿀 때까지 이어집니다. 다음에 그리는 도형과 fillText 로 쓰는 글자는 모두 그 설정을 따릅니다.
이 설정 묶음을 그리기 상태라고 부릅니다. 함수 하나가 색을 바꾸고 되돌려 두지 않으면 뒤에 그리는 코드가 엉뚱한 색을 씁니다. 그래서 save 로 지금 설정을 쌓아 두고 restore 로 되돌립니다.
ctx.save();
ctx.fillStyle = "blue";
ctx.fillRect(0, 0, 10, 10); // 파랑
ctx.restore();
ctx.fillRect(20, 0, 10, 10); // 앞의 색
restore 뒤의 사각형은 save 를 부르기 전의 색으로 칠해집니다. 파랑은 save 와 restore 사이에서만 쓰였습니다.
좌표를 옮기거나 돌리는 변환도 그리기 상태에 들어갑니다. 도형을 돌려 그리고 싶으면 도형이 아니라 좌표축을 돌린 뒤 그립니다. 변환 함수는 셋입니다.
| 메서드 | 하는 일 |
|---|---|
translate(x, y) |
원점을 옮긴다 |
rotate(각도) |
좌표축을 돌린다 |
scale(x, y) |
눈금을 늘리거나 줄인다 |
변환도 한 번 걸면 다음 그리기에 계속 남습니다. 그래서 변환도 save 와 restore 로 감싸 둡니다. 그래야 뒤에 그리는 그림이 함께 돌아가지 않습니다.
경로와 그리기 상태는 fill 이나 stroke 를 부를 때 만납니다. 그 순간 밑그림이 지금 설정된 색과 굵기로 칠해져 픽셀이 됩니다. 칠한 뒤에 남는 것은 픽셀뿐입니다.
flowchart TD
E["canvas 요소"] --> X["2D 컨텍스트 · getContext 로 꺼낸다"]
X --> S["그리기 상태 · 색 · 선 굵기 · 글꼴 · 변환"]
X --> P["경로 · 밑그림 선"]
S --> F["fill 또는 stroke"]
P --> F
F --> G["픽셀 격자에 칠해진다"]
픽셀만 남기는 즉시 모드
Canvas API 는 칠한 결과만 기억합니다. 「여기 삼각형이 있다」는 정보는 칠하는 순간 사라지고 픽셀 색만 남습니다. 이렇게 그리는 방식을 즉시 모드라고 합니다.
반대편은 유지 모드입니다. 그린 도형을 기억해 두는 방식입니다. 도형이 남아 있으니 속성만 바꾸면 다시 그리는 일은 브라우저가 맡습니다.
웹 페이지 자체가 유지 모드로 그려집니다. 브라우저는 페이지의 태그들을 나무 모양의 객체 묶음으로 들고 있습니다. 이 묶음이 DOM(Document Object Model, 문서 객체 모델)입니다.
SVG(Scalable Vector Graphics, 확장 가능한 벡터 그래픽)는 도형을 태그로 적어 두는 그림 형식입니다. SVG 로 그린 원 하나는 DOM 안에 요소 하나로 남습니다. 원의 위치 값을 고치면 브라우저가 알아서 다시 그립니다.
즉시 모드에서 도형을 옮기려면 판을 지우고 새 위치에 다시 그려야 합니다. 그래서 움직이는 그림은 한 장면마다 지우기와 그리기를 되풀이합니다. 이 되풀이를 애니메이션 루프라고 부릅니다.
루프는 requestAnimationFrame 으로 돌립니다. 브라우저가 다음 화면을 그리기 직전에 함수 하나를 불러 달라고 맡기는 함수입니다. 탭이 안 보이면 브라우저가 부르기를 멈추거나 늦춥니다.
화면은 1초에 정해진 횟수만큼 새로 그려집니다. 이 횟수를 주사율이라고 합니다. requestAnimationFrame 은 이 횟수에 맞춰 대개 1초에 수십 번 불립니다.
let x = 0;
function frame() {
ctx.clearRect(0, 0, c.width, c.height);
ctx.fillRect(x, 50, 20, 20);
x += 2;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
frame 은 불릴 때마다 판 전체를 clearRect 로 지웁니다. 사각형은 2픽셀 오른쪽에 다시 그립니다. 그리고 다음 장면을 맡깁니다. 이 되풀이 덕에 사각형이 오른쪽으로 미끄러지는 것처럼 보입니다.
즉시 모드의 대가가 하나 더 있습니다. 그림판은 어느 픽셀이 어느 도형인지 모릅니다. 사용자가 누른 좌표가 어느 도형 위인지는 코드가 직접 계산해야 합니다. 이 계산을 히트 테스트라고 합니다.
보통은 그린 도형의 목록을 코드가 따로 들고 있는 것입니다. 클릭이 오면 그 목록을 돌며 좌표가 어느 도형 안에 드는지 견줍니다. 경로라면 isPointInPath 가 점이 밑그림 안에 드는지 알려 줍니다.
픽셀 읽기와 쓰기
getImageData 는 그림판의 한 영역을 픽셀 배열로 꺼냅니다. 픽셀 하나는 빨강·초록·파랑의 세기와 불투명도, 네 값으로 적힙니다. 각 값은 0부터 255까지입니다. 이 네 값을 묶어 RGBA(Red Green Blue Alpha, 빨강·초록·파랑·알파)라고 부릅니다.
ctx.fillStyle = "red";
ctx.fillRect(0, 0, 50, 30);
const img = ctx.getImageData(10, 10, 1, 1);
img.data; // [255, 0, 0, 255]
(10, 10) 은 방금 칠한 사각형 안의 점입니다. 돌아온 네 값은 빨강만 가득하고 완전히 불투명하다는 뜻입니다.
배열 값을 바꾼 뒤 putImageData 로 돌려 넣으면 그림이 바뀝니다. 사진을 흑백으로 바꾸는 필터가 이렇게 돕니다. 픽셀마다 세 색의 평균을 내어 세 값에 똑같이 넣습니다.
그림판을 파일로 꺼내는 함수도 있습니다. toBlob 은 그림판을 이미지 파일로 바꿔 Blob 객체에 담아 줍니다. Blob 은 브라우저가 파일 내용을 들고 다니는 객체입니다.
toDataURL 은 같은 그림을 data:image/png 로 시작하는 긴 문자열 하나로 바꿉니다. 이 문자열은 그림 파일 내용을 글자로 옮겨 적은 주소입니다. <img> 태그의 주소 칸에 넣으면 따로 파일이 없어도 그 그림이 보입니다.
다른 출처의 그림과 오염된 캔버스
다른 사이트의 그림을 그림판에 그리면 그 뒤로 getImageData·toBlob 같은 픽셀 읽기가 막힙니다. 왜 막히는지 보려면 출처와 동일 출처 정책부터 알아야 합니다.
출처는 주소 앞머리의 스킴과 호스트와 포트를 묶은 단위입니다. https://a.example 과 https://b.example 은 호스트가 달라 다른 출처입니다.
브라우저는 한 출처의 스크립트가 다른 출처의 데이터를 마음대로 읽지 못하게 막습니다. 이 규칙이 동일 출처 정책입니다. 사용자가 로그인해 둔 다른 사이트의 내용을 엉뚱한 페이지가 빼 가지 못하게 하려는 것입니다.
다른 출처의 그림을 그림판에 그리는 것 자체는 됩니다. 대신 그 뒤로 그림판은 오염된 캔버스가 됩니다. 오염된 판에서 getImageData·toBlob·toDataURL 을 부르면 SecurityError 가 납니다. 막지 않으면 사용자에게만 보이는 다른 사이트의 그림을 스크립트가 픽셀로 빼 갈 수 있습니다.
오염을 피하려면 양쪽이 다 나서야 합니다. 그림을 내주는 서버는 CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유) 헤더로 읽기를 허락합니다. 페이지는 그림을 부르기 전에 이미지의 crossOrigin 속성을 걸어 그 허락을 청합니다.
flowchart TD
A["다른 출처의 그림을 부른다"] --> B{"페이지가 crossOrigin 을 걸었나"}
B -->|아니오| T["그려지지만 판이 오염된다"]
B -->|예| C{"서버가 CORS 헤더로 허락했나"}
C -->|아니오| N["그림을 불러오지 못한다"]
C -->|예| K["오염되지 않는다"]
한쪽만 갖추면 판이 오염되거나 그림을 불러오지 못합니다. 둘 다 갖춰야 그림을 그린 뒤에도 픽셀을 읽을 수 있습니다.
업로드 전에 사진 줄이기
백엔드 개발자가 Canvas API 를 자주 만나는 곳은 이미지 업로드입니다. 사진을 서버로 보내기 전에 브라우저에서 크기를 줄이는 기능이 이 API 로 돌아갑니다. 서버는 이미 줄어든 파일을 받습니다.
drawImage 는 이미지를 그림판에 그리는 함수입니다. 그릴 크기를 함께 주면 그 크기로 늘이거나 줄여 그립니다. 작은 그림판에 큰 사진을 줄여 그린 뒤 toBlob 으로 꺼내 올립니다.
const c = document.createElement("canvas");
c.width = 800;
c.height = 600;
c.getContext("2d").drawImage(photo, 0, 0, 800, 600);
c.toBlob(blob => upload(blob), "image/jpeg");
photo 는 사용자가 고른 사진 파일을 미리 불러 둔 이미지 객체입니다. 코드는 이 사진을 가로 800, 세로 600 픽셀로 줄여 그립니다. 그 그림을 이미지 파일로 꺼냅니다. upload 는 그 파일을 서버로 보내는 함수라고 가정합니다.
이렇게 올라온 파일은 원본 사진 파일이 아닙니다. 브라우저가 그림판의 픽셀로 새로 만든 이미지입니다. 그래서 카메라가 원본에 적어 둔 촬영 정보는 따라오지 않습니다.
고해상도 화면에서 흐려지는 그림
고해상도 화면에서는 그림판의 그림이 흐릿하게 보일 때가 있습니다. 까닭을 보려면 크기를 재는 단위 셋을 갈라야 합니다. 셋은 늘 같은 크기가 아닙니다.
격자 칸은 앞에서 픽셀이라 부른 그림판의 칸입니다. CSS 픽셀은 CSS(Cascading Style Sheets, 캐스케이딩 스타일 시트)가 페이지 배치에 쓰는 길이 단위입니다. 화면의 점은 모니터가 실제로 빛을 내는 가장 작은 점입니다. 이 절에서는 이 세 이름만 씁니다.
그림판에는 크기가 두 가지 있습니다. width·height 속성이 정하는 격자 칸 수가 하나입니다. CSS 가 CSS 픽셀로 정하는 화면 속 크기가 다른 하나입니다. 둘이 다르면 브라우저가 격자를 늘이거나 줄여 보여 줍니다.
고해상도 화면은 CSS 픽셀 하나를 화면의 점 여러 개로 그립니다. 그 배율이 devicePixelRatio 입니다. 격자 칸 수를 CSS 픽셀 수에 맞춰 두면 격자 칸 하나가 화면의 점 여러 개로 늘어나 그림이 흐려집니다.
처방은 격자 칸 수를 배율만큼 늘리는 것입니다. 화면 속 크기는 CSS 로 원래대로 둡니다. 좌표 눈금은 scale 로 맞춥니다.
const r = window.devicePixelRatio; // 예: 2
c.width = 300 * r; // 600
c.style.width = "300px";
ctx.scale(r, r);
격자는 가로 600칸이 됩니다. 화면에는 300 CSS 픽셀 폭으로 보입니다. scale 덕에 그리는 코드는 여전히 300칸짜리 판이라고 여기고 좌표를 적으면 됩니다.
SVG · WebGL 과 가르는 기준
웹에서 그림을 그리는 길은 Canvas API 말고도 둘이 더 있습니다. 도형을 요소로 남기는 SVG 와 GPU 로 그리는 WebGL 입니다. 셋을 가르는 기준은 무엇을 기억하나와 도형이 얼마나 많나입니다.
| Canvas API | SVG | WebGL | |
|---|---|---|---|
| 기억하는 것 | 픽셀 | 도형 하나하나 | 픽셀 |
| 도형을 옮기려면 | 지우고 다시 그린다 | 요소의 속성을 바꾼다 | 지우고 다시 그린다 |
| 잘 맞는 그림 | 점이 많은 차트 · 2D 게임 · 사진 편집 | 아이콘 · 도형이 적은 도표 · 확대해도 선명해야 하는 그림 | 3D 장면 · Canvas API 로도 버거울 만큼 많은 점 |
| 약한 대목 | 클릭 판정과 글자 선택을 코드가 맡는다 | 요소가 많아지면 메모리와 다시 그리는 일이 는다 | 부르는 법이 길고 어렵다 |
도형이 적고 하나하나 눌러야 하면 SVG 가 편합니다. 도형이 많고 자주 바뀌면 Canvas API 가 가볍습니다. 3D 이거나 Canvas API 로도 느리면 WebGL 로 갑니다.
그림판 속 글자와 도형은 DOM 에 없습니다. fillText 로 쓴 글자도 픽셀일 뿐이라 선택하거나 페이지 검색으로 찾을 수 없습니다. 화면 글자를 소리로 읽어 주는 프로그램인 스크린 리더도 그림 내용을 읽지 못합니다. 그래서 버튼과 본문 글은 보통 HTML 로 둡니다. 그림판에는 그림만 그립니다.
관련 항목
Canvas API 를 이루는 구성 요소
canvas 요소 · 렌더링 컨텍스트 · 그리기 상태 · 경로 (그래픽스) · 변환 행렬 · ImageData · Blob · OffscreenCanvas
Canvas API 와 같은 역할을 두고 겨루는 그리기 방식
SVG · WebGL · WebGPU · DOM · CSS
Canvas API 가 따르는 그리기 모델
즉시 모드 · 유지 모드 · 비트맵 · 벡터 그래픽스 · 래스터화 · 픽셀 · RGBA
Canvas API 로 움직이는 그림을 돌리는 도구
requestAnimationFrame · 애니메이션 루프 · 프레임 · 주사율 · 히트 테스트 · 웹 워커
Canvas API 의 픽셀 읽기를 막는 보안 규칙
동일 출처 정책 · 출처 · CORS · 오염된 캔버스 · SecurityError
Canvas API 가 속하는 상위 분류
다른 이름: 캔버스 API · Canvas 2D API · 2D 캔버스 · CanvasRenderingContext2D