단일 실행 파일 Node.js 앱 만들기 (고통 없이)

발행: (2025년 12월 31일 오후 11:00 GMT+9)
8 min read
원문: Dev.to

I’m happy to translate the article for you, but I don’t see the article’s content in your message—only the source link. Could you please provide the text you’d like translated? Once I have the full content, I’ll keep the source line unchanged and translate the rest into Korean while preserving all formatting, markdown, and technical terms.

단일 실행 파일 애플리케이션 (SEA)

Node.js는 오늘날과 같은 방식으로 배포되도록 설계된 것이 아니었습니다.
이 글을 끝까지 읽으면 실행 가능한 명령줄 바이너리를 얻게 됩니다 – node 설치도, npm install도, “내 컴퓨터에서는 동작해”도 필요 없습니다. 단지 바이너리 하나만 있으면 됩니다.

그리고 CLI뿐만 아니라, 같은 방식으로 배포되는 실제 GUI 실행 파일도 만들 수 있습니다.

Node.js는 제대로 활용하면 놀라운 C++ 런타임입니다.
하지만 도구 체인 문제와 끝없이 이어지는 CommonJS ↔ ESM 호환성 혼란 때문에 고통을 겪는 경우가 많습니다.

Node.js에 더 나은 도구 체인을 제공하고자 하는 취지에서, 제가 지금까지 본 가장 아름다운 번들러인 Zig 번들러에 영감을 받아, Node의 가장 잘 활용되지 않은 기능에 집중하기로 했습니다.

빠른 시작

// build.js
import { release } from "sea-builder";

const production = {
  entry: "app/index.js",
  name: "myapp",
  platforms: ["linux-x64", "win-x64"],
  outDir: "./dist",
  assets: {
    "README.md": "./README.md",
  },
};

release(production)
  .then((res) => console.log(res))
  .catch((err) => console.log(err));

그게 전부입니다 – 실행 파일에는 다음이 포함됩니다:

  • 여러분의 앱
  • Node 런타임
  • 모든 에셋

장점

  • 빠른 시작(특히 코드 캐시와 함께)
  • 스냅샷과 같은 저수준 유틸리티
  • 예측 가능한 네이티브 모듈
  • 개발 환경이 아닌 실제 소프트웨어와 같은 사용자 경험

이것이 Go, Rust, Zig가 배포하는 방식입니다. Node도 할 수 있습니다 – 단지 좋은 툴링이 없었을 뿐이죠. 이것이 sea‑builder가 하는 일입니다.

참고: Node.js 20+가 필요합니다. node -v 명령으로 버전을 확인하세요.

설치

npm init -y && npm i sea-builder

프로젝트 구조

app/
  index.js
  README.md
cross-build.js

예시: 간단한 CLI

// app/index.js
const { getAsset, isSea } = require("node:sea");

console.log(`Hello, ${process.argv[2] || "World"}!`);
console.log(`Running from: ${process.execPath}`);
console.log(`Platform: ${process.platform}`);
console.log("Running as SEA!\n");

const readme = getAsset("README.md", "utf8");
console.log("readme:", readme);

// binary asset (returns ArrayBuffer)
const configBinary = getAsset("README.md");
console.log(
  "Binary size:",
  configBinary.byteLength,
  "bytes"
);
// cross-build.js
const { release } = require("sea-builder");

const production = {
  entry: "app/index.js",
  name: "myapp",
  platforms: ["linux-x64", "win-x64"],
  outDir: "./dist",
  assets: {
    "README.md": "./README.md",
  },
};

release(production)
  .then((res) => console.log(res))
  .catch((err) => console.log(err));

빌드 실행:

node cross-build.js

sea-builder는 올바른 Node 바이너리를 다운로드하고 패치합니다. 쉽죠?

예시: 윈도우형 (GUI) 앱 – tessera.js

tessera.js는 제가 만든 Node.js 렌더러입니다.
자세한 내용은 [How I Built a Graphics Renderer for Node.js] 기사에서 확인하세요.
이 가이드에서는 준비된 레포지토리를 사용할 것입니다.

git clone https://github.com/sklyt/tessera-seaexample.git
cd tessera-seaexample
npm i
node ./tesseraexample/build.js

생성된 실행 파일은 ./dist/img.exe에 위치합니다.

왜 먼저 CommonJS로 번들링하나요?

ESM은 SEA를 포함한 모든 좋은 것들의 적입니다.
우선 모든 것을 CommonJS로 번들링한 뒤, 그 번들을 sea-builder에 넘깁니다.

// inside build.js (esbuild step)
await build({
  entryPoints: [entryJs],
  bundle: true,
  platform: "node",
  target: "node22",          // 최소 Node 버전에 맞게 조정
  format: "cjs",
  outfile: bundlePath,

  // 네이티브 바이너리는 외부에 둡니다
  external: ["*.node", "*.dll"],

  sourcemap: false,
  logLevel: "info",

  define: {
    require: "require",
  },
});

GUI 앱을 위한 SEA 설정

const bundlerOptions = {
  entry: bundlePath,               // 번들된 스크립트
  name: appName,
  platforms: targets,
  outDir: outDir,
  assets: {
    "win-x64": {
      "prebuilds/win32-x64/renderer.node": join(
        __dirname,
        "/prebuilds/win32-x64/renderer.node"
      ),
      "prebuilds/win32-x64/glfw3.dll": join(
        __dirname,
        "/prebuilds/win32-x64/glfw3.dll"
      ),
      "prebuilds/win32-x64/raylib.dll": join(
        __dirname,
        "/prebuilds/win32-x64/raylib.dll"
      ),
    },
    "linux-x64": {
      "prebuilds/linux-x64/renderer.node": join(
        __dirname,
        "/prebuilds/linux-x64/renderer.node"
      ),
    },
  },

  // 선택적 플래그 (코드 캐시는 현재 플랫폼에서만 작동합니다;
  // 교차 컴파일 빌드를 위해 CI 워크플로를 사용하세요)
  useCodeCache: false,
  useSnapshot: false,
};

console.log("Calling sea() with bundled entry and platform assets...");
await sea(bundlerOptions);
console.log("sea() finished. Outputs are in", path.resolve(outDir));

SEA에서 자산 로드하기

// img.js (helper)
const getter = (name) => {
  try {
    return getAsset(name); // string or ArrayBuffer
  } catch {
    return null;
  }
};

const { Renderer, FULLSCREEN, RESIZABLE } = loadRendererSea({
  assetGetterSync: getter,
});

이제 공유 가능한 GUI 실행 파일이 준비되었습니다.

향후 참고: sea-builder의 다음 버전에서는 터미널이 GUI 실행 파일과 함께 실행되지 않을 예정입니다. 필요하다면 바이너리 헤더를 직접 패치할 수 있습니다. 하지만 솔직히 이미 잘 동작합니다.

sea-builder 설정 예시

module.exports = {
  // Development build – fast iteration
  dev: {
    entry: "src/index.js",
    name: "myapp-dev",
    platforms: "current",
    outDir: "./build",
    useCodeCache: true,
  },

  // Production build – all platforms
  production: {
    entry: "src/index.js",
    name: "myapp",
    platforms: "all",
    outDir: "./dist",
    clean: true,
    assets: {
      "config.json": "./config/production.json",
      "README.md": "./README.md",
      LICENSE: "./LICENSE",
    },
  },

  // Server build – Linux only
  server: {
    entry: "src/server.js",
    name: "myapp-server",
    platforms: ["linux-x64", "linux-arm64"],
    outDir: "./dist/server",
    useCodeCache: true,
    assets: {
      "config.json": "./config/server.json",
    },
  },

  // CLI tool – small and portable
  cli: {
    entry: "src/cli.js",
    name: "myapp-cli",
    platforms: ["linux-x64", "win-x64", "macos-arm64"],
    outDir: "./dist/cli",
    useCodeCache: true,
    assets: {
      "help.txt": "./assets/help.txt",
    },
  },
};

요약

  • SEA는 Node 앱, 런타임 및 모든 자산을 포함하는 단일 바이너리를 배포할 수 있게 해줍니다.
  • sea-builder는 올바른 Node 바이너리를 다운로드하고, 패치를 적용하며, 코드를 번들링(가능하면 CommonJS 형태)하는 과정을 자동화합니다.
  • 결과는 Go/Rust/Zig 네이티브 바이너리와 같은 느낌을 줍니다 – 빠른 시작, 예측 가능한 네이티브 모듈, 그리고 깔끔한 사용자 경험.

한 번 작성하고 어디서든 실행할 수 있는 Node.js의 단순함을 직접 체험해 보세요!

빌드 구성

{
  // …
  cli: {
    entry: './src/cli.js',
    platforms: 'all',
    outDir: './dist/cli'
  }
}

Note: useCodeCache: true는 현재 플랫폼에서만 작동합니다. 이는 플랫폼에 특화되어 있기 때문입니다. 대상별로 캐시된 바이너리를 빌드하려면 CI를 사용하세요.

Build Script (build.js)

// build.js
const { sea } = require('sea-builder');
const config = require('./sea.config');

// node build.js production
const profile = process.argv[2] || 'dev';

if (!config[profile]) {
  console.error(`Unknown profile: ${profile}`);
  console.log('Available profiles:', Object.keys(config).join(', '));
  process.exit(1);
}

console.log(`Building with profile: ${profile}`);

sea(config[profile]).then(results => {
  console.log(`\nBuilt ${results.length} executable(s)`);
});

나에 대한 더 많은 내용

  • Node.js에서 진화 알고리즘 시각화
  • tessera.js 저장소

읽어 주셔서 감사합니다!

저를 찾아주세요

Back to Blog

관련 글

더 보기 »