커스텀 프로토콜로 일렉트론 클라이언트 실행시키기

웹/ETC|2024. 4. 9. 18:57

목적

일렉트론으로 빌드한 A,B 클라이언트가 있다고 하자.

커스텀 프로토콜을 활용하여 A 클라이언트 내에서 로그인 후 특정 버튼을 클릭하면,

B 클라이언트가 실행되면서 A에서 로그인한 정보까지 넘겨주려고 한다. 

 

슬랙 웹페이지에서 슬랙 애플리케이션을 실행시켜 주듯이...

거기다 특정 페이지로 이동까지 시키는 등 다양한 액션이 필요할 수 있다.

 

시작!

우선! windowsOS와 macOS가 각각 작동하는 방식이 다르기 때문에..
여기저기서 쿠키를 등록할 수 있으므로 같은 코드 반복을 방지하기 위해 관련 처리 함수를 만든다.

 

const setCookie = async (key: string, value: string) => {
  session.defaultSession.cookies.set({
    domain: ".doodoo.com",
    url: process.env.DOODOO_HOST ?? "https://doodoo.com",
    name: key,
    value,
  });
};

const setCookieFromProtocol = async (protocolArgs: URLSearchParams | null) => {
  if (protocolArgs && protocolArgs.has("loginInfo")) {
    const loginInfo = protocolArgs.get("loginInfo");
    if (loginInfo !== null) {
      await setCookie("loginInfo", loginInfo);
    }
  }
};

 

windowsOS

빌드한 클라이언트를 설치할 때 커스텀 프로토콜을 함께 등록해 주도록,

NSIS 스크립트를 작성하고 config.json에 해당 스크립트 파일을 등록해 준다. 

*electron-builder에서 커스텀 NSIS스크립트를 등록하는 방법은 두 가지!

config.json에서 include와 script 옵션을 이용할 수 있다. ( 참고 : NSIS - electron-builder )

 

installScript.nsh

Var SystemDrive
RequestExecutionLevel admin

!macro customInstall
    DeleteRegKey HKCR "doodoo" # 이전에 등록된 경우 프로토콜 제거
    SetRegView 64
    # URL 프로토콜 핸들러 등록
    WriteRegStr HKCR "doodoo" "URL protocol" "" 
    # 매개변수를 받아 경로에 있는 파일을 실행시킴
    WriteRegStr HKCR "doodoo\shell\open\command" "" '"$클라이언트 실행파일 경로" "%1"'

    SetRegView 32
    WriteRegStr HKCR "doodoo" "URL protocol" ""
    WriteRegStr HKCR "doodoo\shell\open\command" "" '"$클라이언트 실행파일 경로" "%1"'

!macroend

!macro customUnInstall
    DeleteRegKey HKCR "doodoo"
!macroend

* preInit등 기본적으로 작성되는 부분은 제외하고 프로토콜을 등록하는 부분만 작성하였음 *

 

config.json

"nsis": {
    "oneClick": true,
    "createDesktopShortcut": true,
    "createStartMenuShortcut": true,
    "perMachine": true,
    "allowElevation": true,
    "shortcutName": "두두두둥=3",
    "allowToChangeInstallationDirectory": false,
    "installerLanguages": ["en_US", "ko_KR"],
    "language": "1042",
    "include": "./installScript.nsh"
  },

 

process.argv는 Node.js 프로세스에 전달된 commandLine arguments를 포함하는 배열이다.

Electron 또한 Node.js를 기반으로 하기 때문에 process.argv를 사용할 수 있다!

 

일반적으로 process.argv에는 아래 값들이 포함된다.

1. Node.js 실행 파일의 경로

2. 실행 중인 Javascript 파일의 경로

3. 그 이후의 커맨드라인에서 전달된 추가 인수

 

예를 들어서 doodoo://userInfo=1234 를 호출하면 process.argv에는 아래와 같은 값이 포함된다.

["$클라이언트 실행파일 경로/DooDooClient.exe",
"/.../app/main.js",
"doodoo://userinfo=1234"]

 

will-finish-launching 이벤트는 애플리케이션 실행 과정 중 초기 단계에서 발생하기 때문에,

이 이벤트가 발생하는 시점에서 이미 process.argv에 커스텀 프로토콜 URL이 포함돼 있다.

그러니까 파라미터도 함께 읽어올 수 있게 되는 것이다! 

app.on("will-finish-launching", async () => {
  if (platform === "darwin") {
    return;
  }

  const protocolUrl = process.argv.find((arg) => arg.startsWith("doodoo://"));

  if (protocolUrl) {
    const query = protocolUrl.substring("doodoo://".length);
    const params = new URLSearchParams(query.substring(0, query.length - 1));
    protocolArgs = params;
  }

  if (mainWindow) {
    await setCookieFromProtocol(protocolArgs);
  }
});

 

그런데, 이미 다른 방법으로 애플리케이션이 켜져 있는 상태에서 프로토콜을 호출하면 어떻게 될까?

*기본적으로 macOS에서는 동일한 클라이언트를 여러 개 실행시키는 게 제한된다.

윈도우에서도 app.requestSingleInstanceLock()을 통해 설정할 수 있다.

이 때는 새로운 창을 켜는 게 아니기 때문에 will-finish-launching 이벤트가 트리거되지 않는다.
클라이언트가 켜져 있다고 해도 특정 페이지로 이동하는 액션 등을 취하고 싶다면 second-instance 이벤트를 정의해 준다.

app.on("second-instance", async (event, commandLine, workingDirectory) => {
  // winOS: 클라이언트 켜져 있을 때 프로토콜 처리
  if (platform === "darwin") {
    return;
  }

  const urlStr = commandLine.find((arg) => arg.startsWith("doodoo://"));

  if (urlStr) {
    const query = urlStr.substring("doodoo://".length);
    // 윈도우는 맨 뒤에 / 같이 들어가므로 별도 처리
    const params = new URLSearchParams(query.substring(0, query.length - 1));
    protocolArgs = params;

    if (!params) {
      return;
    }

    if (mainWindow) {
      await setCookieFromProtocol(protocolArgs);
    }
  }
});

 

will-finish-launching에서는 process.argv에서 URL을 불러 왔다.

이는 second-instance에서 받아올 수 있는 commandLine(argv)와 동일하게 작동하므로 둘 중에 무엇을 사용해도 무관하다.

 

main.ts

import os from "node:os";
import { BrowserWindow, app } from "electron";
import { setCookieFromProtocol } from "./window.js";

let mainWindow: BrowserWindow | undefined;
let protocolArgs: URLSearchParams | null = null;
const platform = os.platform();

app.whenReady().then(async () => {
  if (mainWindow) {
    await setCookieFromProtocol(protocolArgs);
  }
  await mainWindow?.loadURL("doodoo.com");
});

app.on("second-instance", async (event, commandLine, workingDirectory) => {
  // winOS: 클라이언트 켜져 있을 때 프로토콜 처리
  if (platform === "darwin") {
    return;
  }

  const urlStr = commandLine.find((arg) => arg.startsWith("doodoo://"));

  if (urlStr) {
    const query = urlStr.substring("doodoo://".length);
    // 윈도우는 맨 뒤에 / 같이 들어가므로 별도 처리
    const params = new URLSearchParams(query.substring(0, query.length - 1));
    protocolArgs = params;

    if (!params) {
      return;
    }

    if (mainWindow) {
      await setCookieFromProtocol(protocolArgs);
    }
  }
});

// winOS : 클라이언트 꺼져 있을 때 프로토콜 처리
app.on("will-finish-launching", async () => {
  if (platform === "darwin") {
    return;
  }

  const protocolUrl = process.argv.find((arg) => arg.startsWith("doodoo://"));

  if (protocolUrl) {
    const query = protocolUrl.substring("doodoo://".length);
    const params = new URLSearchParams(query.substring(0, query.length - 1));
    protocolArgs = params;
  }

  if (mainWindow) {
    await setCookieFromProtocol(protocolArgs);
  }
});

 

 

macOS

config.json을 이용해 프로토콜을 등록해 준다. (참고 : Common Configuration - electron-builder )

{
  "productName": "DooDooClient",
  "appId": "doodoo.client",
  ... 다른 옵션들
  "protocols": {
    "name": "doodoo",
    "schemes": ["doodoo"]
  }, ... 그 외 옵션들
}

 

macOS에서는 윈도우OS와 달리 커스텀 프로토콜 URL이 process.argv에 직접 포함되지 않기 때문에,

open-url 이벤트* 처리를 통해 urlString을 읽어 와서 처리해 주어야 한다. 

그리고 URL이 열릴 때마다 호출되기 때문에 윈도우OS처럼 여러 종류의 이벤트를 처리해 주어야 할 필요도 없어 비교적 간단하다.

*open-url 이벤트는 macOS에서만 지원된다.

 

main.ts

import { BrowserWindow, app } from "electron";
import { setCookieFromProtocol } from "./window.js";

let mainWindow: BrowserWindow | undefined;
let protocolArgs: URLSearchParams | null = null;

app.whenReady().then(async () => {
  if (mainWindow) {
    await setCookieFromProtocol(protocolArgs);
  }
  await mainWindow?.loadURL("doodoo.com");
});
// MacOS : 프로토콜 처리
app.on("open-url", async (event, urlStr) => {
  event.preventDefault();

  const query = urlStr.substring("doodoo://".length);
  const params = new URLSearchParams(query);
  protocolArgs = params;

  if (!params) {
    return;
  }

  protocolArgs = params;

  if (mainWindow) {
    await setCookieFromProtocol(protocolArgs);
  }
});

 

 

참고용)대략적인 electron의 이벤트 실행 순서

*일부 이벤트는 조건에 따라 발생 여부와 순서가 달라질 수 있으며,
macOS 전용 이벤트는 다른 OS에서는 발생하지 않는다. 또한 일부 이벤트는 여러 번 발생할 수도 있다.

  1. will-finish-launching
  2. ready
  3. browser-window-created (윈도우가 생성될 때마다 발생)
  4. web-contents-created (웹 컨텐츠가 생성될 때마다 발생)
  5. child-process-gone (자식 프로세스가 종료될 때마다 발생)
  6. open-file (파일을 열 때마다 발생, macOS에서만 사용)
  7. open-url (URL을 열 때마다 발생, macOS에서만 사용)
  8. activate (애플리케이션이 활성화될 때 발생, macOS에서만 사용)
  9. continue-activity (다른 작업에서 애플리케이션으로 전환될 때 발생, macOS에서만 사용)
  10. browser-window-blur (윈도우가 비활성화될 때마다 발생)
  11. browser-window-focus (윈도우가 활성화될 때마다 발생)
  12. certificate-error (인증서 오류가 발생할 때마다 발생)
  13. select-client-certificate (클라이언트 인증서를 선택해야 할 때마다 발생)
  14. login (기본 세션 또는 어떤 세션이든 인증이 필요할 때마다 발생)
  15. gpu-info-update (GPU 정보가 업데이트될 때마다 발생)
  16. accessibility-support-changed (접근성 지원이 변경될 때마다 발생, macOS/windowsOS에서만 사용)
  17. session-created (새 세션이 생성될 때마다 발생)
  18. second-instance (두 번째 인스턴스가 실행될 때 발생)
  19. window-all-closed (모든 윈도우가 닫힐 때 발생)
  20. before-quit (애플리케이션이 종료되기 전에 발생)
  21. will-quit (애플리케이션이 곧 종료될 때 발생)
  22. quit (애플리케이션이 종료될 때 발생)

댓글()