캔버스에는 실제로 몇 개의 픽셀이 있나요?
Chrome 84부터 ResizeObserver는 실제 픽셀로 요소의 크기를 측정하는 devicePixelContentBox
라는 새로운 상자 측정을 지원합니다. 이를 통해 특히 고밀도 화면의 컨텍스트에서 픽셀 단위로 완벽한 그래픽을 렌더링할 수 있습니다.
배경: CSS 픽셀, 캔버스 픽셀, 실제 픽셀
em
, %
, vh
와 같은 추상적인 길이 단위를 사용하는 경우가 많지만 결국 픽셀로 귀결됩니다. CSS에서 요소의 크기나 위치를 지정할 때마다 브라우저의 레이아웃 엔진은 결국 해당 값을 픽셀 (px
)로 변환합니다. 이는 많은 역사를 지니고 있으며 화면에 있는 픽셀과 느슨한 관계만 있는 'CSS 픽셀'입니다.
오랫동안 96DPI ('인치당 도트 수')로 모든 사용자의 화면 픽셀 밀도를 추정하는 것이 합리적이었습니다. 즉, 지정된 모니터에는 cm당 약 38픽셀이 있었습니다. 시간이 지나면서 모니터가 커지거나 작아지거나 동일한 표면적에 더 많은 픽셀이 표시되기 시작했습니다. 웹의 많은 콘텐츠가 글꼴 크기를 비롯한 크기를 px
로 정의한다는 사실과 결합하면 이러한 고밀도 ('HiDPI') 화면에서 텍스트를 읽을 수 없게 됩니다. 대책으로 브라우저는 모니터의 실제 픽셀 밀도를 숨기고 대신 사용자가 96DPI 디스플레이를 사용한다고 가정합니다. CSS의 px
단위는 이 가상 96 DPI 디스플레이의 1픽셀 크기를 나타내므로 'CSS 픽셀'이라는 이름이 붙었습니다. 이 단위는 측정 및 포지셔닝에만 사용됩니다. 실제 렌더링이 발생하기 전에 물리적 픽셀로의 변환이 발생합니다.
이 가상 디스플레이에서 사용자의 실제 디스플레이로 어떻게 이동할까요? devicePixelRatio
를 입력합니다. 이 전역 값은 단일 CSS 픽셀을 형성하는 데 필요한 실제 픽셀 수를 나타냅니다. devicePixelRatio
(dPR)가 1
이면 약 96DPI의 모니터에서 작업하는 것입니다. Retina 화면을 사용하는 경우 dPR은 2
일 가능성이 높습니다. 휴대전화에서는 2
, 3
, 2.65
와 같이 더 높고 이상한 dPR 값을 흔히 볼 수 있습니다. 이 값은 정확하지만 모니터의 실제 DPI 값을 도출할 수는 없습니다. dPR이 2
이면 CSS 픽셀 1개가 실제 픽셀 2개에 정확히 매핑됩니다.
1
입니다.너비는 3440픽셀이며 디스플레이 영역은 79cm입니다.
따라서 해상도는 110DPI가 됩니다. 96에 가깝지만 정확하지는 않습니다.
이러한 이유로 대부분의 디스플레이에서 <div style="width: 1cm; height: 1cm">
의 크기가 정확히 1cm로 측정되지 않습니다.
마지막으로 dPR은 브라우저의 확대/축소 기능의 영향을 받을 수도 있습니다. 확대하면 브라우저에서 보고된 dPR이 증가하여 모든 항목이 더 크게 렌더링됩니다. 확대하는 동안 DevTools 콘솔에서 devicePixelRatio
를 확인하면 소수점 값이 표시됩니다.

devicePixelRatio
이 표시된 DevTools<canvas>
요소를 추가해 보겠습니다. width
및 height
속성을 사용하여 캔버스의 픽셀 수를 지정할 수 있습니다. 따라서 <canvas width=40 height=30>
은 40x30픽셀 캔버스입니다. 하지만 40x30픽셀로 표시된다는 의미는 아닙니다. 기본적으로 캔버스는 width
및 height
속성을 사용하여 내재적 크기를 정의하지만, 알고 있는 모든 CSS 속성을 사용하여 캔버스의 크기를 임의로 조정할 수 있습니다. 지금까지 배운 내용을 바탕으로 모든 시나리오에서 이 방법이 이상적이지는 않다는 것을 알 수 있습니다. 캔버스의 한 픽셀이 여러 실제 픽셀을 포함하거나 실제 픽셀의 일부만 포함할 수 있습니다. 이로 인해 불쾌한 시각적 아티팩트가 발생할 수 있습니다.
요약하자면, 캔버스 요소에는 그릴 수 있는 영역을 정의하는 지정된 크기가 있습니다. 캔버스 픽셀 수는 CSS 픽셀로 지정된 캔버스의 디스플레이 크기와 완전히 독립적입니다. CSS 픽셀 수는 실제 픽셀 수와 다릅니다.
Pixel의 완벽함
캔버스 픽셀을 실제 픽셀에 정확하게 매핑하는 것이 바람직한 시나리오도 있습니다. 이 매핑이 달성되면 '픽셀 완벽'이라고 합니다. 픽셀 단위로 완벽한 렌더링은 특히 하위 픽셀 렌더링을 사용하거나 밝기가 번갈아 가며 바뀌는 선이 긴밀하게 정렬된 그래픽을 표시할 때 텍스트를 읽기 쉽게 렌더링하는 데 중요합니다.
웹에서 픽셀 단위로 완벽한 캔버스에 최대한 가까운 결과를 얻기 위해 다음과 같은 접근 방식을 사용해 왔습니다.
<style>
/* … styles that affect the canvas' size … */
</style>
<canvas id="myCanvas"></canvas>
<script>
const cvs = document.querySelector('#myCanvas');
// Get the canvas' size in CSS pixels
const rectangle = cvs.getBoundingClientRect();
// Convert it to real pixels. Ish.
cvs.width = rectangle.width * devicePixelRatio;
cvs.height = rectangle.height * devicePixelRatio;
// Start drawing…
</script>
예리한 독자라면 dPR이 정수 값이 아닌 경우 어떻게 되는지 궁금할 것입니다. 좋은 질문입니다. 바로 이 부분이 전체 문제의 핵심입니다. 또한 백분율, vh
또는 기타 간접 값을 사용하여 요소의 위치나 크기를 지정하면 분수 CSS 픽셀 값으로 확인될 수 있습니다. margin-left: 33%
가 있는 요소는 다음과 같은 직사각형으로 끝날 수 있습니다.

getBoundingClientRect()
호출의 결과로 소수점 이하 픽셀 값을 표시하는 DevToolsCSS 픽셀은 순전히 가상이기 때문에 이론적으로는 픽셀의 일부를 사용하는 것이 괜찮지만 브라우저가 실제 픽셀에 대한 매핑을 어떻게 파악할까요? 분수 물리적 픽셀은 존재하지 않기 때문입니다.
Pixel 스냅
요소를 실제 픽셀에 정렬하는 단위 변환 프로세스의 부분을 '픽셀 스냅'이라고 하며, 이름 그대로 분수 픽셀 값을 정수 실제 픽셀 값으로 스냅합니다. 이러한 현상이 발생하는 방식은 브라우저마다 다릅니다. dPR이 1인 디스플레이에 너비가 791.984px
인 요소가 있는 경우 한 브라우저는 요소를 792px
실제 픽셀로 렌더링하고 다른 브라우저는 791px
로 렌더링할 수 있습니다. 단 하나의 픽셀만 벗어났지만, 픽셀 단위로 완벽해야 하는 렌더링에는 단 하나의 픽셀도 치명적일 수 있습니다. 이로 인해 흐려지거나 모아레 효과와 같은 아티팩트가 더 많이 표시될 수 있습니다.

(이 이미지를 새 탭에서 열어야 크기 조절 없이 볼 수 있습니다.)
devicePixelContentBox
devicePixelContentBox
는 기기 픽셀 (즉, 실제 픽셀) 단위로 요소의 콘텐츠 상자를 제공합니다. ResizeObserver
의 일부입니다. Safari 13.1 이후로 ResizeObserver가 모든 주요 브라우저에서 지원되지만 devicePixelContentBox
속성은 현재 Chrome 84 이상에서만 지원됩니다.
ResizeObserver
: 요소의 document.onresize
와 유사함에 설명된 대로 ResizeObserver
의 콜백 함수는 페인트 전, 레이아웃 후에 호출됩니다. 즉, 콜백에 대한 entries
매개변수에는 관찰된 모든 요소의 크기가 페인팅되기 직전에 포함됩니다. 위에서 설명한 캔버스 문제의 맥락에서 이 기회를 활용하여 캔버스의 픽셀 수를 조정하여 캔버스 픽셀과 실제 픽셀 간의 정확한 일대일 매핑을 확보할 수 있습니다.
const observer = new ResizeObserver((entries) => {
const entry = entries.find((entry) => entry.target === canvas);
canvas.width = entry.devicePixelContentBoxSize[0].inlineSize;
canvas.height = entry.devicePixelContentBoxSize[0].blockSize;
/* … render to canvas … */
});
observer.observe(canvas, {box: ['device-pixel-content-box']});
observer.observe()
의 옵션 객체에 있는 box
속성을 사용하면 관찰할 크기를 정의할 수 있습니다. 따라서 각 ResizeObserverEntry
는 항상 borderBoxSize
, contentBoxSize
, devicePixelContentBoxSize
를 제공하지만 (브라우저에서 지원하는 경우) 콜백은 관찰된 상자 측정항목이 변경된 경우에만 호출됩니다.
이 새로운 속성을 사용하면 캔버스의 크기와 위치를 애니메이션으로 처리할 수 있으며 (사실상 소수점 픽셀 값을 보장함) 렌더링에 모아레 효과가 표시되지 않습니다. getBoundingClientRect()
를 사용하는 접근 방식에서 모아레 효과가 나타나는 방식과 새로운 ResizeObserver
속성을 사용해 이를 방지하는 방법을 확인하려면 Chrome 84 이상의 데모를 살펴보세요.
기능 감지
사용자의 브라우저가 devicePixelContentBox
를 지원하는지 확인하려면 요소를 관찰하고 속성이 ResizeObserverEntry
에 있는지 확인합니다.
function hasDevicePixelContentBox() {
return new Promise((resolve) => {
const ro = new ResizeObserver((entries) => {
resolve(entries.every((entry) => 'devicePixelContentBoxSize' in entry));
ro.disconnect();
});
ro.observe(document.body, {box: ['device-pixel-content-box']});
}).catch(() => false);
}
if (!(await hasDevicePixelContentBox())) {
// The browser does NOT support devicePixelContentBox
}
결론
픽셀은 웹에서 놀라울 정도로 복잡한 주제이며 지금까지는 요소가 사용자의 화면에서 차지하는 실제 픽셀 수를 정확하게 알 수 있는 방법이 없었습니다. ResizeObserverEntry
의 새로운 devicePixelContentBox
속성은 이 정보를 제공하며 <canvas>
를 사용하여 픽셀 단위로 완벽한 렌더링을 할 수 있습니다. devicePixelContentBox
은 Chrome 84 이상에서 지원됩니다.