synology / self-hosting / security / performance

웹 운영

headless Chrome에 --window-size=390을 줘도 화면 폭이 500px인 이유

headless Chrome은 390px 창을 500px로 올려 렌더링합니다. 직접 잰 값과 CDP setDeviceMetricsOverride로 모바일 폭을 고정하는 의존성 없는 Node 스크립트를 정리했습니다.

모바일 화면을 점검하려고 headless Chrome을 --window-size=390,844로 띄워 스크린샷을 찍었는데, 오른쪽이 잘린 이미지가 나왔습니다. 처음엔 CSS에서 뭔가 가로로 넘치는 줄 알았습니다.

근데 CSS를 열기 전에 페이지 안에서 innerWidth를 찍어 보니 390이 아니라 500이었습니다. 페이지는 500px 폭으로 레이아웃을 계산했고, 스크린샷만 390px로 잘린 거였습니다. 고칠 CSS는 없었습니다.

창 크기를 바꿔 가며 다시 재 봤습니다

이 글을 정리하면서 같은 현상이 지금도 나는지 다시 확인했습니다. Windows의 Chrome 153에서 아래 HTML 하나를 열고, 창 너비만 바꿔 가며 값을 받아 봤습니다.

<!doctype html>
<meta name="viewport" content="width=device-width,initial-scale=1">
<pre id="o"></pre>
<script>
  o.textContent = 'innerWidth=' + innerWidth +
    ' clientWidth=' + document.documentElement.clientWidth;
</script>
chrome --headless=new --disable-gpu --window-size=390,844 --dump-dom file:///.../vp.html
--window-size 값과 페이지가 받은 폭 (Chrome 153, headless=new)
지정한 창 너비innerWidthclientWidth
390500500
500500500
600584584

390을 줘도 500을 줘도 똑같이 500이 나옵니다. 500보다 작은 창은 500으로 올려서 쓰는 것처럼 보입니다. --headless=old로 바꿔도 390 → 500으로 같았습니다. (600일 때 584가 나오는 16px 차이는 이번엔 따로 파고들지 않았습니다.)

실제 사이트에서도 확인했습니다. 창을 390으로 준 상태에서 rapsso.com 홈을 열면 innerWidth 500, 문서의 scrollWidth 485가 나옵니다. 485px짜리 레이아웃을 390px 이미지로 찍으니 오른쪽 95px가 잘려 보였던 겁니다.

창 크기가 아니라 viewport를 직접 지정합니다

해결은 창 크기에 기대지 않고 Chrome DevTools Protocol(CDP)의 Emulation.setDeviceMetricsOverride로 페이지가 쓸 viewport를 직접 정하는 겁니다. 저는 width 390, height 844, mobile true를 넣었습니다. mobile true를 빼면 viewport 메타 태그 처리가 데스크톱 방식으로 남기 때문에 같이 넣는 게 맞습니다.

Puppeteer 같은 라이브러리를 쓰면 한 줄이지만, 점검용으로는 의존성 없이 돌리고 싶어서 Node 22에 기본으로 들어 있는 fetch와 WebSocket만으로 짰습니다. Chrome을 디버깅 포트로 띄우고, override 전 값 한 번, override 후 URL별 값을 찍습니다.

import { spawn } from 'node:child_process';
const CH='C:/Program Files/Google/Chrome/Application/chrome.exe';
const p=spawn(CH,['--headless=new','--disable-gpu','--remote-debugging-port=9333','--user-data-dir='+process.env.TMPD,'--window-size=390,844','about:blank']);
await new Promise(r=>setTimeout(r,2500));
const list=await (await fetch('http://127.0.0.1:9333/json')).json();
const ws=new WebSocket(list.find(t=>t.type==='page').webSocketDebuggerUrl);
await new Promise(r=>ws.onopen=r);
let id=0; const pend={};
ws.onmessage=e=>{const m=JSON.parse(e.data); if(m.id&&pend[m.id]){pend[m.id](m.result);delete pend[m.id];}};
const send=(method,params={})=>new Promise(r=>{const i=++id;pend[i]=r;ws.send(JSON.stringify({id:i,method,params}));});
const ev=async()=> (await send('Runtime.evaluate',{expression:'JSON.stringify({iw:innerWidth,sw:document.documentElement.scrollWidth,h1:document.querySelectorAll("h1").length})',returnByValue:true})).result.value;
const go=async u=>{await send('Page.navigate',{url:u}); await new Promise(r=>setTimeout(r,2500));};
await send('Page.enable');
const urls=process.argv.slice(2);
await go(urls[0]); console.log('NO_OVERRIDE',urls[0],await ev());
await send('Emulation.setDeviceMetricsOverride',{width:390,height:844,deviceScaleFactor:3,mobile:true});
for(const u of urls){await go(u); console.log('OVERRIDE',u,await ev());}
ws.close(); p.kill();
TMPD=./chrome-prof node cdp.mjs https://rapsso.com/ https://rapsso.com/notes/ ...

2026년 9월 27일에 돌린 결과입니다.

NO_OVERRIDE https://rapsso.com/ {"iw":500,"sw":485,"h1":1}
OVERRIDE https://rapsso.com/ {"iw":390,"sw":390,"h1":1}
OVERRIDE https://rapsso.com/notes/ {"iw":390,"sw":390,"h1":1}
OVERRIDE https://rapsso.com/notes/mobile-viewport-minimum-width-check/ {"iw":390,"sw":390,"h1":1}
OVERRIDE https://rapsso.com/about/ {"iw":390,"sw":390,"h1":1}

override를 건 뒤로는 innerWidth와 scrollWidth가 모두 390으로 맞습니다. scrollWidth가 innerWidth보다 크면 그 페이지는 진짜로 가로로 넘치는 거라, 이 두 값만 비교하면 스크린샷을 눈으로 보지 않아도 걸러집니다.

그래서 점검 순서를 이렇게 바꿨습니다

  1. 스크린샷을 보기 전에 페이지 안에서 innerWidth부터 찍습니다. 원하는 폭이 아니면 CSS를 볼 이유가 없습니다.
  2. 모바일 폭은 창 크기 옵션이 아니라 setDeviceMetricsOverride로 고정합니다.
  3. URL마다 scrollWidth와 innerWidth를 비교합니다. 같으면 통과, 크면 가로 넘침입니다.
  4. 한두 페이지가 아니라 sitemap에 있는 주소 전체를 같은 조건으로 돌립니다.

9월 2일에 처음 이 방식으로 바꿨을 때는 당시 sitemap 11개 페이지를 모두 돌렸고, 전부 scrollWidth 390에 h1 1개로 실패 0건이었습니다. 처음 스크린샷만 믿고 CSS를 고쳤으면 멀쩡한 레이아웃을 건드릴 뻔했습니다.