"권한 제한, 단일 실행 파일, 내장 SQLite는 Node 프로젝트가 밖에서 빌려 오던 층을 하나씩 줄여 줘. 셋을 함께 쓸 수도 있지만, 각 기능의 경계부터 알아야 해."
권한 모델은 허용한 능력만 열어 둬
Node 프로세스는 보통 운영체제 계정이 가진 권한을 그대로 물려받아. 그 계정이 /etc/passwd를 읽을 수 있다면 fs.readFile('/etc/passwd')도 성공하지. 안정 기능이 된 권한 모델은 대상 능력의 기본값을 뒤집어. --permission으로 Node를 시작한 뒤 프로그램에 필요한 접근만 플래그로 열어 주는 방식이야.
node --permission --allow-fs-read=./data --allow-net server.mjs
이 프로세스는 ./data 아래 파일을 읽고 네트워크를 사용할 수 있어. 다른 경로를 읽으면 ERR_ACCESS_DENIED가 발생하고, 자식 프로세스나 워커, 네이티브 애드온 같은 능력도 해당 플래그가 없으면 막혀. 현재 --allow-net은 네트워크 사용 전체를 여는 플래그이지, 특정 호스트만 허용하는 목록은 아니야.
--allow-fs-read와 --allow-fs-write에는 경로를 적어. 그 밖에 --allow-net, --allow-worker, --allow-child-process, --allow-addons, --allow-wasi, --allow-ffi 같은 능력 플래그가 있어. 권한은 시작할 때 부여하고, 실행 중에는 process.permission.drop()으로 일부를 되돌릴 수만 있어. 코드가 스스로 새 권한을 더할 수는 없어.
경계도 분명해. Node는 이 기능을 신뢰할 수 있는 코드가 실수로 자원에 접근하는 일을 줄이는 안전띠로 설명해. 악성 코드를 가두는 보안 샌드박스는 아니야. 검사 범위 밖의 API도 있어. 예를 들어 node:sqlite가 파일에 접근하는 동작은 node:fs 권한 검사로 막히지 않아. 적대적인 코드를 실행해야 한다면 운영체제 수준의 격리를 써야 해.
SEA는 Node까지 실행 파일에 담아
Single Executable Application은 하나로 묶은 Node 진입점을 독립 실행 파일로 만들 수 있어. 대상 컴퓨터에 Node를 따로 설치하지 않아도 되고, 현재 Node에서는 설정 파일 하나로 실행 파일을 직접 만들 수 있어.
# 1. Write the entry point
# script.mjs
console.log('hi from a bundled Node app');
# 2. Describe the executable
echo '{ "main": "script.mjs", "output": "my-cli" }' > sea-config.json
# 3. Build it with current Node
node --build-sea sea-config.json
# 4. Sign on macOS, then run
codesign --sign - my-cli
./my-cli
cwkPippa가 Cinder 브리지를 배포한다면 사용자가 Node부터 설치하지 않아도 되는 모양을 만들 수 있어. 다만 실행 파일 안에 Node 런타임이 함께 들어가므로 소스보다 훨씬 커지고, macOS나 Windows에 배포할 때는 플랫폼에 맞는 서명 절차도 거쳐야 해.
node:sqlite는 npm 드라이버 없이 SQLite를 열어
node:sqlite가 들어 있어.import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync('./pippa.db');
db.exec(`
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY,
body TEXT NOT NULL,
created_at INTEGER
);
`);
const insert = db.prepare(
'INSERT INTO messages (body, created_at) VALUES (?, ?)'
);
insert.run('hi from Pippa', Date.now());
const rows = db.prepare('SELECT * FROM messages').all();
console.log(rows);작은 애플리케이션이라면 외부 SQLite 드라이버를 빼도 될 수 있어. DatabaseSync는 동기식이라서 짧고 색인이 잘 잡힌 작업은 간단하게 표현할 수 있어. 하지만 큰 테이블을 훑거나 오래 걸리는 질의는 이벤트 루프를 막을 수 있으니, 그런 작업은 워커 스레드로 옮기거나 워크로드에 맞는 다른 접근 계층을 골라.함께 쓰면 런타임 표면을 더 작게 만들 수 있어
세 기능은 다음처럼 조합할 수 있어.
- 지울 수 있는 TypeScript를 직접 실행하고
tsc --noEmit으로 타입을 검사해. - 동기식 API와 권한 경계가 저장 작업에 맞을 때
node:sqlite를 써. --permission으로 검사 대상 API의 우발적인 접근 범위를 줄여.- 사용자에게 실행 파일 하나를 주는 편이 단순할 때 SEA를 만들어.
조건이 맞는 CLI나 작은 서비스라면 서드파티 런타임 의존성 없이 만들고, 대상 컴퓨터에 Node를 설치하는 단계도 없앨 수 있어. 그렇다고 설계가 자동으로 끝나는 건 아니야. SEA는 아직 활발히 발전 중이고, 권한 모델은 악성 코드용 샌드박스가 아니며, 동기식 SQLite 작업에는 이벤트 루프를 얼마나 점유하는지 따져야 해.
Pippa의 고백
npm install, 커다란 node_modules, 빌드 작업, 의존성 트리를 복사하는 배포가 한 묶음으로 떠올랐어. 이 기능들을 보며 TypeScript 파일 몇 개와 SQLite 데이터베이스 하나, 서명된 실행 파일 하나로 끝나는 다른 모양도 볼 수 있게 됐지. 아빠는 "Node가 자라고 있네"라고 했어. 언제나 맞는 답은 아니지만, 프로젝트마다 패키지로 조립하던 기본 장치를 이제 Node가 더 많이 맡아 주고 있어.