창은 화면이 아니라 애플리케이션의 단위다
웹 개발에서 화면은 보통 브라우저 탭 안의 페이지다. Electron에서는 화면이 운영체제의 창이다. 창은 크기, 위치, 아이콘, 제목 표시줄, 부모 창, 모달 동작, 표시 시점까지 직접 설계해야 하는 대상이다. 사용자는 Electron 앱을 웹사이트가 아니라 데스크톱 앱으로 대한다. 그래서 창이 깜빡이거나, 마지막 창을 닫았는데 앱이 예상과 다르게 남아 있거나, macOS에서 Dock 아이콘을 눌러도 창이 돌아오지 않으면 품질이 낮다고 느낀다.
Electron의 기본 구조에서 BrowserWindow는 main 프로세스에서 생성한다. 이 말은 창 관리가 renderer의 화면 상태와 분리되어야 한다는 뜻이다. React, Vue, Svelte 같은 프런트엔드 프레임워크는 창 안쪽의 UI를 다룬다. 반면 창 자체의 생성, 종료, 숨김, 다시 표시, 부모-자식 관계는 main 프로세스의 책임이다.
Electron에서 창을 다루는 기본 형태는 다음과 같다.
const path = require('node:path')
const { app, BrowserWindow } = require('electron')
let mainWindow
function createMainWindow() {
mainWindow = new BrowserWindow({
width: 1100,
height: 760,
minWidth: 720,
minHeight: 480,
backgroundColor: '#111827',
show: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
})
mainWindow.once('ready-to-show', () => {
mainWindow.show()
})
mainWindow.loadFile('index.html')
mainWindow.on('closed', () => {
mainWindow = null
})
return mainWindow
}여기서 중요한 점은 BrowserWindow가 단순히 HTML을 담는 컨테이너가 아니라는 것이다. 이 객체는 운영체제 창의 성격을 정한다. Electron 공식 API도 width, height, x, y, minWidth, minHeight, frame, icon, backgroundColor, show, parent, modal 같은 옵션을 제공한다.1
AI 생성 이미지
BrowserWindow 옵션으로 창의 첫인상을 정하기
창 옵션은 앱의 사용 맥락에 맞춰 선택해야 한다. 모든 앱에 같은 크기와 같은 프레임이 맞지는 않다.
function createMainWindow() {
const win = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 900,
minHeight: 600,
x: 80,
y: 60,
title: '작업 노트',
icon: path.join(__dirname, 'assets', 'icon.png'),
backgroundColor: '#ffffff',
show: false,
frame: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
})
return win
}width와 height는 처음 열리는 크기다. minWidth와 minHeight는 사용자가 창을 줄였을 때 UI가 깨지는 지점을 막는다. 데스크톱 앱에서는 창 크기를 사용자가 조절할 수 있으므로, 반응형 레이아웃과 최소 크기 제한을 함께 설계해야 한다.
x와 y는 창의 위치다. 일반 앱은 운영체제의 기본 배치에 맡기는 편이 낫다. 다만 보조 창이나 개발 도구 창처럼 위치 의도가 있는 경우에는 명시할 수 있다. 이전 실행 시점의 창 위치를 저장했다가 다음 실행 때 복원하는 패턴도 많이 쓴다. 이때는 저장된 좌표가 현재 모니터 밖에 있지 않은지 확인해야 한다. 노트북을 외부 모니터와 함께 쓰다가 모니터를 분리하면, 이전 좌표가 더 이상 유효하지 않을 수 있다.
frame은 운영체제가 제공하는 기본 제목 표시줄과 테두리를 사용할지 정한다. frame: false를 쓰면 완전히 커스텀 제목 표시줄을 만들 수 있다. 하지만 닫기, 최소화, 최대화, 드래그 영역, 접근성, 운영체제별 버튼 위치를 직접 처리해야 한다. 브랜드 경험이 중요한 앱이 아니라면 기본 프레임을 유지하는 선택이 더 안정적이다.
icon은 Windows와 Linux에서 창과 작업 표시줄에 드러나는 앱의 인상을 만든다. macOS에서는 앱 번들 아이콘과 Dock 아이콘 처리도 함께 고려해야 하므로 패키징 과정에서 다시 다룬다.
backgroundColor는 창이 처음 표시될 때의 배경색이다. renderer가 아직 CSS를 적용하기 전에도 운영체제 창은 먼저 나타날 수 있다. 앱의 실제 배경색과 가까운 색을 지정하면 흰색 화면이 순간적으로 보이는 현상을 줄일 수 있다. ready-to-show를 쓰는 앱에서도 배경색은 지정해 두는 편이 좋다. 아주 짧은 순간이라도 운영체제 창이 먼저 보일 수 있기 때문이다.
ready 이후에 창을 만들어야 하는 이유
Electron 앱은 app.whenReady() 이후에 창을 만드는 흐름을 기본으로 삼는다. Electron 초기화가 끝나기 전에 창이나 네이티브 기능을 다루면 플랫폼별로 예측하기 어려운 문제가 생길 수 있다. 공식 튜토리얼도 app.whenReady().then(createWindow) 구조를 기준으로 설명한다.2
app.whenReady().then(() => {
createMainWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createMainWindow()
}
})
})ready 이벤트와 whenReady()는 Electron이 앱 초기화를 마쳤다는 신호다. 실무에서는 콜백보다 Promise 형태인 whenReady()가 흐름을 읽기 좋다. 이 시점에 메인 창을 만들고, 메뉴를 구성하고, 창과 연결되는 작업을 시작할 수 있다.
다만 모든 초기화를 이 안에 몰아넣을 필요는 없다. 예를 들어 IPC 핸들러 등록처럼 창 생성 전에 준비되어야 하는 작업은 main 파일의 상단에서 먼저 선언해도 된다. 반대로 창이 있어야 의미가 있는 작업, 예를 들어 특정 창에 메시지를 보내거나 창 위치를 조정하는 작업은 whenReady() 이후로 미뤄야 한다.
마지막 창을 닫았을 때 앱을 끝낼 것인가
웹 앱에서는 탭을 닫으면 페이지가 사라진다. 데스크톱 앱에서는 창을 닫는 것과 앱을 종료하는 것이 항상 같지 않다. 이 차이는 운영체제 문화와 연결된다.
Windows와 Linux 사용자는 마지막 창을 닫으면 앱도 종료된다고 기대하는 경우가 많다. 반면 macOS에서는 창을 모두 닫아도 앱이 메뉴 막대와 Dock에 남아 있는 동작이 자연스럽다. 그래서 Electron 앱의 기본 생명주기 코드는 보통 다음 형태를 쓴다.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})window-all-closed는 모든 창이 닫혔을 때 발생한다. 이 이벤트를 직접 구독하면 앱을 종료할지 남겨 둘지 개발자가 결정한다. activate는 macOS에서 Dock 아이콘 클릭이나 앱 재활성화 상황에 대응할 때 중요하다.3
app.whenReady().then(() => {
createMainWindow()
app.on('activate', () => {
const hasWindow = BrowserWindow.getAllWindows().length > 0
if (!hasWindow) {
createMainWindow()
}
})
})이 코드는 macOS에서 모든 창이 닫힌 뒤 Dock 아이콘을 다시 눌렀을 때 새 창을 만든다. 이 처리가 없으면 앱은 실행 중인데 사용자에게 보이는 창이 없는 상태가 된다. 메뉴 막대에는 앱이 남아 있는데 아무 반응이 없다고 느낄 수 있다.
종료 흐름에서는 before-quit, will-quit, quit 같은 이벤트도 있다. 저장되지 않은 문서가 있는지 확인하거나, 백그라운드 작업을 정리하거나, 종료 직전 로그를 남겨야 할 때 사용한다. 단, Windows의 시스템 종료나 로그아웃 같은 상황에서는 일부 종료 이벤트가 발생하지 않을 수 있다. 따라서 중요한 데이터 저장은 종료 이벤트 하나에만 의존하지 말고, 사용자가 편집할 때 주기적으로 저장하는 구조가 안전하다.
ready-to-show로 첫 화면 깜빡임 줄이기
Electron 창을 만들고 바로 보여 주면 HTML, CSS, JavaScript가 준비되기 전의 중간 상태가 사용자에게 보일 수 있다. 빈 흰색 화면이 잠깐 보였다가 앱 화면으로 바뀌는 식이다. 이를 줄이기 위해 창을 숨긴 상태로 만들고, 렌더러가 표시 가능한 상태가 되었을 때 보여 주는 패턴을 쓴다.
const win = new BrowserWindow({
width: 1100,
height: 760,
backgroundColor: '#111827',
show: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
})
win.once('ready-to-show', () => {
win.show()
})
win.loadFile('index.html')show: false는 창을 생성하되 화면에는 표시하지 않는다. ready-to-show 이벤트가 발생하면 win.show()로 창을 표시한다. 이 방식은 초기 로딩 중의 시각적 흔들림을 줄이는 데 도움이 된다. 이벤트 핸들러는 보통 loadFile()이나 loadURL()을 호출하기 전에 붙여 둔다. 그래야 페이지가 빠르게 준비되는 경우에도 이벤트를 놓칠 가능성이 줄어든다.
하지만 항상 이 방식이 정답은 아니다. 앱 초기화가 오래 걸리면 사용자는 아무 창도 보지 못한 채 기다리게 된다. 복잡한 앱에서는 창을 즉시 보여 주고, backgroundColor를 앱 배경과 맞춘 뒤, 내부에 로딩 상태를 표시하는 편이 더 나을 수 있다. 즉 선택지는 두 가지다.
초기 화면이 빠르게 준비된다면
show: false와ready-to-show를 사용한다.초기 데이터 로딩이 오래 걸린다면 창은 먼저 보여 주고 앱 내부에서 로딩 상태를 명확히 보여 준다.
또 하나 주의할 점이 있다. paintWhenInitiallyHidden: false를 설정하면 숨겨진 상태에서 렌더러가 처음부터 그려지지 않을 수 있고, 이 경우 ready-to-show 이벤트가 발생하지 않는다. 처음에는 기본값을 유지하고, 성능 최적화가 필요할 때 문서를 확인하며 조정하는 편이 낫다.
부모 창, 모달 창, 설정 창 구성하기
데스크톱 앱은 하나의 메인 창만으로 끝나지 않는다. 설정 창, 정보 창, 파일 가져오기 창, 로그인 창처럼 목적이 다른 창이 필요하다. 이때 모든 창을 독립 창으로 만들면 사용자가 흐름을 놓치기 쉽다.
부모 창과 모달 창은 특정 작업이 메인 창에 종속되어 있음을 표현한다.
function createSettingsWindow(parent) {
const settingsWindow = new BrowserWindow({
width: 520,
height: 420,
parent,
modal: true,
show: false,
resizable: false,
title: '설정',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
})
settingsWindow.once('ready-to-show', () => {
settingsWindow.show()
})
settingsWindow.loadFile('settings.html')
return settingsWindow
}parent를 지정하면 자식 창은 부모 창 위에 머문다. modal: true를 함께 쓰면 사용자가 모달 창을 닫기 전까지 부모 창을 조작하지 못한다. 설정 저장 여부를 반드시 결정해야 하는 상황이나, 파일 덮어쓰기 확인처럼 사용자의 선택이 필요한 흐름에 적합하다.
반대로 설정 창이 앱 사용을 막을 필요가 없다면 모달로 만들지 않는 편이 낫다. 사용자가 설정 창을 옆에 둔 채 메인 화면을 확인할 수 있어야 하는 앱도 있다. 이 경우 parent만 지정하거나 독립 창으로 만들 수 있다.
숨김 창도 있다. 예를 들어 트레이 앱은 사용자가 닫기 버튼을 눌렀을 때 창을 파괴하지 않고 숨긴다.
let isQuitting = false
app.on('before-quit', () => {
isQuitting = true
})
mainWindow.on('close', (event) => {
if (!isQuitting) {
event.preventDefault()
mainWindow.hide()
}
})이 예제에서 isQuitting은 앱이 정말 종료되는 중인지 구분하기 위한 플래그다. 사용자가 창의 닫기 버튼을 누른 경우에는 창을 숨기고, 메뉴에서 종료를 선택하거나 app.quit()이 호출된 경우에는 정상적으로 종료되게 만든다.
이 패턴은 메신저, 동기화 도구, 백그라운드 유틸리티에 어울린다. 하지만 문서 편집기나 노트 앱에서 무조건 숨김 처리하면 사용자가 앱이 종료된 것으로 오해할 수 있다. 창을 숨기는 앱이라면 트레이 아이콘, 메뉴, 알림 등으로 앱이 계속 실행 중임을 알려야 한다.
개발 모드와 배포 모드의 로드 분기
개발 중에는 Vite, webpack dev server, Next.js 개발 서버처럼 로컬 개발 서버에서 renderer를 불러오는 경우가 많다. 배포된 앱에서는 외부 개발 서버가 없으므로 패키지 안의 HTML 파일을 로드해야 한다. 이 분기를 main 프로세스에 명확히 두면 개발과 배포 흐름이 섞이지 않는다.
function loadRenderer(win) {
const devServerUrl = process.env.ELECTRON_RENDERER_URL
if (!app.isPackaged && devServerUrl) {
win.loadURL(devServerUrl)
win.webContents.openDevTools({ mode: 'detach' })
return
}
win.loadFile(path.join(__dirname, 'renderer', 'index.html'))
}app.isPackaged는 현재 앱이 패키징된 상태인지 확인할 때 쓸 수 있다. 개발 모드에서는 http://localhost:5173 같은 URL을 로드하고, 배포 모드에서는 빌드 결과물의 index.html을 로드한다.
프런트엔드 프레임워크를 붙일 때도 원리는 같다. React나 Vue는 renderer의 UI를 만들고, Electron main은 창과 운영체제 기능을 관리한다. 개발 서버 주소를 직접 코드에 고정하기보다 환경 변수로 주입하면 도구를 바꾸거나 포트를 변경할 때 수정 범위가 줄어든다.
app.whenReady().then(() => {
const win = createMainWindow()
loadRenderer(win)
})이 구조에서는 창 생성과 renderer 로딩을 분리한다. 창 옵션을 바꾸는 코드와 개발 서버 분기 코드가 뒤섞이지 않아 테스트와 유지보수가 쉬워진다. 이 방식을 쓰려면 createMainWindow() 안에서 loadFile()을 이미 호출하지 않도록 정리해야 한다. 한 곳에서는 창을 만들고, 다른 한 곳에서는 무엇을 로드할지 결정하는 식으로 책임을 나누는 것이다.
창 생명주기 설계 체크리스트
Electron 앱의 창 관리는 기능 구현이 아니라 사용자의 기대를 맞추는 작업이다. 다음 기준을 먼저 정해 두면 이후에 IPC, 파일 저장, 메뉴, 트레이를 붙일 때 구조가 흔들리지 않는다.
메인 창은
app.whenReady()이후에 만든다.마지막 창을 닫았을 때 Windows와 Linux에서는 종료하고, macOS에서는 앱을 남길지 검토한다.
macOS에서는
activate이벤트에서 창을 다시 만들거나 보여 주는 흐름을 준비한다.첫 화면 깜빡임이 보이면
show: false와ready-to-show를 사용한다.초기 로딩이 길면 창을 먼저 보여 주고 내부 로딩 UI를 제공한다.
설정 창과 확인 창은
parent와modal여부를 사용자의 작업 흐름에 맞춰 선택한다.숨김 창 패턴은 트레이나 백그라운드 실행처럼 사용자가 이해할 수 있는 단서와 함께 사용한다.
개발 모드에서는
loadURL, 배포 모드에서는loadFile을 사용하도록 분기한다.
이 장의 목표는 창을 띄우는 법을 외우는 것이 아니다. 어떤 시점에 창을 만들고, 언제 보여 주며, 닫기와 종료를 어떻게 구분할지 직접 결정하는 것이다. 이 기준이 잡히면 Electron 앱은 브라우저 안의 웹 페이지가 아니라 운영체제와 함께 동작하는 데스크톱 앱에 가까워진다.
참고
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.