본문 바로가기
C.W.K.
Stream
Lesson 05 of 05 · published

앞으로 나오지 않던 창

~12 min · bare-binary, macos, foreground, war-story, dev-loop

Level 0툴 빌려 쓰는 사람
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"앱은 떴어. 창도 나타났고. 그런데 dock 아이콘이 없고, app switcher 에도 없고, 앞으로 오지도 않았어. 앱은 멀쩡했어. 앱으로 등록이 안 됐을 뿐이지."

증상 셋

개발 중에 native 창이 열리긴 했어. 그런데 서로 이어진 세 가지가 틀어져 있었지. dock 에 아이콘이 없어. 애플리케이션 switcher 에서도 빠져 있고. 그리고 절대 맨 앞으로 안 와. 터미널 뒤에 열리고, 키 입력은 계속 아까 focus 가 있던 앱으로 가. 증상은 셋인데 원인은 하나였고, 그 원인이 앱 코드에는 아예 없었어. 앱은 맞게 짜여 있었고, 문제는 그게 떠지는 방식이었어.

맨 바이너리는 앱이 아니야

개발용 명령이 맨 실행 파일, 그러니까 날것의 바이너리를 만들어서 돌려. 운영체제가 '앱' 으로 알아보는 구조를 갖춘 폴더인 제대로 된 application bundle 이 아니라. OS 는 이 둘을 아주 다르게 다뤄. bundle 은 시스템의 launch services 에 정상적인, 앞으로 나올 수 있는 애플리케이션으로 등록돼. 맨 바이너리는 그 과정을 통째로 건너뛰고. OS 가 얘를 진짜 앱으로 등록한 적이 없으니까 dock 에도 없고, switcher 항목도 없고, 앞으로 나올 정당한 권리도 없는 거야.

프로그램이 어떻게 포장됐느냐가, 코드와 무관하게, OS 가 그걸 어떻게 다룰지를 바꿔. 똑같이 컴파일된 로직인데도 OS 가 그걸 등록된 애플리케이션으로 보느냐 떠도는 바이너리로 보느냐에 따라 다르게 굴어. 코드는 맞아 보이는데 동작이 이상하면 포장이랑 띄우는 경로를 의심해. 코드 자체가 아니라 코드를 둘러싼 환경 말이야.

framework 설정이 안 먹힌 이유

자연스러운 해법은 앱의 activation policy 를 framework 설정으로 'regular', 그러니까 앞으로 나올 수 있는 앱으로 세우는 거야. 그런데 여기 함정이 있어. 그 framework 층 설정이 띄워진 모양이 맨 바이너리일 때는 살아 있는 애플리케이션 객체까지 전달이 안 됐어. 그 설정은 제대로 된 bundle 맥락 안에서 도는 걸 전제하거든. 그 맥락 밖에서는 조용히 안 먹혀. 설정은 맞았는데도 안 됐던 거야. 그게 기대는 전제가 거기 없었으니까.

맞는 설정도 전제가 없으면 조용히 실패할 수 있어. 설정은 대개 맥락을 전제해. bundle 이라든가, 초기화된 하위 시스템이라든가, 특정한 띄우기 경로라든가. 그 맥락이 없으면 설정이 에러를 내지도 않아. 그냥 안 먹혀. 제일 헷갈리는 버그가 문서상으로는 맞는데 기댈 게 없어서 아무 힘이 없는 설정이야. 설정만 보지 말고 그게 전제하는 것도 확인해.

진짜 해법 둘

나가는 길이 둘이야. 직접적인 쪽은 framework 아래 시스템 층에서 activation policy 를 'regular' 로 밀어 넣고 앱을 명시적으로 앞으로 끌어오는 native 호출을 하는 거야. 전달이 안 되던 framework 설정을 우회하는 거지. 다른 쪽은 맨 바이너리 띄우기를 그만두고 진짜 application bundle 을 빌드해서 그걸 돌리는 거고. 그러면 OS 가 처음부터 제대로 등록해. 앞쪽은 개발 흐름을 그 자리에서 고치고, 뒤쪽은 개발용 결과물을 진짜 결과물이랑 맞춰 놓는 거야.

증상을 그 자리에서 고칠 수도 있고, 결과물을 프로덕션이랑 맞출 수도 있어. 개발용 지름길이 버그를 만들면, 그 지름길 둘레로 땜질하거나(native 호출) 지름길을 없애고 진짜 결과물을 쓰거나(bundle) 할 수 있어. 둘 다 유효해. 고를 건 빠른 그 자리 수정이냐, 애초에 놀라게 만든 개발과 실제 사이의 틈을 없애느냐야.

엉뚱한 손잡이부터 잡지 마

이런 혼란 속에서는 관련 있어 보이는 설정 플래그를 죄다 시도해 보고 싶어져. 이거 강제 focus, 저거 항상 맨 위. 그런데 그건 진짜 원인인 '이 앱이 등록된 앱이 아님' 을 안 건드려. 게다가 그중 적어도 하나는 상황을 더 나쁘게 만들어. 항상 맨 위를 강제하면 원래 문제 위에 키보드 focus 까지 깨질 수 있거든. 규율은 겉만 가리는 창 플래그를 만지기 전에 뿌리인 등록과 activation policy 부터 고치는 거야.

난 곧장 창 플래그로 갔어. focus 설정, 항상 맨 위, 관련 있어 보이는 손잡이란 손잡이는 다. 그중 하나가 focus 문제를 더 나쁘게 만들었고, 그제야 짐작을 멈추고 이 창이 어떻게 떠지고 있는지가 실제로 뭐가 다른지 묻게 됐어. 답은 플래그랑 아무 상관이 없었어. OS 가 진짜 앱으로 여긴 적 없는 맨 바이너리를 돌리고 있었던 거지. 난 현관문 없는 집에서 커튼을 고쳐 달고 있었던 거야. 뿌리를 먼저 고치고, 꾸미는 건 그다음에.

Code

고침 A, Rust 코어에서: 같은 요청을 한 층 아래로·rust
// 개발 바이너리는 bundle 이 아니라서, 프레임워크가 제공하는 선언적
// 활성화 정책 설정이 살아 있는 NSApplication 까지 안 내려가.
// 고침: 같은 요청을 한 층 아래 Cocoa 층에서 직접 해.
#[cfg(target_os = "macos")]
{
    builder = builder.setup(|app| {
        // 1층: 프레임워크 수준 설정. 맞는 설정이고, 이것만으로는
        // 맨몸 바이너리한테 안 먹혀. 그래도 명시해 두는 건 나중에
        // 설정이나 프레임워크가 흔들릴 때를 대비한 문서 역할이야.
        app.set_activation_policy(ActivationPolicy::Regular);

        // 2층: 같은 요청을 singleton NSApplication 한테. 이쪽이
        // OS launch services 를 타고, 그게 dock 아이콘이랑 앱 전환기
        // 항목이랑 맨 앞으로 나올 권리를 사 줘. 키 입력이 web view 에
        // 닿기 위한 전제 조건이고.
        use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy};
        use objc2_foundation::MainThreadMarker;

        if let Some(mtm) = MainThreadMarker::new() {
            let ns_app = NSApplication::sharedApplication(mtm);
            ns_app.setActivationPolicy(NSApplicationActivationPolicy::Regular);
            ns_app.unhide(None);
            ns_app.activate();
        }
        Ok(())
    });
}
// macOS 로 막아 놔서 다른 타깃은 전부 깨끗하게 컴파일돼.
원인 하나, 증상 셋, 진짜 해법 둘·text
증상 (원인 하나가 보인 세 얼굴):
  - dock 아이콘 없음
  - app switcher 에서 빠짐
  - 절대 맨 앞으로 안 옴. 키 입력이 아까 focus 있던 앱으로 감

진짜 원인:
  개발용 명령이 application bundle 이 아니라 맨 바이너리를 돌림.
  -> OS 가 이걸 정상적인, 앞으로 나올 수 있는 앱으로 등록한 적이 없음.

안 먹히는 것:
  framework 층의 'activation policy = regular' 설정
  -> 맨 바이너리에서는 살아 있는 앱 객체까지 전달이 안 됨
     (그 설정은 여기 없는 제대로 된 bundle 맥락을 전제함)

먹히는 것 (하나 골라):
  A) framework 아래 native 호출로 activation policy = regular 를 밀어 넣고,
     앱을 명시적으로 앞으로 끌어옴
  B) 진짜 application bundle 을 빌드해서 그걸 돌림 (개발 결과물 == 진짜 결과물)

더 나쁘게 만드는 것:
  뿌리보다 먼저 창 플래그(강제 focus, 항상 맨 위)에 손대기
  -> 항상 맨 위가 원래 버그 위에 키보드 focus 까지 깨뜨릴 수 있음

External links

Exercise

'프로덕션에서는 되는데 개발에서는 이상함'(또는 반대) 버그를 만난 적을 떠올려 봐. 진짜 원인이 코드에 있었어, 아니면 개발용 결과물과 진짜 결과물의 차이(포장, 띄우는 경로, 환경)에 있었어? 개발 실행이 실제 실행이랑 다른 점을 전부 나열해 봐. 그 차이 하나하나가 안 맞는 동작의 후보 설명이야.
Hint
개발과 실제 결과물이 흔히 다른 지점들. 맨 바이너리냐 bundle/설치본이냐, debug 냐 release 냐, 소스 디렉토리에서 돌리냐 설치된 자리에서 돌리냐, 환경 변수나 작업 디렉토리가 다르냐. 이 중 뭐든 같은 코드를 OS 나 런타임이 다르게 다루게 만들 수 있어.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.