웹브라우저 호출 한방에 폰트랩이 나가떨어진다고?
폰트랩에서 돌아가는 파이썬 스크립트를 만들다 보면 가장 신경 쓰이는 것이 있다. 기능이 별로인 것은 문제되지 않는다. 화면 디자인이 촌스러운 것도 문제되지 않는다. 가장 우려스러운 것은 스크립트로 인해 폰트랩이 크래시, 즉 따운되는 것이다. 그렇지 않아도 폰트랩은 원래 크래시가 잦은 프로그램이다. 여기에 잘못 만들어진 스크립트가 힘을 보탠다면 수많은 노력으로 만들어 놓은 '이 시간의' 결과물이 한순간에 날아가니, 쏟아지는(아니, 쏟아질) 비난을 어찌 다 감수하랴.
스크립트 개발 과정은 어쩔 수 없이 수많은 폰트랩 크래시를 겪어야 한다. 그러나 이것은 내가 감당해야 할 몫이니 오히려 반갑다. 오류를 수정하고, 기능을 개선하고, 새로운 기능을 추가하는 수많은 요청과 답변 속에서 스크립트가 점점 완성되어 가는 것을 보며 '그래, 폰트랩 세계에 이것 하나는 남겨야지' 하는 왠지 모를 사명감과 즐거움이 샘솟는다. 오히려 열 번 이내로 완성된 스크립트가 의외의 오류를 품고 있는 경우가 있다. 손쉽게 완성된 것일수록 검증이 얕았다는 뜻이기 때문이다.
모든 폰트랩 사용자가 가지고 있는 update-install.py
이렇게 완성한 나름의 최종본을 사용하다가 어느 날 문득 update-install.py를 실행했더니 폰트랩이 따운되는 것을 발견했다. 엥? update-install.py는 폰트랩 프로그램이 기본으로 제공하는 스크립트이고, 내용은 딱 세 줄뿐인데?
#FLM: Update / Install Scripts
import webbrowser
webbrowser.open("https://extend.fontlab.com/", new=0, autoraise=True)
그것도 웹브라우저를 여는 단순한 역할만 하는데 폰트랩이 따운된다고? 뭔가 싸한 느낌이 들기 시작하면서 지금까지 만들어온 스크립트를 하나하나 실행한 뒤 update-install.py를 마저 실행해보았다. 열에 아홉이 폰트랩 따운이다. 내가(아니, 클로드와 코덱스가) 만든 것만 그러나?
현존하는 스크립트의 대부분을 따운시키는 update-install.py
여러 스크립트를 테스트한 결과, 폰트랩 스크립트의 제왕이라 할 수 있는 TypeRig의 Delta Machine도 따운이고 TypeRig Manager도 따운이다. 박민우님이 만든 MW 스크립트의 Jitter Glyphs, Wiggle Glyphs도 따운이다. 단순한 웹브라우저 Open 동작 하나가 수많은 스크립트를 타격하여 폰트랩을 나가떨어지게 하고 있는 것이다. 기본 웹브라우저가 네이버 웨일이라서 이런 현상이 벌어지는지 의심되어 구글 크롬으로 바꿔 실험해봐도 결과는 마찬가지였다.
따운되는 상황도 종잡을 수 없다. 어떤 때는 아무 문제없이 update-install.py가 실행되다가, 어떤 때는 지속적으로 따운된다. 따운되는 장면을 캡처하려고 시도할 때는 따운되지 않고, 무심결에 실행했을 때는 또 변함없이 따운이라는 환장할 동작을 보여준다.

update-install.py를 실행하지 않으면 될 것 아닌가 하는 생각도 해보았다. 하지만 이것은 폰트랩이 기본적으로 제공하는 스크립트이고, TypeRig 라이브러리 도움 없이 단독으로 실행 가능한 스크립트라서 폰트랩 사용자라면 실행하지 않고는 못 배기게 되어 있다. 이름이 알파벳 U로 시작하는 까닭에 스크립트 메뉴에서 제일 아래에 자리잡고 있으며, 이름마저 Update / Install Scripts로 되어 있으니 저절로 마우스 커서를 끌어들인다. 와, 이걸 어떡하지.
크래시 리포트 파일인 Report.wer와 크래시 덤프 파일인 FontLab 8.exe.xxxxx.dmp를 클로드와 코덱스에게 넘겨주고 해법을 찾아 나설 수밖에 없다. 덤프 파일은 800MB에 육박하니 클로드에게는 통째로 업로드할 수 없어, 덤프 파일 분석기인 WinDbg로 필요한 부분만 추출하여 전달해야 했다.
누더기로 변하는 코드, 그러나 기워야 한다
클로드는 코드를 분석하더니 수많은 지점에 방어 코드를 심어놓아야 한다고 제시했다. "이 스크립트를 실행한 상태에서는 Update / Install Scripts를 실행하지 마세요."라는 안내문을 달아놓을 수는 없으니, 결국은 '살려야 한다'는 신념 아래 여기저기에 웹브라우저 Open 대응 방어 코드를 심어놓기로 했다. 프로토타입부터 시작하여 기능 개선과 에러 수정을 거치며 여기저기 해져온 코드가 이제는 누더기가 되어가는 느낌이다. 수선 장인처럼 예쁘고 멋있게 기우기 위해 주석을 다듬고 또 다듬는다. 로직은 함부로 손댈 수 없으니 코드에 AI가 삽입해놓은 주석이라도 다듬는 것이다.
Fontlab 개발사가 update-install.py를 올바르게 손보아 배포하여 방어 코드가 더 이상 필요 없어지는 그날, "이 스크립트에 설정된 웹브라우저 Open 방어 코드를 모두 삭제하라. 관련 주석도 모두 삭제하라."는 문구를 AI 대화창에 입력하고, 엔터를 땅 누르는 그 순간을 기다린다.
<폰트랩의 문제적 스크립트: update-install.py>
※ 후기
클로드와 수많은 대화를 통해 최종 해법을 수립했습니다. 테스트 결과 안정적인 동작을 보여주고 있습니다.
1. 폰트랩 크래시의 원인: webbrowser.open()을 메인 GUI 스레드에서 호출하면 → ShellExecuteW가 여는 nested 메시지 펌프도 메인 스레드의 큐를 돌린다 → 그 큐 안에 Qt 창으로 갈 메시지가 있으면 그대로 처리하다가 PythonQt 쪽 Python 콜백(슬롯)을 건드리게 되고, 이때 CPython이 Py_BEGIN_ALLOW_THREADS로 GIL을 놓은 상태에서 재진입이 일어나며 크래시가 발생한다.
2. 가장 간단한 해법: 웹브라우저 백그라운드 우회 가드(WebBrowser BG Bypass Guard) 설치 → update-install.py가 보내는 웹브라우저 호출을 우리 스크립트가 가로채서 대신 수행해준다. webbrowser.open()을 완전히 별도의 background 스레드(threading.Thread)에서 호출하면, ShellExecuteW가 여는 nested 펌프는 그 background 스레드 자신의 메시지 큐를 돈다. 이 스레드는 어떤 Qt 창도 소유하지 않았으니 그 큐에는 애초에 Qt로 갈 메시지가 없다. 따라서 PythonQt 콜백이 이 펌프 도중에 불려나올 이유가 구조적으로 사라진다.
3. 우회 가드가 이미 동작하고 있는 경우 우회 가드를 가지고 있는 다른 스크립트가 나중에 실행되면 Python의 모듈 캐싱 메커니즘에 의해 자신은 우회 가드 설치를 Skip한다. 따라서 우회 가드가 중복 설치되지 않아 안정적이다.
4. 이후 대책 : 웹브라우저 백그라운드 우회 가드(WebBrowser BG Bypass Guard)만 설치하고, 웹브라우저 호출 방어를 위해 소스 코드에 심어놓은 방어코드 1, 2, 3, 4, 5를 걷어낸다. 이제 누더기를 벗고 깔끔한 새 옷으로 갈아입을 수 있다.
# ── 웹브라우저 백그라운드 우회 가드 (WebBrowser BG Bypass Guard)
import threading as _threading
import webbrowser as _webbrowser
_BG_GUARD_ATTR = '_claude_webbrowser_bg_guard'
if not getattr(_webbrowser, _BG_GUARD_ATTR, False):
_original_webbrowser_open = _webbrowser.open
def _bg_thread_open(url, new=0, autoraise=True):
_threading.Thread(
target=_original_webbrowser_open,
args=(url,),
kwargs={'new': new, 'autoraise': autoraise},
daemon=True,
).start()
return True
_webbrowser.open = _bg_thread_open
_webbrowser.open_new = lambda url: _bg_thread_open(url, new=1)
_webbrowser.open_new_tab = lambda url: _bg_thread_open(url, new=2)
setattr(_webbrowser, _BG_GUARD_ATTR, True)