JIN.PROC // v3.1
~/ / 프로젝트 / 2026-04-29-lazyclaude-deep-4-pwa-mac-app

[LazyClaude 심층 4] PWA + 72KB macOS .app — 패키지 매니저 없이 배포하기

Service Worker 한 장과 쉘 스크립트 한 줄로 "앱처럼" 만든다

DATE 2026.04.29 UPDATED 2026.04.29 READ ~ 3 MIN WORDS 528

LazyClaude 심층 시리즈 4편. 설치 라는 마찰 자체를 없애는 두 갈래 — PWA와 .app 번들.

왜 패키징이 어려운가

오픈소스 도구를 쓰려고 할 때 가장 큰 장벽은 기술적 어려움 이 아니라 설치 절차 다.

  • “Homebrew 깔고…” — 안 깐 사람도 있다
  • “npm 글로벌 설치…” — 권한 문제 흔함
  • “Docker 띄우고…” — 일상 도구로 쓰기에는 무거움

LazyClaude는 이 마찰을 0에 가깝게 가져가기로 했다. 두 진입점을 동시에 만든다.

진입점 A — PWA (브라우저 → 앱)

웹 표준으로 풀 수 있는 가장 가벼운 방법.

1. Web App Manifest

// dist/manifest.webmanifest
{
  "name": "LazyClaude",
  "short_name": "LazyClaude",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#0b0b0c",
  "theme_color": "#0b0b0c",
  "icons": [
    { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" },
    { "src": "/icon-mask.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
  ]
}

display: standalone이 핵심이다. 주소창 없이 앱 창 으로 뜬다.

2. Service Worker — 최소 구현

// dist/sw.js
const CACHE = "lzc-v1";
const ASSETS = ["/", "/manifest.webmanifest", "/icon-192.png"];

self.addEventListener("install", (e) => {
  e.waitUntil(caches.open(CACHE).then(c => c.addAll(ASSETS)));
});

self.addEventListener("fetch", (e) => {
  const url = new URL(e.request.url);
  if (url.pathname.startsWith("/api/")) {
    e.respondWith(fetch(e.request));
    return;
  }
  e.respondWith(
    caches.match(e.request).then(r => r || fetch(e.request))
  );
});

전략은 한 줄로:

  • /api/*network-first (실시간성)
  • 나머지 — cache-first (앱 셸 즉시 표시)

3. HTML 등록

<link rel="manifest" href="/manifest.webmanifest">
<script>
  if ("serviceWorker" in navigator) {
    navigator.serviceWorker.register("/sw.js");
  }
</script>

이걸로 Chrome/Edge/Safari 모두에서 “홈 화면에 추가” / “앱 설치” 메뉴가 뜬다.

진입점 B — 72KB macOS .app 번들

PWA로 충분하지 않은 사용자를 위해, 진짜 독립 앱 도 같이 제공한다. Spotlight·Dock에서 검색되고, 더블클릭하면 서버 라이프사이클까지 자동.

.app 번들의 구조

macOS의 .app은 그냥 특정 구조의 디렉터리 다.

LazyClaude.app/
└── Contents/
    ├── Info.plist
    ├── MacOS/
    │   └── LazyClaude
    └── Resources/
        └── lazyclaude.icns

Info.plist (최소)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>CFBundleName</key>            <string>LazyClaude</string>
  <key>CFBundleIdentifier</key>      <string>dev.cmblir.lazyclaude</string>
  <key>CFBundleVersion</key>         <string>1.0</string>
  <key>CFBundleExecutable</key>      <string>LazyClaude</string>
  <key>CFBundleIconFile</key>        <string>lazyclaude</string>
  <key>LSUIElement</key>             <false/>
  <key>NSHighResolutionCapable</key> <true/>
</dict>
</plist>

실행 파일 — 쉘 스크립트로 충분

Contents/MacOS/LazyClaude 자체를 실행 가능한 쉘 스크립트로 만든다. macOS는 텍스트 실행 파일도 실행 권한 만 있으면 그냥 실행한다.

#!/bin/bash
set -euo pipefail

REPO="$HOME/.lazyclaude"
PORT=8765

if curl -fsS "http://127.0.0.1:$PORT/api/version" >/dev/null 2>&1; then
  open "http://127.0.0.1:$PORT/"
  exit 0
fi

cd "$REPO"
nohup python3 server.py --port "$PORT" >/dev/null 2>&1 &
sleep 0.4
open "http://127.0.0.1:$PORT/"

필요한 것은:

  • 실행 권한 (chmod +x Contents/MacOS/LazyClaude)
  • macOS Gatekeeper 우회를 위한 quarantine 속성 제거 (xattr -dr com.apple.quarantine LazyClaude.app)

설치 자동화

Makefile에 한 줄로:

install-mac:
	./scripts/build-mac-app.sh
	cp -R build/LazyClaude.app /Applications/
	xattr -dr com.apple.quarantine /Applications/LazyClaude.app || true
	@echo "Installed to /Applications. Spotlight: 'LazyClaude'."

총 번들 크기는 아이콘이 거의 다 차지한다 — 72KB 정도.

두 진입점이 동시에 의미 있는 이유

  • PWA: 브라우저에 한 번만 들어가면 됨. 데스크톱·모바일 동일.
  • .app: macOS 사용자가 서버 자체를 모르고도 쓸 수 있게. 종료 후에도 다음에 더블클릭이면 다시 시작.

같은 코드베이스, 같은 단일 HTML, 두 가지 접근 방식.

사이닝과 노타라이즈는?

오픈소스 + 로컬 전용에선 공식 코드 사이닝 이 필수가 아니다. Gatekeeper에 부딪히면:

  • 우클릭 → 열기 (1회)
  • 또는 xattr -dr com.apple.quarantine

상업 배포라면 Apple Developer 계정이 필요하지만, LazyClaude는 로컬 개발 도구 의 컨텍스트에서 충분히 직관적인 우회 를 받아들이기로 했다.

한 줄 요약

가장 좋은 설치 절차는 설치하지 않는 것이다. 다음으로 좋은 건 더블클릭 한 번.


다음 편: LazyClaude 심층 5 — Hyper Agent: 자기 개선 sub-agent의 메커니즘 이전 편: LazyClaude 심층 3 — 첫 페인트 차단 풀기