ABOUT ME

-

Today
-
Total
-
  • Electron 배포 및 자동 업데이트 (feat. AWS S3 & electron-updater)
    Study/Frontend 2023. 9. 4. 16:10
    반응형

     

    Electron Build Lifecycle

     

    https://www.electronforge.io/core-concepts/build-lifecycle

     

    데스크탑 애플리케이션을 빌드하고 배포하는 과정에서 주목해야 할 부분은 배포 가능한 파일을 생성하는 단계이다. 웹과 달리 패키징 단계에서 생성된 번들을 가지고 OS별 설치 프로그램을 생성해야 한다. 이 단계에서 대응하고자 하는 OS별, 플랫폼별로 모두 빌드해줘야 한다.

     

    electron-builder를 선택한 이유

    electron 앱을 패키징하고 배포하기 위해 사용하는 대표적인 도구는 Electron Forge로 문서에서 소개되어 있다. Electron Forge의 가장 큰 장점은 분산되어 있는 배포 작업용 패키지들을 통합해서 빌드 파이프라인을 최소한의 구성으로 생성할 수 있다는 것이다. electron-builder 역시 분산되어 있는 패키지들을 어느 정도 통합해 두긴 했지만 배포에 필요한 스크립트를 대부분 직접 짜야한다는 것이다.

    서로 장단점이 있지만 electron-builder를 사용하고자 했던 가장 큰 이유는 자동 업데이트 기능에 있다.

    업데이트가 있을 때마다 유저가 새로운 설치 파일을 직접 다운로드하는 것은 굉장히 번거로운 일이다. 그렇다면 애플리케이션에서 자동으로 업데이트가 있음을 감지하고 다운로드한다면 어떨까? 이것을 가능하게 해주는 기능이 바로 electron의 자동 업데이트 기능이다.

     

    Electron Forge로 자동 업데이트를 구성하려면 오픈 소스로 깃허브에 릴리즈 하거나 자체 업데이트 서버를 구성해야 했다. 그러나 우리 프로젝트는 오픈 소스가 아닐뿐더러 업데이트 서버를 자체적으로 운영하기에는 관리 포인트가 늘어나기 때문에 좋은 방법이 아니었다.

    그래서 자동 업데이트를 지원하는 electron-builder에서 소개하는 electron-updater를 함께 사용하기로 했다. 

    A complete solution to package and build a ready for distribution Electron, Proton Native app for macOS, Windows and Linux with “auto update” support out of the box. 

    electron-builder를 소개하는 문구에서 볼 수 있듯이 별도의 구성 없이 "auto update"를 지원한다는 것을 강조하고 있음을 볼 수 있다.

     

    By default, Electron Forge will only build your app for the operating system it's running on.

    그리고 빌드에 관한 electron-forge 문서를 살펴보면 실행 중인 운영체제용으로만 앱을 빌드할 수 있고, 크로스 플랫폼 빌드를 하기 위해서는 별도로 빌드 머신을 이용해서 빌드 파이프라인을 구성해야 한다고 되어있다. 그래서 개발 편의성을 위해 macOS에서도 windows 앱을 빌드하고 배포할 수 있도록 electron-builder를 선택하기로 결정했다.

     

    AWS S3 버킷 생성 및 AWS Access key 구성

    Window App Store 또는 Mac App Store에 올리지 않는다면 누구나 웹에서 앱을 다운로드할 수 있도록 배포할 필요가 있다. 이를 위해 AWS S3에 public으로 게시하는 방법을 아래에서 소개하겠다. 

     

    1. AWS IAM 서비스에서 access 키 발급

    우선 AWS IAM 서비스에서 사용자를 추가해서 발급받은 키(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)를 저장해 둔다.

    그리고 파일 읽기 및 업로드 등을 위해 AmazonS3FullAccess 권한을 부여한다. 

     

    2. AWS S3 버킷 생성

    버킷을 생성할 때 객체 소유권 - ACL은 활성화하고, 퍼블릭 액세스 차단은 모두 비활성화해 준다. 그리고 버킷 정책을 아래와 같이 추가한다.

    {
        "Version": "2012-10-17",
        "Id": "Policy1234",
        "Statement": [
            {
                "Sid": "asdf",
                "Effect": "Allow",
                "Principal": "*",
                "Action": "s3:GetObject",
                "Resource": "arn:aws:s3:::your-app/*"
            }
        ]
    }

     

    3. AWS access key 추가

    Amazon S3 options. AWS credentials are required, please see getting your credentials. Define AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables. Or in the ~/.aws/credentials

    위와 같이 문서에서 제공한 방법은 두 가지이다. 환경변수를 추가하는 것과  컴퓨터 루트 폴더에 추가하는 것이다.

    • 방법1. env 파일에 환경변수로 추가하기
    AWS_ACCESS_KEY_ID = 
    AWS_SECRET_ACCESS_KEY =
    • 방법2. AWS CLI 설치해서 설정하기
     

    최신 버전의 AWS CLI 설치 또는 업데이트 - AWS Command Line Interface

    이전 버전에서 업데이트하는 경우 unzip 명령을 실행하면 기존 파일을 덮어쓸지 묻는 메시지가 표시됩니다. 스크립트 자동화와 같은 경우에 이러한 프롬프트를 건너뛰려면 unzip에 대한 -u 업데이

    docs.aws.amazon.com

    위 문서를 참고해서 AWS CLI를 설치한다. 설치 후 키를 추가하기 위해 터미널에서 aws configure 명령어를 실행한다.

    실행 후 IAM에서 발급받은 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY 를 입력하고, 생성한 버킷의 region을 입력한다.

    완료 후 터미널에서  aws configure list 명령어를 실행해서 잘 추가됐는지 확인한다.

     

    electron-builder 구성

     

    Publish - electron-builder

    Publish The publish key contains a set of options instructing electron-builder on how it should publish artifacts and build update info files for auto update. String | Object | Array where Object it is Keygen, Generic Server, GitHub, S3, Spaces or Snap Sto

    www.electron.build

    파일을 AWS S3에 배포하기 위해서 publish 옵션을 구성해줘야 한다. 세부적인 옵션은 위 문서를 참고하면 된다.

    electron-builder 구성은 디폴트로는 electron-builder.yml 파일에서 작성하면 된다.

     

    appId: com.electron.app
    //...생략
    publish:
        provider: s3 //배포 공급자
        bucket: your-app //S3 버킷 이름
        region: ap-southeast-1 //S3 버킷 지역
        acl: public-read //S3 ACL 권한
        publishAutoUpdate: true // 자동 업데이트 정보 파일 배포 여부

     

    그리고 package.json에 OS 별로 빌드 스크립트를 작성해 준다. 공식 문서에는 빌드가 병렬로 진행되기 때문에 하나의 빌드 명령어에 멀티 플랫폼을 지정해주는 것이 좋다고 나와 있다.

    "build": "npm run typecheck && electron-vite build",
    "deploy:win": "npm run build && electron-builder --win --x64 --config --publish always",
    "deploy:mac-x64": "npm run build && electron-builder --mac --x64 --config --publish always",
    "deploy:mac-arm64": "npm run build && electron-builder --mac --arm64 --config --publish always"
    "deploy:all": "npm run build && electron-builder -mwl --config --publish always"

     

    위와 같이 구성해 놓으면 S3로 배포할 준비는 완료된 것이다.

     

    배포 완료

    테스트로 deploy:mac-arm64 배포 스크립트를 실행하면 S3로 배포되는 것을 아래 로그에서 볼 수 있다.

     

    S3 버킷을 확인하면 아래와 같이 파일이 업로드된 것을 확인할 수 있다.

     

     

    electron-updater 자동 업데이트

    electron-updater를 사용하면 간단한 설정 몇 가지만 진행하면 자동 업데이트 기능을 구현할 수 있다. 자동 업데이트 가능한 대상은 디폴트로 DMG, AppImage, NSIS 가 있다.

    그리고 macOS의 경우 자동 업데이트가 작동하려면 반드시 코드 서명이 필요하고, windows의 경우는 코드 서명이 없어도 가능하다. 코드 서명에 대한 글은 이전 글에서 자세히 작성해 뒀다.

     

    electron-updater 라이브러리에서 자동 업데이트의 핵심 함수는 checkForUpdates이다. 이 함수를 통해 업데이트 여부를 체크하고 업데이트가 필요하다면 업데이트를 진행하게 된다. 

    그렇다면 업데이트 여부를 어떻게 체크하는 걸까? 정답은 latest.yml 파일에 있다. 위 배포 스크립트를 실행하면 latest.yml (또는 latest-mac.yml for macOS, 또는 latest-linux.yml for Linux)  파일이 생성되고 함께 올라간다. 이 파일에는 파일의 버전 정보, 배포 날짜 등이 담겨있는데, 버전을 변경해서 배포하게 되면 버전 정보, 배포 날짜 등이 수정돼서 올라간다. 앱이 실행되면 이 파일을 읽어서 버전이 다를 경우 자동 업데이트가 실행되는 구조이다.

     

    자동 업데이트를 진행하는 방식에는 두 가지가 있다.

    1. 사용자의 의사와는 관계없이 자동으로 업데이트를 진행하는 방식 - checkForUpdatesAndNotify
    2. 사용자가 직접 업데이트를 진행해야 하는 방식 - checkForUpdates

     

    checkForUpdatesAndNotify를 사용하게 되면 업데이트 가능한 상황에는 운영체제의 시스템 알림 창을 띄워서 업데이트 진행 여부를 묻게 된다. 여기서 주의해야할 점은 시스템 알림 설정에서 앱에 대한 알림을 꺼두면 업데이트 알림을 볼 수 없다. 다만 알림은 오지 않더라도 업데이트 다운로드는 자동으로 되고 앱을 종료하고 다시 실행하게 되면 업데이트된 버전이 실행된다. 따라서 업데이트 알림을 반드시 보여주고 싶거나 업데이트 여부를 유저가 결정하도록 하게 하려면 직접 업데이트 알림창 UI를 구현해서 렌더링해야한다.

     

    이와 달리 checkForUpdates를 사용하면 사용자에게 별도의 알림 없이 업데이트를 진행하게 된다. 흔히 말하는 잠수함 패치와 같은 것으로 보면 된다. 여기서 주의해야 할 점은 업데이트를 진행한다는 것은 업데이트 버전 파일만 다운로드하는 것이다. 업데이트된 파일을 실행하려면 프로그램을 종료한 후에 다시 실행해야 한다.

    이를 위해 유저가 직접 프로그램을 종료한 후에 다시 실행하는 방법도 있지만, 업데이트 다운로드 완료 이후에 바로 업데이트 된 파일을 실행하고 싶다면 다운로드 완료 시점에 quitAndInstall 메소드를 호출하면 된다.

     

    그리고 macOS 앱은 일반적으로 닫기 버튼을 클릭해도 애플리케이션이 완전하게 종료되는 것이 아니라 'active' 상태로 있다. 위와 같이 앱에 흰색 dot이 표시가 되어 있으면 'active' 상태라고 할 수 있다.

    이처럼 macOS 전용 데스크톱앱은 기본적으로 close와 quit 액션이 나뉘어 있어서 electron에서도 두 가지 액션에 대한 가이드를 주고 있다. 닫기 버튼을 클릭해서 윈도우 창이 닫힐 때(close) 앱이 종료되지(quit) 않도록 데스크탑앱을 개발해야 한다. electron 앱에서는 디폴트로 닫기 버튼을 클릭하면 앱이 종료되는데, macOS에서는 앱을 종료하지 않도록  'window-all-closed' 이벤트에서 예외처리를 해줘야 한다.

    위에서 언급한 'active' 된 상태는 macOS 운영체제에서만 있는 상태이므로 electron 공식 문서에도 보면 'activate'라는 이벤트가 macOS 전용으로 있는 것을 확인할 수 있다. 그래서 자동 업데이트 여부 체크하는 코드를 'activate' 이벤트가 발생했을 때도 추가해줘야 한다. 그렇지 않으면 앱을 종료하고 다시 실행했을 때만 자동 업데이트 여부가 체크된다.


    checkForUpdates 함수는 앱이 실행됐을 때 한 번 호출되기 때문에 앱이 실행 중일 때 업데이트를 지속적으로 감지하려면 setInterval을 통해 업데이트 여부를 주기적으로 체크해 주는 코드를 추가하면 된다.

     

    위 내용을 바탕으로 electron-updater를 활용한 자동 업데이트 기능 구현 코드는 아래와 같다. 

     

    import { app, shell, BrowserWindow } from 'electron'
    import { join } from 'path'
    import { electronApp, optimizer, is } from '@electron-toolkit/utils'
    import { autoUpdater } from 'electron-updater' //🔥 추가
    import log from 'electron-log' //🔥 추가
    
    function createWindow(): void {
      //...생략
    }
    
    /**🔥 자동 업데이트 log start **/
    autoUpdater.on('checking-for-update', () => {
      log.info('checking-for-update')
    })
    autoUpdater.on('update-available', () => {
      log.info('update-available')
    })
    autoUpdater.on('update-not-available', () => {
      log.info('update-not-available')
    })
    autoUpdater.on('error', (err) => {
      log.info('error : ' + err)
    })
    autoUpdater.on('download-progress', (progressObj) => {
      const log_message = 'download: ' + progressObj.percent + '%'
      log.info(log_message)
    })
    /**🔥 자동 업데이트 log end **/
    
    autoUpdater.on('update-downloaded', () => {
      log.info('update-downloaded')
      // 🔥 업데이트 파일 자동 다운로드 이후 애플리케이션 종료하고 다시 실행
      autoUpdater.quitAndInstall() 
    })
    
    app.whenReady().then(() => {
      electronApp.setAppUserModelId('com.electron')
    
      app.on('browser-window-created', (_, window) => {
        optimizer.watchWindowShortcuts(window)
      })
    
    
      //🔥 자동 업데이트 등록 
      autoUpdater.checkForUpdates()
      /**
      주기적으로 업데이트 감지하려면 아래와 같이 코드 작성
      setInterval(() => {
      	autoUpdater.checkForUpdates()
      }, 30000)
      */
    
      createWindow()
    
      app.on('activate', () => {
        if (BrowserWindow.getAllWindows().length === 0) {
    		autoUpdater.checkForUpdates() //🔥 mac os용
    		createWindow()
      })
    })
    
    app.on('window-all-closed', () => {
      if (process.platform !== 'darwin') {
        app.quit()
      }
    })

     

    그리고 위 코드를 보면 electron-log를 활용한 부분을 볼 수 있다. 자동 업데이트에 대한 로그를 쉽게 볼 수 있도록 위와 같이 로깅 작업을 해두면 좋다. 로그 파일은 아래와 같은 위치에서 확인할 수 있다.

    • on Linux: ~/.config/{app name}/logs/{process type}.log
    • on macOS: ~/Library/Logs/{app name}/{process type}.log
    • on Windows: %USERPROFILE%\AppData\Roaming\{app name}\logs\{process type}.log

     

     

    반응형

    댓글