About

로컬 개발 화면을 Mac 앱으로 — Electron 말고 manifest 한 장과 launchd

iframe 도 껍데기 앱도 아니다. 브라우저가 그 페이지에 전용 창을 붙여 준다

매일 아침 여는 개인용 화면이 하나 있다. 로컬 Rails 개발 서버에 떠 있는 페이지다. 이걸 브라우저 탭 말고 Mac 앱처럼 쓰고 싶었다. Electron 이나 Tauri 부터 떠올렸는데, 결국 웹 앱 manifest 한 장과 launchd 하나로 끝났다.

이대로 하면 http://myapp.localhost:3000/ 같은 개발 화면이 Dock 아이콘 달린 독립 창이 된다.

무엇이 앱이 되는가

iframe 처럼 다른 페이지 안에 칸을 만드는 게 아니다. 브라우저가 그 페이지에 전용 창을 붙여 준다. 탭도 주소창도 없고 Cmd+Tab 에 따로 뜬다. 창 자체가 그 페이지다.

Dock icon My Desk no tabs · no URL bar · own Cmd+Tab entry Chrome app window (from manifest.json) Rails dev server myapp.localhost:3000 same code, no cache launchd at login start only if the port is free

파일 세 개와 한 줄

manifest 와 아이콘 두 장을 public/ 에 두고, 레이아웃 <head> 에 한 줄을 넣는다.

{
  "name": "My Desk",
  "short_name": "Desk",
  "start_url": "/",
  "display": "standalone",
  "icons": [
    { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
  ]
}

<link rel="manifest" href="/manifest.json">

막힌 곳이 두 군데 있었다.

처음엔 파일 이름을 manifest.webmanifest 로 했다. Rails 개발 서버가 이걸 text/plain 으로 내려 보냈다. manifest.json 으로 바꾸니 application/json 이 됐다.

service worker 가 있어야 설치 버튼이 뜨는 줄 알았다. 아니었다. 2026-10-01, macOS 14.6.1 · Chrome 154 에서 service worker 없이도 localhost 페이지에 beforeinstallprompt 가 왔다. 설치하니 ~/Applications/Chrome Apps/ 아래에 앱이 생겼다. service worker 가 없으니 캐시도 없어서, 코드를 고치면 창에도 그대로 반영된다. 이름 · 아이콘 같은 manifest 변경만 Chrome 이 늦게 반영한다.

설치 버튼을 직접 달 때

beforeinstallprompt 의 prompt() 는 한 번만 쓸 수 있다. 한 번 띄웠다가 닫으면 새로고침 전까지 버튼이 먹통이 된다. 기회를 써 버렸으면 「주소창 오른쪽 설치 아이콘이나 ⋮ 메뉴 → 페이지를 앱으로 설치」 를 안내하게 바꿨다.

let deferred = null;
window.addEventListener('beforeinstallprompt', (e) => {
  e.preventDefault();
  deferred = e;
  installButton.hidden = false;
});
installButton.addEventListener('click', async () => {
  if (!deferred) { showMenuHint(); return; }
  deferred.prompt();
  await deferred.userChoice;
  deferred = null; // 두 번은 못 쓴다
});

설치 확인 창은 페이지 밖, 브라우저 UI 쪽에 뜬다. 그래서 확장 프로그램 같은 자동화로는 마지막 「설치」 를 누를 수 없다. 거기는 사람이 누른다.

서버는 launchd 가 띄운다

아이콘을 눌렀는데 서버가 꺼져 있으면 소용없다. 로그인 때 launchd 가 개발 서버를 띄우게 했다.

우리 저장소의 기동 명령(make lad)은 포트를 잡고 있는 프로세스를 먼저 kill -9 한다. 그래서 포트가 비어 있을 때만 띄우게 감쌌다. launchd 작업이 끝날 때 띄운 서버까지 같이 죽지 않게 AbandonProcessGroup 도 켰다.

<key>ProgramArguments</key>
<array>
  <string>/bin/zsh</string><string>-lc</string>
  <string>lsof -iTCP:3000 -sTCP:LISTEN || (cd ~/src/myapp &amp;&amp; make lad)</string>
</array>
<key>RunAtLoad</key><true/>
<key>AbandonProcessGroup</key><true/>

Electron · Tauri 와 견주면

방법 드는 것 얻는 것
manifest 로 설치 파일 3개 + link 한 줄 Dock 아이콘, 주소창 없는 창, Cmd+Tab
Tauri · Electron 껍데기 새 프로젝트 · 빌드 · 서명 메뉴 막대, 전역 단축키, 창 상태 기억
서버까지 묶은 독립 앱 Rails · DB 동봉 오프라인

껍데기를 씌워도 안은 같은 화면이다. 메뉴 막대 아이콘이나 전역 단축키가 꼭 있어야 하면 Tauri 로 간다. Dock 에서 눌러 바로 열리면 그만인 개인 도구라면 manifest 로 충분하다. 빌드 · 서명 파이프라인을 하나 더 두는 건 관리비만 는다.

아직 확인 못 한 것도 있다. Safari 의 「Dock에 추가」 가 같은 manifest 로 되는지, 서버가 꺼졌을 때 앱 창이 무엇을 보여주는지는 안 봤다.

사내 관리 화면이나 개인 도구를 「앱처럼」 쓰고 싶은데 어디까지 만들어야 할지 모르겠다면 https://www.negabaro.com/contact 로 물어봐도 된다.

동작 흐름

1

public/ 에 manifest.json 과 아이콘 192 · 512 를 두고 <head> 에 link 한 줄

2

Chrome 에서 페이지를 앱으로 설치 → Dock 아이콘과 전용 창

3

launchd 가 로그인 때 포트가 비었을 때만 개발 서버를 띄운다

장점

  • ✓ 빌드 · 서명 없음, 코드 변경이 그대로 반영
  • ✓ service worker 없이도 설치됐다

단점

  • ✗ 메뉴 막대 아이콘 · 전역 단축키는 없다
  • ✗ 설치의 마지막 버튼은 사람이 눌러야 한다
← Postgres 볼륨 경로가 3년 틀렸는데 멀쩡했던 이유 — VOLUME 선언이 몰래 붙인 이름 없는 볼륨 Finder 색 태그는 AI 가 아니라 과거의 내가 붙였다 — xattr 속 「빨간색\n6」 문자열 →