Electron ์‹œ์ž‘ํ•˜๊ธฐ (1) ๐Ÿ™‹โ€โ™€๏ธ

๋ฐ•ํฌ์ˆ˜ยท2024๋…„ 4์›” 29์ผ
0
post-thumbnail

์ด๋ฒˆ ํฌ์ŠคํŠธ๋Š”, ์‹ค์ œ ์‹คํ–‰ํ•˜๋Š” ๋ชจ์Šต์ด ์•„๋‹Œ electron์„ ์‚ฌ์šฉํ•  ๋•Œ ์ดํ•ดํ•˜๊ณ  ์žˆ์–ด์•ผ ํ•˜๋Š” ๊ธฐ๋ณธ์ ์ธ ๊ฐœ๋…๋“ค์— ๋Œ€ํ•ด ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

Electron์ด๋ž€?

Electron์€ HTML, CSS, Javascript๋ฅผ ์‚ฌ์šฉํ•ด desktop application์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ํ”„๋ ˆ์ž„์›Œํฌ ์ž…๋‹ˆ๋‹ค. Chromium๊ณผ Node.js๋ฅผ single binary file์— ์ถ”๊ฐ€ํ•จ์œผ๋กœ์จ, Electron์€ Javascript ์ฝ”๋“œ ๋ฒ ์ด์Šค๋กœ Windows, macOS, Linux์—์„œ ๋™์ž‘ํ•˜๋Š” cross-platfrom-apps๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค.

์ด๋Ÿฐ ์ •์˜๋ฅผ ๊ฐ€์ง„ Electron์€ Javascript ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ React, Nextjs์™€๋„ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๋˜ํ•œ Next์™€ Electron์„ ๋” ์œ ์—ฐํ•˜๊ฒŒ ๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋„๊ตฌ๋กœ Nextron์ด ์กด์žฌํ•˜๋Š”๋ฐ, ์šฐ๋ฆฌ์˜ ํ”„๋กœ์ ํŠธ๋Š” React + Electron์„ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

1. ์ผ๋ ‰ํŠธ๋ก ์€ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ• ๊นŒ? ๐Ÿค”

์ผ๋ ‰ํŠธ๋ก ์€ ๋ฉ”์ธ(main)ํ”„๋กœ์„ธ์Šค์™€ ๋ Œ๋”๋Ÿฌ(renderer)ํ”„๋กœ์„ธ์Šค, ๋‘๊ฐ€์ง€ ํ”„๋กœ์„ธ์Šค๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.
์ผ๋ ‰ํŠธ๋ก  ์•ฑ์€ ๋‹จ ํ•˜๋‚˜์˜ ๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ฐ€์ง€๊ณ , ๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค๋Š” Node.js ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘ํ•˜๋ฉฐ ๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค์—์„œ๋Š” ๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค๋“ค์„ ๊ด€๋ฆฌํ•˜๊ณ  ๊ฐ๊ฐ์˜ ๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค๋Š” ์„œ๋กœ ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค์™€ ๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค ๊ฐ„์— ํ†ต์‹ ์ด ์ด๋ค„์ ธ์•ผ ํ•˜๋Š”๋ฐ, ์ผ๋ ‰ํŠธ๋ก ์—์„œ๋Š” ์ด๋ฅผ ipcMain๊ณผ ipcRenderer์™€ ๊ฐ™์€ IPC ๋ชจ๋“ˆ์„ ํ†ตํ•ด ํ”„๋กœ์„ธ์Šค ๊ฐ„์˜ ํ†ต์‹ ์„ ์ด๋ฃจ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.


2. โ›… main.js ํŒŒ์ผ์˜ ์—ญํ• 

์šฐ๋ฆฌ๋Š” ํ”„๋กœ์ ํŠธ์˜ ๋ฃจํŠธ ํด๋”์— main.js๋ผ๋Š” ํŒŒ์ผ์„ ๋งŒ๋“ค์—ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ฉ๋‹ˆ๋‹ค.
electron์„ ์„ค์น˜ํ•œ ํ›„, package.json์—์„œ "main":"main.js"๋ผ๋Š” ์ฝ”๋“œ๋ฅผ ๋„ฃ์–ด์ค„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๋Š” main.js๋Š” ๋ชจ๋“  Electron ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ entry point๋ผ๋Š” ๊ฒƒ์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.

์ด ์Šคํฌ๋ฆฝํŠธ๋Š” Node.js ํ™˜๊ฒฝ์—์„œ ์‹คํ–‰๋˜๋Š” main process๋ฅผ ์ œ์–ดํ•˜๋ฉฐ ์•ฑ์˜ lifecycle ์ œ์–ด, ๊ธฐ๋ณธ ์ธํ„ฐํŽ˜์ด์Šค ํ‘œ์‹œ, ๊ถŒํ•œ์ด ์žˆ๋Š” ์ž‘์—… ์ˆ˜ํ–‰ ๋ฐ render processes ๊ด€๋ฆฌ๋ฅผ ํ•ฉ๋‹ˆ๋‹ค.

main.js์— ์ฝ”๋“œ๋ฅผ ์•„๋ž˜์ฒ˜๋Ÿผ ์ž…๋ ฅํ–ˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ด ๋ด…์‹œ๋‹ค.

// main.js 
console.log(`Hello from Electron ๐Ÿ˜‹`);

์ผ๋ ‰ํŠธ๋ก ์˜ main process๋Š” Node.js runtime์ด๋ฏ€๋กœ electron ๋ช…๋ น์œผ๋กœ ์ž„์˜์˜ Node.js ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. electron์„ ์‹คํ–‰์‹œํ‚จ ํ›„, ํ„ฐ๋ฏธ๋„์— Hello from Electron ๐Ÿ˜‹์ด ์ถœ๋ ฅ๋๋‹ค๋ฉด, main process entry point๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์„ฑ๋˜์—ˆ๋‹ค๊ณ  ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


โญ electron์—์„œ each window๋Š” local HTML ํŒŒ์ผ ๋˜๋Š” remote ์›น ์ฃผ์†Œ์—์„œ ๋กœ๋“œํ•  ์ˆ˜ ์žˆ๋Š” ์›นํŽ˜์ด์ง€๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.
ํ”„๋กœ์ ํŠธ์— index.htmlํŒŒ์ผ์ด ์žˆ๋‹ค๋Š” ๊ฐ€์ • ํ•˜์— main.js ํŒŒ์ผ์˜ ๋‚ด์šฉ์„ ์•„๋ž˜์ฒ˜๋Ÿผ ๋ฐ”๊พผ๋‹ค๊ณ  ์ƒ๊ฐํ•ด ๋ด…์‹œ๋‹ค.

// main.js
const { app, BrowserWindow } = require('electron');

const createWindow = () => {
	const win = new BrowserWindow({
    	width : 800,
        height : 500,
    })
    
    win.loadFile('index.html');
}

app.whenReady().then(() => {
	createWindow()
})

์ฒซ๋ฒˆ์งธ ์ค„์—์„œ๋Š” CommonJS ๋ชจ๋“ˆ ๊ตฌ๋ฌธ์„ ์‚ฌ์šฉํ•ด ๋‘ ๊ฐœ์˜ Electron ๋ชจ๋“ˆ์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

app : application์˜ lifecycle๋ฅผ ์ œ์–ดํ•ฉ๋‹ˆ๋‹ค.
BrowserWindow : app windows๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
๐Ÿšฉ ECMAScript ๋ชจ๋“ˆ(import๊ตฌ๋ฌธ)์€ ํ˜„์žฌ Electron์—์„œ ์ง์ ‘ ์ง€์›๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์œ„ ์ฝ”๋“œ์—์„œ ๋งŒ๋“  createWindow() ํ•จ์ˆ˜๋Š” ์›น ํŽ˜์ด์ง€๋ฅผ ์ƒˆ BrowserWindow ์ธ์Šคํ„ด์Šค๋กœ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค. BrowserWindows๋Š” ์•ฑ ๋ชจ๋“ˆ์˜ ready ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•œ! ํ›„์—๋งŒ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด์ œ, electron์„ ์‹คํ–‰ํ•˜๋ฉด ์›น ํŽ˜์ด์ง€๋ฅผ ํ‘œ์‹œํ•˜๋Š” ์ฐฝ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์—ด๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
์•ฑ์ด ์ฐฝ์— ํ‘œ์‹œํ•˜๋Š” ๊ฐ ์›นํŽ˜์ด์ง€๋Š” renderer process๋ผ๋Š” ๋ณ„๋„์˜ ํ”„๋กœ์„ธ์Šค์—์„œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

โœ” ์ด๋•Œ, renderer process๋Š” ์ผ๋ฐ˜์ ์ธ ํ”„๋ก ํŠธ์—”๋“œ ์›น ๊ฐœ๋ฐœ์— ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๊ณผ ๋™์ผํ•œ JavaScript api ๋ฐ ๋„๊ตฌ์— ์•ก์„ธ์Šค ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

2-1. ๐ŸŒผ main.js์—์„œ ์•ฑ ์ฐฝ์˜ ์ˆ˜๋ช…์ฃผ๊ธฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋‹ค,

application windows๋Š” ์šด์˜์ฒด์ œ ๋งˆ๋‹ค ๋‹ค๋ฅด๊ฒŒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ์ ์œผ๋กœ ์ด๋Ÿฌํ•œ ๊ทœ์น™์„ ์ ์šฉํ•˜๋Š” ๋Œ€์‹  ์ผ๋ ‰ํŠธ๋ก ์€ ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋ฅผ ๋”ฐ๋ฅด๊ณ ์ž ํ•  ๊ฒฝ์šฐ ์•ฑ ์ฝ”๋“œ์—์„œ ์ด๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ์„ ํƒ๊ถŒ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

์•ฑ ๋ฐ, BrowserWindow ๋ชจ๋“ˆ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•˜์—ฌ ๊ธฐ๋ณธ ์ฐฝ ๊ทœ์น™์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

electron์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ํ”Œ๋žซํผ ์ข…๋ฅ˜

 - win32(Windows)
 - linux(Linux)
 - darwin(macOS)

window ๋ฐ linux์—์„œ ๋ชจ๋“  ์ฐฝ์„ ๋‹ซ์œผ๋ฉด ์ผ๋ฐ˜์ ์œผ๋กœ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์ด ์™„์ „ํžˆ ์ข…๋ฃŒ๋ฉ๋‹ˆ๋‹ค.

์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“œ๋Š” ์ผ๋ ‰ํŠธ๋ก  ์•ฑ์—์„œ ์ด ํŒจํ„ด์„ ๊ตฌํ˜„ํ•˜๋ ค๋ฉด main.js์—์„œ window-all-closed ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•˜๊ณ  app.quit()๋ฅผ ํ˜ธ์ถœํ•ด ์‚ฌ์šฉ์ž๊ฐ€ ์•ฑ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.

app.on('window-all-closed', () => {
	if (process.platform !== 'darwin') app.quit()
})

์œ„์˜ ์ฝ”๋“œ๋ฅผ main.js์— ์ž…๋ ฅํ•˜๋ฉด ์•ฑ์„ ๋‹ซ์•˜์„ ๋•Œ ํ”„๋กœ๊ทธ๋žจ์ด ์ข…๋ฃŒ๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


๐Ÿšฉ ๋ฐ˜๋Œ€๋กœ macOS ์•ฑ์€ ์ผ๋ฐ˜์ ์œผ๋กœ ์ฐฝ์ด ์—ด๋ฆฌ์ง€ ์•Š์•„๋„ ๊ณ„์† ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ฐฝ์ด ์—†์„ ๋•Œ ์•ฑ์„ ํ™œ์„ฑํ™” ํ•˜๋ฉด ์ƒˆ ์ฐฝ์ด ์—ด๋ฆฝ๋‹ˆ๋‹ค.

์ด ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋ ค๋ฉด ์•ฑ ๋ชจ๋“ˆ์˜ ํ™œ์„ฑํ™” ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•˜๊ณ  ์—ด๋ ค ์žˆ๋Š” BrowserWindows๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ๊ธฐ์กด createWindow() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

app.whenReady().then(() => {
	createWindow()
    
    app.on('activate', () => {
    	if (BrowserWinodw.getAllWindows().length === 0) createWindow()
    })
})

์œ„ ์ฝ”๋“œ๋ฅผ main.js์— ์ถ”๊ฐ€ํ•˜๋ฉด ์ด์ œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ฐฝ์ด ์—†์„ ๋•Œ, ์•ฑ์„ ํ™œ์„ฑํ™” ํ•˜๋ฉด ์ƒˆ ์ฐฝ์ด ์—ด๋ฆฌ๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


โญ 2-1์˜ ๋‚ด์šฉ๋“ค์„ ๋ฐ”ํƒ•์œผ๋กœ ๊ธฐ๋ณธ์ ์ธ main.js์˜ ๊ตฌ์„ฑ์„ ๋ณด์ž๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

const { app, BrowserWindow } = require('electron')

const createWindow = () => {
	const win = new BrowserWindow({
    	width : 800, 
        height : 600
    })
    
    win.loadFile('index.html')
}

app.whenReady().then(() => {
	createWindow()
    
    app.on('activate', () => {
    	if (BrowserWinodw.getAllWindows().length === 0) {
        	createWindow()
        }
    })
})

app.on('window-all-closed', () => {
	if (process.platform !== 'darwin') {
    	app.quit()
    }
})

3. PreloadScript๋ฅผ ์‚ฌ์šฉํ•ด๋ณด๊ธฐ

๐Ÿ”บ preload script๋ž€ ?
์ผ๋ ‰ํŠธ๋ก ์˜ ๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค๋Š” ์ „์ฒด ์šด์˜์ฒด์ œ ์ ‘๊ทผ ๊ถŒํ•œ์ด ์žˆ๋Š” Node.js ํ™˜๊ฒฝ์ž…๋‹ˆ๋‹ค. ์ผ๋ ‰ํŠธ๋ก  ๋ชจ๋“ˆ ์™ธ์—๋„ Node.js ๋‚ด์žฅ ๋ฐ npm์„ ํ†ตํ•ด ์„ค์น˜๋œ ๋ชจ๋“  ํŒจํ‚ค์ง€์— ์—‘์„ธ์Šค ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๋ฐ˜๋ฉด, ๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค๋Š” ์›น ํŽ˜์ด์ง€๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ๋ณด์•ˆ์ƒ์˜ ์ด์œ ๋กœ ๊ธฐ๋ณธ์ ์œผ๋กœ Node.js๋ฅผ ์‹คํ–‰ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
๊ทธ๋ž˜์„œ ์ผ๋ ‰ํŠธ๋ก ์˜ ์„œ๋กœ ๋‹ค๋ฅธ ํ”„๋กœ์„ธ์Šค(๋ฉ”์ธ, ๋ Œ๋”๋Ÿฌ) ์œ ํ˜•์„ ํ•จ๊ป˜ ์—ฐ๊ฒฐํ•˜๋ ค๋ฉด preload๋ผ๋Š” ํŠน์ˆ˜ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
-> ์ฆ‰, Node.js๋ฅผ ๋ Œ๋”๋Ÿฌ์—์„œ ์ง์ ‘์ ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— preload๋ฅผ ํ†ตํ•ด ๋ฐ›์•„์™€ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด๋ผ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

preload scripts๋Š” ์›นํŽ˜์ด์ง€๊ฐ€ renderer์— load ๋˜๊ธฐ ์ „์— ์ฃผ์ž…๋ฉ๋‹ˆ๋‹ค.
์ด ๋•Œ, ๋ Œ๋”๋Ÿฌ์— ํ•„์š”ํ•œ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด preload์—์„œ contextBridge api๋ฅผ ํ†ตํ•ด global objects๋ฅผ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


๐Ÿšฉ preload experience

preload.js ํŒŒ์ผ์„ ๋ฃจํŠธ ํด๋”์— ์ƒ์„ฑ ํ›„, ์•„๋ž˜์ฒ˜๋Ÿผ Chrome, Node ๋ฐ Electron ๋ฒ„์ „์„ renderer์— ๋…ธ์ถœํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

// preload.js

const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('versions', {
	node : () => process.versions.node,
    chrome : () => process.versions.chrome,
    electron : () => process.versions.electron,
})

preload ์Šคํฌ๋ฆฝํŠธ๋ฅผ renderer process์— ์—ฐ๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„  main.js์˜ BrowserWInodw ์ƒ์„ฑ์ž์˜ webPreferences.preload ์˜ต์…˜์— ์ „๋‹ฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

// main.js
const { app, BrowerWindow } = require('electron')
const path = require('path');

const createWindow = () => {
	const win = new BrowserWindow({
    	width : 800,
        height : 600,
        webPreferences : {
        	preload : path.join(__dirname, 'preload.js'),
        },
    })
   	win.loadFie('index.html')
}

app.whenReady().then(() => {
	createWinow()
})

- __dirname : ๋ฌธ์ž์—ด์€ ํ˜„์žฌ ์‹คํ–‰ ์ค‘์ธ ์Šคํฌ๋ฆฝํŠธ์˜ ๊ฒฝ๋กœ๋ฅผ ๊ฐ€๋ฆฌํ‚ต๋‹ˆ๋‹ค.
- path.join : ์—ฌ๋Ÿฌ ๊ฒฝ๋กœ๋ฅผ ๊ฒฐํ•ฉํ•ด ๊ฒฐํ•ฉ๋œ ๊ฒฝ๋กœ ๋ฌธ์ž์—ด์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

์ด์ œ renderer.js๋ผ๋Š” ํŒŒ์ผ์ด ์žˆ๋‹ค๋Š” ๊ฐ€์ • ํ•˜์— ๋ฒ„์ „ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!

// renderer.js

const information = document.getElementById('info')
information.innerText = `This app is using Chrome (v${versions.chrome()}), Node.js (v${versions.node()}), and Electron (v${versions.electron()})`

์ด์ œ, index.html์— renderer ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋„ฃ์œผ๋ฉด ์•ฑ ์‹คํ–‰์‹œ ๋ฒ„์ „ ์ •๋ณด๊ฐ€ ๋‚˜์˜ค๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

4. ํ”„๋กœ์„ธ์Šค ๊ฐ„ ํ†ต์‹  ๐Ÿ”ฅ

์œ„์—์„œ ๋งํ•œ ๋ฐ”์™€ ๊ฐ™์ด, ์ผ๋ ‰ํŠธ๋ก ์˜ ๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค์™€ ๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ์—ญํ• ์„ ํ•˜๋ฉฐ ์ƒํ˜ธ ๊ตํ™˜ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

๋‹ค์‹œ ํ•œ ๋ฒˆ ์ •๋ฆฌํ•˜์ž๋ฉด renderer process์—์„œ ์ง์ ‘ node.js api์— ์•ก์„ธ์Šคํ•˜๊ฑฐ๋‚˜ main process์—์„œ HTML DOM์— ์•ก์„ธ์Šคํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

๐ŸŽ ์ด ๋ฌธ์ œ์— ๋Œ€ํ•œ ํ•ด๊ฒฐ์ฑ…์€ ํ”„๋กœ์„ธ์Šค ๊ฐ„ ํ†ต์‹ (IPC)๋ฅผ ์œ„ํ•ด ์ผ๋ ‰ํŠธ๋ก ์˜ ipcMin ๋ฐ ipcRenderer ๋ชจ๋“ˆ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์›น ํŽ˜์ด์ง€์—์„œ main process๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด๋ ค๋ฉด ipcMain.handle์„ ์‚ฌ์šฉํ•ด main process handler๋ฅผ ์„ค์ •ํ•œ ๋‹ค์Œ ipcRenderer.invoke๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•˜์—ฌ preload script์—์„œ handler๋ฅผ trigger ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
-> ์ €๊ฐ™์€ ๊ฒฝ์šฐ handler๋กœ ํ•จ์ˆ˜๋ฅผ ๋“ฑ๋กํ•˜๊ณ  ์ด๋ฅผ preload์—์„œ ํ˜ธ์ถœํ•œ๋‹ค๊ณ  ์ดํ•ดํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ์•„๋ž˜์™€ ๊ฐ™์ด renderer, preload, main ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์„ค์ •ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•ด๋ด…์‹œ๋‹ค.
// renderer.js
const func = async () => {
	const response = await window.versions.ping()
    console.log(response) // prints out 'pong'
}

func()

// preload.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('versions', {
	node : () => process.versions.node,
    chrome : () => process.versions.chrome,
    ping : () => ipcRenderer.invoke('์‹คํ–‰'),
    electron : () => process.versions.electron,
})

// main.js

const createWindow = () => {
	const win = new BrowserWindow({
    	width : 800,
        height : 600,
        webPreferences : {
        	preload : path.join(__dirname, 'preload.js'),
        },
     })
     ipcMain.handle('์‹คํ–‰', () => '์‹คํ–‰!')
     win.loadFile('index.html')
}

์šฐ๋ฆฌ๋Š” main์—์„œ '์‹คํ–‰'์ด๋ผ๋Š” ์ฑ„๋„๋กœ ํ•จ์ˆ˜๋ฅผ ๋“ฑ๋กํ•˜๊ณ , preload์—์„œ '์‹คํ–‰'์ด๋ผ๋Š” ์ฑ„๋„์„ ํ˜ธ์ถœํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด๋ฅผ renderer ์Šคํฌ๋ฆฝํŠธ์—์„œ ๋ถˆ๋Ÿฌ์™€ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
-> ์ด๋ ‡๊ฒŒ ๋ Œ๋”๋Ÿฌ์—์„œ node.js api๋ฅผ ์ง์ ‘ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹Œ, preload๋ฅผ ํ†ตํ•ด ๋ฉ”์ธ๊ณผ ๋ Œ๋”๋Ÿฌ๊ฐ€ ํ†ต์‹ ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

5. context-isolated process โš’

์šฐ๋ฆฌ๊ฐ€ electron์˜ ์„œ๋กœ ๋‹ค๋ฅธ ํ”„๋กœ์„ธ์Šค์˜ ํ†ต์‹  ๋ฐฉ๋ฒ•์„ ์™„๋ฒฝํžˆ ํŒŒ์•…ํ•˜๊ธฐ ์œ„ํ•ด์„ , preload script๋ฅผ ์‚ฌ์šฉํ•ด Node.js๋ฅผ import ํ•ด์˜ค๋Š” ๊ฒƒ๊ณผ context-isolated renderer process์—์„œ electron ๋ชจ๋“ˆ์„ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์„ ์ดํ•ดํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

1) Renderer to main example

renderer process์—์„œ main process๋กœ ๋‹จ๋ฐฉํ–ฅ IPC ๋ฉ”์‹œ์ง€๋ฅผ ์‹คํ–‰ํ•˜๋ ค๋ฉด ipcRenderer.send API๋ฅผ ์‚ฌ์šฉํ•ด ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ธ ๋‹ค์Œ ipcMain.on api์—์„œ ์ˆ˜์‹ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์•„๋ž˜ ์ฝ”๋“œ๋Š” ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹ ์˜ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

// renderer.js

const seetButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
	const title = titleInput.value
    window.electronAPI.setTitle(title)
})

// preload.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
	setTitle : title => ipcRenderer.send('set-title', title),
})

// main.js
const { app, BrowserWindow, ipcMain } = require("electron")
const path = require("path")

function createWindow() {
  const mainWindow = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
    },
  })

  ipcMain.on("set-title", (event, title) => {
    const webContents = event.sender
    const win = BrowserWindow.fromWebContents(webContents)
    win.setTitle(title)
  })

  mainWindow.loadFile("index.html")
}

app.whenReady().then(() => {
  createWindow()

  app.on("activate", function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

app.on("window-all-closed", function () {
  if (process.platform !== "darwin") app.quit()
})

2) Renderer to main example

์–‘๋ฐฉํ–ฅ IPC์˜ ์ผ๋ฐ˜์ ์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ renderer process ์ฝ”๋“œ์—์„œ main process ๋ชจ๋“ˆ์„ ํ˜ธ์ถœํ•˜๊ณ  ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ด๋Š” ipcMain.handle๊ณผ ์Œ์„ ์ด๋ฃจ๋Š” ipcRenderer.invoke๋ฅผ ์‚ฌ์šฉํ•ด ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์•„๋ž˜ ์ฝ”๋“œ๋Š” ์–‘๋ฐฉํ–ฅ ํ†ต์‹ ์˜ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

// renderer.js
const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')

btn.addEventListener('click', async () => {
	const filePath = await window.eletronAPI.openFile()
    filePathElement.innerText = filePath
})

// preload.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('eletronAPI', {
	openFile : () => ipcRenderer.invoke('dialog:openFile),
})

// main.js
const { app, BrowserWindow, ipcMain, dialog } = require("electron")
const path = require("path")

async function handleFileOpen() {
  const { canceled, filePaths } = await dialog.showOpenDialog()
  if (canceled) {
    return
  } else {
    return filePaths[0]
  }
}

function createWindow() {
  const mainWindow = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
    },
  })
  mainWindow.loadFile("index.html")
}

app.whenReady().then(() => {
  ipcMain.handle("dialog:openFile", handleFileOpen)
  createWindow()

  app.on("activate", function () {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow()
    }
  })
})

app.on("window-all-closed", function () {
  if (process.platform !== "darwin") app.quit()
})

3) Main to renderer example

๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค์—์„œ ๋ Œ๋”๋Ÿฌ ํ”„๋กœ์„ธ์Šค๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ผ ๋•Œ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ›๋Š” ๋ Œ๋”๋Ÿฌ๋ฅผ ์ง€์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ฉ”์‹œ์ง€๋Š” WbContents ์ธ์Šคํ„ด์Šค๋ฅผ ํ†ตํ•ด renderer process๋กœ ์ „์†ก๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ด WebContents ์ธ์Šคํ„ด์Šค์—๋Š” ipcRenderer.send์™€ ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” send ๋ฉ”์„œ๋“œ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

// main.js
const { app, BrowserWindow, Menu, ipcMain } = require('electron')
const path = require('path')

function createWindow() {
	const mainWindow = new BrowserWindow({
    	webPreferences : {
        	preload : path.join(__dirname, 'preload.js'),
        },
    })
    
    const memu = Memu.buildFromTemplate([
    	{
        	label : app.name,
            submenu : [
            	{
                  click : () => mainWindow.webContents.sned('update-counter', 1),
                  label : 'Increment',
                },
                {
                  click : () => mainWindow.webContents.send('update-counter', -1),
                  label : 'Decrement',
                }
              ]
            },
	])
    
    Memu.setAppicationMemu(menu)
    mainWindow.loadFile('index.html')
    
    // Open the DevTools
    mainWindow.webContents.openDevTools() // ์›น์ฒ˜๋Ÿผ ์•ฑ์—์„œ๋„ ๊ฐœ๋ฐœ์ž ์ฐฝ์„ ์—ด์–ด์ค€๋‹ค.
    
app.whenReady().then(() => {
	ipcMain.on('counter-value', (_event, value) => {
    	console.log(value)
    })
    createWindow()
    
    app.on('activate', function () {
    	if (BrowserWindow.getAllWindows().length === 0) createWindow()
    })
})

app.on('window-all-closed', function () {
	if (process.platform !== 'darwin') app.quit()
})

// preload.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
	handlerCounter : callback => ipcRenderer.on('update-counter', callback)
})

์—ฌ๊ธฐ๊นŒ์ง€, ์šฐ๋ฆฌ๊ฐ€ ์ผ๋ ‰ํŠธ๋ก ์„ ์‚ฌ์šฉํ•  ๋•Œ ์•Œ์•„๋‘ฌ์•ผ ํ•  ์ค‘์š”ํ•œ ๊ฐœ๋…์— ๋Œ€ํ•œ ์ •๋ฆฌ๋ฅผ ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ํฌ์ŠคํŠธ์—๋Š” React์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•œ ์‹ค์ œ ์‹คํ–‰ ๊ณผ์ •์„ ๋‹ด์•„๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.โ˜บ

์ถœ์ฒ˜ :
https://tech.kakao.com/2021/08/17/frontend-growth-11/
https://www.electronjs.org/docs/latest/
https://sonicce99.github.io/electron/#learning-goals-1

profile
ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž์ž…๋‹ˆ๋‹ค :)

0๊ฐœ์˜ ๋Œ“๊ธ€