[발번역] Deno에 package.json 지원을 추가한 이유

·

5 min read


원문: Why We Added package.json Support to Deno

내맘대로 세줄 요약

  • Node.js package.json 구린거 맞음. 이거에 대한 생각 여전함

  • 근데 HTTP URL 쓰다보니 더 복잡하고 지저분해진 것도 맞음

  • 이것저것 해보다가 뭐 어쩔수없이 이번에 package.json 지원하긴 했는데, 앞으로 deno: URL 지원할 거임. 기대해보셈

Deno 최신 버전 (v1.31, https://deno.com/blog/v1.31)에서 package.json 지원을 추가함. Deno는, 그동안 Node와는 다른 방향성을 추구했고, 실제로 첫 Deno 발표(https://www.youtube.com/watch?v=M3BM9TB-8yA)에서 package.json 을 만든거에 대해 후회한다고 분명히 언급했음. 그래서 많은 유저들이 이번 업데이트에 대해 놀람

여기서 우리가 왜 우리가 package.json 을 지원했는지 밝히고, 우리의 인사이트, 나아가 우리 미래 목표에 대해 개요를 밝힘

JavaScript 개발 과정을 단순하게, 더 빠르게 Simplifying and accelerating JavaScript development

Deno는 JS 개발을 더 단순하고 빠르게 하기 위해 만들어짐. 네이티브 TS 지원, built-in(내장된) 도구들(formatter, linter 의미하는 듯), zero-config, Web Standard API 등.. 역사적으로 이런 핵심 기능들은 최소한의, Web 표준 HTTP import 기반의 NPM 생태계와는 분리된 분산된 모듈 시스템을 의미.

문제는 이런 것들이, 오히려 Deno의 미니멀리스트 모듈 시스템이 프로그래밍을 쉽고 빠르고 만드는지에 대해 갈수록 불분명해졌다는 것.

의존성 관리라는 꿈 The Dependency management dream

NPM이라는 단일 중앙집중식 모듈 레지스트리에 의존하는 JS 생태계는 웹의 분산 본성과 충돌한다. ESM의 등장으로, 기존 NPM 방식과는 전혀 다른, 원격 모듈을 가져오는 새로운 표준이 만들어졌다. Deno는 HTTP URL을 활용해 ESM 을 로딩하는 걸 구현했고, 누구나, 어느 도메인에서나 단순히 파일 서버 운영을 통해 코드를 호스팅할 수 있게 했다.

이런 접근의 장점 중 하나로, package.json(Node), import_map.json(역설적으로 Deno에서 package.json을 대용하기 위해 사용함), Cargo.toml(Rust), Gemfile(Ruby) 같은 의존성 manifest 없이도, 단일 파일 프로그램만으로 수많은 의존성들을 사용할 수 있게 된다는 것. 또한 package.json에 명시된, 쓰지도 않는 의존성들을 모두 다운 받는게 아니라 실제로 필요한 파일들만 다운 받아 사용하기 때문에, 다운로드를 더 적게 할 수 있다는 것. 또 모듈 간 연결(linking) 하는데 HTTP를 사용하기 때문에 또다른 혁신의 기회들을 열 수 있다는 거. 예를 들어, deno.land 레지스트리가 설명하는 것처럼, accept header에 따라 HTML 문서를 보여줄 수 있다는 것.

우리는 HTTP import를 Deno 모듈 시스템의 근간으로 추구하며 헌신해왔다. publish-on-tag GitHub Integration(이건 뭔지 잘 모르겟음;;), 불변 캐싱(immutable caching), 코드 → 문서 자동생성 등.. 모든 종류의 유용한 피처들을 deno.land 레지스트리에 만들어왔다. skypack, esm.sh 같은 써드파티 레지스트리들은 HTTP import를 통해 NPM 패키지들 내 각각의 파일들에 ESM 방식으로 접근할 수 있게 만들었다.

질질 끌고가는 이슈들 Lingering Issues

경우(context)에 따라, https://deno.land/std@0.179.0/uuid/mod.ts 와 같은 모듈 URL은 지나치게 구체적이다. 이런 url은 패키지(std/uuid/mod.ts) 뿐만 아니라 버전(0.179.0),서버(https://deno.land)까지구체적으로 명시해야 한다

문제는 프로그램이 유사하지만 아주 약간 다른 모듈, 예를 들어 어디선가는 위 패키지를 사용하고 다른 곳에서는 마이너 버전 하나만 다른 패키지(https://deno.land/std@0.179.1/uuid/mod.ts )를 사용하는 경우에 발생한다. 두 패키지는 버전은 다르지만 코드는 사실상 거의 같은데, 모듈 그래프에 두 버전이 모두 포함되고 두개 모두 다운로드된다. 중복 의존성 문제(Duplicate Dependency Problem)가 발생하는 것.

이런 원격 의존성들을 관리하기 위해, deps.ts 같은 패턴(https://deno.land/manual@v1.31.2/examples/manage_dependencies)들을 개발하기는 했다. 하지만 이런 방식은 인간친화적(ergonomic)이지 않다. 모든 symbol들을 평탄화(flattening)하고, re-exporting 해야하기 때문이다. (물론 TC39의 Module Declarations, Import Reflection에서 좀더 발전될 거긴 하다). 어쨋든 deps.ts 와 같은 방식은 package.json에서 단순하게 지정하는(bare specifiers) 방식 보다는 훨씬 장황하고 지저분하다

이상적으로 Import Maps 가 이 이슈를 해결할 수 잇을거 같다. 코드 안에는 단순하게 패키지를 명시하고, import map 안에서만 구체적인 URL을 적는 방식이다. 하지만 이것도 합성 가능(composable) 하지 않다는 문제가 있다(https://github.com/WICG/import-maps/issues/137). 배포된 라이브러리는 bare specifier와 Import map을 동시에 사용할 수 없고, 그 라이브러리를 사용하는 top-level 프로젝트는 그 라이브러리 Import map을 합성(compose) 할 수 없다.

esm.sh, skypack과 같은 트랜스파일 서버들은 많은 NPM 모듈들을 Deno로 가져오는 역할을 잘 해주고 있지만, 역시 내재된 한계를 갖고 있다. 예를 들어, 어떤 NPM 모듈이 런타임에 어떤 데이터 파일을 로드하면, 이 서버들은 호환되는 버전을 서빙할 수 없게 된다.(?? 뭔 얘긴지 잘모르겟음) 이런 이슈들은 수준 이하의 개발자 경험을 만들고 있다

단순한 패키지명 명시가 갖는 큰 힘 The power of bare specifiers

import express from "express";

위와 같은 import 문은 간결하고 친숙하다.

Bare Specifiers("express")는 그 의존성에 대해 애매한 참조를 갖기 때문에 유용하다. semver 해결(resolution)을 통해 중복 의존성 문제를 해결하기 때문이다.

하지만 라이브러리가 bare specifier를 사용해 만들어졌다면, 이러한 bare specifiers가 어떤걸 가리키고 있는지 분명하게 해야 하고, 반드시 의존성 manifest가 필요하게 된다.

호환성을 통해 더 간결하고 빠르게 만들기 Simplifying and accelerating with compatibility

우리는 Deno 사용자들이, Node를 사용할 때보다 더 효율적으로 일할 수 있게 만들고 싶다.

개발자들은 성가신 거 없이 라이브러리를 Import하고 싶어한다. 그래서 우리는 v1.28에서 npm specifier 지원을 추가했다.

라이브러리를 넘어서, 개발자들은 기존 Node 프로젝트들을 바로 Deno에서 실행하고 싶어한다. 그래서 우리는 package.json 지원을 새로 추가했다.

Deno의 “후퇴하는 듯한 호환성(backwards compatibility)”은, Node와 NPM의 불온전한 레거시 피처들과는 거리를 두고 있다. 예를 들어 Deno는 NPM import에서 CommonJS만 지원하고 있다. 또 사용자들이 NPM 의존성 밖에서 Node 내장 모듈들을 호출할 때 bare specifier를 쓰지 못하게 하고, node: 를 반드시 붙이도록 하고 있다. (ex. fsnode:fs) setTimeout 은, Timeout 객체를 반환하는 Node와는 다르게, 웹표준에 따라 number를 반환한다. postinstall 스크립트를 임의로 실행하는 것도 제한하고, 사용자 환경(user space) 보안 권한을 강제한다. “후퇴하는 듯한 호환성”은 JS 생태계가 진화하거나 개선되지 못하게 하는 걸 의미하지 않는다.

Deno는 항상 URL을 통해 코드들이 연결되도록 지원할 것이고, 브라우저에서 HTTP import를 할 수 있도록 계속해서 노력할 것이다. 이는 매우 중요하다. Deno에서 코드 연결하는 방법은 https: URL 외에도, v1.28부터 npm: URL이 가능하다. github: URL처럼 개발 속도를 더욱 빠르게 할 수 있는 다른 방법들도 쉽게 상상할 수있다.

새로운 메이저 버전 A new major version

몇달내 우리는 Deno의 새로운 메이저 버전을 출시하기 위해 노력하고 있다. 이 버전의 주요 테마는 Deno workflow에서 bare specifier 활용도를 더욱 높이는 것이다

NPM 모듈과 함께 Deno는 환상적인 “후퇴하는 듯한 호환성”을 보여줬지만, 우리는 Deno 사용자들이 Deno 코드를 거기에 분산시키는 걸 추천하지 않는다. 만약 Deno 코드를 Node-Npm 프로젝트들에서 사용해야 되는 경우가 있다면, 공식 Deno-to-NPM 컴파일러인 DNT를 사용하길 추천한다. 원본 TS에서 타입 선언 파일과 Node와 호환되는 JS 파일들로 트랜스파일해 고품질 NPM 패키지로 뽑아준다. 하지만 진정한 TS-first 세계에서 살기 위해, 컴파일된 아웃풋보다는 실제 TS 코드를 퍼뜨리고 연결하는게 제일 좋다

중복 의존성 문제를 해결하고, TS-first Deno 모듈 레지스트리를 좀더 인간친화적으로 사용할 수 있도록, 다음 메이저 버전은 deno: URL Scheme을 소개할 것이다. HTTP URL 보다는 이 특별한 URL을 통해, Deno 런타임은 semver resolution과 모듈 중복 제거를 할 수 있게 될 것이다. full URL을 써야될 필요도 없어진다.

예를 들어 Oak 패키지를 import 하는 경우는 이렇게 될 것이다.

import oak from "deno:oak@12";

메이저 버전만 명시되는 것이 주목하라. 구체적인 semver resolution 로직은 런타임에 실행될 것이다.

또한 package.json workflow의 현대적(modern) 대안인 import map 사용도 가능해질 것이다. 이는 deno.json 설정 파일에서 명시될 것이다

{
  "imports": {
    "oak": "deno:oak@12",
    "chalk": "npm:chalk@5"
  }
}

이 설정은 코드에서 “oak”, “chalk” 등 bare specifier를 사용할 수 있게 한다. Oak은 Deno 레지스트리에서, chalk는 NPM 레지스트리에서 다운로드 된다.

코드에선 다음과 같이 단순해질 것이다.

import oak from "oak";
import chalk from "chalk";

import map workflow를 쓰든지, Node.js package.json workflow를 쓰든지, Deno는 개발 속도를 더욱 빠르게 할 수 있는, 믿을만한 툴이 되는 것이 목표다. 이 세계의 기본 프로그래밍 언어인 JS는, 지속적으로 생태계와 도구들을 발전시키는 노력을 들일 만한 가치가 있다