Electron으로 데스크탑 앱을 만들고 프로덕션에 올리기까지 생겼던 이슈들과 해결 방법을 정리해 보았다.
OS 알림 (Notification)
알림 핸들러가 안 먹어요! — OS 공통
•
Electron 공식 문서 등에 있는 예제대로 하면 알림 전송은 잘 되나 알림 핸들러(click, reply 등)이 작동하지 않을 가능성이 크다. 높은 확률로 Notification 인스턴스의 레퍼런스를 들고 있지 않아 인스턴스가 GC되는 경우다.
•
다음과 같이 Map이나 Set을 만들어 Notification 인스턴스를 직접 참조하게 하면 된다.
const notifications: Map<string, Notification> = new Map()
const notification = new Notification({ /* ... */ })
const notificationId = uuid()
notifications.set(notificationId, notification)
notification.on('click', () => {
notifications.delete(notificationId)
// ...
})
notification.show()
TypeScript
복사
•
위처럼 알림 객체를 직접 관리할 경우 객체가 계속 쌓이는 경우를 생각해야 한다. 특히 알림 close 이벤트는 알림이 OS에서 닫혔음을 보증하지 않으므로, OS에서 알림이 삭제됐어도 Map에 객체가 계속 남아있을 수 있다.
설치했더니 알림 핸들러가 안 먹어요! Windows
•
◦
%AppData%\Microsoft\Windows\Start Menu\Programs\하위에 프로그램을 가리키는 바로가기가 존재해야 한다.
When a win32 desktop application sends an interactive toast notification, Windows checks if the start menu contains a shortcut to the sending binary. #
For notifications on Windows, your Electron app needs to have a Start Menu shortcut with an AppUserModelID and a corresponding ToastActivatorCLSID. #
◦
해당 바로가기로 프로그램을 실행했는가에 대한 것이 아니라, 시작 메뉴 폴더에 바로가기가 존재하느냐에 대한 것이다.
•
Squirrel 인스톨러의 경우 앱 설치 시 com.squirrel.{AppName}.{ExeName}을 AUMID로 갖는 바로가기를 생성한다.
◦
AppName은 공백을 제거한 앱 이름, ExeName은 공백 및 “.exe”를 제거한 실행 파일 이름
◦
Electron에서 Squirrel 사용을 감지했을 경우 해당 값을 자동으로 AUMID로 맞춰주기는 하지만, 뭔가 잘 안 된다면 런타임에 app.setAppUserModelId(id) 등으로 앱의 AUMID를 커스텀하는 부분이 없는지 확인해보자.
알림 센터에 있는 알림들은 핸들러가 안 먹어요! — Windows
•
현재로서는 Electron 내장 Notification 모듈에 구현되어 있지 않다.
•
알림 센터(Actions center)의 알림과 통신하려면 별도의 라이브러리(electron-windows-notifications)를 사용하거나, NodeRT 등으로 WinRT/UWP API를 직접 다뤄야 한다.
알림 소리는 커스텀할 수 없나요? — OS 공통
•
보통은 알림 자체는 silent 옵션을 줘서 소리 없이 보내고, 알림 소리만 따로 Renderer에서 재생하는 식으로 해결한다.
•
OS 네이티브 단에서 알림 소리를 지정하고 싶으면 각 OS별로 추가 작업이 필요하다.
macOS
•
Notification 생성 시 sound 옵션을 지정할 수 있는데, 이는 macOS 한정 옵션이다. OS 내장 사운드(System Preferences > Sound) 이름을 넣으면 알림이 올 때 해당 사운드가 재생된다.
•
◦
또는 다음 위치 중 하나에 존재해도 된다:
▪
~/Library/Sounds
▪
/Library/Sounds
▪
/Network/Library/Sounds
▪
/System/Library/Sounds
•
Windows
•
Notification 옵션 중 toastXml 프로퍼티를 사용하면 소리뿐만 아니라 알림 레이아웃, 컨텐츠 배치, 각종 input 사용 등 App notification을 fully customize할 수 있다. 예시와 XML은 여기에서 볼 수 있다.
Toast notification과 App notification은 같은 용어이나, 각종 문서에서는 이를 혼용하여 사용하는 것 같다.
•
이 XML 내에 Custom audio를 지정할 수 있는데, 제약 사항은 다음과 같다:
◦
커스텀 알림 소리는 Windows 10 build 10586 이후부터 작동하는데, 이전 버전에서는 알림 소리가 아예 재생되지 않는다. 런타임에 Windows 버전을 보고 지원하지 않는 버전이면 audio 프로퍼티를 넣지 않는 식의 구현이 필요하다.
If you send a Toast that contains custom audio to a Desktop device before Version 1511, the toast will be silent. Therefore, for Desktop pre-Version 1511, you should NOT include the custom audio in your Toast notification, so that the notification will at least use the default notification sound. #
◦
사운드 파일에 ms-appx:///나 ms-appdata:/// scheme으로만 접근해야 한다. AppX Package를 사용할 것이 아니라면 ms-appx는 사용할 수 없고, ms-appdata:///roaming/...처럼 %AppData% 내의 사운드 파일 경로를 지정해주어야 한다.
•
<toast>
<visual>
<binding template='ToastGeneric'>
<text>Notification text.</text>
</binding>
</visual>
<audio src='ms-appx:///Audio/NotificationSound.mp3'/>
</toast>
XML
복사
•
다음과 같은 오디오 포맷을 지원한다: .aac, .flac, .m4a, .mp3, .wav, .wma
OS의 방해 금지 설정 등을 알 수는 없나요? — OS 공통
•
각 OS API를 직접 건드리거나 별도의 라이브러리를 사용해야 한다.
macOS
•
macos-notification-state 패키지가 있다. 내부적으로 CGSessionCopyCurrentDictionary API로 화면이 잠겨 있는지, sleep 상태인지 등을 가져오며, macOS 버전을 고려한 DND 판별 로직이 구현되어 있다.
Windows
•
windows-notification-state 패키지가 있다. 내부적으로 가져오는 것은 Win32 API의 QUERY_USER_NOTIFICATION_STATE enum으로, Direct3D 프로그램이 전체 화면으로 실행 중인지, 화면 보호기가 작동 중인지 등의 상태가 있다.
실행 관련
설치했더니 바로가기로 실행이 안 돼요! Windows
•
패키징 관련
제 컴퓨터에서는 잘 되는데 다른 사람 컴퓨터에서는 안 돼요! (실행 경고가 떠요!) OS 공통
•
코드 사이닝 문제일 확률이 높다. 올바른 종류의 인증서를 사용했는지, 인증서가 expire되거나 revoke되지는 않았는지, 로컬 환경에 코드 사이닝에 사용되는 툴이 설치되어 있는지 등 꼼꼼하게 점검해보아야 한다.
•
macOS(darwin)
•
MAS(Mac App Store) 외부에 배포될 앱은 기본적으로 Code Signing(서명)되어있어야 하고, macOS 10.15(Catalina)부터는 Notarization(공증) 과정까지 필요하다.
Notarization을 하지 않은 앱은 최초 실행 시 이러한 경고 메시지가 뜬다.
Code signing + Notarization을 마친 앱은 정상적으로 실행할 수 있다.
•
애플리케이션 파일(.app)은 Developer ID Application 인증서로 서명해야 한다.
•
설치 파일(.pkg)은 Developer ID Installer 인증서로 서명해야 한다. 보통 darwin 타겟의 앱을 pkg 설치 파일로 배포하는 경우는 많이 없으므로 생략되는 경우가 많다.
•
macOS(mas)
•
MAS에 배포할 앱은 Code Signing(서명)만 되어있으면 되고, Notarization(공증)은 선택이다. 또한, 코드 사이닝 과정에 Provisioning profile이 추가로 필요하다.
•
패키징된 앱을 로컬에서 실행해보고 싶을 때, 애플리케이션 파일(.app)을 Apple Development 인증서 + macOS App Development 프로파일로 서명해야 한다.
Apple Development 인증서와 Mac Developer 인증서는 같은 인증서다.
◦
◦
서명된 앱은 프로비저닝 프로파일에 명시된 기기 목록(Provisioned devices)에 있는 기기에서만 실행이 가능하다. 내 기기가 프로파일에 없으면 내 기기 UDID가 포함된 프로파일을 새로 만들고 이걸로 서명해야 한다. ‘모든 기기’에서 허용 같은 건 없으며, MAS에 배포할 앱 파일을 실행시켜 볼 방법은 이것밖에 없다.
•
MAS에 설치 파일을 업로드하려면, 애플리케이션 파일(.app)을 Apple Distribution 인증서 + Mac App Store Distribution 프로파일로 서명하고, 설치 파일(.pkg)을 Mac Developer Installer(3rd Party Mac Developer Installer) 인증서로 서명해야 한다.
Apple Distribution 인증서와 3rd Party Mac Developer Application, Mac App Distribution 인증서는 같은 인증서다.
◦
동일하게 프로비저닝 프로파일에는 서명하려는 앱의 App ID(Bundle ID)와 서명에 사용한 인증서가 명시되어야 한다.
◦
배포를 위해 서명된 앱은 로컬에서 실행할 수 없다. Transporter 등으로 App Store Connect에 업로드 후 TestFlight로 테스트는 해볼 수 있다.
Windows
•
EV(Extended Validation) Code Signing 인증서도 있는데, 이 인증서로 서명하면 Chrome 등의 브라우저에서 다운로드할 때나 Microsoft SmartScreen filter에 걸리지 않는다. Private key를 무조건 하드웨어 토큰 형식으로 관리해야 하는 단점이 있다.
•
보통 코드 사이닝 과정에 내부적으로 SignTool.exe (Sign Tool)을 사용한다. Windows Visual Studio나 .NET Framework Tools에 포함되어 있으니 없으면 설치하자.
코드 사이닝에 사용할 인증서가 ‘키체인에서 신뢰되지 않음’이래요! — macOS
•
Apple Worldwide Developer Relations Certification Authority가 설치되어 있지 않다면 설치하자.
App Store Connect에서 ‘Not Available for Testing’이라고 떠요! macOS(mas)
•
Entitlements 문제일 확률이 높다. @electron/osx-sign의 preAutoEntitlements 옵션을 끄고 Entitlements 파일을 직접 작성해서 적용해보자.
◦
Main app용, Login helper app용, 나머지 리소스용(inherit) 총 3개의 plist 파일이 필요하다.
◦
@electron/osx-sign의 optionsForFile 옵션에 넣을 (filePath: string) => PerFileSignOptions 함수를 적절히 구현해서 각 리소스에 맞는 plist 파일을 적용할 수 있다. 예시로 electron-builder 내부의 entitlement 파일 결정 로직을 참고하자.
const getEntitlements = (filePath: string) => {
// check if root app, then use main entitlements
if (filePath === appPath) {
if (customSignOptions.entitlements) {
return customSignOptions.entitlements
}
const p = `entitlements.${entitlementsSuffix}.plist`
if (resourceList.includes(p)) {
return path.join(this.info.buildResourcesDir, p)
} else {
return getTemplatePath("entitlements.mac.plist")
}
}
// It's a login helper...
if (filePath.includes("Library/LoginItems")) {
return customSignOptions.entitlementsLoginHelper
}
// Only remaining option is that it's inherited entitlements
if (customSignOptions.entitlementsInherit) {
return customSignOptions.entitlementsInherit
}
const p = `entitlements.${entitlementsSuffix}.inherit.plist`
if (resourceList.includes(p)) {
return path.join(this.info.buildResourcesDir, p)
} else {
return getTemplatePath("entitlements.mac.plist")
}
}
TypeScript
복사
◦
App Store Connect에 잘 올라갔는데 TestFlight에서 받아서 실행하면 앱이 크래시돼요! macOS(mas)
•
다음 사항을 확인해 보자:
◦
hardenedRuntime 옵션을 꺼야 한다. MAS에 업로드할 Electron 앱은 hardenedRuntime 옵션을 꺼야 한다.
No, hardenedRuntime: true won't work for App Store submitted apps. See this thread: #22656 (comment) #
◦
인스턴스를 하나로 유지하기 위한 app.requestSingleInstanceLock() 등의 로직이 실행되면 안 된다. mas 타겟에서는 실행되지 않도록 하자.
◦
앱 내의 Info.plist에 ElectronTeamID 항목에 Apple Team ID가 기입되어 있어야 한다. 보통은 @electron/osx-sign의 preAutoEntitlements 옵션에 의해 자동으로 기입되나, 해당 옵션을 껐을 경우 extendInfo 옵션으로 직접 지정해주는 등의 조치가 필요하다.
And the app bundle's Info.plist must include ElectronTeamID key, which has your Apple Developer account's Team ID as its value:
<plist version="1.0">
<dict>
...
<key>ElectronTeamID</key>
<string>TEAM_ID</string>
</dict>
</plist>
XML
복사
◦
MAS에 업로드할 앱은 Notarize할 필요 없다. 혹시라도 notarization을 수행하고 있다면 해당 과정을 빼 보자.
The exception is for Mac App Store (MAS) apps, where notarization is not required because the MAS submission process involves a similar automated check. #
사이닝 + 공증한 앱을 실행하면 앱이 크래시돼요! — macOS(darwin)
•
코드 사이닝 시 hardenedRuntime 옵션을 켜야 하고, 메인 앱 entitlements에 com.apple.security.cs.allow-jit key가 true로 되어 있어야 한다.