콘텐츠로 건너뛰기
Codex

화면 녹화 권한 없이 다른 창의 위치를 읽는 법 (macOS·Windows)

권한 없이 읽히는 창 정보와 아닌 것

권한 팝업 하나가 설치를 끝낸다

데스크톱 펫을 만들면서 다른 앱의 창 위치가 필요했다. 캐릭터가 창틀 위에 올라서고 걸어다니려면 화면에 어떤 창이 어디 있는지 알아야 한다.

문제는 macOS에서 이걸 잘못 요청하면 화면 녹화 권한 팝업이 뜬다는 것이다. 캐릭터 하나 띄우는 프로그램이 “이 앱이 화면을 녹화하려 합니다”를 띄우면, 대부분은 거기서 지운다.

결론부터 쓰면 이렇다. 창의 위치와 크기는 권한 없이 읽힌다. 권한이 필요한 건 창 제목이다. 제목을 안 읽으면 팝업도 없다.

실제로 재봤다

이 맥에서 확인한 값이다. CGWindowListCopyWindowInfo로 화면에 있는 창을 전부 가져와, 필드별로 몇 개나 채워져 있는지 셌다.

창 52개
  kCGWindowBounds     52
  kCGWindowOwnerPID   52
  kCGWindowOwnerName  52
  kCGWindowName(제목)  1

위치·크기(Bounds), 소유 프로세스(OwnerPID), 앱 이름(OwnerName)은 52개 전부 읽혔다. 제목만 1개다.

그 1개도 확인해 봤더니 사용자 앱이 아니라 Window Server가 소유한 창이었다. 즉 일반 앱 창의 제목은 하나도 안 읽혔다.

화면 녹화 권한을 준 적이 없는 상태에서 나온 결과다. 권한이 필요한 필드와 아닌 필드가 이렇게 갈린다.

왜 제목만 다른가

애플이 창 제목을 화면 내용과 같은 급으로 본다. 제목에는 열어 둔 파일 이름, 보고 있는 문서, 접속한 사이트가 그대로 들어간다. 무제.txt - 메모장2026년_연봉협상.xlsx - Excel은 노출되는 정보의 무게가 다르다.

반면 창이 화면 어디에 얼마만 한 크기로 있는지는 그 자체로 개인정보가 되기 어렵다. 그래서 앞은 막고 뒤는 열어 뒀다.

캐릭터가 창틀 위를 걸으려면 뒤만 있으면 된다. 어떤 앱의 창인지도 알 필요가 없다.

그래서 이렇게 부른다

copy_window_info(
    kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
    kCGNullWindowID,
)

OnScreenOnly는 지금 화면에 보이는 창만, ExcludeDesktopElements는 바탕화면 아이콘 같은 것을 뺀다.

여기서 꺼내 쓰는 건 세 가지다.

무엇 쓰는 이유
kCGWindowBounds 위치와 크기 발판 후보
kCGWindowLayer 창 층위 일반 창(0)만 고른다
kCGWindowOwnerPID 소유 프로세스 자기 창을 발판으로 삼지 않으려고

kCGWindowName은 안 읽는다. 이 한 줄을 안 쓰는 것이 권한 팝업을 없앤다.

마지막 항목이 없으면 캐릭터가 자기가 들어 있는 창 위에 올라서려고 한다. 자기 발밑을 밟는 셈이라 위치가 진동한다.

Windows는 다른 API, 같은 원칙

Windows에는 화면 녹화 권한이라는 개념이 없다. 대신 다른 함정이 있다.

EnumWindows            열려 있는 창을 훑는다
IsWindowVisible        보이는 것만 남긴다
DwmGetWindowAttribute  실제 테두리를 얻는다

GetWindowRect를 쓰면 안 된다. 이 함수는 창 주변의 보이지 않는 그림자 여백까지 포함한 사각형을 준다. 그대로 발판으로 쓰면 캐릭터가 창 위가 아니라 허공에 서 있는 것처럼 보인다.

DwmGetWindowAttributeDWMWA_EXTENDED_FRAME_BOUNDS(값 9)를 넘기면 눈에 보이는 테두리를 준다.

거기에 두 가지를 더 걸러야 한다.

거를 것 무엇
DWMWA_CLOAKED (14) 숨겨진 창 다른 가상 데스크톱의 창이 발판이 된다
WS_EX_TOOLWINDOW 도구 창 툴바·팝업이 발판이 된다

가상 데스크톱을 쓰는 사람에게는 첫 번째가 특히 중요하다. 걸러내지 않으면 지금 화면에 없는 창 위에 캐릭터가 올라선다.

좌표계를 한 번은 맞춰야 한다

Windows에서 얻는 값은 물리 픽셀이다. 프론트엔드가 쓰는 건 논리 좌표다. 고해상도 모니터에서는 두 값이 다르다.

그래서 캐릭터 발이 있는 모니터의 DPI로 변환한다. 화면마다 배율이 다를 수 있으니 아무 모니터 기준으로 나누면 어긋난다.

macOS는 처음부터 논리 좌표로 준다. 이 변환이 없다.

창이라고 다 발판은 아니다

위치를 읽었다고 끝이 아니다. 밟을 수 있는 것만 남긴다. 실제로 쓰는 값이다.

기준 이유
최소 발판 너비 200 캐릭터가 설 폭이 안 되면 발판이 아니다
화면 위쪽 여유 270 이보다 위면 캐릭터 창이 화면 밖으로 나간다
디스플레이 인접 허용 2 이 거리 안이면 두 화면이 붙어 있다고 본다
가장자리 통로 최소 겹침 200 지나갈 폭은 있어야 넘어간다

두 번째가 직접 겪은 것이다. 캐릭터 창은 발 위치를 기준으로 배치되는데, 발판이 화면 꼭대기에 너무 가까우면 창 윗부분이 화면 밖으로 밀린다. 그래서 화면 상단 270px 안쪽의 창 윗변은 발판에서 뺐다. 그런 창은 캐릭터가 밟지 않고 지나쳐 내려간다.

같이 나오는 질문

창 제목이 꼭 필요하면

그때는 권한을 요청하는 수밖에 없다. 다만 요청 시점을 고르는 게 낫다. 실행하자마자 묻지 말고, 제목이 필요한 기능을 사용자가 켤 때 묻는다.

권한을 왜 필요로 하는지 화면에서 먼저 설명하고 시스템 팝업을 띄우는 것도 차이가 크다. 맥락 없이 뜬 팝업은 대체로 거부된다.

`kCGWindowOwnerName`도 권한이 필요한가

아니다. 실측에서 52개 전부 읽혔다. 앱 이름은 제목과 달리 열어 둔 파일이나 문서 내용을 드러내지 않는다.

이 프로젝트는 진단용 코드에서만 쓰고 있다. 어떤 앱의 창이 어떻게 잡히는지 확인할 때다. 캐릭터를 움직이는 데는 필요 없다.

접근성 권한으로 하면 안 되나

AXUIElement로 창 정보를 얻는 방법도 있다. 다만 접근성 권한도 팝업이 뜨고, 사용자가 보기에는 화면 녹화만큼이나 무거운 요청이다. 위치만 필요하다면 권한 없는 쪽이 낫다.

이 방법으로 못 하는 것

창의 z 순서를 정확히 알기 어렵고, 어떤 창이 지금 활성인지도 알 수 없다. 위치와 크기만 얻는 대신 그만큼을 포기한다. 캐릭터를 창틀에 올려놓는 데는 부족하지 않았다.


다음에 볼 건 음성 인식 쪽이다. 조용한 방에서 마이크를 켜 두면 Whisper가 아무도 하지 않은 말을 받아 적는다. 그게 어떤 형태로 나오고 어떻게 걸러내는지 정리한다.

이 블로그 더 보기

이 블로그는 실제 프로젝트를 AI와 함께 굴리면서 남긴 기록입니다.

  • 새 글은 RSS로 받아볼 수 있습니다.
  • 시리즈 전체는 여기에 정리해 두었습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다