Web → Desktop · Migration Notes

기존 Next.js + PWA를
Electron으로 이식하기

채용공고 주업무 = 웹 제품을 Electron 등 데스크톱으로 확장. 그래서 출발 질문 — Joint의 Next.js 웹을 Electron으로 이식하면 어디서 막히고 어떻게 푸는가? 로딩·알림·배포를 훑되, 가장 먼저·확실히 막히는 알림부터 실제로 검증했음. (웹 알림이 Service Worker + FCM 경로라면 Electron에서 그대로 될지가 첫 관문 — 정확한 전략은 실제 구조 확인 필요.)

스택  Next.js · Electron · Node(ws) 관점  PWA 래핑 가설 → 프로토타입 검증 결론  URL 로드 셸 + WS 알림, 나머지는 열린 질문
01 / 출발점

출발점 — Next.js PWA를 Electron으로 그냥 감싸면 될까?

그래서 프로토타입의 출발 질문 — "기존 웹을 Electron으로 그냥 감싸면, 어디서 막히고 그걸 어떻게 푸는가?"

이건 가상 시스템 설계가 아니라 이미 있는 웹을 데스크톱으로 옮기는 실무 그 자체. 이식 전체로 보면 답할 게 셋인데, 그중 가장 먼저·확실히 막히는 알림부터 검증했음.

02 / 프레임워크

왜 Tauri가 아니라 Electron인가

"왜 Electron이냐"는 당연히 나올 질문 → 대안을 낮잡지 않고 비교. Tauri는 진지한 후보 — 2.0 stable(2024.10), 번들·메모리·보안·모바일에서 우위. "아직 불안정"이라는 인상은 낡음.

관점 Tauri v2 Electron
렌더링 엔진 OS WebView → macOS는 WebKit, 플랫폼별 차이 실재 번들된 Chromium → 모든 OS에서 렌더링 일관
팀 스택 정합 네이티브 계층은 Rust (러닝코스트) TS/React/Next 그대로 — Rust 세금 없음
번들·메모리 가볍다 무겁다 (Chromium 동봉)
성숙한 웹 제품 확장 그린필드엔 매력적 배포·서명·자동업데이트 생태계 성숙 → 리스크 낮음

그럼에도 지금 Joint엔 Electron — 이유 셋:

균형 잡힌 결론 새 그린필드라면 Tauri v2도 충분히 후보. 다만 성숙한 웹 제품 확장 초기엔 렌더링 일관성·스택 정합·배포 성숙도 때문에 Electron이 리스크 낮음. 번들·모바일이 중요해지면 재검토.
03 / 첫 결정

웹을 어떻게 로드하나 — URL 로드 vs 로컬 번들

셸이 웹을 띄우는 방식 = 두 갈래. 이 선택이 SSR 가용 · 배포 횟수 · 오프라인 여부를 전부 규정 → 이식의 첫 결정.

기준 로컬 번들 · loadFile 원격 URL · loadURL
렌더링 모델 Next.js output:'export' (SSG) 가능 배포된 웹을 그대로 → SSR·미들웨어 살아있음
SSR / API / 미들웨어 서버 기능 사용 불가 기존 웹과 동일하게 동작
배포 웹 배포 + 앱 재빌드·재배포 이중 관리 웹 배포 1회 → 데스크톱도 자동 최신화
오프라인 번들이 로컬에 있어 강함 흰 화면 위험 — 기존 SW 캐싱으로 완화
Joint 적합성 제품을 export 제약 안으로 욱여넣어야 함 이미 PWA 서비스 + AWS 배포 존재 → 자연스러움

Joint 기준 — 공개된 구조(PWA 서비스 중)로 보면 사실상 정해져 있다고 판단.

단, URL 로드도 공짜는 아님 — 새로 생기는 트레이드오프 (조사만 함, 대부분 미구현 · 향후 과제):

이 프로토타입에서 실제로 한 것 셸이 mainWindow.loadURL(APP_URL)로 웹을 띄움.
· 개발: APP_URL = http://localhost:3010 (Next 개발 서버)
· 프로덕션: 이 값을 배포 웹 주소로 교체만 하면 됨
위 트레이드오프(오프라인·세션 등)는 인지만, 구현 안 함 — 이식의 다음 단계.
04 / 대표 문제

이식하면 가장 먼저 막히는 것 = 알림

URL 셸을 만들면 곧바로 부딪히는 게 알림 — 웹에서 당연하던 푸시가 Electron 안에선 그대로 안 됨. 그래서 이식의 대표 문제로 잡고, 경로별 트레이드오프를 따져 하나 선택 + 프로토타입까지.

표준 Web Push는 Electron에서 경로가 막힌다

정확히 말하면 "Electron에서 FCM을 못 받음"(X) → "표준 Web Push 경로(SW + PushManager)로는 못 받음"(O). 이 구분이 다음 선택지로 이어짐.

우회로는 있지만, 그래서 더 배제했다

데스크톱은 트레이 상주 + WebSocket이면 충분하다

근거 = 레퍼런스 앱의 실제 동작. 슬랙·디스코드도 트레이 상주 시만 알림, 완전 종료 시 무알림 — 결함이 아니라 의도된 설계. 모바일과 데스크톱은 전제가 다름.

환경 전제 알림에 필요한 것
모바일 OS가 앱을 수시로 종료 (메모리 관리) 앱과 독립된 OS 푸시(FCM·APNs) 필수
데스크톱 사용자가 Quit하기 전엔 프로세스 유지, 트레이 상주가 자연스러움 상시 WS 연결만으로 충분

즉 "앱 꺼져도 알림"용 OS 푸시는 데스크톱 현 단계엔 과한 설계. 완전 종료 무알림 = 슬랙·디코와 동일한 의도 → 표준 WebSocket 경로 선택. 표준 의존성만 쓰니 비공식 프로토콜 리스크도 없음.

경로 성격 판정
WebSocket + IPC 렌더러가 WS 수신 → IPC로 메인 → 네이티브 알림. 표준 의존성만 사용. 선택
표준 Web Push (SW+FCM) Electron에 Push Service 부재 → 경로 자체가 막힘. 불가
electron-push-receiver Node가 FCM·MCS 직접 구현. 비공식 리버스 프로토콜, 2024.6 이후 불안정. 배제
OS 네이티브 푸시 (WNS·APNs) "앱 꺼져도 알림"의 정석. 지금 단계엔 과함. 향후
"백그라운드"는 두 가지 — 구분이 핵심 웹에서도 백그라운드 알림이 되는 경우가 있는데, "백그라운드"의 의미가 두 가지로 갈리기 때문임.
· ① 창 최소화 / 탭 비활성 (브라우저·탭은 생존) — 페이지 JS가 살아있어 WS 유지 → new Notification() 그대로 뜸. 웹이든 Electron이든 동작. (WS 수신은 타이머와 달리 백그라운드에서도 throttle 거의 안 됨)
· ② 브라우저 완전 종료 / 모바일 OS suspend — 페이지 JS·WS가 사라져 알림을 띄울 주체가 없음. 이때만 SW+FCM이 유일한 경로 → FCM의 존재 이유가 여기.
즉 웹의 백그라운드 알림은 ① 한정. Electron은 트레이 상주로 프로세스를 유지해 ①을 항상 보장, 그래서 FCM 없이 ②까지 커버함.

정리 — 결정은 두 단계: 전송(강제) + 표시(선택)

알림을 "전송(메시지를 어떻게 받나)"과 "표시(받은 걸 어떻게 띄우나)"로 쪼개면 판단이 명확해짐.

단계 선택 근거
전송 WebSocket 강제 Electron에서 표준 Web Push(FCM) 경로가 막힘 → 메시지를 받아올 방법이 상시 WS뿐. 선택이 아니라 결정됨.
표시 IPC → 메인 네이티브 선택 렌더러 웹 Notification으로도 뜨고 방 전환까지 됨. 다만 트레이에 숨긴 창 복원은 메인만 가능 → 알림 로직을 렌더러·메인으로 쪼개게 됨. 어차피 Electron 셸이라 창 제어·앱 정체성(setAppUserModelId)·클릭 라우팅이 이미 메인에 모여 있음 → 알림도 메인 네이티브로 일원화하는 게 자연스럽고, 웹 노티를 굳이 쓸 이유가 없음.

"웹으로도 알림이 뜨는데 왜 네이티브냐"의 답은 "웹은 안 된다"가 아니라 "어차피 Electron이라 굳이 웹 노티를 쓸 이유가 없다" — 창 제어·정체성·클릭 라우팅이 이미 메인에 있으니 알림도 거기로 일원화. 그 경로가 다음 섹션의 WS → preload → 메인 → 네이티브.

05 / 구현

프로토타입 아키텍처와 실제 동작

실제 동작하는 데모로 구현. 셸이 웹 UI를 loadURL로 띄우고, 렌더러가 받은 WS 메시지 → preload 브리지(contextBridge) → 메인 프로세스 → 네이티브 알림.

데모 서버 Node + ws room · presence · 브로드캐스트 WebSocket 렌더러 (Next.js) 웹 UI · loadURL(:3010) WS 수신 · 렌더링 focus·알림 표시 판단 IPC preload 메인 (Electron) 트레이 상주 · close→hide backgroundThrottling:false Notification 생성 Joint 새 메시지 · 거래처 A OS 네이티브 알림 클릭 → 창 복원 · 라우팅 (open-room)
흐름: 서버가 WS로 브로드캐스트 → 렌더러가 수신하고 알림을 띄울지 판단 → 띄운다면 IPC로 메인에 넘겨 네이티브 알림 → 클릭하면 창 복원 후 라우팅.

핵심 = 직접 구현·동작 확인한 것조사·설계만 한 것을 섞지 않기. 아래가 그 구분.

직접 구현 · 동작 확인됨 설계·인지만 함 (미구현)

직접 구현하고 시연으로 확인한 것 확인됨

예시 앱 로직 — 이식 검증이 아니라 데모 스캐폴딩 방별 unread 배지 · 활성 방(포커스+보는 중) 알림 생략 · 다른 방 알림+배지 = 알림 메커니즘을 사실적으로 시연하려고 넣은 예시 채팅 UX. 실제 Joint가 멀티룸 채팅인지·알림 로직이 어떤지는 미확인 → 이 부분은 "이식 검증 항목"이 아님.
단, 여기서 실제 이식에도 그대로 넘어가는 설계 질문 하나"알림을 띄울지 말지를 클라이언트가 판단하나, 서버가 게이팅하나?" 이 데모는 렌더러가 자기 상태(활성 방·포커스)로 판단하고, 서버는 전체 브로드캐스트만 함(글로벌 WS). presence(온라인·자리비움 같은 접속 상태)는 보고되나 표시용이고 서버 게이팅은 안 함. 결함이 아니라 데모에서 선택한 구조이며, 프로덕션에선 서버로 옮기는 게 자연스러운지가 열린 질문.

인지했지만 이 데모에선 구현하지 않은 것 설계

06 / 정리

여기까지는 일반론, 확정은 실제 구조를 봐야 한다

여기까지 = 레퍼런스·공개 제약 기반의 일반론. 프로토타입으로 "이식하면 이런 문제 → 이렇게 풀림"까지는 직접 확인. 단, 정확한 전략은 Joint 실제 내부 구조 확인 후 확정 — 겸손이 아니라 실무의 순서.

1
웹 로딩 — URL 로드로 SSR을 살린다배포된 Next 웹을 loadURL로 띄우는 셸. 웹 배포 1회로 데스크톱 최신화. (동작 확인)
2
이식하면 먼저 막히는 건 알림표준 Web Push는 Electron에서 경로 자체가 막힘. "FCM 불가"가 아니라 "표준 경로 불가"가 정확.
3
데스크톱은 트레이 상주 + WS로 충분슬랙·디코와 동일. 비공식 우회 프로토콜은 리스크 과다로 배제. (WS→IPC→네이티브 알림 동작 확인)
4
남은 건 실제 구조를 봐야 정해진다catch-up·세션·배포는 인지만 함. 확정하려면 Joint의 실제 스택 확인이 먼저.

실제 구조를 확인하면 정해질 열린 질문들

이 프로토타입 = 완성 제품이 아니라, 이식이라는 실무를 어떤 순서·근거로 결정하는지 보여주는 기록. 다 풀어놓기보다 무엇이 아직 열려 있는지 안다는 걸 남기려 함.