JavaScript 런타임의 새로운 패러다임을 제시하는 Deno가 2024년 10월, 2.0 버전을 출시했습니다. 2018년 첫 공개 이후 약 6년 만의 주요 업데이트입니다. 흥미롭게도 현재 JavaScript 런타임의 선두 주자인 Node.js와 Deno는 모두 개발자 라이언 달(Ryan Dahl)이 개발하였습니다. 라이언 달은 Deno를 처음 공개할 당시 “Node.js에 대한 10가지 후회(10 Things I Regret About Node.js)” 라는 발표를 합니다. 이 발표에서 그는 Node.js의 초기 설계에서 놓쳤던 점들과 그로 인해 발생한 문제들을 언급했습니다. 달은 이러한 한계를 해결하기 위해 Deno를 개발하기 시작했다고 밝혔죠. 개발자가 자신이 만든 프로젝트를 공개적인 자리에서 회고하고 비판하며 이를 개선한 새로운 프로젝트를 발표하는 것은 흔한 광경은 아닙니다. 그렇다면, Node.js는 어떤 점에서 태생적 한계를 가졌을까요? Deno가 이를 어떻게 풀어낼 수 있었을까요? 이 글에서는 Deno 가 무엇인지부터, Deno 1.0과 Deno 2.0의 주요 특징에 대해 살펴보도록 하겠습니다. <출처: 디노 공식 블로그> 그래서 Deno가 정확히 뭔데?Deno is the open-source JavaScript runtime for the modern web.Deno는 현대 웹을 위한 오픈 소스 자바스크립트 런타임입니다. 공식 홈페이지에서는 Deno를 이렇게 소개하고 있습니다. 이 문장을 이해하기 위해서는 4가지 용어를 먼저 살펴봐야 하는데요, 바로 웹(Web), 오픈 소스(Open source), 자바스크립트(JavaScript), 그리고 런타임(run-time)입니다. 현대 웹먼저 웹은 인터넷을 통해 접근할 수 있는 상호 연결된 공개 웹페이지들의 시스템입니다. 이 시스템은 정보 검색, 소통, 상업 등 다양한 목적으로 사용되며 현대 생활의 필수 요소가 되었습니다. 디노가 ‘현대 웹’을 위한 런타임이라는 것은 최근 웹의 변화에 따른 환경의 요구를 충족하기 위해 설계되었음을 뜻합니다. 여기서 ‘최신 웹 환경의 요구’라는 표현이 모호할 수 있습니다. 구체적인 예를 들어보겠습니다. Node.js는 2024년 8월 출시된 v22.7.0에서야 TypeScript 문법을 실험적으로 지원하기 시작했습니다. 반면 Deno는 2018년 첫 공개 때부터 이미 JavaScript, TypeScript, 그리고 WebAssembly를 기본적으로 지원하고 있었습니다. 이 밖에도 Deno는 여러 가지 검증된 특징을 갖추고 있습니다. 단일 실행 파일(Single execution file), 선택적 접근 권한(파일 시스템, 네트워크, 환경변수 등), Promise 기반의 API, ESM(ECMAScript Modules) 기본 지원, 그리고 URL을 이용한 모듈 관리 등이 그 예입니다. 이 요소들이 모두 새롭거나 혁신적인 것은 아닙니다. URL을 이용한 모듈 관리는 이미 Go에서, 선택적 접근 권한은 이미 브라우저에서 사용되고 있었습니다. 하지만 Deno는 이처럼 검증된 기능들을 JavaScript 런타임에 통합하여 제공한다는 특징을 가집니다. 이는 Deno가 ‘현대 웹’의 요구사항을 잘 반영하며 웹 개발자들에게 더 안전하고 효율적인 개발 환경을 제공하고 있음을 보여줍니다. 오픈 소스오픈 소스란 “오픈 소스 소프트웨어(Open Source Software, OSS)”를 일컬으며 공개적으로 액세스할 수 있도록 설계되어 누구나 적합하다고 생각하는 대로 코드를 보고, 수정하고, 배포할 수 있는 코드를 말합니다.오픈 소스에 대해 더 자세히 알고 싶다면 다음의 글을 참고해 주세요: “[IT 상식사전] 정보 자유화의 핵심, 오픈소스 운동” JavaScriptJavaScript는 경량의 인터프리터 언어(또는 즉석 컴파일 언어)로, 일급 함수(First-Class Function)를 지원하는 프로그래밍 언어입니다. 주로 웹 페이지의 스크립트 언어로 널리 알려져 있지만, Node.js, Apache CouchDB, Adobe Acrobat과 같은 비브라우저 환경에서도 사용됩니다. 런타임이러한 언어의 실행 환경을 런타임(Runtime)이라고 합니다. 초기에 JavaScript의 런타임 환경은 주로 브라우저 내부에 국한되어 있었습니다. 그러나 Node.js의 등장으로 브라우저 밖에서도 실행할 수 있는 언어로 달라졌고, 지금은 서버 사이드 프로그래밍 언어로서의 위치를 확고히 하였습니다. Node.js와 DenoNode.js는 전통적으로 쓰여온 오픈소스 크로스-플랫폼 자바스크립트 런타임입니다. 우리가 알고 있는 많은 프로젝트들이 Node.js를 기반으로 하고 있습니다. 노션이나 슬랙 같은 데스크톱 앱(Electron), 거의 대부분의 현대 프레임워크(React, Vue.js, Angular, Svelte), 크롤링 시스템(Nest.js) 등 수많은 분야에서 사용되고 있지요. 하지만 이러한 Node.js를 개발한 라이언 달조차 그 한계를 극복하기 어려워 Deno를 내놓았습니다. 그렇다면 Node.js가 지닌 한계는 무엇일까요? Node.js가 놓친 것들 PromiseJavaScript 개발자라면 흔히들 “콜백 지옥”이라는 표현을 들어보았을 것입니다. 다음과 같이 중첩된 콜백으로 비동기 작업을 처리할 때면, 코드 가독성이 급격히 떨어지고 유지보수가 어려워지는 문제가 발생합니다. function getUser(userId, callback) { setTimeout(() => { console.log(`User ${userId} fetched`) callback({ id: userId, name: 'Alice' }) }, 1000) } function getPosts(userId, callback) { setTimeout(() => { console.log(`Posts for user ${userId} fetched`) Node.js의 초기 버전에는 이를 대체할 Promise가 한때 생겼지만, 다시 제거되었습니다. 따라서 오늘날 널리 쓰이는 Async/Await 문법도 지원되지 않았습니다. 이 때문에 비동기 처리는 주로 콜백 함수를 활용해야 했고, 이는 콜백 지옥 문제를 초래하곤 했습니다. 보안(Security)V8 자체는 매우 강력한 보안 샌드박스를 제공합니다. 그러나 Node.js의 설계 과정에서 특정 애플리케이션에 대해 이 보안 모델을 유지하는 방법을 더 깊이 고민했다면, Node.js는 다른 언어에서는 제공하지 않을 만큼 강력한 보안을 보장할 수도 있었습니다. 예를 들어 린터(linter)와 같은 도구가 컴퓨터와 네트워크에 완전한 접근을 가질 필요는 없습니다. 하지만, 기존 Node.js 환경에서는 모든 스크립트가 파일 시스템과 네트워크에 자유롭게 접근할 수 있습니다. 곧 보안 측면에서 불필요한 위험이 발생할 수 있습니다. 이는 우리가 iOS나 macOS와 같은 현대적인 OS에서 경험하는 세분화된 권한 제어(fine-grained permission control)와 다른 모습입니다. 최근의 OS는 단순한 식당 대기 앱에 마이크 권한을 부여하거나, 메모 앱이 위치 정보를 요청하는 경우 사용자가 이를 허용할지 선택할 수 있습니다. 이처럼 특정 기능이 불필요한 권한을 가지지 않도록 제한하는 것입니다. 빌드 시스템(The Build System)Node.js의 빌드 시스템 선택 역시 아쉬움으로 남았습니다. V8이 크롬(Chrome)을 통해 GYP를 사용하기 시작하면서, Node.js도 자연스럽게 이를 따랐습니다. 그러나 이후 크롬은 GYP를 버리고 GN으로 전환했고, 그 결과 Node.js만이 GYP를 계속 사용하게 되었죠. 유지보수가 활발하지 않은 시스템을 떠안게 된 셈입니다. 문제는 GYP라는 것이 단순한 내부 도구가 아니란 점입니다. V8과 바인딩을 시도하는 모든 개발자는 이를 직접 다뤄야 하는데, GYP는 JSON과 유사해 보여도 사실은 파이썬(Python) 기반 변형 문법을 사용합니다. 직관적이지 않고, 작성하기도 어려우며, 사용자 경험이 좋다고 할 수도 없죠. 이는 마치 현대적인 빌드 시스템이 아닌, 어딘가 어색한 방식으로 만들어진 DSL을 강제로 사용해야 하는 것과 같습니다. 처음부터 더 나은 빌드 시스템을 선택했거나, 크롬이 GN으로 전환할 때 함께 움직였다면 어땠을까요? package.jsonNode.js 생태계에서 package.json은 필수적인 역할을 하지만, 시간이 지나면서 불필요한 정보들이 점점 더 추가되었습니다. 예를 들어, license, repository, description 같은 항목들은 패키지 실행과 직접적인 관련이 없으며, 결국 보일러플레이트 노이즈(boilerplate noise)로 쌓여만 갔습니다. 처음에는 필수 정보만을 담았던 파일이 점점 무거워지고, 불필요한 메타데이터를 포함하는 방향으로 발전해 버린 것입니다. 더 근본적인 문제는 의존성 관리 방식입니다. 모듈을 중앙화된 저장소에서 관리하도록 설계된 점이 문제입니다. 그러나 이 저장소가 민간 기업에 의해 운영되는 사유 서비스라는 점에서 근본적인 한계를 극복하기도 어렵습니다. 오픈 소스 생태계의 핵심 철학은 분산과 자율성인데, 패키지 관리 시스템은 오히려 단일 조직에 종속된 형태로 자리 잡았죠. 만약 상대 경로와 URL을 기반으로 직접 모듈을 불러오는 방식이 채택되었다면, 파일 경로가 곧 버전을 의미하게 됩니다. 그랬다면 굳이 dependencies 목록을 유지할 필요도 없었을 것입니다. 그러나 현재의 package.json 구조에서는 의존성 목록을 일일이 나열해야 하며, 이는 지속적인 유지보수 부담을 초래합니다. 이러한 구조는 패키지의 가용성, 지속 가능성, 그리고 생태계의 독립성을 위협할 가능성이 있습니다. 만약 초기 설계에서 분산형 패키지 관리 방식이 고려되었다면, 지금과는 다른 방향으로 발전했을지도 모릅니다. node_modulesnode_modules의 필요 이상으로 거대한 크기를 풍자한 그림 <출처: TekForge> Node.js의 node_modules 구조 역시 의도는 좋았지만, 결과적으로 많은 복잡성을 초래했습니다. 가장 큰 문제는 모듈 해석 알고리즘(module resolution algorithm)을 지나치게 복잡하게 만든 것입니다. node_modules 경로를 계층적으로 탐색하는 방식은 단순한 모듈 로딩을 어렵게 만들었고, 프로젝트가 커질수록 관리가 까다로워졌죠. 모듈을 프로젝트 내에 기본적으로 포함하는 vendored-by-default 방식은 의존성 관리의 안정성을 높이려는 의도로 나왔지만, 사실 $NODE_PATH 환경 변수를 사용했더라도 같은 효과를 낼 수 있었습니다. 또한, Node.js의 모듈 로딩 방식은 브라우저의 환경과도 크게 달라졌습니다. 브라우저에서는 URL을 기반으로 모듈을 로드하는 반면, Node.js는 node_modules로 복잡한 경로 탐색을 수행해야 합니다. 이로 인해 서버와 클라이언트 간의 모듈 로딩 방식이 일관되지 않게 되었고, 이는 ESM 도입 이후에도 여전히 혼란을 일으키고 있습니다. 이제 와서 이 구조를 되돌리는 것은 사실상 불가능하지만, 만약 초기에 더 단순한 방식이 도입되었다면 어땠을까요? 지금보다 훨씬 더 깔끔한 모듈 시스템을 가질 수 있지 않았을까요? require(“module”)에서 .js 확장자 생략Node.js에서 require(“module”)을 사용할 때, .js 확장자를 생략할 수 있게 한 것 역시 결국 불필요한 모호성을 초래한 선택이었습니다. 이는 브라우저의 JavaScript 동작 방식과도 다릅니다. 브라우저에서는 <script> 태그 src 속성에 .js 확장자를 생략할 수 없습니다. 이처럼 명시적인 경로를 요구하는 것이 더 직관적이었을 것입니다. 그러나 Node.js는 확장자를 생략하는 방식을 허용하였고, 이로 인해 모듈 로더가 파일 시스템을 여러 경로에서 조회하며 사용자가 의도한 파일을 추측해야하는 비효율적인 과정이 생겨났습니다. 결과적으로, 단순히 .js 확장자를 명시하지 않는 것만으로 불필요한 파일 시스템 접근이 증가하고, 예상치 못한 모듈 해석 방식이 발생할 가능생이 생긴 것이죠. 초기에 더 명확한 규칙을 도입했더라면, 이러한 문제 역시 피할 수 있었을 것입니다. index.jsNode.js는 특정 경로를 불러올 때 index.js를 기본 진입점(entry file)으로 간주하는 규칙을 도입했지만, 이는 결국 모듈 해석 알고리즘을 더 복잡하게 만들었습니다. 경로 내에서 index.js를 자동으로 찾도록 하면서, 모듈 로더가 추가적인 파일 시스템 탐색을 수행해야 하기 때문입니다. 더군다나 require가 package.json의 main 속성을 지원한 이후로는 index.js가 필수 기능이 아니게 되었습니다. 애초에 package.json이 존재하는 환경에서는 불필요한 규칙이 되어버린 것이죠. 결국 index.js를 기본값으로 정한 것은 큰 이점 없이, 모듈 로딩 과정만 복잡하게 만든 선택이었습니다. Deno의 탄생이러한 한계를 극복하기 위해 탄생한 것이 바로 Deno입니다. 라이언 달이 직접 지적한 Node.js의 문제들은 구조적 제약으로 작용하고 있습니다. 그중 많은 부분이 이제는 돌이키기 어려운 선택들이었죠. Deno는 Node.js가 갖고 있던 태생적인 한계를 극복하려고 합니다. 즉, 더 현대적이고 안전한 자바스크립트 및 타입스크립트 런타임 환경을 제공하기 위해 개발된 프로젝트라는 뜻입니다. Deno 1.0; (당시로선) 혁신적인 해결책Deno는 초기 Node.js의 문제점을 근본적으로 재해석하며, 여러 측면에서 개선된 해결책을 제시합니다. Promise 기반 비동기 처리 지원가장 먼저 Promise 기반의 비동기 처리 지원을 손꼽을 수 있습니다. Deno의 모든 비동기 처리는 처음부터 Promise에 기반하기 때문에 EventEmitter와 콜백 패턴에 의존했던 Node.js와는 크게 차이를 보입니다. 예를 들어, Node.js에서는 파일을 읽기 위해 전통적인 fs 모듈과 Promise 인터페이스를 제공하는 fs/promises 모듈이 혼재되어 개발자가 혼란을 겪을 수 있습니다. 반면, Deno는 처음부터 Promise 기반 API를 제공해 코드의 일관성과 가독성을 크게 향상시켰습니다. const data = await Deno.readTextFile('example.txt') console.log(data) 보안: 런타임 설계의 원칙또한, Deno는 Secure-by-Default 철학을 채택하며, 보안을 런타임 설계의 핵심 원칙으로 삼았습니다. 기본적으로 Deno 프로그램은 파일 시스템, 네트워크, 그리고 기타 민감한 리소스에 접근할 수 없는 상태로 시작합니다. 이러한 리소스에 접근하려면 명시적으로 권한 플래그를 거쳐 허용해야 하죠. 이 같은 접근 방식은 애플리케이션뿐만 아니라 의존성에도 동일하게 적용되어, 프로그램 실행 시 어떤 리소스에 접근하는지 자연스럽게 확인하도록 합니다. 예를 들어, --allow-read 플래그를 사용하지 않고 스크립트를 실행하면, 해당 애플리케이션은 파일 시스템을 읽을 수 없습니다. Rust 크레이트로 구현한 모듈화Deno의 또 다른 혁신은 내부 구성 요소 모듈화를 Rust 크레이트로 구현한 점입니다. 앞서 언급한 GYP 대신, Deno는 Rust의 빌드 생태계를 활용하여 보다 현대적이고 유연한 모듈화를 실현했습니다. 특히, rusty_v8 크레이트는 V8 엔진과의 통합을 담당하는 중요한 구성 요소로, V8의 C++ API와 최대한 유사한 인터페이스를 제공하면서도 Rust의 안전성과 성능을 그대로 유지할 수 있게 설계되었습니다. 그 외 의존성을 관리하기 위해 Deno는 웹 브라우저처럼 URL을 통해 외부 코드를 직접 가져오는 기능을 내장하고 있습니다. 그 덕분에 하나의 파일만으로도 복잡한 동작을 구현할 수 있습니다. 또한 타입스크립트를 기본으로 지원하기 때문에, Node.js 에서는 추가적으로 설치하거나 설정해야 했던 번거로움이 없습니다. 최신 타입스크립트의 정적 타입 검사 기능을 바로 활용할 수 있죠. Deno 2.0Deno 2.0은 이러한 철학을 유지하면서도 한 걸음 더 나아간 모습을 보여줍니다. 대표적인 업데이트로는 Node.js 및 npm과의 호환성 강화, package.json 및 node_modules에 대한 네이티브 지원, 새로운 패키지 관리 명령어 도입, 표준 라이브러리 안정화, 프라이빗 npm 레지스트리에 대한 지원, 워크스페이스 및 모노레포 지원, LTS 릴리즈 제공, 그리고 JavaScript 라이브러리를 공유하기 위한 현대적인 레지스트리인 JSR 도입 등이 있습니다. 세부 사항을 하나씩 자세히 확인해 보도록 하겠습니다. Deno 공식 페이지의 직접 만들기 기능으로 구현한 Deno 2.0 <출처: https://deno.com/> Node.js 및 npm 호환성 강화기존 Deno는 Node.js 및 npm과의 호환성이 제한적이어서, 풍부한 Node.js 생태계를 활용하는 데 어려움이 있었습니다. Deno 2.0에서는 이러한 호환성을 대폭 개선했습니다. package.json과 node_modules를 기본적으로 지원하며, npm 워크스페이스까지 활용할 수 있게 된 것이죠. 다만 이러한 변화로 일부 개발자들은 Deno의 초기 설계 철학이 희석될 것을 우려하고 있기도 합니다. Node.js와 호환을 위해 도입된 기능이 Deno의 간결함과 보안성을 저해할 수 있다는 걱정이 나온 거죠. 이에 대한 자세하고 긴 답변은 Deno 블로그에서 확인할 수 있습니다. 의존성 관리 개선기존 Deno에는 공식적인 패키지 관리 도구가 없었기 때문에, 개발자들은 외부 모듈을 직접 URL로 가져와 의존성을 관리했습니다. 이러한 접근법은 모듈의 버전 관리와 의존성 해결에 어려움을 부르곤 했습니다. 이를테면, 각 파일에서 모듈을 가져올 때마다 정확한 버전을 명시해야 했고, 프로젝트 전반에 동일한 모듈의 버전을 일관되게 유지하기도 까다로웠습니다. 또한 모듈 업데이트 시 각 파일을 수동적으로 수정해야 했기 때문에, 대규모 프로젝트에서는 이러한 작업이 비효율과 오류를 유발할 가능성이 높았습니다. Deno 2.0은 새로운 명령어로 의존성 관리를 간소화합니다.deno install: 프로젝트의 deno.json 또는 package.json 파일에 정의된 의존성을 설치합니다.deno add, deno remove: deno.json 또는 package.json 파일에 의존성을 추가하거나 제거할 수 있습니다. 워크스페이스 지원요즘 개발 프로젝트에서는 워크스페이스와 모노레포를 활용하여 여러 패키지와 애플리케이션을 단일 저장소에서 효율적으로 관리합니다. Deno 2.0도 이러한 요구를 충족하기 위해 워크스페이스와 모노레포 지원을 도입했습니다. 이러한 기능들은 대규모 프로젝트의 코드 베이스 관리를 쉽게 만들어 협업을 돕습니다. 개발자 경험 향상Deno 2.0은 개발자 경험을 향상시키기 위해 다양한 도구와 기능을 통합했습니다. 대표적으로 기존에 서드파티로 사용하던 Prettier를 대신하는 내장 코드 포맷터, ESLint를 대신하는 린터, 테스트 프레임워크, 문서 생성기 등이 있습니다. 별도의 설정이나 추가 패키지 없이도 이들 도구를 즉시 활용할 수 있기에, 개발자들이 코드 작성에 집중할 수 있도록 지원합니다. 또한 표준 라이브러리의 안정화로 개발자들에게 신뢰할 수 있는 모듈을 제공합니다. 앞서 소개한 코드 포맷팅이나 테스팅 모듈 외에도 CLI 유틸리티, 네트워크 도구, 데이터 조작 기능을 갖춘 컬렉션 유틸리티 등이 있습니다. 마치며Deno 2.0이 나온 지도 벌써 3달이 훌쩍 지났습니다. Deno가 출시된 이래 가장 큰 변화를 겪고 난 후라 JavaScript 커뮤니티와 개발자들 사이에서 많은 논의가 이어지고 있습니다. 특히 Node.js와의 호환성을 강화하면서도, 초기 Deno가 추구했던 보안성과 간결함을 유지하려는 노력이 큰 관심을 받는 듯합니다. 언어학을 전공한 제게는 흥미롭게도, Deno라는 이름은 Node의 철자를 재배열한 어구전철(語句轉綴, Anagram)입니다. 이는 Node.js의 아이디어와 철학을 기반으로 하면서도 새로운 방향성을 제시하려는 Deno의 의도를 나타냅니다. 이제 Deno는 단순히 Node.js의 대체제가 아니라, Node.js가 해결하지 못한 문제를 개선한, 현대적인 웹 개발 환경 맞춤형 런타임을 목표로 하고 있습니다. 이번 Deno 2.0은 더 나은 호환성과 편의성을 제공하면서도, 개선된 개발자 경험을 제공합니다. 그런 만큼 앞으로 Deno가 점차 많은 프로젝트에서 채택될 가능성도 올라갔습니다. (요즘IT의 프론트엔드도 Deno로 개발해 보면 재밌겠네요.) 앞으로 Deno가 웹 개발 생태계에 어떤 영향을 미칠지 기대가 됩니다. ©️요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.