프론트엔드 개발을 하다 보면 디자인 시안에 0.5px border가 있는 경우를 자주 만난다. 그런데 border-width: 0.5px를 그대로 적용해도 개발자 도구를 열어보면 대부분 1px로 찍혀 있다. “브라우저가 0.5px을 아예 못 그리는 건가?” 싶지만, 어떤 환경에서는 또 정확히 0.5px이 보이기도 한다.
이 차이의 핵심은 DPR(Device Pixel Ratio)이다.
DPR(Device Pixel Ratio)이란?
DPR은 논리적 픽셀 1개를 표현하기 위해 실제로 사용되는 물리적 픽셀의 개수를 나타내며, 다음과 같이 계산된다.
DPR은 논리적 픽셀 1개를 화면에 표현하기 위해 실제로 사용되는 물리적 픽셀의 배율이다.
DPR = 물리적 픽셀 수 ÷ 논리적 픽셀 수물리적 픽셀 (Physical Pixel)
물리적 픽셀은 디스플레이 화면을 구성하는 실제 하드웨어 픽셀이다. 화면에 빛을 내는 가장 작은 단위라고 생각하면 된다.
예를 들어 아이폰 16 Pro의 화면 해상도가 1206 x 2622 라고 하면, 이건 가로로 1206개, 세로로 2622개의 물리적 픽셀이 실제로 존재한다는 의미다.
논리적 픽셀 (Logical Pixel, CSS Pixel)
논리적 픽셀은 개발자가 CSS에서 사용하는 추상화된 단위다. 우리가 코드에서 width: 100px이라고 작성할 때의 그 px가 바로 논리적 픽셀이다.
논리적 픽셀은 디바이스의 물리적 해상도와 무관하게 일관된 크기를 제공하기 위해 만들어진 개념이다. 덕분에 개발자는 디바이스마다 다른 물리적 해상도를 신경 쓰지 않고, 하나의 CSS 코드로 다양한 디바이스에 대응할 수 있다.
앞서 예로 든 아이폰 16 Pro의 경우, 논리적 픽셀은 402 × 874 이다. 물리적 픽셀(1206 x 2622)을 논리적 픽셀(402 × 874)로 나누면 정확히 3이 나온다. 즉, 이 디바이스의 DPR은 3이다.
DPR에 따른 픽셀 변환
DPR을 이해했으니 이제 각 DPR에서 논리적 픽셀이 어떻게 물리적 픽셀로 변환되는지 보자.
- DPR 1 (일반 모니터): CSS 1px = 물리적 1px
- CSS 1px을 1개의 물리적 픽셀로 렌더링
- DPR 2 (맥북 레티나 디스플레이): CSS 1px = 물리적 2x2px (총 4개)
- CSS 1px을 4개의 물리적 픽셀로 렌더링
- DPR 3 (아이폰 Pro): CSS 1px = 물리적 3x3px (총 9개)
- CSS 1px을 9개의 물리적 픽셀로 렌더링
![]()
그래서 0.5px은 언제 그려질까?
DPR 1 (일반 모니터)
일반 모니터에서는 논리적 1px을 표현하기 위해 물리적으로도 1개의 픽셀만 사용한다. 그런데 0.5px을 그리려면? 물리적으로 0.5px을 표현할 방법이 없다. 픽셀은 화면의 최소 단위니까 "반쪽 픽셀"은 존재할 수 없는 것이다.
브라우저가 이를 감지해서 반올림하거나 안티앨리어싱(흐릿하게) 처리한다. 그래서 의도한 얇은 선이 아니라 1px만큼 두껍거나 흐릿한 선으로 보이는 것이다.
논리적 0.5px × DPR 1 = 물리적 0.5px (불가능) → 1px로 반올림DPR 2 (맥북 레티나)
DPR이 2인 디바이스에서는 이야기가 달라진다. 논리적 1px을 표현하기 위해 물리적 픽셀 4개를 사용할 수 있다.
논리적 0.5px × DPR 2 = 물리적 1px (가능!)논리적 0.5px에 DPR 2를 곱하면 물리적으로 정확히 1px이 된다. 온전한 1개의 물리적 픽셀로 표현할 수 있게 되는 것이다. 그래서 DPR 2 이상의 디바이스에서는 0.5px이 제대로 렌더링된다.
브라우저 줌과 DPR의 관계
재미있는 점은 브라우저 확대/축소를 해도window.devicePixelRatio 값이 변한다는 것이다. (브라우저마다 동작 다를 수 있음)
// DPR 1 Device 기준
// 100% 줌
console.log(window.devicePixelRatio); // 1
// 200% 줌
console.log(window.devicePixelRatio); // 2
// 50% 줌
console.log(window.devicePixelRatio); // 0.5브라우저(크로미움)의 줌이 DPR을 변화 시키는 동작에 대해서는 예전에 논의가 이뤄졌던 것 같다.
Update window.devicePixelRatio on browser zoom (content upscaling)
브라우저 줌의 동작 원리
브라우저 줌은 CSS 픽셀 자체의 크기를 조정한다. 200% 줌을 하면 CSS 1px이 물리적으로 차지하는 공간이 2배가 되는 것이다.
중요한 점은, 요소의 CSS 픽셀 값 자체는 변하지 않는다는 거다. width: 128px로 정의된 요소는 200% 줌을 해도 여전히 128 CSS 픽셀이다. 다만 각 CSS 1픽셀이 차지하는 물리적 공간이 커지는 것이다.
- 100% 줌: CSS 1px = 물리적 1px (기준)
- 200% 줌: CSS 1px = 물리적 2x2px (CSS 픽셀이 4배 커짐)
- 50% 줌: CSS 1px = 물리적 0.5x0.5px (CSS 픽셀이 1/4로 작아짐)
0.5px는 줌에 어떻게 영향받을까?
100% 줌 (DPR 1): 0.5px × 1 = 0.5px (물리적) → 1px로 반올림 ✗
200% 줌 (DPR 2): 0.5px × 2 = 1px (물리적) → 렌더링 가능 ✓즉, 200% 확대를 하면 DPR이 2배가 되어 0.5px 표현이 가능해진다.
반대로 레티나 디스플레이(DPR 2)에서 50% 축소를 하게 되면 DPR이 1로 떨어지게 된다. 이 경우 논리적 0.5px을 표현하기 위한 물리적 픽셀이 0.5개가 되어버려, 다시 일반 모니터처럼 선명한 0.5px 렌더링이 불가능해진다.
DPR과 이미지 렌더링
DPR은 border 같은 UI 요소뿐만 아니라 이미지를 렌더링할 때도 중요한 의미를 가진다.
고해상도 디스플레이와 이미지 품질
DPR 2인 레티나 디스플레이에서 300x300px 크기로 이미지를 표시한다고 생각해보자. 브라우저는 이 CSS 300px을 표현하기 위해 실제로는 600x600px의 물리적 픽셀 공간을 사용한다.
근데 만약 300x300px 크기의 이미지 파일을 사용하면 어떻게 될까? 브라우저는 이 이미지를 600x600 물리 픽셀 공간에 맞추기 위해 확대해야 한다. 그 결과 이미지가 흐릿하게 보이게 된다.
CSS 300px × DPR 2 = 물리적 600px
하지만 이미지 파일은 300px → 확대 필요 → 흐릿함이 문제를 해결하려면 DPR에 맞는 크기의 이미지를 제공해야 한다.
- DPR 1: 300x300px 이미지면 충분
- DPR 2: 600x600px 이미지가 필요 (2배)
- DPR 3: 900x900px 이미지가 필요 (3배)
srcset과 브라우저의 자동 선택
문제는 사용자마다 다른 DPR의 디바이스를 사용한다는 거다. 모든 사용자에게 가장 큰 이미지를 제공하면 저해상도 디스플레이 사용자는 불필요하게 큰 파일을 다운로드하게 되고, 작은 이미지만 제공하면 고해상도 디스플레이 사용자는 흐릿한 이미지를 보게 된다.
HTML의 srcset 속성이 이 문제를 해결한다.
<img
srcset="
photo-320w.jpg,
photo-480w.jpg 1.5x,
photo-640w.jpg 2x
"
src="photo-640w.jpg"
alt="설명"
style="width: 320px"
/>위 코드에서
- 이미지는 CSS로 320px 크기로 표시된다
srcset에 여러 해상도의 이미지를 정의한다
브라우저는 디바이스의 DPR을 확인하고 자동으로 적절한 이미지를 선택한다
- DPR 1 디바이스:
photo-320w.jpg로드 (1x는 생략 가능) - DPR 1.5 디바이스:
photo-480w.jpg로드 - DPR 2 이상 디바이스:
photo-640w.jpg로드
DPR이 2 이상인 고해상도 디스플레이에서는 더 큰 이미지 파일이 로드되어 선명한 화질을 제공한다.
Next.js Image 컴포넌트의 동작
Next.js의 <Image> 컴포넌트는 이 원리를 자동으로 처리해준다.
<Image
src="/photo.jpg"
width={300}
height={300}
alt="설명"
/>Next.js는 내부적으로 여러 크기의 이미지를 생성하고 srcset을 자동으로 만들어준다. 개발자가 직접 여러 버전의 이미지를 준비하거나 srcset을 작성할 필요를 덜어준다.
마무리
처음에는 단순히 "왜 0.5px이 안 그려지지?" 라는 의문에서 알아보았지만, DPR이라는 개념을 이해하고 나니 0.5px이 그려지지 않는 이유 뿐만 아니라
같은 CSS 코드가 디바이스마다 다르게 렌더링되는 이유, 고해상도 디스플레이에서 오히려 이미지가 흐릿하게 보이는 이유, srcset의 존재의의 등 많은 것들을 이해할 수 있게 됐다.
참조