idea·blog
개발 노트

아이패드를 노트북 보조 모니터로 — 웹으로는 못 만드는 도구를 만든 이야기

Windows 가상 디스플레이 생성, 화면 캡처, H264 하드웨어 인코딩, WebRTC 전송까지 — 브라우저 밖에서만 가능한 화면 확장 도구의 개발 기록입니다.

2026.07.11

책상에 놀고 있는 iPad Air가 있어서, 노트북의 보조 모니터로 쓰는 도구를 만들었습니다. 같은 WiFi에 있으면 노트북 화면 옆에 가상 모니터가 하나 생기고, 그 화면이 iPad 브라우저에 실시간으로 뜹니다. 창을 그쪽으로 끌어다 놓으면 진짜 듀얼 모니터처럼 동작합니다.

이 블로그의 다른 프로젝트들과 달리, 이 글에는 "데모 열어보기" 버튼이 없습니다. 왜 없는지가 사실 이 글에서 제일 하고 싶은 이야기입니다.

웹이 할 수 있는 것과 없는 것

"화면을 다른 기기로 보내기"의 절반은 웹 기술로 됩니다. 브라우저에는 화면 캡처 API(getDisplayMedia)가 있고, WebRTC로 실시간 영상을 P2P 전송할 수 있죠. 실제로 이 블로그의 화상회의 데모에 있는 화면 공유가 정확히 그 조합입니다.

그런데 "보조 모니터"는 다릅니다. 미러링(지금 보는 화면 복제)이 아니라 확장이 되려면, 운영체제에 "모니터가 하나 더 연결됐다"고 믿게 만들어야 합니다. 가상 디스플레이를 만드는 건 드라이버/OS API 영역이라 브라우저가 절대 할 수 없고, 그래서 이 도구의 핵심은 웹이 아니라 사용자 PC에서 도는 네이티브 프로그램입니다. 웹사이트에 이식해서 방문자에게 서비스할 수 없는 구조적인 이유입니다.

구성은 세 조각입니다.

  • 호스트(Windows, Python): 가상 디스플레이를 잡고, 그 영역을 캡처해서 H264로 인코딩한 뒤 WebRTC로 송출
  • 시그널링 서버(Node, 89줄): 호스트와 뷰어가 서로를 찾게 해주는 소켓 중계
  • 뷰어(iPad 브라우저): 받은 영상을 전체 화면으로 재생하는 HTML 한 장

640×480의 함정

처음 버전의 최대 골칫거리는 화질이었습니다. 가상 디스플레이가 640×480에 갇혀 있었거든요. Windows에서 디스플레이 모드는 EnumDisplaySettings로 나열하고 ChangeDisplaySettingsEx로 바꾸는데, 가상 디스플레이가 지원하는 모드 목록을 제대로 순회해서 더 나은 모드로 협상하도록 고치고 나서야 실사용 가능한 해상도가 나왔습니다.

지금 기본 프리셋은 1600×900 / 30fps / 8000kbps입니다. 1920×1080보다 인코딩 부담이 가볍고, 로컬 WiFi 대역폭에서 30fps가 안정적으로 나오는 지점이라서요. 화면 캡처 후 긴 변을 1600px로 제한하는 상한도 호스트에 박아뒀습니다.

인코더는 하드웨어부터

CPU로 1600×900@30fps를 실시간 인코딩하면 노트북 팬이 비행기가 됩니다. 그래서 호스트는 H264 인코더를 이 순서로 시도합니다.

h264_nvenc (NVIDIA) → h264_qsv (Intel) → h264_amf (AMD) → libx264 (소프트웨어)

GPU 인코더가 하나라도 잡히면 CPU 부담이 크게 떨어지고, 전부 실패하면 libx264로 폴백해서 어쨌든 동작은 합니다. 픽셀 포맷도 인코더에 따라 달라져서(libx264는 yuv420p, 하드웨어 계열은 nv12) 그 분기도 함께 처리했습니다.

커서가 안 보인다

화면 캡처의 고전적인 함정: 캡처된 프레임에는 마우스 커서가 없습니다. 처음엔 커서 위치에 화살표 그림을 대충 얹었는데, 텍스트 커서(I빔)나 리사이즈 커서로 바뀌는 게 반영이 안 되니 iPad 쪽에서 조작감이 이상했습니다.

지금은 Windows API(CURSORINFO)로 현재 실제 커서의 비트맵을 가져와 프레임에 알파 블렌딩합니다. 커서 모양별로 비트맵을 캐시해서 매 프레임 비용도 줄였고요. 사소해 보이는데, 이런 게 "쓸 만한 도구"와 "데모"의 차이를 만들더라고요.

다음 단계 — Python에서 Rust로

지금 호스트는 Python(aiortc + mss + OpenCV)인데, 시작 시간과 런타임 의존성(가상환경, 패키지 설치)이 무겁습니다. 그래서 단일 실행 파일로 배포할 수 있는 Rust 네이티브 호스트로 옮기는 작업을 스캐폴드부터 진행 중입니다. 현재는 Windows 디스플레이 목록 조회와 CLI 검증까지 구현됐고, 시작 런처가 Rust 감독 프로세스를 먼저 띄워 가상 디스플레이를 재연결한 뒤 기존 스택을 실행하는 형태로 물려 있습니다.

서비스가 아니라 도구로 남기기

이 프로젝트는 처음부터 "내 책상 위 문제"를 풀려고 만들었고, 그 이상으로 키우지 않기로 했습니다. 공개 서비스가 되려면 방문자마다 네이티브 프로그램을 설치해야 하고(가상 모니터는 웹으로 불가능), 기기 조합마다 드라이버 지원과 인코더 상황이 달라 지원 부담이 곱해집니다. 반면 로컬 전용으로 두면 시그널링 서버가 89줄로 끝나고, 인증도 방 이름 하나로 충분합니다.

모든 프로젝트가 서비스가 될 필요는 없다 — 는 걸 받아들이고 나니, 오히려 도구 자체를 다듬는 데 집중할 수 있었습니다. 만들면서 배운 건(WebRTC 협상, 하드웨어 인코딩, Windows 디스플레이 API) 이렇게 글로 남깁니다.


관련 글