Ankita v2.5.2: 실제로 시작되는 앱 - DOM 탐지기, 엄격한 CSP, 그리고 빈 창
요약
Ankita v2.5.2 핫픽스는 데스크톱 앱이 빈 창으로 실행되는 버그를 수정했습니다. 이 문제는 공유 모듈에 포함된 DOM 탐지기가 초기화 과정에서 함수 컴파일을 유발했고, CSP가 이를 차단하면서 발생한 '공유 모듈 경계 실패'였습니다. 해결책은 상수를 순수 정책 모듈로 이동시키고 보안(CSP) 태세를 유지하는 것이었습니다.
핵심 포인트
- 버그의 원인은 공유 모듈에 숨겨진 DOM 탐지기 때문이었습니다.
- 상수는 무해하지만, 포함된 모듈 전체가 문제를 일으킬 수 있습니다.
- 수정은 재작성이 아닌 '값'을 안전한 위치로 이동시키는 것이 핵심입니다.
- CSP를 유지하며 보안 경계를 강화하는 것이 중요합니다.
어제 Ankita v2.5.1을 배포했고, 약 46분 후에 v2.5.2를 배포했습니다. 이 버전은 제가 그냥 넘어갈 수 없는 버그에 대한 긴급 수정이었습니다. 바로 데스크톱 앱이 빈 창으로 실행되는 문제였습니다. 충돌도 아니고, 오류 대화 상자도 아니었습니다. 개발 빌드와 패키징된 빌드 모두에서 그저 아무것도 없었습니다. 동반자가 깨어나기를 거부하는 것은 가장 고장 난 상태에 가깝습니다.
무슨 일이 있었는지, 실제로 무엇이 문제를 해결했는지, 그리고 애초에 이 버그가 통과할 수 있게 했던 테스트의 허점은 무엇인지 설명하겠습니다.
증상: 앱이 없는 빈 창
빈 창이 문제의 단서였습니다. Electron에서 React가 마운트되기 전에 죽는 렌더러(renderer)는 사용자에게 도움이 되는 오류를 던지지 않습니다. 페이지는 로드되고, CSP도 제 역할을 하며, 앱은 그저 나타나지 않을 뿐입니다. 마치 중요한 것 외에는 모든 것이 괜찮은 것처럼 보였습니다.
원인: 공유 모듈에 숨겨진 DOM 탐지기
릴리스 노트가 명확하게 말하고 있습니다. 공유 브라우저 진행 상황 메타데이터(shared browser progress metadata)가 호스트 전용 DOM 탐지기를 가져왔고, 이는 모듈 초기화 시 함수를 컴파일합니다. 렌더러의 Content Security Policy (CSP)는 React가 마운트되기 전에 이를 올바르게 거부했습니다.
이것을 자세히 설명하겠습니다. 이 버그의 형태 자체가 흥미로운 부분이기 때문입니다:
- 브라우저 진행 상황 메타데이터를 담당하는 모듈은 **폼 필드 제한(form-field limit)**이라는 것이 필요했습니다. 이는 순수한 상수일 뿐, DOM과 관련된 것은 전혀 아니었습니다.
- 하지만 이 상수는 호스트 전용 DOM 탐지기도 포함하고 있는 모듈에서 가져와졌습니다. 이 코드는 DOM을 탐지하며, 모듈 초기화 시점에 이를 수행하는 함수를 컴파일합니다.
- 상수를 가져오는 과정이 전체 모듈을 끌어들였습니다. 탐지기는 모듈 초기화 중에 함수 컴파일 단계를 실행했습니다.
- 렌더러의 CSP는
unsafe-eval을 올바르게 거부하면서 이 컴파일을 차단했습니다. - React가 마운트되지 않았습니다. 빈 창이 나타났습니다.
이것은 전형적인 공유 모듈 경계 실패(shared-module boundary failure)입니다. 상수는 무해했지만, 그 상수가 존재했던 모듈은 그렇지 않았던 것입니다. 아무도 고의로 잘못된 코드를 작성하지 않았습니다. 누군가 순수한 값을 잘못된 영역에 두었고, 나중에 가져오는 과정이 전체 영역을 끌고 온 것뿐입니다.
해결책: 값은 이동시키고 보안은 유지하기
수정 사항은 재작성이 아니라 이동이었습니다: 이제 폼 필드 제한(form-field limit)은 **순수 연산 정책 모듈(pure operation-policy module)**에 위치하며, 원래의 참조 모듈(ref module)이 호환성을 위해 이를 다시 내보내어 하위 시스템에 아무것도 깨지지 않도록 했습니다.
여기서 중요한 의도적인 비변경 사항 두 가지가 있습니다:
- 브라우저 배치 처리(Browser batching)는 유지됩니다. 제한이 필요했던 메타데이터 파이프라인은 이전과 정확하게 작동합니다. 단지 상수(constant)의 출처만 이동했을 뿐입니다.
- CSP는 여전히
unsafe-eval을 거부합니다. CSP 블록 처리 시 정책을 완화하고 싶은 유혹이 있습니다. 하지만 저희는 그렇게 하지 않았습니다. 보안 태세는 변경되지 않았으며, 코드가 이를 준수하도록 옮겨졌을 뿐입니다.
솔직히 가장 자랑스러운 부분이 바로 이 부분입니다. 압박 속에서 배포된 핫픽스(hotfix)의 경우, 보안 경계를 '일시적으로' 약화시키고 나중에 다시 복구하는 것을 잊기 가장 쉬운 때이기 때문입니다.
테스트 격차: 우리는 부팅 과정이 아닌 컴포넌트를 테스트하고 있었다
이전 시작(startup) 테스트는 CSP가 없는 고립된 컴포넌트만 실행했습니다. 컴포넌트 테스트로는 실제 정책 하에서만 발생하는 모듈 초기화 실패(module-init failure)를 잡아낼 수 없습니다. 설계상, 문제가 발생한 부분은 프레임 안에 없었기 때문입니다.
새로운 회귀 테스트는 **실제 부팅 경로인 HTML과 CSP가 포함된 완전한 렌더러 진입점(renderer entry points)**을 실행합니다. 단순히 축소된 버전이 아닙니다. 초점이 맞춰진 시작/진행/브라우저 복구 스위트(startup/progress/browser recovery suite)는 실제 Playwright 및 연결된 Chrome 폼 배치와 stale-ref 복구를 포함하여 33/33을 통과했습니다.
더 넓은 수치로 보면: 전체 순차 로컬 스위트는 1,121개 성공, 실패 0건을 보고합니다 (Windows에서 사전에 존재하던 POSIX 권한 건너뛰기 제외). TypeScript 및 Vite 빌드는 성공했습니다. 패키징된 ASAR는 실제 패키징된 프리로드(preload) 및 오프라인 엔진 장치(offline engine fixtures)와 함께 네이티브 Electron에 마운트되었으며, 렌더러 예외는 없었고, CSP는 변경되지 않았으며, 실행 파일은 2.5.2로 보고되었습니다.
솔직히 말하자면, 이 시리즈의 목적이 바로 그것입니다: v2.5.2는 2.5.1 버전에서 기록된 connected-Chrome Windows CI 탐색/장바구니 오류를 해결하지 못했습니다. 배포는 로컬로 확인된 Windows 패키지를 사용했으며, 정확한 커밋(exact-commit) 원격 CI 통과 여부는 주장되지 않습니다. 전체 전후 추적(before/after traces), 패키징 검사, 그리고 남아있는 검증 한계는 리포지토리의 docs/desktop-startup-findings.md에 있습니다 — 가감 없는 버전을 원한다면 읽어보세요.
실제로 논쟁하고 싶은 질문
제가 완전히 해결할 수 없는 디자인적 긴장감이 있습니다: 모듈 수준 사이드 이펙트(module-level side effects)가 바로 이 버그가 존재했던 전적인 이유입니다 (임포트 시점에 컴파일되는 함수로, 모든 호출자에게 보이지 않습니다). 그리고 일반적인 조언은 '모듈 스코프에서 사이드 이펙트를 하지 마라'입니다. 하지만 실제 코드베이스 — 저의 것도 포함하여 — 계속해서 이를 재성장시키고 있습니다. 왜냐하면 임포트 시 한 번 초기화되는 DOM 프로브는, CSP가 금지하는 함수를 컴파일할 때까지 매우 편리하기 때문입니다.
여러분은 이것을 린팅 규칙(lint rules), 아키텍처 테스트(architecture tests), 아니면 단순히 규율로 통제하나요? 그리고 부트 테스트(boot tests)에서는 어디에 선을 긋나요 — 배포된 HTML을 사용한 전체 렌더러 진입점(full renderer entries)으로 할지, 아니면 거의 문제가 발생하지 않는 경로에 대한 과도한 테스트일까요?
Ankita는 제가 만든 오픈 소스 데스크톱 AI 동반자입니다 — 로컬 우선(local-first), 사용자 모델 사용, MIT 라이선스를 따릅니다. 리포지토리는 여기에 있으며, v2.5.2 릴리스 노트에 전체 검증 세부 정보가 있습니다. 만약 여러분이 자신만의 CSP 대 편리성 전투를 치러본 경험이 있다면, 어떻게 선을 그었는지 진심으로 듣고 싶습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기