⚡ Electron6. 패키징 & 배포02. asar 아카이브 — 패키징 포맷과 그 한계

02. asar 아카이브 — 패키징 포맷과 그 한계

asar수만 개 작은 파일을 단일 아카이브로 묶는 성능 최적화 포맷이다. 암호화봉인도 아니고 — 누구나 한 줄 명령으로 추출할 수 있다. 이 문서는 asar의 구조, 무엇을 unpack해야 하는지, 그리고 왜 보안이 아닌지를 정확히 짚는다.


한 줄 답

asar = “Atom Shell Archive” — tar 비슷한 posix-free 단일 아카이브 포맷. fs 시스템 콜을 수만 번 → 한 번으로 줄인다. 그 대신 암호화·서명·보호는 하나도 없다. 비밀이 있다면 asar가 답이 아니다.


Why — 왜 asar가 필요한가

Electron 앱은 node_modules 안에 수만~수십만 개의 작은 JS 파일을 들고 있다.

패턴파일 수OS가 하는 일
node_modules 그대로80,000+각각 open + stat + read + close (×4 시스템 콜)
app.asar로 묶음1 (+ unpack 몇 개)mmap 1번 + 내부 offset 계산
효과실측 (보고된 사례)
시작 시간800ms → 350ms (디스크 캐시 cold start)
앱 폴더 크기동일 (압축 없음)
Windows Defender 스캔 시간수십 초 → 수 초 (스캔 대상 파일 수가 줄어듦)
macOS Spotlight 인덱스수만 항목 → 1 항목

이 효과의 진짜 이유는 압축이 아니라 파일 시스템 메타데이터 비용이다. 작은 파일 N개의 합보다 큰 파일 1개의 read가 훨씬 빠르다.


How — asar의 구조

┌──────────────────────────────────────────┐
│  Pickle Header                           │
│  (4 bytes size + 4 bytes header size)    │
├──────────────────────────────────────────┤
│  JSON Header                             │
│  {                                       │
│    "files": {                            │
│      "package.json": {                   │
│        "size": 384, "offset": "0"        │
│      },                                  │
│      "dist": {                           │
│        "files": {                        │
│          "main.js": {                    │
│            "size": 102400,               │
│            "offset": "384"               │
│          }                               │
│        }                                 │
│      }                                   │
│    }                                     │
│  }                                       │
├──────────────────────────────────────────┤
│  File Contents (concatenated, raw)       │
│  package.json bytes ...                  │
│  main.js bytes ...                       │
│  ...                                     │
└──────────────────────────────────────────┘
  • Pickle 포맷: Chromium 내부의 IPC 직렬화 포맷 재사용.
  • JSON 헤더: 파일 트리 + 각 파일의 offset·size. 디렉터리는 files 키로 재귀.
  • 파일 본문: 그냥 concat. 압축 없음, 암호화 없음.

Electron 런타임의 패치

Electron은 Node fs 모듈을 몽키패치app.asar 안의 경로를 마치 디렉터리인 것처럼 처리한다.

// 사용자 코드
const fs = require('fs');
fs.readFileSync('/Applications/MyApp.app/Contents/Resources/app.asar/dist/main.js');
// → Electron이 asar 내부 offset으로 변환 → 한 번의 read

그래서 대부분의 Node 코드가 asar에 대해 의식 없이 동작한다. 단, child_process나 OS API에 물리 경로를 넘기는 경우는 다르다 (아래).


What — 무엇을 unpack해야 하는가

asarUnpack은 빌드 시 특정 패턴은 asar 밖에 풀어두는 옵션이다. 결과 폴더는 app.asar.unpacked/.

unpack이 반드시 필요한 경우

종류이유
네이티브 모듈 (.node)dlopen물리 경로를 요구. asar 가상 경로에서는 못 연다
자식 프로세스 실행 파일 (ffmpeg.exe, sidecar)spawn은 OS의 execve를 호출 — 가상 경로 모름
OS API에 path를 넘기는 리소스macOS NSImage, Windows LoadLibrary
사용자가 fs.watch로 감시해야 하는 파일asar 내부 파일은 watch 불가
dynamic require의 절대 경로webpack output이 절대 경로를 굽는 경우

electron-builder 예시

asar: true
asarUnpack:
  - "**/*.node"           # 모든 네이티브 바인딩
  - "node_modules/sharp/**/*"   # libvips 의존 — .node + .so/.dll
  - "node_modules/sqlite3/build/Release/*.node"
  - "ffmpeg/**/*"          # 사이드카 binary
  - "resources/templates/**/*"  # 사용자가 직접 fs로 열어야 하는 템플릿

빌드 결과:

MyApp.app/Contents/Resources/
├── app.asar                    # 코드 대부분
└── app.asar.unpacked/          # unpack된 파일
    ├── node_modules/sharp/...
    └── ffmpeg/ffmpeg

코드에서 unpack 경로 참조

import { app } from 'electron';
import path from 'path';
 
// 개발 모드에서는 그냥 절대 경로
// 패키징 후에는 asar.unpacked 경로
const ffmpegPath = app.isPackaged
  ? path.join(process.resourcesPath, 'app.asar.unpacked', 'ffmpeg', 'ffmpeg')
  : path.join(__dirname, '..', 'ffmpeg', 'ffmpeg');
 
import { spawn } from 'child_process';
spawn(ffmpegPath, ['-i', input, output]);

함정: __dirname이 asar 내부면 asar 가상 경로가 들어간다. ffmpegPath.asar가 들어 있다면 잘못된 것. 대신 process.resourcesPathResources 폴더에서 시작.


What — asar는 보안이 아니다

# 풀기 — 한 줄
npx @electron/asar extract app.asar ./extracted
 
# 또는 단일 파일 확인
npx @electron/asar list app.asar
 
# 또는 핫패치
npx @electron/asar extract-file app.asar dist/main.js
# (수정 후)
npx @electron/asar pack ./extracted app.asar
사실의미
asar는 plaintext concat 이다`strings app.asar
수정 후 다시 pack 가능asar 자체에는 무결성 서명 없음. 외부 code signing만이 변조 감지
API 키를 asar에 넣지 말 것그냥 환경변수보다 5초 더 어려운 정도
비즈니스 로직 보호가 필요하면별도로 난독화 (terser mangle) 또는 V8 bytecode (bytenode)

진짜 무결성은 어디서 오나

레이어무결성 방식
asar 내부없음 (포맷이 그렇다)
.app / .exe 전체OS 수준 code signing (mac: Developer ID, win: Authenticode) — 04 code signing
다운로드 채널TLS + (electron-updater는) latest.yml의 sha512
런타임 변조 감지macOS hardened runtime + Apple notarization ticket stapling

즉, “이 binary가 변조되지 않았다”의 책임은 asar 밖에 있다. asar는 그 책임을 못 진다.

asar integrity (Electron 30+, 부분적 해결)

Electron 30부터는 빌드 시 asar 헤더에 hash를 박고, 런타임에 검증하는 asar integrity 기능이 도입됐다 (macOS 우선).

# electron-builder
asarIntegrity: true   # 기본값
  • macOS는 .appInfo.plist에 hash를 박아 .app 서명 안에 묶어버린다 → asar 교체 시 서명 깨짐 → Gatekeeper 차단.
  • Windows는 비슷한 메커니즘이 v32+에서 들어오는 중.
  • 단, 이건 변조 감지비밀 보호가 아니다. 코드는 여전히 plaintext.

What-if — 흔한 함정

함정증상원인해결
spawn(path.join(__dirname, 'ffmpeg'))이 ENOENT패키지 후만 실패__dirnameapp.asar/... → OS가 그 경로 모름app.asar.unpacked 경로로
native module이 dlopen failed패키지 후만 실패.node가 asar 안에 들어감asarUnpack: ["**/*.node"]
작은 파일이 무진장 많아서 asar가 수백 MBasar 만들기 자체가 느림 + asar 패치/diff 비용무거운 의존성 (puppeteer가 Chromium까지 끌고 옴)files/extraResources로 제외 또는 따로 unpack
비밀이 asar 안에 있다고 안심한 줄 명령으로 노출됨asar는 보안이 아님환경변수 + OS keychain (electron/safeStorage)
빌드 후 asar 안의 한 줄을 핫패치하려고 풀어서 수정 → 재서명 안 함macOS에서 안 열림code signature가 깨짐절대 사후 수정 금지. 수정하려면 재빌드 + 재서명
watch 모드 dev에서는 잘 되는데 prod에서만 경로가 이상개발 시엔 raw 폴더, prod는 asar경로 분기 누락app.isPackaged로 분기

Insight — 흥미로운 이야기

“asar는 GitHub이 만든 게 아니다 — Chromium의 Pickle을 재활용했다”

Pickle은 Chromium IPC의 직렬화 포맷이다. 헤더 구조(4-byte length prefix)가 asar 헤더와 동일한 이유 — Electron 팀이 이미 코드베이스에 있는 도구를 재활용했다. 새 포맷을 발명하지 않고 옆 동네 코드를 가져온 전형적인 시스템 디자인.

“V8 bytecode 컴파일로 진짜 보호하고 싶다면 — bytenode”

JS 소스를 V8 bytecode(.jsc)로 미리 컴파일하면 코드 노출이 상당히 어려워진다. 단점: V8 버전이 정확히 일치해야 함 (Electron upgrade 시 깨짐), 디버깅 어려움, 시작 시간 오히려 느려질 수 있음. 그래서 전부 bytecode가 아니라 민감한 라이센스 검증 등 일부만 적용하는 게 일반적.

“asar integrity가 Electron 30에서 들어온 이유 — Discord 해킹 사건”

2022~2023년 BetterDiscord 같은 클라이언트 mod의 인기와 함께, asar를 핫패치해 malicious code 주입하는 공급망 공격 시나리오가 현실적 우려로 떠올랐다. Electron 팀의 응답이 asar integrity.app 서명이 asar의 hash까지 커버하게 만드는 방식. 즉 asar 자체에 봉인을 박은 게 아니라 OS 코드 서명의 그늘 안에 asar를 집어넣었다.


요약 + Mermaid

asar는 수만 개 작은 파일 → 단일 아카이브성능 최적화다. 압축도, 암호화도, 봉인도 아니다. 네이티브 모듈·사이드카·OS API path 인자는 asarUnpack 으로 풀어둬야 작동한다. 비밀 보호는 asar가 아니라 환경변수 + OS keychain, 변조 감지는 OS 코드 서명 (+ Electron 30+의 asar integrity)이 책임진다.