이전 포스팅에서 일렉트론이 무엇인지에 대해 간략하게 알아보았습니다.
1. 데스크탑 애플리케이션 프레임워크, 일렉트론에 대해 알아보자.
시작하며 지금까지 일렉트론을 통해 pc앱 개발/빌드/유지보수를 해오면서 개인적으로 공부도 하고 정리도 하고 회사 내에서 간간이 세미나도 진행해 왔다. 하지만 돌아보면 A to Z의 글로 흐름 있
todayscoding.tistory.com
이번에는 일렉트론의 프로세스 구조가 어떻게 되어있는지, 프로세스 간 통신을 어떻게 하는지에 대해 좀 더 자세히 알아보겠습니다.
일렉트론에서 가장 중요한 개념은 ‘격리’ 와 ‘통신’이다.
프로세스 구조를 모른 채로 Electron을 활용해서 개발을 하다 보면같은 js로 짜인 코드 간의 호출인데도 '왜 될 거 같은데 안되지?', '값을 넘겼는데 왜 못 받지?' 등 알 수 없는 미궁 속에 빠지기 쉽고 헤어 나오기 쉽지 않습니다. (아무 지식이 없이 현업에서 Electron 으로 구성된 소스의 QA 업무를 맡게 되었던 제가 그랬습니다..ㅜ) 그렇기 때문에 일렉트론을 활용하기로 결정했다면, 빨리 무언가를 만들어야겠다는 생각 이전에 프로세스 구조에서 가장 크게 나타나는 특징인 '격리'와 '통신'에 대해 충분히 이해하고 숙지하고 넘어가는 것을 권합니다.
- Process Model
- Context Isolation
- Inter-Process Communication
1. Process Model (프로세스 모델)
Electron은 크롬과 같은 크로미움이라는 엔진을 내장하고 있습니다. 그렇기 때문에 Chromium의 다중 프로세스 아키텍처를 상속합니다. Electron을 이해하기 위해 같은 엔진을 쓰는 크롬을 비유하면 더 이해가 쉬울 것 같아 크롬 브라우저에 비유를 해보겠습니다.
사실 브라우저는 엄청나게 복잡한 애플리케이션입니다. 웹 콘텐츠를 표시하는 기본 기능 외에도 타사 프로그램을 로드하는데에서 오는 책임, 여러 페이지를 탭을 이용해 로드하고 관리하는 것 등과 같은 많은 부차적인 책임이 있습니다. 이전에는 브라우저가 일반적으로 이 모든 기능에 대해 단일 프로세스를 사용했다고 합니다. 이렇게 단일 프로세스를 사용하는 패턴은 열려 있는 각 탭의 오버헤드 (어떤 처리를 하기 위해 들어가는 간접적인 처리 시간 · 메모리)가 적다는 것을 의미하지만, 열려있는 탭 중의 웹 사이트 하나가 충돌하거나 응답하지 않으면 전체 브라우저에 영향을 미치게 됩니다.
그렇다면 그 반대인 다중 프로세스 아키텍처의 이점은 무엇일까요? 위 사진에서 탭마다 렌더러 프로세스를 하나 사용하는 경우를 생각해 봅시다. 4개의 탭이 열려 있고 각 탭은 독립적인 렌더러 프로세스에 의해 실행됩니다. 이때 'iyos'라는 탭이 응답하지 않으면 그 탭만 닫고 실행 중인 다른 탭 '구글 검색'으로 이동할 수 있습니다. 만약 모든 탭이 하나의 프로세스에서 실행 중이었다면, 탭이 하나만 응답하지 않아도 모든 탭이 응답하지 못하게 되었겠죠. 크롬은 이렇게 단일 프로세스를 사용하는 데에서 오는 문제를 해결하기 위해 ‘다중 프로세스 모델’을 도입했고, 일렉트론이 그 프로세스 모델을 상속받고 있다고 생각하면 이해가 쉽습니다.
이 개념을 그대로 일렉트론의 다중 프로세스 모델을 잘 활용 중인 한 서비스를 예로 보겠습니다 :)
채팅창에서 오류가 발생하여 응답하지 않는 상태가 된다고 해도, 채팅 목록에서의 동작조차 멈추지는 않습니다. 이렇게 각각의 창마다 띄워지는 프로세스를 단일로 사용하지 않고, 영향을 주지 않도록 다중으로 사용하는 것도 일렉트론의 다중프로세스 모델을 이용했기 때문이라고 생각할 수 있습니다.
물론 단점도 있습니다. 동일한 프로세스의 스레드들은 메모리를 공유할 수 있는 데 반해, 모든 창마다 따로 프로세스를 띄우게 되면 서로 다른 프로세스는 메모리를 공유할 수 없기 때문에 전체 앱의 메모리 사용량이 더 많아질 수밖에 없습니다.
다만 다중 프로세스 아키텍처는 안정적이고 빠른 사용자 경험과 보안을 제공한다는 장점이 있기 때문에, 크로미움 개발팀은 이런 장점을 살리면서도 메모리를 절약할 수 있는 새로운 아키텍처를 도입하기 위해 지속적으로 노력하고 있다고 합니다.

크롬의 엔진을 기반으로 한 일렉트론도 마찬가지로 크롬의 아키텍처에 맞게 프레임워크 자체 버전 업데이트를 2주에 1번 주기적으로 진행하고 있고, 그에 따라 Electron을 사용하여 배포를 하는 개발자들은 최소 1달에 1번은 버전 트래킹을 해야 한다는 숙명을 지니게 됩니다..
이 과정을 자동화하고 싶다면, update-electron-app 모듈을 사용하는 것도 가능합니다. 모듈을 런타임 종속성으로 설치하고, main 프로세스에서 호출하면, Electron의 GitHub 공식 릴리스마다 자체적으로 업데이트됩니다. 자세한 내용은 공식 문서를 참고하시면 좋을 것 같습니다.
2. Context Isolation (컨텍스트 격리)

컨텍스트 격리란 공식문서에 나와있는 말 그대로 일렉트론 내부 로직과 정의된 스크립트(preload)가 각각 동작하도록 설정하는 것입니다. 사실 브라우저의 작업을 여러 프로세스에 나눠서 처리하는 방법의 큰 장점은 보안입니다. 정의해 놓은 preload와 일렉트론 내부 로직이 분리되어 있기 때문에, 렌더러 프로세스처럼 임의의 사용자 입력을 처리하는 프로세스가 임의의 파일에 접근하지 못하게 제한합니다.
컨텍스트가 격리되어있지 않으면 어떤 보안적 위험이 있을까요?
예를 들어 node.js에서 모듈을 불러오기 위해 사용하는 require 함수에 공격자가 접근할 수 있게 되면, 자식 프로세스를 생성 및 호출할 수 있게 하는 'child_process'로 임의의 명령과 파일을 실행할 수 있게 되는 것이죠. 마치 사용자의 pc에서 명령 프롬프트를 사용하는 것처럼요.
Electron 12 버전 이전에는 격리가 디폴트 설정이 아니었기 때문에 렌더러 프로세스의 권한이 굉장히 자유로웠습니다. 하지만 12 이후로는 디폴트 설정이 되어있기 때문에, 특별한 경우가 아니라면 설정을 변경하지 않는 것이 좋습니다.
3. Inter-Process Communication (프로세스 간 통신)
프로세스들이 격리되어 있다는 뜻은 각자 메모리를 공유하고 있지 않다는 뜻입니다. 그렇다면 프로세스 간 커뮤니케이션을 하는 별도의 통신이 필요하다는 생각이 당연히 드실 텐데요, 그래서 등장한 것이 IPC 통신입니다.
일렉트론의 ipc 모델은 위와 같이 구성되어 있습니다.내부로직을 주로 main 프로세스에, 외부와 통신하는 로직과 말 그대로 render를 위한 로직들은 renderer process에 넣고 서로 send와 on을 통해 주고받는 형태입니다. 주고받는다는 개념은 채팅 소켓을 활용해 보신 분들은 쉽게 이해하실 것 같습니다.
이제 세 개의 창으로 이루어진 서비스의 구조의 통신을 상상해 보겠습니다.
지금부터는 더 이상 ‘창’이 아닌 ‘렌더러 프로세스’다라는 보다 정확한 개념으로 보셔야 합니다. 각각의 renderer precess 가 url을 로드하거나 html을 읽어 사용자에게 화면을 보여주고 있겠죠. 그리고 이들은 앞서 설명했다시피, 독립된 프로세스로 존재하기 때문에 서로가 서로에게 영향을 줄 수 없습니다. 그리고 가장 위의 main precess는 동작을 위한 주 로직과 각각의 renderer precess와 통신을 주고받을 수 있는 구조로 내부에 따로 완전하게 격리되어 있습니다.
여기에서 만약 renderer 1에서 클릭이라는 이벤트를 통해 renderer 2의 창이 변경되는 로직을 구현하고 싶다면, 그들 간에 직접적인 소통이 불가한데 어떻게 해야 할까요? 이 역할을 main process를 통해 구현해야 하고 inter process communication, 즉 ipc 통신이 필요해집니다.
이때, ipc 통신을 위해 preload 가 아주 중요한 역할을 합니다. 사전 로드 스크립트(preload) 에는 웹 콘텐츠가 로드되기 전에 렌더러 프로세스에서 실행되는 코드가 포함되어 있습니다. 이 스크립트는 렌더러 컨텍스트 내에서 실행되지만, Node.js API에 대한 액세스 권한을 가짐으로써 더 많은 권한이 부여됩니다.
preload 할 파일은 아래와 같이 브라우저 윈도우를 만들 때 (랜더러 프로세스를 생성할 때)부터 설정할 수 있습니다.
const { BrowserWindow } = require('electron')
//...
const win = new BrowserWindow({
webPreferences: {
preload: 'path/to/preload.js',
},
})
//...
이렇게 preload와 연결된 렌더러는 전역을 공유받지만, 위에서 설명했듯이 contextIsolation 이 설정되어 있는 경우 직접 내부에 접근이 불가합니다. 이때는 Renderer Process의 모듈인 contextbridge를 이용해야 합니다.말 그대로 컨텍스트간 다리 역할을 하는 모듈입니다. 모듈 내 exposeinmainworld을 통해 메인에게 노출시킬 메소드를 지정할 수 있고, render가 main에게 메세지를 send 할 수 있도록 중간다리 역할을 해줍니다.
이렇게 잘 구현된 중간다리 preload를 가진 렌더러 프로세스는 main 에게요청을 던질 수 있고, main 또한 on으로 전달받아 내부 로직을 실행시키도 다시 타켓 렌더러에게 리턴을 던져줄 수 있습니다.
말로 하는 설명이 더 어려울 수 있으니, 아래의 간단한 코드로 흐름을 파악해 보시는 것을 추천드립니다.
// Renderer (In isolated world)
window.electron.doThing()
// preload
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld(
'electron',
{
doThing: () => ipcRenderer.send('do-a-thing'),
}
)
// main
const {app, BrowserWindow, ipcMain} = require('electron')
const path = require('path')
//...
function setTitle (event, title) {
const win = getTargetWindow()
win.setTitle(title)
}
app.whenReady().then(() => {
ipcMain.on('do-a-thing', setTitle)
}
//...
위에서 설명한 패턴은 가장 일반적으로 사용하게 되는 Renderer에서 Main으로 향하는 단방향 통신의 패턴이었습니다. 이 내용을 이해하셨다면 양방향으로 통신하는 방법과 Main에서 Renderer로 통신하는 방법 또한 어렵지 않게 이해하실 수 있을 것입니다. 심화적인 내용은 공식문서에서 확인 가능합니다.
지금까지 Electron의 프로세스 구조와 통신에 대해 알아보았습니다. 살짝이라도 일렉트론을 시도해 보신 분들이 '아 그래서 안되던 거구나!'라고 알게 되었으면 좋겠다는 생각이 들어요.
혹시 이번 포스팅에서 궁금한 점이 있으시거나, 더 추가해서 설명이 필요한 부분 등등에 대한 의견이 있다면 댓글로 남겨주세요!
그럼 이번 포스팅은 여기서 마치겠습니다.
'Dev Note > Electron' 카테고리의 다른 글
1. 데스크탑 애플리케이션 프레임워크, 일렉트론에 대해 알아보자. (2) | 2023.02.26 |
---|---|
일렉트론 커스텀 네비게이션바 (0) | 2021.08.24 |