Deno 2의 npm 호환성, JSR(JavaScript Registry), import map, deno add를 통한 패키지 관리, 그리고 Node.js에서 Deno로의 마이그레이션 전략을 다룹니다.
Deno 1.x가 아무리 뛰어난 설계와 보안을 갖추고 있었더라도, npm 생태계와의 단절은 치명적인 약점이었습니다. 수백만 개의 npm 패키지를 사용할 수 없다는 것은 실무에서 Deno를 채택하기 어렵게 만드는 가장 큰 장벽이었습니다.
Express, React, Prisma, zod, lodash 같은 핵심 라이브러리를 사용할 수 없다면, 아무리 런타임이 좋아도 프로덕션에 적용하기 어렵습니다. Deno 팀도 이 현실을 직시했고, Deno 2에서 이 문제를 정면으로 해결했습니다.
Deno 2에서는 npm: 접두사를 통해 npm 패키지를 직접 import할 수 있습니다. 별도의 설치 과정이 필요 없습니다.
// npm 패키지를 직접 import
import express from "npm:express@4";
import { z } from "npm:zod@3";
import chalk from "npm:chalk@5";
// 바로 사용 가능
const app = express();
const UserSchema = z.object({
name: z.string(),
email: z.string().email(),
});
console.log(chalk.green("서버 시작"));
app.listen(3000);npm: 접두사 뒤에 패키지명과 버전을 지정하면, Deno가 자동으로 해당 패키지를 다운로드하고 캐시합니다. 전역 캐시를 사용하므로 한 번 다운로드한 패키지는 다른 프로젝트에서도 재사용됩니다.
npm: 접두사를 사용하면 Deno가 내부적으로 Node.js 호환 레이어를 활성화합니다. require(), __dirname, process 등 Node.js 전용 API가 해당 패키지 내에서 동작하도록 호환성 심(shim)을 제공합니다.
// 정확한 버전
import lodash from "npm:lodash@4.17.21";
// 메이저 버전 범위
import express from "npm:express@4";
// 마이너 버전 범위
import zod from "npm:zod@3.22";
// 최신 버전 (프로덕션에서는 권장하지 않음)
import dayjs from "npm:dayjs";Deno 2는 선택적으로 node_modules 디렉토리를 생성할 수 있습니다. 일부 도구나 라이브러리가 node_modules의 존재를 전제로 동작하는 경우에 유용합니다.
{
"nodeModulesDir": "auto"
}nodeModulesDir 옵션에는 세 가지 값을 사용할 수 있습니다.
"none" - node_modules를 생성하지 않음 (기본값)
"auto" - 필요할 때 자동으로 node_modules 생성
"manual" - npm install로 직접 관리하는 node_modules 사용기존 Node.js 프로젝트를 마이그레이션할 때는 "auto"로 시작하여 호환성을 확보한 후, 점진적으로 "none"으로 전환하는 것을 권장합니다.
npm 레지스트리는 JavaScript 생태계의 핵심 인프라이지만, 몇 가지 구조적 한계를 안고 있습니다.
1. TypeScript를 네이티브로 지원하지 않음
- .d.ts 파일을 별도로 관리해야 함
- @types/* 패키지에 의존
2. ESM/CJS 이중 발행의 복잡성
- 두 모듈 시스템을 모두 지원하려면 빌드 설정이 복잡
3. 패키지 품질 검증 부재
- 누구나 발행 가능, 품질 기준 없음
4. 보안 검증 한계
- 패키지 소유권 탈취 사례 빈발JSR(JavaScript Registry)는 이러한 한계를 해결하기 위해 Deno 팀이 만든 차세대 패키지 레지스트리입니다.
// jsr: 접두사로 JSR 패키지 import
import { assertEquals } from "jsr:@std/assert@1";
import { join } from "jsr:@std/path@1";
import { parse } from "jsr:@std/csv@1";
// Deno 표준 라이브러리는 모두 JSR에서 관리됩니다JSR 패키지를 프로젝트에 추가하는 방법은 다음과 같습니다.
# JSR 패키지 추가
deno add jsr:@std/assert
deno add jsr:@std/path
# npm 패키지도 추가 가능
deno add npm:express@4
deno add npm:zod
# 추가된 패키지는 deno.json의 imports에 자동 등록됩니다{
"imports": {
"@std/assert": "jsr:@std/assert@^1.0.0",
"@std/path": "jsr:@std/path@^1.0.0",
"express": "npm:express@^4.18.0",
"zod": "npm:zod@^3.22.0"
}
}JSR에 패키지를 발행하는 과정은 npm보다 단순합니다. TypeScript 소스를 그대로 발행할 수 있어 빌드 과정이 필요 없습니다.
{
"name": "@myorg/my-library",
"version": "1.0.0",
"exports": "./mod.ts"
}# 패키지 발행
deno publish
# 발행 전 검증 (드라이런)
deno publish --dry-runJSR에 발행된 패키지는 npm에서도 사용할 수 있습니다. JSR이 자동으로 TypeScript를 JavaScript로 변환하고, .d.ts 파일과 package.json을 생성하여 npm 호환 형식으로 제공합니다. 따라서 JSR 패키지 하나만 발행하면 Deno, Node.js, Bun 모두에서 사용 가능합니다.
Import Map은 모듈 경로를 별칭(alias)으로 매핑하는 기능입니다. deno.json의 imports 필드가 이 역할을 합니다.
{
"imports": {
"@/": "./src/",
"@db/": "./src/database/",
"@utils/": "./src/utils/",
"@std/assert": "jsr:@std/assert@^1.0.0",
"express": "npm:express@^4.18.0",
"zod": "npm:zod@^3.22.0",
"hono": "jsr:@hono/hono@^4.0.0"
}
}// Import Map 덕분에 깔끔한 경로 사용 가능
import { UserService } from "@/services/user.ts";
import { connectDB } from "@db/connection.ts";
import { formatDate } from "@utils/date.ts";
import { assertEquals } from "@std/assert";
import { Hono } from "hono";Import Map의 큰 장점은 패키지 버전을 한 곳에서 관리할 수 있다는 점입니다. Node.js에서는 package.json이 이 역할을 하지만, Deno에서는 deno.json의 imports가 그 역할을 합니다.
{
"imports": {
"zod": "npm:zod@^3.22.4"
}
}// src/routes/user.ts
import { z } from "zod"; // 3.22.4 사용
// src/routes/product.ts
import { z } from "zod"; // 동일한 3.22.4 사용
// 버전 변경은 deno.json 한 곳에서만 수행deno.lock 파일은 프로젝트의 모든 의존성에 대한 해시값을 기록합니다. 이를 통해 의존성이 변조되지 않았음을 검증할 수 있습니다.
# Lock 파일 자동 생성/업데이트 (기본 동작)
deno run main.ts
# Lock 파일 무결성 검증 (CI에서 사용)
deno run --frozen main.ts
# Lock 파일 없이 실행 (권장하지 않음)
deno run --no-lock main.ts프로덕션 배포와 CI/CD 환경에서는 반드시 --frozen 플래그를 사용하세요. 이 플래그는 deno.lock에 기록된 해시와 실제 다운로드된 모듈의 해시가 일치하는지 검증합니다. 불일치 시 즉시 에러가 발생하므로, 공급망 공격이나 의존성 변조를 감지할 수 있습니다.
기존 Node.js 프로젝트를 Deno로 마이그레이션하는 것은 한 번에 이루어질 필요가 없습니다. Deno 2의 npm 호환성 덕분에 점진적 마이그레이션이 가능합니다.
가장 먼저, 기존 Node.js 프로젝트를 Deno에서 그대로 실행하는 것부터 시작합니다.
{
"nodeModulesDir": "auto",
"tasks": {
"dev": "deno run --allow-all src/index.ts",
"test": "deno test --allow-all"
}
}이 단계에서는 --allow-all을 사용하여 먼저 동작을 확인하고, 점진적으로 권한을 줄여나갑니다.
package.json 의존성을 deno.json의 Import Map으로 이전합니다.
{
"dependencies": {
"express": "^4.18.0",
"zod": "^3.22.0",
"pg": "^8.11.0"
}
}{
"imports": {
"express": "npm:express@^4.18.0",
"zod": "npm:zod@^3.22.0",
"pg": "npm:pg@^8.11.0"
}
}npm 패키지를 Deno 네이티브 또는 JSR 패키지로 교체하고, Node.js API를 Deno API로 전환합니다.
// Before: Node.js 방식
import { readFileSync } from "node:fs";
const content = readFileSync("./data.json", "utf-8");
// After: Deno 방식
const content = await Deno.readTextFile("./data.json");
// Before: Node.js HTTP 서버
import http from "node:http";
const server = http.createServer((req, res) => {
res.end("Hello");
});
server.listen(3000);
// After: Deno HTTP 서버
Deno.serve({ port: 3000 }, (_req) => {
return new Response("Hello");
});마이그레이션 시 주의해야 할 점들이 있습니다.
1. 네이티브 바이너리 모듈
- node-gyp 기반 C++ 모듈은 호환되지 않을 수 있음
- bcrypt, sharp 등 네이티브 모듈 대안 확인 필요
2. require() 사용 코드
- Deno는 기본적으로 ESM만 지원
- npm 패키지 내부의 require()는 호환 레이어가 처리
- 프로젝트 코드의 require()는 import로 전환 필요
3. 글로벌 변수 차이
- __dirname, __filename은 import.meta로 대체
- process 객체 접근 방식 변경
4. 테스트 프레임워크
- Jest/Mocha에서 Deno.test 또는 호환 러너로 전환// Node.js
const dir = __dirname;
const file = __filename;
// Deno
const dir = new URL(".", import.meta.url).pathname;
const file = new URL(import.meta.url).pathname;
// 또는 @std/path 활용
import { dirname, fromFileUrl } from "jsr:@std/path";
const dir = dirname(fromFileUrl(import.meta.url));마이그레이션 과정에서 문제가 발생하면 deno info 명령으로 의존성 트리를 확인할 수 있습니다. 어떤 모듈이 어디서 로드되는지, 해결 순서는 어떻게 되는지를 시각적으로 보여줍니다.
Deno 2의 패키지 관리는 유연합니다. 프로젝트 상황에 맞는 전략을 선택할 수 있습니다.
| 전략 | 적합한 상황 | 설정 |
|---|---|---|
| JSR 중심 | 새 프로젝트, Deno 네이티브 | jsr: 접두사 사용 |
| npm 호환 | 기존 Node.js 마이그레이션 | npm: 접두사 + nodeModulesDir |
| 혼합 사용 | 점진적 전환 프로젝트 | Import Map에서 두 레지스트리 혼용 |
어떤 전략을 선택하든, Import Map을 통한 중앙 관리와 Lock 파일을 통한 무결성 검증은 항상 유지하는 것이 좋습니다.
다음 장에서는 Deno가 제공하는 웹 표준 API와 Deno 전용 API를 살펴봅니다. fetch, WebSocket, Streams부터 Deno.serve, Deno.KV까지 Deno의 API 설계 철학을 이해합니다.
이 글이 도움이 되셨나요?
Deno의 권한 기반 보안 모델을 심층 분석합니다. 각 권한 플래그의 동작 원리, Node.js와의 보안 비교, 공급망 공격 방어, 그리고 실무 보안 모범 사례를 다룹니다.
Deno가 채택한 웹 표준 API(fetch, WebSocket, Web Crypto, Streams)와 Deno 전용 API(Deno.serve, Deno.KV, Deno.open)를 심층적으로 분석합니다.
Deno 2와 Bun을 심층 비교합니다. 아키텍처 차이, 성능 벤치마크, API 호환성, 생태계 성숙도, 그리고 프로젝트 특성에 따른 선택 기준을 제시합니다.