사례 연구 · Electron

Notion이
텍스트와 HTML을 복사하는 방법.

Notion 페이지 API에서 프리로드와 메인 프로세스를 거쳐 Electron의 클립보드 쓰기까지 추적합니다.

Notion Desktop 7.6.1 · JavaScript · REA

Notion 데스크톱 패키지의 브리지

  1. 01 · 페이지에 공개된 API clipboard.write(data) 프리로드는 호출 가능한 메서드를 노출합니다.
  2. 02 · 프리로드 래퍼 ipcRenderer.invoke 래퍼는 채널과 인수를 전달합니다.
  3. 03 · 메인 프로세스 핸들러 ipcMain.handle 수신 측은 어느 페이지에서 메시지를 보냈는지 확인합니다.
  4. 04 · 시스템 클립보드 clipboard.write(data) Electron은 전달받은 텍스트와 HTML을 기록합니다.

한 채널이 양쪽을 연결합니다: notion:clipboard:write

패키지 코드의 프리로드와 메인 프로세스는 같은 채널을 사용합니다. 래퍼의 구현을 따라가면 그 이름 뒤에 있는 Electron 호출을 확인할 수 있습니다.

복사 작업은 어디로 이어질까요?

페이지에는 clipboard.write 메서드가 보입니다. 프리로드의 실제 구현은 래퍼입니다. 이 래퍼가 무엇을 보내고 어느 코드가 받는지 살펴보는 것이 핵심입니다.

코딩 에이전트

REA를 사용해 Notion이 텍스트와 HTML을 시스템 클립보드에 복사하는 과정을 추적해 주세요. 각 단계의 코드를 보여 주세요.

에이전트가 로컬 Notion 앱 패키지에 접근할 수 있는 상태에서 사용하는 예제 프롬프트입니다.

REA로 읽기 시작할 위치 찾기

REA로 탭 프리로드를 분석하면 페이지에 노출된 API와 IPC 요청 호출을 찾습니다. 두 결과에는 압축된 번들의 소스 범위가 함께 제공됩니다:

REA 분석 결과 → 살펴볼 위치 두 곳

노출된 페이지 API
api_key: "__electronApi"
api_status: "dynamic"
source: preload.js:1:22764–22828
렌더러의 IPC 호출
operation: "invoke"
channel_expression: "e"
source: preload.js:1:6486–6522
REA 4.1.0으로 Notion 탭 프리로드를 새로 분석한 analyze_javascript_application 결과의 일부 필드입니다. 위치는 원본 번들의 줄 번호와 0부터 세는 열 번호입니다.

에이전트는 해당 위치를 열고, 조합된 API에서 clipboard 멤버를 찾아 invokerInMain를 읽습니다. 이 함수의 호출 지점에서는 notion:clipboard:write을 전달합니다. 이 이름 덕분에 에이전트는 메인 번들에서 무엇을 찾아야 할지 정확히 알 수 있습니다. 아래 네 발췌문은 에이전트가 연결한 경로를 보여줍니다.

네 개의 짧은 코드 조각

패키징된 JavaScript를 읽기 쉬운 변수 이름으로 단순화했습니다. 관련 없는 멤버는 생략했습니다.

  1. 페이지에 노출된 API에서 시작

    tab_browser_view/preload.js · API 멤버
    const clipboardApi = {
      write: invokerInMain("notion:clipboard:write")
    };
    
    // Included in the API exposed through contextBridge:
    // window.__electronApi.clipboard

    이 멤버는 채널 이름을 제공합니다. invokerInMain의 구현을 보면 요청이 페이지의 프로세스에서 어떻게 전달되어 나가는지 알 수 있습니다.

  2. 프리로드 래퍼의 구현 추적

    tab_browser_view/preload.js · IPC 보조 함수
    function invokerInMain(channel) {
      return (...args) => invokeInMainAndReturnResult(channel, ...args);
    }
    
    function invokeInMainAndReturnResult(channel, ...args) {
      return ipcRenderer.invoke(channel, ...args);
    }

    반환된 함수는 Electron의 요청·응답 IPC로 인수를 전달합니다. 이제 메인 프로세스에서 notion:clipboard:write을 찾을 수 있습니다.

  3. 수신 핸들러 등록 방식 확인

    main/index.js · 핸들러 도우미
    const handleRequestFromRenderer = {
      addListener(channel, handler) {
        ipcMain.handle(channel, handler);
      }
    };

    메인 프로세스도 래퍼를 사용합니다. addListener 메서드가 해당 채널에 ipcMain.handle 수신 핸들러를 등록합니다.

  4. 쓰기 호출과 발신자 검사 찾기

    main/index.js · 클립보드 수신 측
    handleRequestFromRenderer.addListener(
      "notion:clipboard:write",
      (event, data) =>
        isNotionWebContents(event.sender)
          ? clipboard.write(data)
          : undefined
    );

    여기가 최종 도착점입니다. 발신자를 확인한 뒤 핸들러는 전달받은 데이터를 Electron 클립보드 API에 넘깁니다. 채널과 두 래퍼의 구현을 통해 노출된 메서드와 이 호출이 연결됩니다.

복사한 블록은 구조를 어떻게 유지할까요?

Notion의 웹 편집기는 electronApi.clipboard.write를 통해 텍스트와 HTML을 보냅니다. 블록 데이터는 로컬에 유지하고, 복사한 HTML에 ID를 넣어 둘을 연결합니다.

HTML과 저장된 블록 데이터의 연결

시스템 클립보드로 전송됨
text: "Project notes"
html: "<p>Project notes</p>"
    + "<!-- notionvc: <copy-id> -->"
Notion이 로컬에 저장
id: <copy-id>
mimeTypes:
  text/_notion-blocks-v3-production
    → structured block data
  1. HTML의 ID 읽기
  2. 같은 로컬 ID 찾기
  3. Notion 블록 데이터 복원
텍스트와 ID는 예시입니다. 기록된 모듈 테스트에서는 ID가 같으면 블록 데이터를 복원하고, 저장 항목이 만료됐으면 텍스트와 HTML만 반환합니다.

붙여넣을 때 Notion은 HTML의 ID를 찾아 저장된 사본과 대조합니다. ID가 일치하면 블록 데이터를 복원합니다.

2026년 7월 13일 캐시된 웹 자산에서 기록한 클립보드 형식입니다.

추가 예제와 출처

다른 예제: Markdown 표

Notion은 Markdown을 파싱해 블록 데이터로 바꿉니다. 이 예제는 표가 머리글 행과 데이터 행으로 바뀌는 과정을 보여줍니다.

Notion Markdown 파서를 거친 표

마크다운 입력
| A | B |
| - | - |
| 1 | 2 |
구조화된 파서 출력 · 발췌
table · headerRow: true
  table_row
    cells: ["A"], ["B"]
  table_row
    cells: ["1"], ["2"]
추출한 Markdown 파서의 실행 기록입니다. 표가 행과 셀로 변환됩니다.
다른 경로: 오프라인 SQLite 연결

Notion의 렌더러는 IPC를 통해 getSqliteMeta를 요청합니다. 메인 프로세스는 메시지 채널을 만들고 한쪽 포트는 렌더러에, 다른 쪽 포트는 SQLite 유틸리티 프로세스에 전달합니다. 이후의 SQL 문 배치와 결과는 이 포트를 통해 오갑니다.

이 전달 과정을 추적하면 데이터베이스 작업이 어디서 실행되는지, 첫 IPC 요청만 추적하면 왜 이후 통신을 놓치는지 알 수 있습니다.

출처 및 발췌문 읽기 방법

클립보드 브리지는 Notion Desktop 7.6.1의 app.asar 안에 있는 탭 프리로드와 main/index.js에서 가져왔습니다. 발췌문은 짧은 변수 이름을 풀어 쓰고 관련 없는 코드를 생략했습니다. 발신자 검사 함수는 내부의 isNotionWebContents 호출과 함께 표시했습니다.

가상 클립보드와 Markdown 예제는 2026년 7월 13일 캐시된 웹 자산의 모듈 747001, 58169, 736810, 422457에서 가져왔습니다. 이 자산에는 별도 버전이 있습니다. 저장된 실험에서는 추출한 모듈과 작은 테스트 코드를 실행했습니다. 출력은 해당 실험의 결과이며 데스크톱 UI 세션을 캡처한 것은 아닙니다.

여기의 REA 결과는 공개된 REA 4.1.0 패키지로 저장된 탭 프리로드를 집중 분석한 기록입니다. API와 invoke 호출의 위치를 찾습니다. 클립보드 멤버와 대응 수신부는 패키지의 래퍼 구현을 읽어 연결했습니다.

Markdown 예제는 파서 모듈 422457을 사용합니다. 저장된 모듈 테스트에서 페이지와 채팅 필터 모두 이 표를 보존했습니다. 붙여넣기·가져오기, 구조화된 Markdown, AI·채팅 파서는 진입점과 중간 형식이 각각 다릅니다.

상단