Skip to content
Published on

Deno 2.9 — deno desktop 실험, 락파일 그대로 마이그레이션, 그리고 24시간 공급망 기본값

공유하기
Authors

들어가며 — 6월 25일의 Deno 2.9, 3주간의 패치

지난 5월 Deno 2 시대 정리 글에서 Deno의 방향 전환 — Node를 거부하던 런타임이 Node 호환을 1급 시민으로 받아들인 것 — 을 다뤘습니다. 이번 글은 그 후속입니다. Deno 2.9가 2026년 6월 25일에 나왔고, GitHub 릴리스 태그 기준으로 v2.9.0(6월 25일) 이후 v2.9.1(7월 1일), v2.9.2(7월 8일), v2.9.3(7월 15일)까지 매주 패치가 따라붙었습니다. 직전 마이너인 2.8.0이 5월 22일이었으니, 5주 간격의 릴리스 리듬입니다.

릴리스 노트의 헤드라인은 deno desktop이지만, 제 판단으로 실무자에게 더 중요한 건 두 가지 다른 변화입니다. 기존 npm·pnpm·yarn·Bun 락파일을 그대로 읽어 들이는 마이그레이션 경로, 그리고 공급망 방어 기능이 기본값으로 켜진 것. 하나씩 뜯어보고, 벤더가 낸 성능 숫자는 측정 조건과 빠진 것까지 같이 읽겠습니다. 참고로 타입스크립트 컴파일러 쪽 격변은 TS 7 GA와 네이티브 컴파일러 글에서 따로 다뤘습니다 — 이 글은 런타임 계층 이야기입니다.

deno desktop — Electron의 자리에 들어온 실험

deno desktop은 웹 스택으로 네이티브 데스크톱 앱을 만드는 새 서브커맨드입니다(PR #33441, 6월 16일 머지). UI는 웹뷰에서 돌고, 로직은 Deno에서 돌고, 결과물은 단일 배포 바이너리입니다. 가장 단순한 형태는 엔트리포인트가 UI를 서빙하는 것인데, 데스크톱 엔트리포인트 안의 Deno.serve()는 웹뷰가 여는 포트에 자동으로 바인딩되므로 포트 배선이 필요 없습니다.

// main.ts — 이게 전부입니다
Deno.serve(() =>
  new Response(
    "<!DOCTYPE html><h1>Hello from Deno desktop</h1>",
    { headers: { "content-type": "text/html" } },
  )
);
deno desktop main.ts        # 네이티브 창이 뜹니다
deno desktop                # 현재 디렉토리의 웹 프레임워크 자동 감지
deno desktop --hmr          # 개발 중 HMR

프레임워크 자동 감지는 deno compile과 같은 메커니즘을 쓰고, 공식 릴리스 노트 기준으로 Next.js, Astro, Fresh, Remix, Nuxt, SvelteKit, SolidStart, TanStack Start, Vite SSR을 지원합니다. 런타임에는 Deno.BrowserWindow(창 제어와 웹뷰-Deno 브리지), Deno.Tray(시스템 트레이), macOS용 Deno.Dock, 네이티브 다이얼로그로 렌더링되는 prompt()/alert()/confirm(), 바이너리 패치를 백그라운드로 적용하는 Deno.autoUpdate()가 들어왔습니다.

렌더링 엔진은 --backend로 고릅니다. 기본값 webview는 OS 내장 엔진 — Windows의 WebView2, macOS·리눅스의 WebKit — 을 쓰므로 바이너리가 작고 실행이 빠르지만, 렌더링이 호스트 OS의 엔진 버전에 좌우됩니다. --backend cef는 Chromium Embedded Framework로 Chromium을 통째로 번들해서 모든 플랫폼에서 동일한 렌더링을 보장하는 대신, 릴리스 노트 표현으로 수십 MB가 추가되고 빌드 시 다운로드가 필요합니다. 정확히 Tauri(웹뷰) 대 Electron(번들 엔진)의 트레이드오프를 한 도구 안에서 고르는 구조입니다.

배포 쪽이 흥미롭습니다. --output 확장자를 따라 macOS는 .app·.dmg, Windows는 .exe·.msi, 리눅스는 .AppImage·.deb·.rpm으로 나오고, MSI와 deb·rpm 인스톨러가 순수 Rust로 만들어지기 때문에 플랫폼별 패키징 툴체인 없이 리눅스 CI 한 대에서 --all-targets로 전 플랫폼 바이너리를 뽑을 수 있습니다. 지원 타깃은 deno compile과 같은 다섯 개 — 리눅스 x64/arm64, Windows x64, macOS x64/arm64 — 이고, Windows arm64는 아직 없습니다.

언제 쓰지 말아야 하나. 벤더 스스로 2.9에서는 experimental이며 표면이 아직 안정화 중이라고 못박고 있습니다. 프로덕션 데스크톱 앱을 실험 플래그 위에 올리는 건 그 자체로 답이 나온 결정이고, 오늘 출시할 물건이라면 Electron·Tauri라는 검증된 선택지가 그대로 있습니다. webview 백엔드를 고르면 "사용자 OS의 WebKit·WebView2 버전마다 렌더링이 다를 수 있다"는 Tauri 사용자들이 이미 아는 고통이 따라오고, cef를 고르면 Electron식 용량이 돌아옵니다. 이 실험의 진짜 의미는 지금 당장의 프로덕션 채택이 아니라, "런타임·컴파일러·패키징이 이미 한 몸인 도구가 데스크톱까지 먹으려 한다"는 방향 신호에 가깝습니다.

락파일 시드 — 이번 릴리스에서 가장 실무적인 기능

전환 비용의 가장 큰 덩어리는 언제나 "정성껏 고정해 둔 의존성 그래프를 잃는 것"입니다. 2.9는 이 지점을 직접 겨냥합니다. deno.lock이 없는 프로젝트에서 deno install을 실행하면 package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lock을 읽어 정확히 같은 해석 버전과 무결성 해시로 deno.lock을 시드합니다(bun.lock 지원 PR #35394). 재해석이 없으므로 몰래 올라가는 버전도 없다는 게 공식 설명입니다. 시드는 한 방향입니다 — 외부 락파일을 읽어 deno.lock을 만들고, 이후 관리는 deno.lock이 맡습니다. 되쓰기(write-back)는 하지 않습니다.

$ deno install
Seeded deno.lock from package-lock.json

pnpm 모노레포도 챙겼습니다. npm·yarn·Bun이 쓰는 package.json의 workspaces 필드는 원래 읽었지만 pnpm만 별도 파일을 쓰는데, 이제 pnpm-workspace.yaml을 발견하면 packages·catalog·catalogs 설정을 기존 필드를 건드리지 않고 이관해 줍니다(#34993). 그리고 진짜 node 바이너리가 설치돼 있지 않은 환경에서는 PATH에 스탠드인 node를 놓아 자신에게 포워딩하고 Node CLI 인자를 번역합니다(#34969) — Turbopack 워커 풀처럼 node를 직접 호출하는 도구를 위한 조치이고, 진짜 node가 있으면 절대 가리지 않으며 DENO_DISABLE_NODE_SHIM=1로 끌 수 있습니다.

이 묶음의 의미는 분명합니다. "Deno로 갈아타기"가 의존성 그래프 재구축이라는 도박이 아니라, 기존 락파일을 유지한 채 명령 두 개로 시험해 보고 아니면 돌아올 수 있는 가역적 실험이 됐다는 것. 다만 락파일 형식 호환의 엣지 케이스 — pnpm이 의존성 키에 peer를 인라인 인코딩하는 방식 같은 — 는 릴리스 직전까지 후속 PR로 다듬어지고 있었으므로, 복잡한 pnpm 모노레포라면 시드 결과의 버전들이 원본과 일치하는지 확인하고 넘어가는 게 안전합니다.

Node 26 호환 — "호환 타깃"이라는 말의 정확한 의미

2.9는 Node.js 호환 타깃을 Node 26으로 올렸습니다. process.version이 v26.3.0을 보고하고(#34747), Deno가 돌리는 node-compat 테스트 스위트도 26.3.0으로 올라갔습니다(#34746). 맥락을 붙이면 — endoflife.date 기준 Node 26은 2026년 5월 5일에 나온 Current 라인이고 LTS 승격은 2026년 10월 28일 예정이며, 실제 최신은 7월 8일의 26.5.0입니다. 즉 호환 타깃은 실제 Node보다 패치 두 개 뒤를 따라갑니다.

실질적인 개선도 몇 가지 있습니다. import "fs" 같은 맨(bare) 내장 모듈 지정자가 플래그 없이 무조건 node:fs로 해석되고, node_modules의 패키지가 내장 모듈을 가리던 버그도 Node와 같은 규칙(내장이 항상 승리)으로 고쳐졌습니다. Node-API는 버전 10을 보고하고, node:testmock.module()·mock.timers·파일 스냅샷이 붙었으며, process.resourceUsage() 같은 빠져 있던 API가 채워졌습니다.

정직하게 짚을 부분: "호환 타깃 26"은 Deno가 자신이 돌리는 호환 테스트 스위트의 기준 버전을 26.3.0으로 맞췄다는 뜻이지, Node 26 생태계가 100% 돈다는 인증이 아닙니다. 네이티브 애드온이나 V8 내부에 기대는 도구, Node 내부 구현 디테일에 의존하는 패키지는 여전히 개별 확인 대상입니다. 그 경계가 어디인지는 릴리스 노트가 말해 주지 않으므로, 후보 프로젝트에서 직접 테스트 스위트를 돌려 보는 것 외의 지름길은 없습니다.

벤더 벤치마크 정확히 읽기

릴리스 노트의 성능 숫자입니다. 전부 벤더 자체 측정이고, 조건은 이렇게 명시돼 있습니다 — 전용 x86_64 리눅스 박스 한 대, 서버와 부하 생성기를 서로 겹치지 않는 코어에 고정, 동시성 100, oha 3회 실행의 중앙값, 콜드 스타트는 hyperfine 150회 실행의 평균, 비교 대상은 Deno 2.8.0.

항목2.82.9변화
콜드 스타트 (hello world)34.2 ms17.3 ms1.98배 빠름
Deno.serve "realworld" (JSON POST + Bearer 헤더 에코)56.8k req/s72.4k req/s1.27배
Deno.serve plaintext77.0k req/s85.6k req/s1.11배
Deno.serve 1 MiB 응답1,617 req/s1,907 req/s1.18배
피크 RSS, realworld142 MB64 MB2.2배 감소
피크 RSS, 1 MiB 응답197 MB63 MB3.1배 감소

배율은 원 수치와 산술적으로 맞아떨어집니다(직접 나눠 확인했습니다). 개선의 출처도 릴리스 노트가 밝히고 있습니다 — 스냅샷에서 node: 글로벌을 지연 로딩으로 빼고, Node 부트스트랩을 Node 워커로 한정하고, 지연 로딩되는 ESM에 V8 코드 캐시를 붙이고, 스냅샷을 축소한 것이 콜드 스타트의 절반을 만들었고, HTTP 처리량은 Deno 소유의 HTTP/1.1 서빙 경로(#34446)가, RSS 평탄화와 함께 crypto.subtleconsole의 JS에서 Rust로의 포팅이 나머지를 만들었습니다.

이 숫자들이 말하지 않는 것도 그만큼 분명합니다.

  • 비교 대상이 자기 자신입니다. 2.8 대비 2.9이지, Node나 Bun과의 비교가 아닙니다. "1.27배"는 갈아탈 이유가 아니라 업그레이드할 이유입니다.
  • 루프백 트래픽입니다. 같은 박스에서 코어만 나눠 쏜 부하입니다. 1 MiB 워크로드의 1,907 req/s는 대역폭으로 약 16 Gbps에 해당하는데, 이건 NIC를 거치지 않는 루프백이라 가능한 수치입니다. 실제 네트워크·TLS 종단이 끼면 그림이 달라집니다.
  • HTTP/1.1 평문 경로입니다. 새 서빙 경로 자체가 HTTP/1.1 대상이고, TLS나 HTTP/2 수치는 제시되지 않았습니다.
  • 처리량과 RSS뿐입니다. 지연 분포 — p99 같은 꼬리 지연 — 는 없습니다. 3회 중앙값이라 분산 정보도 없습니다.
  • 에코 워크로드입니다. "realworld"라는 이름이 붙었지만 DB도 디스크도 없는 JSON 에코입니다. 실제 앱의 병목은 대개 런타임 바깥에 있습니다.

그 위에서 하나만 고르라면 저는 처리량이 아니라 메모리를 보겠습니다. 2.8에서는 RSS가 워크로드에 따라 약 94 MB에서 197 MB까지 자랐는데, 2.9는 측정된 워크로드들에서 62 MB 부근으로 평탄하게 유지됩니다. 같은 머신에 더 많은 인스턴스를 올릴 수 있다는 뜻이라, 컨테이너 밀도로 비용을 계산하는 팀에게는 처리량 1.1~1.3배보다 이쪽이 실질적인 변화입니다 — 단, 해당 워크로드들에서의 결과라는 단서는 그대로 둡니다.

공급망 기본값 — 24시간 대기가 디폴트가 됐다

2.9에서 조용히 가장 큰 정책 변화는 이것입니다. 2.6에서 도입됐던 min-release-age — 게시된 지 일정 시간이 지나지 않은 npm 패키지 버전의 설치를 거부하는 기능 — 가 24시간 창으로 기본 활성화됐습니다(#35458, 릴리스 당일 머지). 근거는 단순합니다. npm 공급망 공격의 상당수는 악성 버전이 게시되고 하루 이틀 안에 탐지·게시 철회되므로, 기다리는 것만으로 큰 부류의 공격이 걸러집니다. 지난주 axios 사건 정리에서 본 것처럼 provenance가 있어도 뚫리는 구멍이 있는 만큼, 시간 지연은 단순하지만 실효적인 방어층입니다.

# .npmrc — 명시 값이 기본값보다 우선합니다
min-release-age=72h    # 3일로 늘리기
min-release-age=0      # 완전히 끄기

운영에서 걸릴 수 있는 지점을 짚어 두면 — 자사 패키지를 npm에 게시하고 곧바로 설치하는 파이프라인은 이제 기본 설정에서 24시간 동안 실패합니다. 핫픽스를 게시 즉시 배포에 태우는 워크플로라면 .npmrc에서 값을 조정해야 하고, 이 트레이드오프(신선한 패치 지연 vs 공격 창 차단)는 각자 계산할 몫입니다. npm 패키지에 적용되는 기능이라는 점도 명확히 해 둡니다.

여기에 옵트인 방어가 하나 더 얹혔습니다. trust-policy=no-downgrade는 pnpm의 설계를 따라 각 버전의 게시 방식 — 2FA 라이브 승인이 붙는 staged publishing, provenance 증명이 붙는 trusted publishing, provenance 단독 — 의 신뢰 순위를 매기고, 이전 버전들보다 약한 증거로 게시된 새 버전의 해석을 하드 에러로 만듭니다. 유지보수자 토큰 탈취 공격(2025년 8월의 s1ngularity 사건이 릴리스 노트에 예시로 등장합니다)의 전형적 흔적인 "provenance로 잘 게시되던 패키지가 갑자기 평범한 토큰 게시로 나오는" 상황을 잡는 장치입니다. 레지스트리 전반의 provenance 채택이 아직 고르지 않아 기본값은 꺼져 있다는 설명까지, 릴리스 노트가 스스로 한계를 밝히는 부분은 그대로 옮겨 둡니다.

업그레이드할 때 걸리는 것들

동작 변경이 하나 있습니다. Deno.serve의 자동 응답 압축이 기본 비활성으로 바뀌었습니다. 기존 버전의 자동 압축에 기대어 대역폭을 아끼고 있었다면, 2.9로 올리는 순간 응답이 비압축으로 나갑니다 — 서버별로 automaticCompression: true를 주거나 DENO_SERVE_AUTOMATIC_COMPRESSION=1 환경 변수로 프로세스 전체에 되살릴 수 있습니다. CDN 뒤라면 체감이 없겠지만 직접 서빙 중이라면 업그레이드 체크리스트에 올려 둘 항목입니다.

나머지는 실험 딱지가 붙은 채로 들어왔습니다. CSS 모듈 임포트(with { type: "css" }로 CSSStyleSheet를 받는 웹 표준 방식)는 --unstable-raw-imports 플래그 뒤에 있고, deno compile --bundle — 트리셰이킹 후 임베드해서 바이너리를 줄이는 — 은 실험 플래그이며 벤더 자체 측정으로 lodash hello world가 11.6 MB에서 1.5 MB로 줄었다는 수치가 제시돼 있습니다(당연히 npm 의존성이 무거운 프로젝트일수록 효과가 크고, 일반화는 각자의 빌드에서 확인할 일입니다).

테스트 러너는 눈에 띄게 두꺼워졌습니다. t.assertSnapshot() 내장 스냅샷, Deno.test.each 파라미터라이즈, --shard CI 분할, 변경된 파일에 영향받는 테스트만 고르는 --changed, 플래키 대응 --retry와 커버리지 임계값까지 — Vitest나 Jest를 부르던 이유를 하나씩 지우는 방향입니다. 그 밖에 deno task의 입력 기반 캐싱, npm ls에 해당하는 deno list, 그리고 ML-KEM·ML-DSA·SLH-DSA 등 NIST 포스트퀀텀 알고리즘의 Web Crypto 구현( OpenSSH 10.4의 ML-DSA 이야기와 같은 흐름의 런타임 버전)도 이번 릴리스에 들어 있습니다.

그래서 갈아탈 것인가

정리해 보겠습니다.

시험해 볼 이유가 생긴 경우

  • 기존 Node 프로젝트인데 락파일을 잃지 않고 가역적으로 실험할 수 있게 됐습니다 — 전환 비용의 구조가 바뀐 게 이번 릴리스의 핵심입니다.
  • 포매터·린터·테스트 러너·태스크 러너·컴파일러를 하나로 줄이고 싶은 팀 — 2.9의 테스트 러너 보강으로 외부 도구 의존이 실제로 줄었습니다.
  • npm 공급망 방어를 도구 기본값으로 받고 싶은 조직 — 24시간 대기 기본화는 현재 주요 런타임 중 가장 공격적인 기본값입니다.

아직 아닌 경우

  • 네이티브 애드온·Node 내부 API에 깊이 기댄 코드베이스 — "호환 타깃 26"이 보장하지 않는 영역입니다.
  • 프로덕션 데스크톱 앱 — deno desktop은 실험이고, Electron·Tauri를 대체할 단계가 아닙니다.
  • Bun 특화 API(bun:sqlite, Bun.serve 등) 위에 선 코드 — 락파일은 읽어 주지만 API는 번역해 주지 않습니다.
  • 이미 Node 26에서 문제없이 돌고 전환으로 얻을 구체적 항목을 못 대는 팀 — 벤치마크 표가 보여 주듯 2.9의 숫자는 Node 대비 우위 주장이 아닙니다.

참고로 경쟁 런타임 쪽 상황을 한 줄 남기면 — Bun의 GitHub 릴리스 기준 마지막 안정판은 5월 13일의 v1.3.14로 두 달째 새 안정 릴리스가 없지만, 저장소 커밋은 오늘도 이어지고 있어 정체로 읽는 건 무리입니다. 다만 매주 패치를 내보내는 Deno와의 릴리스 리듬 차이는 기록해 둘 만한 사실입니다. Bun 쪽 상태 평가는 5월의 Bun 회고를 참고하십시오.

마치며

Deno 2.9를 관통하는 논리는 "전환 비용을 구조적으로 낮추기"입니다. 락파일을 그대로 읽고, node 바이너리 자리를 메꾸고, pnpm 워크스페이스 설정을 이관하고, Node 26 호환 타깃을 따라가는 것 — 전부 같은 방향입니다. 2년 전 Node 호환을 받아들인 자기 부정이, 이제 경쟁자의 락파일 포맷까지 읽는 단계에 왔습니다. 그 위에 얹힌 deno desktop은 아직 실험이지만 도구 통합이라는 같은 논리의 연장이고, min-release-age 기본화는 런타임이 공급망 방어의 기본값을 정하는 시대가 왔다는 신호입니다.

벤더 숫자는 조건과 함께 읽으면 유용했습니다 — 루프백 마이크로벤치마크라는 한계 안에서, 콜드 스타트 절반과 RSS 평탄화는 메커니즘 설명까지 붙은 믿을 만한 개선입니다. 다만 그 표에 Node와 Bun이 없다는 사실이 이 릴리스의 성격을 정확히 말해 줍니다. 2.9는 "우리가 더 빠르다"가 아니라 "옮겨 오는 비용을 줄였다"는 릴리스이고, 그 주장만큼은 숫자가 아니라 기능 목록이 증명하고 있습니다.

참고 자료