공부/기타

[Deno] 신생 JS & TS 런타임 데노(Deno)

Lai_Khan 2020. 6. 2. 15:49
[인프런] 따라하며 배우는 데노 강의를 듣고 정리

Deno란

Deno란 JavaScript와 TypeScript를 실행하기 위한 새로운 Command-line Runtime이다.

 

Deno의 특징

기반 기술

1. Deno의 기반 기술은 V8 Javascript Runtime로 Node와 같다.

2. Node에서는 C++을 이용했는데 Deno에서는 Rust를 이용한다.

3. Tokio를 사용해 Event Loop를 컨트롤한다.

4. Deno에서는 TypeScript를 기본으로 사용할 수 있다.

 

Deno는 왜 만들어졌을까?

Deno는 Node.js를 만든 사람이 만들었다. (Ryan Dahl)

그는 Node.js를 만들고 한참 뒤에 Node.js의 단점과 오래된 기술들이 쓰였다는 것을 깨달았다. 그러나 Node를 업그레이드 하기에는 너무 늦었기에 더 나은 형태의 Node.js를 만들기로 결정했고, 그렇게 만들어진 것이 Deno다.

 

Node.js의 단점(이라고 그가 생각했던 것들)

1. 중앙 배포 방식으로 설계된 모듈 시스템

모든 모듈이 node_modules에 저장되어 관리된다.

너무 집중화가 되어있고, npm은 중앙 서버가 되었다. 하지만 웹은 분산화가 되어야 한다.

 

2. legacy API가 많은데 이를 지원하지 못한다.

 

3. 안정성

Node에서는 파일을 읽고 쓰고 수정할 때, 아무나 중요한 정보에 접근을 할 수가 있다. 이는 안정성에 문제가 있다.

 

Deno의 중요한 특징들

1. 유일하게 ES Modules 시스템 사용

원래는 모듈을 사용할 때, npm install 해서 해당 모듈을 다운 받아 require 해서 해당 모듈을 사용했다.

하지만 이제 Deno에서는 그렇게 하지 않고 직접 URL을 사용해서 어플리케이션에서 모듈을 사용한다.

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

이렇게 URL을 사용함으로서 다운로드와 리소스 업데이트를 직접 하기 때문에 Deno 자체가 패키지 매니저가 된다. (원래 Node 에서는 패키지 매니저로 NPM을 썼다.) 또한 Deno는 처음 실행할 때 dependency를 하드디스크에 저장해놓기 때문에 이후에 오프라인에서도 실행 가능하다.

 

2. 안전성 향상

Deno는 기본적으로 디스크나 네트워크에 접근할 때 권한을 요구한다.

Deno는 Sandbox 같이 작동을 한다, 따라서 권한이 필요하다. 샌드박스란 내부에서 어떤 작업을 하든 밖으로는 영향을 주지 않는 개념을 말한다. 이와 같이 Deno는 Deno 안에서 무엇을 하든지 네트워크나 파일 시스템 같이 중요한 부분에는 영향을 미치지 않도록 설계가 되어 있다. 따라서 파일 시스템이나 네트워크에 접근을 하려면 다음과 같은 flag를 달아서 권한을 명시해야 한다.

Options:
        --allow-read		Allow file system read access
        --arrow-write		Allow file system write access
        --arrow-net		Allow network access
        --arrow-env		Allow environment access
        --arrow-run		Allow running access
        --arrow-all		Allow all permissions

 

3. TypeScript가 내장되어 있다.

TypeScript를 사용하기 위해 수동으로 환경을 설정할 필요가 없다. 내장되어 있어 바로 사용할 수 있다.

 

4. Top-Level await 지원

async/await을 많이 쓰는데, 보통 async/await을 쓸 때는 async로 await 부분을 감싸줬어야 했다. 이렇게.

(async () => {
    for await (const req of s) {
    	req.respond({ body: "Hello World\n" });
    }
})();

Top-Level await는 다음과 같이 async 없이 쓸 수 있다. async 없이 써도 이제 에러가 없다.

for await (const req of s) {
    req.respond({ body: "Hello World\n" });
}

 

5. 브라우저 호환성

Node.js에서 리소스들을 가져올 때 fetch를 사용해야 하는데, fetch를 사용하기 위해서는 해당 모듈을 설치하고 require('module') 로 가져와야 한다. 하지만 브라우저 API 안에는 fetch가 내장되어 있어 그냥 사용할 수 있다. 따라서 Node는 브라우저와의 호환성이 있다고 할 수 없다.

 

Deno에는 fetch API가 내장되어 있어 Node처럼 모듈을 설치하고 불러올 필요 없이 바로 사용할 수 있다. (require 대신 import/export를 사용한다.) 따라서 브라우저와의 호환성을 갖췄다고 할 수 있다.

 

Node와 Deno의 차이점

  Node Deno
Engine V8 V8
Written in C++ & JavaScript Rust & TypeScript
Package managing package managers : npm uses URLs
Importing Packages CommonJS syntax ES Modules
Security full access permissioned access
TypeScript support not built in built in

 

Deno 환경설정

설치

Shell (Mac, Linux):

$ curl -fsSL https://deno.land/x/install/install.sh | sh

PowerShell (Windows):

$ iwr https://deno.land/x/install/install.ps1 -useb | iex

Homebrew (Mac):

$ brew install deno

Chocolatey (Windows):

$ choco install deno

Scoop (Windows):

$ scoop install deno

Build and install from source using Cargo

$ cargo install deno

 

나는 Windows PowerShell로 설치했다.

deno 설치중

 

Getting Started

간단한 프로그램 실행:

$ deno run https://deno.land/std/examples/welcome.ts

또는 좀 더 복잡한 프로그램 실행:

import { serve } from "https://deno.land/std@0.54.0/http/server.ts";
const s = serve({ port: 8000 });
console.log("http://localhost:8000/");
for await (const req of s) {
  req.respond({ body: "Hello World\n" });
}

 

Standard Library

Standard Library란 Node에서 쓰던 모듈들을 모아놓은 것으로 Deno Core 팀에서 직접 관리를 한다고 한다.

http, fs, path, async 등등 익숙한 것들이 많다.

 

Standard Library를 이용해서 프로그램을 하나 만들어보자. (실습ㄱㄱ)

 

1. Console.log → hello

우선 폴더를 하나 만들고 VS Code를 실행한 다음, Deno Extension을 설치하자.

deno의 공식 extension이다.

그리고 파일을 하나 만든다.(이름은 아무거나) 타입은 JS 해도 좋고 TS로 해도 좋다.

console.log('hello');

아무거나 간단히 입력한 다음 deno run 파일명 으로 실행시키면

이렇게 맨 밑에 hello가 뜨면서 실행이 된다.

 

2. DateTime

다음은 Standard Library에서 DateTime이란 모듈을 사용해보자.

새로운 파일을 만들어서 해당 코드를 입력하고 deno run을 해주면

import {
    dayOfYear, 
    currentDayOfYear 
} from "https://deno.land/std/datetime/mod.ts";

console.log(dayOfYear(new Date("2020-04-11")));
console.log(currentDayOfYear());

각각 '해당 날짜가 올해로부터 며칠째인지'와 '오늘이 올해로부터 며칠째인지'가 각각 출력된다.

 

3. Create File

그럼 이제 Create File을 한번 해보자.. 새로운 파일을 생성하고 아래 코드를 입력한다.

const encoder = new TextEncoder();
const helloText = encoder.encode('hello, thank you');
await Deno.writeFile("hello.txt", helloText);

그리고 실행(deno run)을 시켜보자. 안될 것이다. 권한이 없기 때문이다. 파일 시스템에 접근하려면 권한이 있어야 한다. 뒤에 flag를 붙여주자.

deno run --allow-write 파일명

그러면 이제 파일이 생성되어 있는 것을 볼 수 있을 것이다.

 

4. Read File

그럼 이제 파일을 읽어보자!

let file = await Deno.open('hello.txt');
await Deno.copy(file, Deno.stdout);
file.close();

아까 만들어줬던 파일을 열어서 copy를 한다. 이때 반환을 Promise로 하기 때문에 앞에 await를 붙여줘야 한다. 그리고 마찬가지로 실행을 하면...

deno run --allow-read 파일명

 

5. Create Server

마지막으로 서버를 하나 만들어보자.

import { serve } from "https://deno.land/std/http/server.ts";
const s = serve({ port: 3000 });
console.log("http://localhost:3000");
for await(const req of s) {
    req.respond({ body: "Hello, Thank you\n" });
}

서버를 열기 위해 serve라는 모듈을 받아오고, 포트를 지정해 서버를 연다. 그리고 응답을 받아서 보내줄 내용을 respond에 넣어준다. 실행할 때는 마찬가지로 네트워크에 접근하는 것이므로 flag를 붙여줘야 한다.

deno run --allow-net 파일명

실행을 하면 localhost + 아까 열었던 포트로 접속을 할 수가 있다.

 

OAK Framework

Deno와 프레임워크를 이용해서 API 서버를 만들어보자.

Deno 웹사이트에 들어가보면 프레임워크가 참 많은데 그중에서 oak이란 프레임워크를 사용할 것이다.

Deno Framework들. 진짜 많다...

oak 프레임워크를 사용하는 이유는 많은 사람들이 사용하고 있고 Express와 유사하기 때문이다. (Flow가 비슷)

 

기본적으로 서버를 여는 방법은 다음과 같다.

import { Application } from "https://deno.land/x/oak/mod.ts"

const app = new Application();

console.log(`Server is listening port 3000`);
await app.listen({ port: 3000 });

다만 이 코드는 실행해도 라우터가 없기 때문에 에러가 난다.

 

import { Application, Router } from "https://deno.land/x/oak/mod.ts";

const app = new Application();
const router = new Router();

app.use(router.routes());   // router 등록
app.use(router.allowedMethods());   // get, post method 허용

router.get('/', (context) => {  // Deno에서는 context안에 request와 response가 들어있다.
    context.response.body = "Hello, World";
});

console.log(`Server is listening port 3000`);
await app.listen({ port: 3000 });

라우터를 가져와 app에 등록하고 get 메소드로 / 엔드포인트를 받는다. 여기서 인자로 context를 받는데 Node에서는 req, res로 받았지만 Deno에서는 context안에 request와 response가 들어있다.

/books 으로 접속했을 때 책 목록을 띄우는 페이지를 만들어보자.

우선 Book 인터페이스를 만들고 간단하게 배열로 데이터를 만들어둔다.

interface Book {
    id: string,
    title: string,
    author: string
};

let books: Book[] = [
    {
        id: '1',
        title: 'book One',
        author: 'One'
    },
    {
        id: '2',
        title: 'book Two',
        author: 'Two'
    },
    {
        id: '3',
        title: 'book Three',
        author: 'Three'
    },
];

그런 다음 /books 라우터를 생성한다.

router.get('/', (context) => {
    context.response.body = "Hello, World";
})
    .get('/books', (context) => {
        context.response.body = books;
    });

Deno에서는 따로 router를 또 쓸 필요 없이 메소드 체이닝으로 연결을 해줄 수 있다.

그럼 이렇게 출력이 된다.

 

post로 book 정보를 입력해보자.

.post('/book', async (context) => {
    const body = await context.request.body();
});

아까의 get에 이어 붙여서 post /book 메소드를 생성한다. Express에서는 request.body로 전체를 받거나 request.body.title로 하나씩 받거나 했는데 Deno에서는 똑같이 context.request.body()로 받으면 된다. 다만 특이한 점은 항상 Promise로 반환해주기 때문에 앞에 await를 붙여야 한다. 다만 그냥 await만 앞에다가 붙이면 에러가 난다. 이 부분은 Top-Level이 아니기 때문에 async로 감싸줘야 한다.

 

.post('/book', async (context) => {
    const body = await context.request.body();

    // 만약 정보를 제공하지 않았다면
    if(!context.request.hasBody) {
        context.response.status = 400;
        context.response.body = "데이터가 없습니다.";
    } else {
        // 정보를 제공 받았다면
        
    }
});

context.request.hasBody 로 데이터가 있는지 없는지 체크할 수 있다. 데이터가 없다면 status를 설정하고 데이터가 없다는 메시지를 띄운다.

 

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

......

    if(!context.request.hasBody) {
        context.response.status = 400;
        context.response.body = "데이터가 없습니다.";
    } else {
        // 정보를 제공 받았다면
        // 우선 임의로 아이디를 생성하고 제공받은 정보로 book object를 만들어준다.

        const book: Book = body.value;
        book.id = v4.generate();
        books.push(book);
        context.response.status = 201;
        context.response.body = book;
    }

Book Object를 생성해 body.value로 받은 데이터를 book 변수에 넣어준다. id는 uuid를 사용해 생성한다.(v4를 import) 그리고 해당 데이터를 books 배열 안에 넣어준다.

 

postman을 사용해서 테스트를 해보면 잘된다. uuid로 id 생성하는 것도 잘 되고.

 

이제 id를 사용해서 책 정보 하나만 따로 따로 볼 수 있는 페이지를 만들어보자.

.get('/book/:id', async(context) => {
    // books 안에 있는 책들 중에 param과 같은 id를 가진 책 찾기
    
    const book: Book | undefined = books.find((b) => b.id === context.params.id);

    if(book) {
        context.response.status = 200;
        context.response.body = book;
    } else {
        context.response.status = 400;
        context.response.body = "책을 찾지 못했습니다.";
    }
});

끝에 get 라우터를 추가하고 books 안에 있는 책들 중에 param과 같은 id를 가진 책을 find()로 찾는다. 이때 book 변수의 타입은 find() 값이 없을 수도 있으니, Book | undefind로 한다. 그런뒤 값이 있으면 해당 값을 없으면 없다는 메시지를 반환한다.

서버를 실행하고, /book/1로 접속했을 때 id가 1인 책의 값만 잘 뜬다.

 

공부해 본 감상

Node와 정말 비슷하다. Node에서 단점만 개선한 것 같은 기술이다. Node를 해본 사람은 금방 익힐 수 있을 것 같다.


참고 링크

Deno 공식 사이트

Node 제작자가 만든 Deno: 자바스크립트의 새로운 접근