Deno 완벽 가이드 | 보안·TypeScript·표준 라이브러리·Deploy·실전 활용
이 글의 핵심
Deno로 안전한 JavaScript 개발을 구현하는 완벽 가이드. 보안 모델, TypeScript 네이티브, 표준 라이브러리, Deno Deploy까지 실전 예제로 정리. Deno·JavaScript·TypeScript 중심으로 설명합니다.
이 글의 핵심
Deno로 안전한 JavaScript 개발을 구현하는 완벽 가이드입니다. 보안 모델, TypeScript 네이티브, 표준 라이브러리, Deno Deploy까지 실전 예제로 정리했습니다.
실무 경험 공유: Node.js에서 Deno로 전환하면서, 보안이 크게 향상되고 TypeScript 설정이 불필요해진 경험을 공유합니다.
들어가며: “Node.js가 불안해요”
실무 문제 시나리오
시나리오 1: 보안이 걱정돼요
Node.js는 모든 권한을 가집니다. Deno는 명시적 권한이 필요합니다. 시나리오 2: TypeScript 설정이 복잡해요
Node.js는 설정이 필요합니다. Deno는 네이티브 지원합니다. 시나리오 3: 패키지 관리가 번거로워요
npm은 복잡합니다. Deno는 URL로 import합니다.
1. Deno란?
핵심 특징
Deno는 Ryan Dahl(Node.js 창시자)이 만든 JavaScript 런타임입니다. 주요 장점:
- 보안: 기본적으로 샌드박스
- TypeScript: 네이티브 지원
- 표준 라이브러리: 고품질 내장
- URL Import: npm 불필요
- Web API: 표준 준수
2. 설치 및 기본 사용
설치
# macOS/Linux
curl -fsSL https://deno.land/install.sh | sh
# Windows
irm https://deno.land/install.ps1 | iex
기본 명령어
# 파일 실행
deno run main.ts
# 권한 부여
deno run --allow-net --allow-read main.ts
# 모든 권한
deno run -A main.ts
# REPL
deno
3. 보안 모델
권한 시스템
# 네트워크 접근
deno run --allow-net main.ts
# 파일 읽기
deno run --allow-read main.ts
# 파일 쓰기
deno run --allow-write main.ts
# 환경 변수
deno run --allow-env main.ts
# 특정 도메인만
deno run --allow-net=api.example.com main.ts
4. HTTP 서버
기본 서버
// server.ts
Deno.serve({ port: 3000 }, (req) => {
const url = new URL(req.url);
if (url.pathname === '/') {
return new Response('Hello Deno!');
}
if (url.pathname === '/api/users') {
return Response.json([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
]);
}
return new Response('Not Found', { status: 404 });
});
실행
deno run --allow-net server.ts
5. 표준 라이브러리
HTTP
import { serve } from 'https://deno.land/[email protected]/http/server.ts';
serve((req) => {
return new Response('Hello from std/http!');
}, { port: 3000 });
파일 시스템
// 읽기
const text = await Deno.readTextFile('data.txt');
// 쓰기
await Deno.writeTextFile('output.txt', 'Hello Deno!');
// JSON
const data = JSON.parse(await Deno.readTextFile('data.json'));
await Deno.writeTextFile('output.json', JSON.stringify(data));
6. URL Import
외부 모듈
import { serve } from 'https://deno.land/[email protected]/http/server.ts';
import { z } from 'https://deno.land/x/[email protected]/mod.ts';
const userSchema = z.object({
name: z.string(),
email: z.string().email(),
});
serve((req) => {
return Response.json({ message: 'Hello!' });
});
Import Map
// deno.json
{
"imports": {
"std/": "https://deno.land/[email protected]/",
"zod": "https://deno.land/x/[email protected]/mod.ts"
}
}
import { serve } from 'std/http/server.ts';
import { z } from 'zod';
7. Deno KV
기본 사용
const kv = await Deno.openKv();
// 쓰기
await kv.set(['users', '1'], { name: 'John', email: '[email protected]' });
// 읽기
const result = await kv.get(['users', '1']);
console.log(result.value);
// 삭제
await kv.delete(['users', '1']);
// 리스트
const entries = kv.list({ prefix: ['users'] });
for await (const entry of entries) {
console.log(entry.key, entry.value);
}
Atomic Operations
const result = await kv.atomic()
.check({ key: ['users', '1'], versionstamp: null })
.set(['users', '1'], { name: 'John' })
.commit();
if (result.ok) {
console.log('Success');
}
8. Fresh (Web Framework)
설치
deno run -A -r https://fresh.deno.dev
라우트
// routes/index.tsx
export default function Home() {
return (
<div>
<h1>Welcome to Fresh!</h1>
</div>
);
}
// routes/api/users.ts
export const handler = {
GET: () => {
return Response.json([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
]);
},
};
9. Deno Deploy
배포
# GitHub 연동 또는 CLI
deno deploy --project=my-project main.ts
환경 변수
# Deno Deploy Dashboard → Settings → Environment Variables
DATABASE_URL=postgresql://...
10. Deno 런타임 심화: Rust·V8·Tokio·권한·모듈·FFI
이 절에서는 CLI 사용법을 넘어, Deno가 어떤 프로세스 구조로 동작하는지, 권한이 코드 경로 어디에서 걸리는지, 모듈이 어떤 순서로 해석·캐시되는지, FFI로 네이티브 코드를 붙일 때의 제약을 정리합니다. 운영·보안 리뷰·성능 트러블슈팅 관점에서 특히 유용합니다.
10.1 Rust + V8 + Tokio: 프로세스 안에서의 역할 분담
Deno의 코어는 Rust로 작성됩니다. JavaScript 실행은 V8 Isolate가 담당하고, 파일·소켓·타이머 등 비동기 I/O와 스케줄링은 Tokio 런타임이 담당하는 구성이 일반적입니다. 사용자 코드는 V8 안에서 실행되지만, fetch, 파일 읽기, TCP 연결 같은 작업은 Rust 쪽의 네이티브 연산(op, 예전에는 op_crates라고 부르던 경로) 으로 위임됩니다.
이때 중요한 설계 포인트는 다음과 같습니다.
- V8 Isolate: 자바스크립트 힙과 실행 스택을 격리합니다. 워커·서브리소스마다 별도 격리를 두는 패턴이 가능해, 멀티테넌트·샌드박스 요구에 맞춥니다.
- Tokio: 태스크 스케줄러, I/O 드라이버, 타이머를 제공합니다. Deno는 이 위에서 “JS가 요청한 비동기 작업”을 완료하면 마이크로태스크/매크로태스크 규칙에 맞춰 V8으로 결과를 되돌립니다.
- Rust 레이어: 실제 시스템 콜 직전에 권한 검사, 경로 정규화, 리소스 한도 같은 정책을 적용하기 좋은 위치입니다. 즉 “JS가 곧 OS 권한”이 되지 않도록, Rust 경계에서 막는다는 점이 Deno 보안 모델의 골격입니다.
실무적으로는 “CPU 바운드 작업을 JS에서 무한 루프로 돌리면 이벤트 루프가 막힌다”, “네이티브 바인딩이 많은 코드는 op 호출 빈도가 성능을 좌우한다” 같은 관찰이 여기서 나옵니다. CPU 집약 작업은 Worker로 분리하거나, 아래 FFI로 네이티브 쪽에서 처리하는 편이 안전합니다.
10.2 권한 시스템 구현: 무엇이 “허용”을 바꾸는가
CLI 플래그(--allow-net, --allow-read 등)는 내부적으로 비트마스크나 구조체 형태의 권한 집합으로 정리되고, 런타임 초기화 시점에 전역 정책으로 고정됩니다. 이후 Deno.readFile, Deno.connect, Deno.env.get 같은 고수준 API는 호출 직전에 이 정책을 조회합니다.
구현 관점에서 기억할 만한 축은 다음과 같습니다.
- 세분화: 네트워크는 호스트·포트 단위로, 파일은 경로 프리픽스 단위로 허용 목록을 둘 수 있습니다. “전체 허용”과 “최소 권한”의 차이는 운영 사고에서 치명적입니다.
- 서브프로세스·FFI:
--allow-run,--allow-ffi는 권한 범위가 넓습니다. CI·이미지·systemd 유닛에서 플래그를 실수로-A로 고정하지 않았는지가 첫 번째 감사 포인트입니다. - 일관성: 순수 ECMAScript만으로는 파일을 열 수 없고, 결국
Deno.*나 표준 Web API 중 Deno가 연결한 경로를 탑니다. 보안 리뷰 시 “서드파티 모듈이 어떤 API를 호출하는지”를 추적하면 공격 면이 정리됩니다.
요약하면, Deno 권한은 “OS 사용자 권한을 그대로 쓰는 것”이 아니라 런타임이 제공하는 래퍼 함수들에 인가 정책을 심는 것에 가깝습니다. 그래서 런타임 업데이트가 보안 패치의 핵심 경로입니다.
10.3 Import 해석: URL, 캐시, Lock, npm 호환
Deno의 모듈 ID는 본질적으로 URL입니다. 로컬 파일이든 https://든, 해석기는 다음과 같은 파이프라인을 밟습니다.
- 가져오기 지정자(specifier) 정규화: 상대 경로·절대 URL·
npm:·jsr:·data:등 스킴을 규칙에 맞게 펼칩니다. - Import map 적용:
deno.json의"imports"가 여기서 매핑됩니다. 모노레포에서 버전을 한곳에 고정하는 핵심입니다. - 페치·캐시: 원격 모듈은
DENO_DIR아래에 캐시됩니다. 오프라인·재현 가능 빌드를 위해 캐시 디렉터리를 CI 아티팩트로 보존하는 전략이 흔합니다. - 무결성·재현성:
deno.lock은 해석된 의존성 그래프의 지문을 고정합니다. 라이브러리 업데이트는 의도적인 변경으로만 일어나게 합니다. - TypeScript:
.ts/.tsx는 트랜스파일 후 V8이 실행할 수 있는 형태로 맞춥니다. 트랜스파일러·모듈 해석기 버전은 Deno 버전에 묶여 있으므로, 팀 전체가 동일 메이저를 쓰는 것이 중요합니다.
npm: 의존성은 Node 생태 호환을 위해 별도의 해석 규칙과 설치 레이아웃을 탑니다. “Deno가 npm을 쓴다”기보다 “npm 스펙을 Deno 모듈 그래프에 편입한다”에 가깝게 이해하면, 버전 충돌·중복 설치 이슈를 진단하기 쉽습니다.
10.4 네이티브 모듈 로딩과 FFI
Deno는 동적 라이브러리 FFI(Deno.dlopen)를 통해 C ABI를 가진 .dll/.so/.dylib를 불러올 수 있습니다. 이 경로는 메모리 안전성을 스스로 책임져야 하므로, 런타임은 --allow-ffi와 함께 매우 보수적인 기본값을 둡니다.
형태만 보면 다음과 같이 심볼 테이블을 선언하고 네이티브 함수를 호출합니다(실제 시그니처·경로는 플랫폼·빌드 산출물에 맞게 조정해야 합니다).
// 예시: 플랫폼별 .so/.dll 경로를 빌드 파이프라인에서 주입
const path = new URL("./lib/native/release/libexample.so", import.meta.url);
const lib = Deno.dlopen(path, {
add: { parameters: ["i32", "i32"], result: "i32" },
} as const);
const a = 21;
const b = 21;
console.log(lib.symbols.add(a, b)); // 42 — 잘못된 선언이면 UB
실행 시에는 deno run --allow-ffi --allow-read main.ts처럼 라이브러리 파일 읽기와 FFI를 함께 열어줍니다.
실전 체크리스트는 다음과 같습니다.
- 심볼 시그니처: C 헤더와 FFI 선언이 어긋나면 즉시 UB(정의되지 않은 동작)로 이어집니다. 가능하면 공식 바인딩이나 이미 검증된 크레이트를 씁니다.
- 버퍼 수명:
ArrayBuffer,UnsafePointer계열을 넘길 때, 네이티브 쪽이 비동기로 붙잡아 두면 Use-After-Free가 납니다. 수명 규칙을 코드 리뷰에서 명시적으로 적습니다. - 배포: 컨테이너·엣지 런타임마다 사전 빌드된 바이너리와 glibc/musl 같은 ABI가 다릅니다. FFI 바이너리는 타깃 OS·아키텍처별 산출물을 분리하는 편이 안전합니다.
웹Assembly(WebAssembly API)나 Rust로 작성해 Wasm으로 컴파일하는 방법은 FFI 대신 격리된 대안이 될 수 있습니다. 성능·개발 비용·배포 형태를 비교해 선택합니다.
10.5 프로덕션 패턴: 최소 권한·재현성·관측 가능성
운영 환경에서 Deno를 안정적으로 굴리려면 “빠르게 돌아간다”보다 예측 가능하고 감사 가능한가가 우선입니다.
- 권한 최소화: 엔트리포인트 스크립트에 필요한 플래그만 나열하고,
-A는 개발 전용으로 제한합니다. 허용 호스트·경로는 환경별로 분리합니다. - Lockfile 고정: 배포 파이프라인에서
deno.lock을 커밋하고,deno install --frozen(또는--frozen-lockfile)으로 락과 실제 의존성 불일치 시 실패하도록 두거나,deno.json의"lock": { "frozen": true }로 CI 정책을 고정합니다. - 정적 검사:
deno check,deno lint, 테스트를 PR 게이트에 둡니다. 런타임 업그레이드 시에는 마이너 버전도 회귀 테스트가 필요합니다. - 컨테이너: 멀티스테이지 빌드로 바이너리·캐시를 넣고, 런타임 이미지는 공격 면이 작은 베이스를 선택합니다. 엔트리포인트 CMD에 권한 플래그가 하드코딩되어 있는지 인프라 코드로 검토합니다.
- 관측성: 구조화된 로그, 요청 ID, 업스트림 타임아웃,
AbortSignal기반 취소를 표준으로 둡니다. Deno Deploy·자체 호스팅 모두 엣지·서버의 cold start·지역성을 전제로 SLO를 잡습니다. - 시크릿: 환경 변수 주입은 오케스트레이터 시크릿·Vault 등으로 하고, 로그에 키가 새지 않도록 마스킹 규칙을 둡니다.
이 패턴들은 Deno만의 독특한 마법이 아니라, URL 기반 모듈 + 명시적 권한이라는 두 축을 운영 규율로 고정하는 작업입니다. 팀 규모가 커질수록 deno.json 작업(tasks), 포맷터·린트 공유, CI 캐시 전략이 합쳐져 “재현 가능한 Deno 레포”가 됩니다.
정리 및 체크리스트
핵심 요약
- Deno: 안전한 JavaScript 런타임
- 보안: 명시적 권한
- TypeScript: 네이티브 지원
- 표준 라이브러리: 고품질 내장
- URL Import: npm 불필요
- Deno KV: 내장 데이터베이스
- 내부 구조: Rust 코어 + V8 Isolate + Tokio 비동기,
Deno.*·Web API 경계에서 권한 검사 - 모듈: URL·import map·
DENO_DIR캐시·deno.lock으로 재현성 - FFI:
Deno.dlopen+--allow-ffi, 시그니처·수명·ABI를 운영 이슈로 관리
구현 체크리스트
- Deno 설치
- HTTP 서버 구현
- 표준 라이브러리 활용
- KV 스토리지 사용
- Fresh 프레임워크 사용
- Deno Deploy 배포
- 권한 최적화
- 런타임·락파일·CI(
--frozen또는lock.frozen)로 의존성 재현성 확보 - FFI·네이티브 의존 시 OS/아키텍처별 산출물·ABI 검증
같이 보면 좋은 글
- Bun 완벽 가이드
- Cloudflare Workers 완벽 가이드
- Node.js 완벽 가이드
이 글에서 다루는 키워드
Deno, JavaScript, TypeScript, Runtime, Security, Backend, Serverless
내부 동작과 핵심 메커니즘
이 글의 주제는 「Deno 완벽 가이드 | 보안·TypeScript·표준 라이브러리·Deploy·실전 활용」입니다. 앞선 튜토리얼을 구현·런타임 관점에서 다시 압축합니다. 시스템·런타임 경계(스케줄링, I/O, 메모리, 동시성)를 기준으로 “입력이 어디서 검증되고, 핵심 연산이 어디서 일어나며, 부작용(I/O·네트워크·디스크)·동시성이 어디서 터지는가”를 한 장면으로 그리면 장애 분석이 빨라집니다.
처리 파이프라인(개념도)
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
경계에서의 지연·실패(시퀀스 관점)
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(프로세스·런타임·게이트웨이) participant D as 의존성(외부 API·DB·큐) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
알고리즘·프로토콜·리소스 관점 체크포인트
- 불변 조건(Invariant): 각 단계가 만족해야 하는 조건(버퍼 경계, 프로토콜 상태, 트랜잭션 격리, 파일 디스크립터 상한)을 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 동일 입력에 동일 출력이 보장되는 순수 층과, 시간·네트워크·스레드 스케줄에 의해 달라질 수 있는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화/역직렬화, 문자 인코딩, syscall 횟수, 락 경합, GC·할당, 캐시 미스처럼 누적 비용을 의심 목록에 넣습니다.
- 백프레셔: 생산자가 소비자보다 빠를 때(소켓 버퍼, 큐 깊이, 스트림) 어디서 어떤 신호로 속도를 줄일지 정의합니다.
프로덕션 운영 패턴
실서비스에서는 기능과 함께 관측·배포·보안·비용·규제가 동시에 요구됩니다.
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율/지연 분위수(p95/p99), 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시 계층·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션 호환성·플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·파일 디스크립터·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 가능한 한 프로덕션에 가깝게 맞추는 것이 재현율을 높입니다.
확장 예시: 엔드투엔드 미니 시나리오
「Deno 완벽 가이드 | 보안·TypeScript·표준 라이브러리·Deploy·실전 활용」을 실제 배포·운영 흐름으로 옮긴 체크리스트형 시나리오입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드 표를 API 또는 이벤트 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 한 화면(로그+메트릭+트레이스)에서 추적한다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지(또는 피처 플래그) 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값이 기대 범위인지 본다.
의사코드 스케치(프레임워크 무관)
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request) // 경계에서 거절
authorize(validated, ctx) // 권한·테넌트
result = domainCore(validated) // 순수에 가까운 규칙
persistOrEmit(result, idempotentKey) // I/O: 멱등·재시도 정책
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성 불안정, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정이 로컬과 다름 | 프로필·시크릿·기본값, 지역 리전 | 단일 소스(예: 스키마 검증된 설정)와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
자주 묻는 질문 (FAQ)
Q. Node.js를 대체할 수 있나요?
A. 대부분의 경우 가능하지만, npm 생태계가 필요하면 Node.js가 더 적합합니다.
Q. npm 패키지를 사용할 수 있나요?
A. 네, npm: 접두사로 사용할 수 있습니다. 예: import express from "npm:express";
Q. 프로덕션에서 사용해도 되나요?
A. 네, Slack, Netlify 등 많은 기업에서 사용합니다.
Q. Bun과 비교하면 어떤가요?
A. Deno가 보안이 더 강력합니다. Bun이 더 빠릅니다.