Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Deno 살펴보기

Deno(dee-no라고 읽는다)는 JavaScript, TypeScript, Webassembly 런타임으로 V8, Rust, Tokio에 기반을 두고 있다. 나는 TypeScript도 안 쓰고 yarn도 안 쓰고 CommonJS로 모듈을 주로 쓰는 이제는 좀 구식이라고 할 수 있는 예전 Node.js 방식에 익숙한 개발자다. 개발할 때 트랜스파일이나 컴파일하는 걸 좋아하지 않는 터라 보통 개발환경도 가능하면 이런 과정 없이 구성하는 편인데 더 이상 TypeScript를 거부할 수는 없기에 TypeScript를 바로 사용할 수 있는 Deno에도 관심을 가지게 되어 사이드 프로젝트에서 써보고 있다. 바로 사용하려고 하니 이해 못하고 있는 부분이 많아서 이해할 겸 조금씩 정리해본다.

Deno는 2018년 10월 JSConf EU에서 Ryan Dahl10 Things I Regret About Node.js라는 발표에서 공개되었다. Ryan Dahl은 Node.js의 창시자로 2009년 JSConf EU에서 처음으로 Node.js를 공개했다.(발표 영상) 계속 Node.js 프로젝트를 이끌다가 2012년 돌연 떠나서 보이지 않다가(개발 말고 다른 일을 한 거로 알고 있다) 2018년 돌아와서 저 발표를 하면서 Deno를 공개하게 된다.(이 영상의 내용을 한국어로 정리해 주신 분이 있어서 링크한다.)

발표 후 4년이 지났지만, Ryan Dahl이 JSConf에서 Deno로 하고 싶다고 한 걸 아직도 잘 유지하고 있다고 생각한다.(깊게까지 본 건 아니고 Deno가 내세우는 특징을 보고 하는 얘기다.) JSConf EU에서 발표하기 위한 프로토타이핑은 Ryan Dahl이 Gov0.1.0부터는 Rust를 선택했다.

Deno는 다음과 같은 특징을 가지고 있다.

  • 웹 플랫폼 기능을 제공하고 웹 플랫폼 표준을 따른다. 그래서 ES modules, web workers, fetch()등을 그대로 사용한다.
  • 기본적으로 안전해서 파일 접근, 네트워크, 환경변수 등을 명시적으로 지정했을 때만 접근할 수 있다.
  • TypeScript를 기본으로 지원한다.
  • 하나의 실행파일로 배포할 수 있다.
  • 코드 포매터나 린터, 테스트 러너 등의 개발 도구를 내장하고 있다.

Deno 설치

문서에 따라 사용하는 OS에 맞게 설치할 수 있다. 나는 릴리스 노트에서 배포 파일을 다운받아서 PATH에 넣어서 사용하고 있다. 이 글을 쓸 때는 1.25.2 버전을 사용했다.

$ deno --version
deno 1.25.2 (release, aarch64-apple-darwin)
v8 10.6.194.5
typescript 4.7.4


Deno 시작하기

처음은 항상 Hello World니까 아래와 같은 간단한 TypeScript 파일을 작성해 보자.

// hello.ts
console.log('Hello World');

이 파일을 실행하려면 deno run 명령어를 사용한다. TypeScript가 아니라 JavaScript 파일이어도 당연히 잘 동작한다.

$ deno run hello.ts
Hello World

deno run은 온라인의 원격 파일도 실행할 수 있는데 Deno의 표준 라이브러리인 std에서 예제 파일도 제공하고 있으므로 가장 간단한 welcome.ts을 실행해 보자. 다양한 예제는 std의 예제 폴더에서 더 확인할 수 있다.

$ deno run https://deno.land/std@0.155.0/examples/welcome.ts
Welcome to Deno!

여기선 TypeScript나 JavaScript를 설명할 목적은 아니므로 간단한 예제를 하나 더 보자. fetch로 GitHub Status를 조회하는 코드다.

// fetch.ts
const res = await fetch('https://www.githubstatus.com/api/v2/status.json');
const json = await res.json()
console.log(json);

이 파일을 deno run으로 실행하면 아까와는 달리 경고가 나오는 것을 볼 수 있다. 전에는 플래그를 다 붙여줬어야 했는데 이젠 개발할 때 편하게 프롬프트가 나와서 y만 눌러줘도 된다.

$ deno run fetch.ts
⚠️  ️Deno requests net access to "www.githubstatus.com". Run again with --allow-net to bypass this prompt.
   Allow? [y/n (y = yes allow, n = no deny)]  y
{
  page: {
    id: "kctbh9vrtdwd",
    name: "GitHub",
    url: "https://www.githubstatus.com",
    time_zone: "Etc/UTC",
    updated_at: "2022-09-10T08:02:14.320Z"
  },
  status: { indicator: "none", description: "All Systems Operational" }
}

앞에서 Deno는 기본적으로 안전하다고 했는데 이 예제가 네트워크에 접근해야 하는데 기본적으로는 네트워크에 접근할 권한이 없기 때문에 경고가 나온 것이다. 위처럼 경고 메시지를 보지 않으려면 deno run--allow-net(혹은 --allow-net=www.githubstatus.com)을 추가하면 된다.

$ deno run --allow-net fetch.ts
{
  page: {
    id: "kctbh9vrtdwd",
    name: "GitHub",
    url: "https://www.githubstatus.com",
    time_zone: "Etc/UTC",
    updated_at: "2022-09-10T08:02:14.320Z"
  },
  status: { indicator: "none", description: "All Systems Operational" }
}

deno run에서는 다음과 같은 플래그로 권한을 허용해 줄 수 있다.

  • --allow-all: 모든 권한 허용
  • --allow-env[=<allow-env>...]: 환경변수 접근 허용
  • --allow-ffi[=<allow-ffi>...]: 라이브러리 동적 로딩 허용
  • --allow-hrtime: 고해상도 시간 측정 허용
  • --allow-net[=<allow-net>...]: 네트워크 접근 허용
  • --allow-run[=<allow-run>...]: 서브 프로세스 실행 허용
  • --allow-read[=<allow-read>...]: 파일시스템 읽기 허용
  • --allow-write[=<allow-write>...]: 파일시스템 쓰기 허용

deno CLI

앞에서 보았던 JavaScript나 TypeScript를 실행하는 deno run 외에도 deno CLI에서는 꽤 많은 기능을 제공하고 있다.

  • deno init으로 프로젝트를 초기화할 수 있다.
  • deno repl로 REPL을 실행해서 JavaScript나 TypeScript를 실행해 볼 수 있다. 대신 타입 검사는 하지 않는다.
  • deno fmt로 코드를 포매팅한다. 따로 코드 관례를 논의할 필요가 없다.
  • deno lint로 코드를 린트할 수 있다. 사용된 린트 규칙은 문서에서 볼 수 있다.
  • deno test로 테스트를 실행할 수 있다.

--watch 플래그도 지원해서 deno run이나 deno test, deno fmt등과 같이 사용할 수 있다. 이 외에도 많은 명령어가 있는데 각각은 사용해 보면서 필요하면 또 정리해보도록 하겠다. 더 많은 명령어는 deno -hdeno help로 볼 수 있다.

설정 파일

Deno에는 TypeScript 컴파일러나 포매터, 린터에 옵션을 줄 수 있는 설정파일을 지원한다. deno.json이나 deno.jsonc를 자동으로 찾아서 사용한다.

API

Deno 1.0.0부터 Deno 네임스페이스의 API는 Stable 상태가 되었지만 Deno 표준 라이브러리는 아직 Stable 상태는 아니다.(그렇지만 표준 라이브러리 없이 개발은 못 할 것 같다.)

Deno의 특징을 설명할 때 얘기했던 것처럼 Deno는 Web Platform API를 지원한다. 앞의 예제에서 fetch()를 아무런 import 없이 사용했던 걸 볼 수 있는데 이게 가능했던 이유가 Deno가 Web Platform API를 지원하기 때문이다. 현재 Deno에서 지원하는 Web API는 문서에서 볼 수 있다. 이러한 API는 브라우저에서도 동일한 API라서 이미 사용해 봤다면 익숙한 장점이 있다.

외부 의존성

아래는 Deno로 작성한 간단한 HTTP 서버다. Deno에서는 상대 경로로 다른 파일을 임포트할 수도 있지만 아래처럼 URL에서 바로 가져와서 외부 모듈을 사용하게 된다. Node.js에서 npm으로 로컬에 설치한 뒤에 로컬의 파일을 불러오는 것과는 다른 구조이고 아직도 나한테 좀 어색한 형식인데 ES Modules를 따르면서 다른 의존성 도구를 사용하지 않기로 하면서 한 결정인 것 같다.

// http-server.ts
import { serve } from "https://deno.land/std@0.155.0/http/server.ts";

function handler(req: Request): Response {
    return new Response("Hello, World!");
  }

serve(handler);

이 외부 모듈을 사용한 코드를 실행하면 다음과 같이 모듈을 다운로드하는 것을 알 수 있다.

$ deno run --allow-net http-server.ts
Download https://deno.land/std@0.155.0/http/server.ts
Download https://deno.land/std@0.155.0/async/mod.ts
Download https://deno.land/std@0.155.0/async/abortable.ts
Download https://deno.land/std@0.155.0/async/deadline.ts
Download https://deno.land/std@0.155.0/async/debounce.ts
Download https://deno.land/std@0.155.0/async/deferred.ts
Download https://deno.land/std@0.155.0/async/delay.ts
Download https://deno.land/std@0.155.0/async/mux_async_iterator.ts
Download https://deno.land/std@0.155.0/async/pool.ts
Download https://deno.land/std@0.155.0/async/tee.ts
Listening on http://localhost:8000/

다운로드받은 모듈은 캐싱되었기 때문에 두 번째 실행 때부터는 다시 다운로드하지 않고 바로 실행한다.

$ deno run --allow-net http-server.ts
Listening on http://localhost:8000/

이는 DENO_DIR 환경변수에 지정된 위치를 캐시 디렉터리로 사용하는데 DENO_DIR를 지정하지 않았다면 다음과 같은 위치를 사용하게 된다.

  • macOS: $HOME/Library/Caches/deno
  • Linux: $XDG_CACHE_HOME/deno 혹은 $HOME/.cache/deno
  • Windows: %LOCALAPPDATA%/deno

찾기가 어렵다면 deno info 명령어로 현재 사용되는 캐시 디렉터리 위치를 확인할 수 있다.

$ deno info
DENO_DIR location: /Users/outsider/Library/Caches/deno
Remote modules cache: /Users/outsider/Library/Caches/deno/deps
Emitted modules cache: /Users/outsider/Library/Caches/deno/gen
Language server registries cache: /Users/outsider/Library/Caches/deno/registries
Origin storage: /Users/outsider/Library/Caches/deno/location_data

$HOME/Library/Caches/deno의 내용을 보면 다음과 같이 되어 있다.

├── dep_analysis_cache_v1
├── deps
│   └── https
│       └── deno.land
│           ├── 11d2d9b95386927aedc1c9249016323287cc39f436b8826e30ab2cf93d3edadd
│           ├── 11d2d9b95386927aedc1c9249016323287cc39f436b8826e30ab2cf93d3edadd.metadata.json
│           ├── ...생략...
│           ├── db6432ed665e08cd6dd67b7f669ca834c294fcfd3d348bc671ee890f2baa9727
│           └── db6432ed665e08cd6dd67b7f669ca834c294fcfd3d348bc671ee890f2baa9727.metadata.json
├── gen
│   ├── file
│   │   └── Users
│   │       └── outsider
│   │           └── Dropbox
│   │               └── deno-examples
│   │                   ├── http-server.ts.js
│   │                   └── http-server.ts.meta
│   └── https
│       └── deno.land
│           ├── 11d2d9b95386927aedc1c9249016323287cc39f436b8826e30ab2cf93d3edadd.js
│           ├── 11d2d9b95386927aedc1c9249016323287cc39f436b8826e30ab2cf93d3edadd.meta
│           ├── ...생략...
│           ├── db6432ed665e08cd6dd67b7f669ca834c294fcfd3d348bc671ee890f2baa9727.js
│           └── db6432ed665e08cd6dd67b7f669ca834c294fcfd3d348bc671ee890f2baa9727.meta
└── npm
  • gen 폴더는 TypeScript를 컴파일한 JavaScript 파일을 보관해 두는 곳으로 파일 변경이 없으면 다시 컴파일하지 않고 캐시 된 파일을 사용한다.
  • deps 폴더는 원격 URL에서 임포트한 파일을 저장해 두는 곳으로 프로토콜에 따라 httpshttp 하위 폴더를 가진다.

코드를 실행하지 않고 외부 의존성만 캐싱하고 싶다면 deno cache를 실행하면 된다.(아래는 캐시 디렉터리를 삭제 후 다시 실행한 것이다.) 이미 캐시 되어 있는데 다시 불러오고 싶다면 deno cache --reload를 사용하면 된다.

$ deno cache http-server.ts
Download https://deno.land/std@0.155.0/http/server.ts
Download https://deno.land/std@0.155.0/async/mod.ts
Download https://deno.land/std@0.155.0/async/abortable.ts
Download https://deno.land/std@0.155.0/async/deadline.ts
Download https://deno.land/std@0.155.0/async/debounce.ts
Download https://deno.land/std@0.155.0/async/deferred.ts
Download https://deno.land/std@0.155.0/async/delay.ts
Download https://deno.land/std@0.155.0/async/mux_async_iterator.ts
Download https://deno.land/std@0.155.0/async/pool.ts
Download https://deno.land/std@0.155.0/async/tee.ts

여기서 Deno가 TypeScript를 다루는 방식도 알 수가 있는데 앞에서도 얘기했듯이 Deno는 TypeScript를 퍼스트 클래스 언어로 다룬다. Web Assembly를 포함해서 JavaScript도 사용할 수 있지만 Deno를 쓴다면 아마 TypeScript를 사용할 가능성이 높다고 생각한다. JavaScript는 큰 생태계를 가진 Node.js가 있기도 하므로 더욱 그렇다.(내가 Node.js에 익숙해서 그렇게 느낄 수도 있다.) 이 캐시구조에서 보듯이 Deno는 TypeScript를 JavaScript로 변환해서 실행하지만 이를 느낄 수 없도록 사용경험을 제공하고 있다. Deno는 TypeScript 컴파일러SWC를 조합해서 사용하고 있다.

의존성 관리

앞에서 보았듯이 Deno는 의존성을 불러올 때 URL을 사용한다. 간단한 예제에서는 문제가 없지만 Deno에는 Node.js와 같은 패키지 관리자가 없기 때문에 프로젝트가 커지면 많은 의존성을 어떻게 관리할 것인가 하는 문제가 생긴다. 문서에 따르면 이를 해결하려고 deps.ts를 사용하는 것이 일반적이라고 권하고 있다. 이는 패키지 관리자라기보다는 ES Modules를 이용해서 원격 의존성을 하나의 파일에 모아놓은 관례에 불과하긴 하다.

아래 두 파일 deps.ts와 이를 사용하는 uuid.ts를 살펴보자.

// deps.ts
export { v4 } from "https://deno.land/std@0.155.0/uuid/mod.ts";
// uuid.ts
import { v4 } from "./deps.ts";

console.log(v4.generate());

deps.ts라는 파일에서 외부 의존성을 모아두고 다른 파일은 모두 이 파일을 임포트해서 사용하는 방식이다.

$ deno run uuid.ts
Download https://deno.land/std@0.155.0/uuid/mod.ts
Download https://deno.land/std@0.155.0/uuid/v1.ts
Download https://deno.land/std@0.155.0/uuid/v4.ts
Download https://deno.land/std@0.155.0/uuid/v5.ts
Download https://deno.land/std@0.155.0/uuid/_common.ts
Download https://deno.land/std@0.155.0/_util/assert.ts
Download https://deno.land/std@0.155.0/bytes/mod.ts
Download https://deno.land/std@0.155.0/bytes/equals.ts
18715b2c-184e-42f8-be40-36d6cfe4dd79

당연히 동작은 잘 하지만 아직 큰 프로젝트를 해본 경험이 없음에도 그리 편해 보이진 않는다. 위 예제에서도 uuid에서 v1이나 v5이 필요하다면 버전을 바꾸지 않아도 deps.ts를 수정해야 한다. 익스포트 약간 수정하는 거고 사용하는 값도 명시적으로 지정하니까 크게 문제는 없지만, 그냥 너무 날것의 느낌이 들었다. 그래도 여러 파일에 외부 모듈이 흩어져 있는것 보다는 의존성 관리하기는 좋아 보인다.

임포트맵

추가로 Deno는 Web Incubator CGimport-maps를 지원하는데 이는 JavaScript의 import가 가져올 URL을 제어할 수 있게 해주는 프로토콜이다. deps.ts 대신 import_map.json을 다음과 같이 작성할 수 있다.

{
    "imports": {
        "http/": "https://deno.land/std@0.155.0/http/"
    }
}

이제 앞에서 처럼 전체 URL을 적는 대신 임포트맵으로 매핑한 http/server.ts 같은 식으로 작성할 수 있다.

// http-server.ts
import { serve } from "http/server.ts";

function handler(req: Request): Response {
  return new Response("Hello, World!");
}

serve(handler);
````

실행할 때 `--import-map` 플래그로 위 임포트맵을 지정해 주면 잘 동작하는 것을 알 수 있다.

```bash
$ deno run --import-map=import_map.json --allow-net http-server.ts
Download https://deno.land/std@0.155.0/http/server.ts
Download https://deno.land/std@0.155.0/async/mod.ts
Download https://deno.land/std@0.155.0/async/abortable.ts
Download https://deno.land/std@0.155.0/async/deadline.ts
Download https://deno.land/std@0.155.0/async/debounce.ts
Download https://deno.land/std@0.155.0/async/deferred.ts
Download https://deno.land/std@0.155.0/async/delay.ts
Download https://deno.land/std@0.155.0/async/mux_async_iterator.ts
Download https://deno.land/std@0.155.0/async/pool.ts
Download https://deno.land/std@0.155.0/async/tee.ts
Listening on http://localhost:8000/

아래처럼 상대경로에 대해 매핑을 해서 프로젝트 내 파일을 임포트할 때도 절대 경로로 사용하는 것이 가능하다.

{
  "imports": {
    "/": "./",
    "./": "./"
  }
}

매번 설정에 넣기가 힘들다면 deno.json 파일에 아래와 같이 임포트맵의 경로를 지정해 줄 수 있다.

{
    "importMap": "import_map.json"
}

개인적으로는 deps.ts보다는 임포트맵이 더 좋아 보인다.

2022/09/17 15:20 2022/09/17 15:20