안녕하세요! 캔버스 튜토리얼을 통해 점점 더 놀라운 기술들을 익혀가고 계시네요. 이번에 가져오신 문서는 바로 '캔버스를 사용해 비디오 조작하기 (Manipulating video using canvas)'입니다.
줌(Zoom)이나 구글 미트(Google Meet)에서 배경을 지우고 다른 화면으로 합성해 주는 '가상 배경' 기능, 다들 한 번쯤 써보셨죠? 바로 그 마법 같은 '크로마키(Chroma-key)' 또는 '그린 스크린' 효과를 브라우저 단에서 순수 자바스크립트와 캔버스로 어떻게 구현하는지 그 원리를 파헤쳐 보는 아주 흥미로운 챕터입니다.
실무에서 비디오 스트리밍을 다룰 때 캔버스가 어떻게 강력한 실시간 필터 도구로 변신하는지, 강사의 팁을 곁들여 쉽고 재미있게 번역해 드릴게요!
<video> 요소의 기능과 <canvas>를 결합하면, 화면에 표시되는 비디오에 다양한 시각적 효과를 결합하기 위해 비디오 데이터를 실시간으로 조작할 수 있습니다. 이 튜토리얼에서는 자바스크립트(JavaScript) 코드를 사용하여 크로마키(chroma-keying, 일명 "그린 스크린 효과")를 수행하는 방법을 시연합니다.
이 콘텐츠를 렌더링하는 데 사용된 HTML 문서는 아래와 같습니다.
<!doctype html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<title>Video test page</title>
<style>
body {
background: black;
color: #cccccc;
}
#c2 {
background-image: url("media/foo.png");
background-repeat: no-repeat;
}
div {
float: left;
border: 1px solid #444444;
padding: 10px;
margin: 10px;
background: #3b3b3b;
}
</style>
</head>
<body>
<div>
<video
id="video"
src="media/video.mp4"
controls
crossorigin="anonymous"></video>
</div>
<div>
<canvas id="c1" width="160" height="96"></canvas>
<canvas id="c2" width="160" height="96"></canvas>
</div>
<script src="processor.js"></script>
</body>
</html>
여기서 꼭 기억하고 넘어가야 할 핵심 포인트는 다음과 같습니다:
c1과 c2라는 ID를 가진 두 개의 canvas 요소를 생성합니다. c1은 원본 비디오의 현재 프레임을 그대로 가져와서 그리는 데 사용됩니다.c2는 크로마키 효과(배경 지우기) 연산을 수행한 '결과물 비디오'를 화면에 보여주는 역할을 합니다. c2에는 CSS background-image를 통해 비디오의 녹색 배경을 대체할 정적인 정지 이미지(foo.png)가 미리 로드되어 깔려 있습니다.processor.js라는 이름의 스크립트 파일에서 불러옵니다.💡 강사의 핵심 팁:
왜 캔버스를 굳이 두 개(c1,c2)나 쓸까요? 이전 시간에 배웠던 픽셀 조작(getImageData)을 떠올려보세요!
1. 원본 비디오를c1캔버스에 그립니다. (이때c1은 사용자 눈에 보이지 않게display: none처리하는 경우가 많습니다. 일명 오프스크린 버퍼 역할이죠.)
2.c1에 그려진 프레임의 픽셀 데이터를 뽑아옵니다.
3. 픽셀을 순회하며 '녹색'인 픽셀만 투명하게 지워버립니다.
4. 그렇게 구멍이 뻥 뚫린 픽셀 데이터를 진짜로 화면에 보여줄c2캔버스에 덮어씌웁니다. 그러면c2아래에 깔아둔 배경 이미지가 투명해진 구멍 사이로 비쳐 보이게 되는 것이죠!
processor.js 안에 있는 자바스크립트 코드는 크게 세 가지 메서드(함수)로 구성되어 있습니다.
doLoad() 메서드는 HTML 문서가 최초로 로드될 때 호출됩니다. 이 메서드의 임무는 크로마키 처리 코드에서 필요로 하는 변수들을 준비하고, 사용자가 비디오 재생 버튼을 누르는 순간을 감지할 수 있도록 이벤트 리스너(event listener)를 셋업하는 것입니다.
const processor = {};
processor.doLoad = function doLoad() {
const video = document.getElementById("video");
this.video = video;
this.c1 = document.getElementById("c1");
this.ctx1 = this.c1.getContext("2d");
this.c2 = document.getElementById("c2");
this.ctx2 = this.c2.getContext("2d");
video.addEventListener("play", () => {
this.width = video.videoWidth / 2;
this.height = video.videoHeight / 2;
this.timerCallback();
});
};
이 코드는 HTML 문서 내에서 우리가 특별히 다루어야 할 요소들, 즉 video 요소와 두 개의 canvas 요소에 대한 참조(reference)를 가져옵니다. 또한, 두 캔버스를 그리기 위한 그래픽 컨텍스트(2d 컨텍스트)에 대한 참조도 가져옵니다. 이것들은 우리가 실제로 크로마키 효과를 수행할 때 요긴하게 쓰일 겁니다.
그다음, addEventListener()를 호출하여 video 요소를 지켜보고 있다가, 사용자가 비디오에서 재생(play) 버튼을 눌렀을 때 알림을 받도록 합니다. 사용자가 재생을 시작하면 반응하여, 이 코드는 비디오의 원본 너비와 높이를 가져온 뒤 각각 절반으로 줄입니다 (이 튜토리얼에서는 크로마키 효과를 수행할 때 비디오 크기를 절반 크기로 줄여서 처리할 예정입니다). 그런 다음 timerCallback() 메서드를 호출하여 본격적으로 비디오를 관찰하고 시각 효과 계산을 시작합니다.
타이머 콜백은 비디오가 재생되기 시작할 때("play" 이벤트가 발생할 때) 최초로 한 번 호출되며, 이후에는 비디오의 매 프레임마다 크로마키 효과를 실행하기 위해 주기적으로 자기 자신을 계속해서 호출(스케줄링)하는 책임을 집니다.
processor.timerCallback = function timerCallback() {
// 비디오가 일시정지 상태이거나 끝났다면 아무것도 하지 않고 함수를 종료합니다.
if (this.video.paused || this.video.ended) {
return;
}
// 프레임을 계산(크로마키 처리)합니다.
this.computeFrame();
// 콜백 함수가 최대한 빨리 다시 실행되도록 예약합니다.
setTimeout(() => {
this.timerCallback();
}, 0);
};
이 콜백이 가장 먼저 하는 일은 비디오가 실제로 재생 중인지 확인하는 것입니다. 만약 재생 중이 아니라면(일시 정지되었거나 끝났다면), 콜백은 아무런 작업도 수행하지 않고 즉시 종료(return)됩니다.
비디오가 재생 중이라면, 현재 비디오 프레임에 크로마키 효과를 수행하는 computeFrame() 메서드를 호출합니다.
콜백이 마지막으로 하는 일은 setTimeout()을 호출하여 가능한 한 빨리 자기 자신(timerCallback)을 다시 호출하도록 스케줄링하는 것입니다. 실제 현업(real world)에서는 비디오의 초당 프레임 수(frame rate, fps)를 미리 파악하고 그에 맞춰 이 스케줄을 조정하게 될 것입니다.
💡 강사의 실무 팁:
여기서 MDN 예제는setTimeout(..., 0)을 사용했지만, 이전 '애니메이션' 챕터에서 우리가 배웠던 것을 기억하시나요? 비디오 프레임을 캔버스에 렌더링할 때는setTimeout보다requestAnimationFrame()을 사용하는 것이 퍼포먼스 면에서 훨씬 더 권장됩니다! 브라우저가 화면을 다시 그릴 준비가 되었을 때만 프레임을 계산하므로 컴퓨터 자원을 효율적으로 사용할 수 있거든요.
아래에 표시된 computeFrame() 메서드는 실제로 픽셀 데이터를 가져와서 크로마키 효과를 팍팍 적용하는 핵심 로직을 담당합니다.
processor.computeFrame = function () {
// 1. 숨겨진 캔버스(c1)에 현재 비디오의 프레임을 그려 넣습니다.
this.ctx1.drawImage(this.video, 0, 0, this.width, this.height);
// 2. c1 캔버스에서 그 프레임의 전체 픽셀 데이터를 뽑아옵니다.
const frame = this.ctx1.getImageData(0, 0, this.width, this.height);
const data = frame.data; // [R, G, B, A, R, G, B, A, ...] 1차원 배열
// 3. 모든 픽셀을 하나하나 순회하면서 검사합니다. (4칸씩 건너뜀)
for (let i = 0; i < data.length; i += 4) {
const red = data[i + 0];
const green = data[i + 1];
const blue = data[i + 2];
// 만약 해당 픽셀이 특정 '녹색/노란색' 배경 범위에 속한다면?
if (green > 100 && red > 100 && blue < 43) {
data[i + 3] = 0; // 그 픽셀의 투명도(Alpha)를 0으로 만들어서 완전히 투명하게 지워버립니다!
}
}
// 4. 구멍이 송송 뚫린(투명해진) 픽셀 데이터를 화면에 보여줄 c2 캔버스에 덮어씌웁니다.
this.ctx2.putImageData(frame, 0, 0);
};
이 루틴이 처음 호출될 때, video 요소는 가장 최근의 비디오 프레임을 보여주고 있으며, 그 원본은 대략 다음과 같이 생겼습니다:

이 비디오 프레임은 첫 번째 캔버스의 그래픽 컨텍스트 ctx1에 복사되는데, 이때 프레임을 절반 크기로 그리기 위해 이전에 저장해 두었던 너비(width)와 높이(height) 값을 사용합니다. 컨텍스트의 drawImage() 메서드 첫 번째 인자로 video 요소 자체를 통째로 넘기기만 하면, 브라우저가 알아서 현재 재생 중인 비디오의 해당 프레임을 딱 캡처해서 컨텍스트에 그려준다는 점을 눈여겨보세요. 그려진 결과는 이렇습니다:

이제 첫 번째 컨텍스트에서 getImageData() 메서드를 호출하여 현재 그려진 비디오 프레임에 대한 가공되지 않은 픽셀 그래픽 데이터의 복사본을 가져옵니다. 이를 통해 우리가 요리조리 조작할 수 있는 원시 32비트 픽셀 이미지 데이터(RGBA)를 얻게 됩니다. 그 후, 프레임 이미지 데이터의 전체 크기를 4로 나누어 이미지 안에 총 몇 개의 픽셀이 있는지(길이)를 계산하게 되죠.
그다음 for 루프가 돌면서 프레임의 모든 픽셀을 쭉 스캔합니다. 각 픽셀에서 빨강(red), 초록(green), 파랑(blue) 값을 추출한 다음, 우리가 걷어내고 싶은 '그린 스크린' 배경색에 해당하는지 판별하기 위해 미리 정해둔 기준 숫자들과 비교합니다. (이 예제에서는 녹색 배경을 지우고 foo.png에서 가져온 정적인 배경 이미지로 대체할 것입니다.)
💡 강사의 보충 설명:
if (green > 100 && red > 100 && blue < 43)이 조건문이 바로 마법의 핵심입니다!
R(빨강)과 G(초록)가 모두 100보다 크면서 B(파랑)가 43보다 작은 색상. 즉, 노란빛이 많이 도는 밝은 녹색을 타겟팅하는 아주 원시적인(간단한) 색상 임계값(Color Thresholding) 알고리즘입니다. 현업에서는 조명이나 그림자 때문에 배경색이 균일하지 않아서 훨씬 더 정교한 수학 공식을 사용하지만, 기본 원리는 완전히 똑같습니다!
그린 스크린 범위에 속한다고 판단된 프레임 이미지 데이터의 모든 픽셀은 투명도(alpha) 값을 0으로 변경해 버립니다. 이는 해당 픽셀이 완전히 투명해졌음을 의미합니다. 결과적으로, 가공이 끝난 최종 이미지는 전체 그린 스크린 영역이 100% 투명하게 구멍이 뚫린 상태가 됩니다.
따라서 ctx2.putImageData를 사용하여 이 데이터를 목적지(두 번째) 캔버스에 그리면, 투명해진 구멍 너머로 c2 캔버스의 CSS 배경(static backdrop)이 자연스럽게 투영되어 오버레이 되는 결과를 얻을 수 있습니다.
완성된 최종 결과 이미지는 다음과 같습니다:

비디오가 재생되는 동안 매 프레임마다 이 과정이 미친 듯이 빠른 속도로 계속 반복되며, 프레임이 하나하나 처리될 때마다 크로마키 효과가 입혀진 부드러운 영상이 화면에 나타나게 됩니다.
이 페이지가 도움이 되었나요? (Was this page helpful to you?)
[ 예 (Yes) ][ 아니오 (No) ]
링크
수고하셨습니다! 이전 시간에 배웠던 getImageData와 putImageData의 개념이 살아 숨 쉬는 동영상에 어떻게 응용되는지 완벽하게 파악하셨을 거예요.
배열의 특정 데이터(녹색 픽셀)만 조건문으로 찾아서 값을 변경하고 다시 그리는 것. 알고리즘이나 자료구조를 공부하시며 다루게 될 데이터 파싱 로직과도 맥락이 같답니다. 이 원리를 잘 기억해 두시면 나중에 캔버스를 활용해 웹 브라우저용 실시간 카메라 필터 앱을 개발하실 수도 있어요! 궁금한 점이 있으시면 언제든 질문해 주세요!