⚡ Electron2. 창 & 라이프사이클02 · app 라이프사이클

02 · app 라이프사이클

이 문서가 답하는 질문: Electron 앱은 정확히 언제 살아나고, 어떤 순서로 죽으며, 왜 macOS는 창을 다 닫아도 안 죽는가? Windows Squirrel 인스톨러가 왜 첫 실행에서 바로 종료해야 하는가?


한 줄 답 (Pyramid Top)

app 모듈은 프로세스 전체의 라이프사이클을 가진 EventEmitter다. 웹은 브라우저가 알아서 처리하지만, Electron은 ready → activate → window-all-closed → before-quit → will-quit → quit 의 흐름을 개발자가 OS별로 직접 분기해야 한다 — 그리고 macOS / Windows / Linux 세 곳에서 같은 코드가 다 다르게 행동한다.


Why — 왜 OS별로 종료 규약이 다른가

세 OS의 철학이 다르다.

OS창 다 닫음 = 종료?이유
Windows✅ 보통 종료”프로그램은 창이다” — Win16 시절 멘탈 모델
macOS❌ 안 종료, dock에 남음NeXTSTEP 1989년부터 “앱은 도큐먼트가 아니라 서비스”
Linux (GNOME/KDE)✅ 보통 종료 (앱 따라)X11/Wayland 자체에 강한 규약 없음

Electron은 이 세 규약을 자동으로 통합하지 않는다 — 대신 모든 이벤트를 노출하고 개발자에게 분기하라고 시킨다. 그 결과 모든 튜토리얼에 똑같이 박혀 있는 12줄 코드가 생겼다.

// 거의 모든 Electron 앱의 시작 코드 — "macOS 패턴"
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});
 
app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) createWindow();
});

이 두 핸들러를 둘 다 빠뜨리지 않는 것이 macOS 사용자 경험의 출발선이다.


How — 전체 이벤트 시퀀스

1) 시작부터 종료까지 한 그림

2) 주요 이벤트 표 (Main 프로세스)

이벤트언제 발생preventDefault 가능?주요 용도
will-finish-launching앱 시작 직후, readymacOS의 open-file/open-url 등록 (이 시점에 등록 안 하면 첫 launch 시 누락)
readyElectron 내부 초기화 완료첫 창 만들기, 메뉴/Tray 초기화
activatemacOS dock 클릭, Windows 작업표시줄 (드물게)창이 없으면 다시 만들기
browser-window-created새 창 생성 직후창 공통 설정 후처리 (메뉴 추가 등)
web-contents-createdwebContents 생성 직후보안 핸들러(setWindowOpenHandler, will-navigate 차단) 일괄 적용
window-all-closed모든 BrowserWindow가 닫혔을 때종료 분기 (macOS 패턴)
before-quit종료 시도 시작“저장하지 않은 변경사항” 확인
will-quit모든 창이 닫힌 후, 종료 직전마지막 cleanup
quit프로세스가 실제로 종료될 때이미 늦음. cleanup은 will-quit에서.
second-instance두 번째 인스턴스 실행 시도 시 (single-instance lock 활성화 상태)기존 창 포커스 + 인자 전달
open-url (macOS)커스텀 protocol로 앱이 호출됨딥링크 처리 (06)
open-file (macOS)Finder에서 파일을 앱에 드롭파일 연결

3) 최소 코드 — 모든 라이프사이클을 다룬 부트스트랩

const { app, BrowserWindow } = require('electron');
const path = require('node:path');
 
let mainWindow;
 
function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1200, height: 800,
    show: false,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      sandbox: true,
    },
  });
  mainWindow.once('ready-to-show', () => mainWindow.show());
  mainWindow.loadFile('index.html');
  mainWindow.on('closed', () => { mainWindow = null; });
}
 
// (1) ready — 진입점
app.whenReady().then(() => {
  createWindow();
 
  // (2) macOS: dock 클릭 시 창 재생성
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});
 
// (3) 종료 분기
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});
 
// (4) 종료 직전 — 저장 다이얼로그 등
app.on('before-quit', async (event) => {
  if (hasUnsavedChanges()) {
    event.preventDefault();
    const choice = await showConfirmDialog();
    if (choice === 'discard') app.exit(0); // 강제 종료
  }
});
 
// (5) 마지막 cleanup
app.on('will-quit', () => {
  closeDatabaseConnections();
  flushLogs();
});

app.quit() vs app.exit(0)

  • quit()before-quit/will-quit 발생, 취소 가능. 정상 종료.
  • exit(0) — 이벤트 무시하고 즉시 종료. 인스톨러나 강제 종료에만.

4) Single Instance — “한 번에 하나만”

기본적으로 Electron 앱을 두 번 실행하면 두 프로세스가 동시에 돈다. 보통은 기존 인스턴스를 깨우고 새 인자를 전달하고 싶다.

const gotLock = app.requestSingleInstanceLock();
 
if (!gotLock) {
  app.quit(); // 두 번째 인스턴스는 즉시 종료
} else {
  app.on('second-instance', (event, argv, workingDirectory) => {
    // 기존 창을 깨움
    if (mainWindow) {
      if (mainWindow.isMinimized()) mainWindow.restore();
      mainWindow.focus();
    }
    // argv에서 딥링크나 파일 경로 추출
    const deepLink = argv.find(a => a.startsWith('myapp://'));
    if (deepLink) handleDeepLink(deepLink);
  });
 
  app.whenReady().then(createWindow);
}
  • Windows에서 protocol handler를 등록하면 필수. 안 그러면 myapp:// 한 번 클릭에 앱이 새로 뜬다.
  • macOS는 OS가 자체적으로 single-instance를 강제하지만, 그래도 lock을 거는 게 플랫폼 통일성 측면에서 권장.

What — OS별 동작 매트릭스

시나리오WindowsmacOSLinux
창 닫기 (X)window-all-closedquitwindow-all-closed대기 (dock에 남음)보통 quit
두 번째 실행별 인스턴스 (lock 없으면)OS가 기존을 활성화 (보통)별 인스턴스
dock/작업표시줄 클릭창이 살아 있으면 포커스activate 이벤트 + 창 없으면 재생성 패턴환경따라 다름
Cmd/Ctrl+Q(Alt+F4) → before-quitbefore-quit(Ctrl+Q) 동일
시스템 종료will-quit 발생will-quit 발생will-quit 발생
강제 종료 (Task Manager)이벤트 없이 죽음동일동일

함정: “강제 종료에도 cleanup 하고 싶다”는 욕심은 위험하다 — 프로세스가 SIGKILL로 죽으면 그 어떤 핸들러도 안 돈다. 영속화는 수시 저장으로 풀어야 한다.


What-if — 흔한 함정

함정증상해법
macOS에서 window-all-closed에 무조건 app.quit()dock 아이콘이 죽음, 사용자가 “왜 매번 새로 켜야 하지?”if (platform !== 'darwin') app.quit()
activate에서 창 재생성 안 함macOS에서 dock 클릭해도 창이 안 뜸activate 핸들러 필수
Single instance lock 없이 protocol 등록딥링크 1번에 앱 N개 뜸requestSingleInstanceLock
before-quit에서 await 없이 dialog다이얼로그 뜨기 전에 앱 죽음event.preventDefault() 먼저, 그 다음 dialog
will-quit에서 IPC 사용렌더러는 이미 죽음 — IPC 못 보냄will-quit순수 Node
app.whenReady() 전에 BrowserWindow 생성”GPU not initialized” 등반드시 whenReady().then(...) 안에서

How (Windows Squirrel) — 인스톨러 훅

electron-squirrel-startupWindows 전용. Squirrel 인스톨러는 설치/제거 중에 앱을 잠깐 실행해서 바로가기를 만들거나 지운다 — 그때 바로 종료해야 한다.

// main.js 최상단 — Electron 임포트 전에 처리
if (require('electron-squirrel-startup')) {
  app.quit(); // ← 인스톨러가 호출한 경우. 보통 앱 로딩 안 함.
  return;
}

내부적으로 다음 argv를 본다:

  • --squirrel-install: 처음 설치
  • --squirrel-updated: 업데이트
  • --squirrel-uninstall: 제거
  • --squirrel-obsolete: 이전 버전 폐기

이 인자 중 하나라도 있으면 바로 죽어야 인스톨러가 다음 단계로 넘어간다. 안 죽으면 설치 중간에 멈춤.

자세한 패키징은 06 패키징에서.


How (macOS dock-click) — activate의 진짜 의미

  • macOS는 “앱은 죽지 않고, 창이 죽는다” 모델.
  • activate는 dock 클릭 외에도 Spotlight에서 앱 검색 → 엔터 시에도 발생.
  • 모든 창이 있는데도 activate가 와서 굳이 새 창 만들 필요는 없다 — getAllWindows().length === 0 가드가 필수.

Insight — before-quit비동기를 지원하지 않는 함정

공식 문서엔 한 줄로 적혀 있지만 1만 번 밟힌 함정

before-quit 핸들러는 동기 함수만 허용한다. event.preventDefault()그 순간 부르지 않으면 종료가 진행된다. 그래서 비동기 dialog를 쓰려면 반드시 sync로 막은 다음 비동기 작업을 시작하고, 끝나면 app.exit()을 직접 호출하는 패턴이 표준이다.

let isQuitting = false;
app.on('before-quit', (event) => {
  if (isQuitting) return;            // 이미 처리 중이면 통과
  if (!hasUnsavedChanges()) return;
  event.preventDefault();             // 일단 막고
  showSaveDialog().then(result => {  // 비동기
    if (result === 'save') saveAndExit();
    else if (result === 'discard') { isQuitting = true; app.quit(); }
    // cancel이면 그냥 막혀 있음
  });
});

이 패턴 없이는 Cmd+Q를 누르는 순간 변경사항이 날아간다. VSCode·Slack도 같은 코드 구조.


다음 문서

  • 창 콘텐츠가 살아 있는 동안 무슨 일이 일어나는가 → 03 webContents.
  • 종료 시 쿠키·세션을 어떻게 정리하는가 → 04 session.

한 단락 요약

app 모듈의 라이프사이클은 ready → activate → window-all-closed → before-quit → will-quit → quit 의 시퀀스이고, 각 노드에서 OS마다 다르게 분기해야 한다. macOS의 “창이 다 닫혀도 앱은 산다”, Windows의 Squirrel 인스톨러 첫 실행 바로 종료, single-instance lock + second-instance argv 전달 이 세 가지가 실무에서 가장 자주 잊히는 세 가지다.