Outsider's Dev Story: TypeScript 카테고리 글 목록https://blog.outsider.ne.kr/Stay Hungry. Stay Foolish. Don't Be Satisfied.2024-03-15T10:12:21+09:00Textcube 1.10.7 : Tempo primo[Book] 러닝 타입스크립트 - 안정적인 웹 프로젝트 운영을 위한 타입스크립트의 모든 것Outsiderhttps://blog.outsider.ne.kr/16542023-02-10T03:15:58+09:002023-02-10T03:15:58+09:00<div>
<fieldset style="padding: 20px 5px 5px 5px;">
<legend><a href="https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=307683870">러닝 타입스크립트 - 안정적인 웹 프로젝트 운영을 위한 타입스크립트의 모든 것 </a></legend>
<table>
<tbody>
<tr>
<td>
<a href="https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=307683870"><img src="//blog.outsider.ne.kr/attach/1/5244693406.jpg" width="350" height="448" alt="책 표지" title="" /></a>
</td>
<td style="vertical-align: top">
<a href="https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=307683870">러닝 타입스크립트 - 안정적인 웹 프로젝트 운영을 위한 타입스크립트의 모든 것 </a> - ⭐⭐⭐⭐
<br>조시 골드버그 지음<br>고승원 옮김<br>한빛미디어
</td>
</tr>
</tbody>
</table>
</fieldset>
<br>
</div>
<p>난 JavaScript를 좋아하고 오랫동안 사용했지만, TypeScript는 그리 좋아하지 않는 편이었다. 이런 건 취향이라서 논리적인 이유가 있는 건 아니지만 프로그래밍 언어에 관한 지식이 많은 편은 아니지만 Java를 하면서 타입 시스템이 좀 불편하게 느껴졌고 동적 언어인 JavaScript가 편했다. 물론 아주 큰 규모의 프로그램을 만들 일이 많지 않았기 때문이기도 하다.</p>
<p>이건 정적 언어가 좋냐 동적 언어가 좋냐보다 내 취향에서는 동적 언어가 맞았던 거 같은데 TypeScript가 처음 등장했을 때 타입이 있어서 오는 장점이 나에겐 별로 다가오지 않았다. "그 타입이 없어서 편한 거였는데 굳이?" 같은 느낌이었고 Node.js나 JavaScript로 코딩하면서도 가능하면 트랜스파일도 하지 않는 개발환경을 만들려고 항상 노력했다.</p>
<p>세상이 TypeScript로 바뀌는 것은 알고 있었지만, 그 흐름을 따라가지 못하고 익숙한 JavsScript에 계속 머물러 있었다.(기술에 에고가 쌓인다는 것은 이런 것일까...) 그렇게 TypeScript를 사용할 기회는 없었지만, 업무로 만들던 Node.js 프로젝트가 팀원이 늘어가면서 TypeScript로 전환하는 결정을 하게 되었다. 타입 자체를 부정하는 것은 아니기 때문에 타입의 장점에 공감한 것도 있고 아무래도 흐름이 많이 바뀌다 보니 CommonJS만으로는 Node.js 생태계에서 어려운 부분도 있고 다양한 이유로 어차피 컴파일은 필요하기 때문이기도 하다.</p>
<p>어쨌든 말이 길었는데 TypeScript로 프로젝트를 하는데 아직도 익숙해지지는 않다 기본적으로 JavaScript의 슈퍼 셋이라서 차차 적응할 줄 알았는데 생각보다는 적응이 쉽지 않았고 JavaScript를 TypeScript로 전환하는 작업도 좀 했었는데 맘처럼 잘 안되어서 계속 도움을 요청하곤 했었다. IDE에서 빨간 줄이 아무리 해도 사라지질 않았다.</p>
<p>TypeScript 공부해야지 하고는 사이드 프로젝트에 Deno를 쓰고 있지만 진도가 잘 안 나가서 TypeScript 코딩은 별로 못하고 있었고 공부도 미루고 있다가 이번에 이 책을 꺼내 들었다. 원래는 <a href="https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=273193135">이팩티브 타입스크립트</a>를 읽으려고 했는데 1월에 신간이 나왔길래 이 책을 먼저 읽었다. 이 책 내용 중에서도 <a href="https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=273193135">이팩티브 타입스크립트</a>를 추천하는 걸 보니 이팩티브 타입스크립트도 조만간 읽어봐야겠다고 생각했다.</p>
<p>다른 책은 안 봐서 비교는 어렵지만 이 책은 실용적으로 TypeScript를 잘 알려준다는 느낌이다. 문법을 설명하다 보면 내용이 많다 보니 자칫 지루해지기 쉬운데 타입을 중심으로 타입스크립트에서 타입을 어떻게 사용해야 하는지 쉽게 잘 설명한다는 느낌이다. 실제로 코딩하다 보면 분명히 또 막히겠지만 이전에 TypeScript로 코딩하면서 어려웠던 부분에서 내가 뭘 잘못 이해하고 있었는지를 알 수 있었다. 당연하다면 당연하게 JavaScript는 이해하고 있다는 것을 전제로 하고 있다.</p>
<blockquote>
<p>배열 타입은 Array <number> 같은 구문으로도 작성할 수 있습니다. 하지만 개발자 대부분은 더 간단한 number[]를 선호합니다.</p>
</blockquote>
<blockquote>
<p>타입스크립트로 작성하는 대부분의 코드에서는 혼동을 일으킬 정도로 제네릭을 많이 사용해서는 안 됩니다.</p>
</blockquote>
<blockquote>
<p>타입 서술어는 속성이나 값의 타입을 확인하는 것 이상을 수행해 잘못 사용하기 쉬우므로 가능하면 피하는 것이 좋습니다. 대부분은 간단한 타입 서술어만으로도 충분합니다.</p>
</blockquote>
<p>그리고 중립적으로 모든 문법을 비슷한 비중으로 설명하는 것이 아니라 문법을 설명하면서도 위처럼 많은 프로젝트에서 선호하는 방식이나 피해야 하는 부분을 뚜렷하게 말해주는 부분도 처음 TypeScript를 공부할 수 있는 입장에서는 편하게 느껴졌다. 뒷부분에 나오는 구문 확장에서 TypeScript가 JavaScript의 구문을 확장한 부분의 단점을 명확히 얘기하면서도 조심해야 할 부분과 현재 생태계에서는 필요한 부분을 얘기해 주는 것도 좋았습니다. TypeScript의 모든 것이라기보다 실용적으로 어떤 부분을 설명하는 게 도움이 될지 고민했다는 점이 느껴져서 좋았다.</p>
<p>후반부에 TSConfig 등의 옵션에 대한 설명도 나와서 참조하기 좋지만, TypeScript로 프로젝트를 하려면 생태계의 도구도 좀 필요한데 이런 부분이 없었던 건 좀 아쉽지만, 타입스크립트 언어 자체에만 집중한다는 면에서는 수긍할 수 있는 부분이다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1654?commentInput=true#entry1654WriteComment">댓글 쓰기</a></strong></p>Deno 팀에서 만든 웹 프레임워크 FreshOutsiderhttps://blog.outsider.ne.kr/16322022-11-05T20:39:37+09:002022-11-05T20:39:37+09:00<p><a href="https://fresh.deno.dev/">Fresh 프레임워크</a>는 <a href="https://deno.land/">Deno</a> 팀에서 만든 JavaScript/TypeScript 웹 프레임워크다. Deno에서는 엣지 컴퓨팅을 위해 <a href="https://deno.com/deploy">Deno Deploy</a>를 만들고 있으므로 엣지 컴퓨팅의 경험을 높이기 위해 만든 걸로 보이지만 꼭 Deno Deploy에서만 사용할 수 있는 것은 아니다. 5월부터 만들기 시작했으니 아직 6개월도 안 된 초기 프로젝트라고 할 수 있다. 아직 관련 정보가 많지 않기 때문에 <a href="https://fresh.deno.dev/docs/introduction">공식 문서</a>를 참고해서 정리했다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/8031404968.jpg" width="750" height="732" alt="Deno 홈페이지" title="" /></p>
<p>아직 Fresh가 맘에 든다기보다는 Deno를 쓰면서 프레임워크를 찾다가 Deno 팀에서 만든 프레임워크라서 사용해 보는 중이다. 아직 초기이고 문서도 공식 문서밖에 없어서 좀 다르게 해보는 건 어려워서 시키는 것만 해보려고 하고 있다. Deno Company의 엣지 컴퓨팅 서비스인 <a href="https://deno.com/deploy">Deno Deploy</a>에도 최적화되어 있는데 이 글에서는 다루지 않는다.<br />
<br></p>
<h1>Fresh 시작하기</h1>
<p>Fresh도 처음 프로젝트를 시작할 수 있는 보일러플레이트 코드를 제공해 주는데 이는 Deno로 실행할 수 있다.(Deno 1.27.0를 사용했다.) Deno의 특징과 연결되어 있어서 Deno도 어느 정도 알 필요가 있다.</p>
<p><code>deno run -A -r https://fresh.deno.dev <Project Name></code> 명령어로 프로젝트를 생성할 수 있는데 <code>-A</code>는 모든 권한을 허용하는 옵션이고 <code>-r</code>은 캐시가 있더라고 다시 로드하는 옵션인데 <code>https://fresh.deno.dev</code>의 패키지가 갱신될 수 있으니 이 옵션을 준 것이다.</p>
<pre class="line-numbers"><code class="language-bash">$ deno run -A -r https://fresh.deno.dev fresh-example
Fresh: the next-gen web framework.
Let's set up your new Fresh project.
Fresh has built in support for styling using Tailwind CSS. Do you want to use this? [y/N] N
Do you use VS Code? [y/N] N
The manifest has been generated for 3 routes and 1 islands.
Project initialized!
Enter your project directory using cd fresh-example.
Run deno task start to start the project. CTRL-C to stop.
Stuck? Join our Discord https://discord.gg/deno
Happy hacking!
</code></pre>
<p>프로젝트 설정을 위해 몇 가지를 묻는데 나중에 설명하겠지만 Fresh는 <a href="https://tailwindcss.com/">Tailwind CSS</a>를 적극적으로 지원한다. 이를 같이 설정할 것인지 묻는데 여기서는 일단 사용하지 않음을 선택했고 난 VS Code도 쓰지 않으니 이 역시 선택하지 않았다. 그러면 3개의 라우트와 1개의 아일랜드로 프로젝트가 초기화되었다고 안내가 나온다.</p>
<p>위에서 <code>deno run</code>을 실행할 때 Fresh의 홈페이지인 <a href="https://fresh.deno.dev">https://fresh.deno.dev</a>를 지정한 게 의아할 수 있다. 이 주소를 <code>curl</code>로 확인해 보면 <code>307 Temporary Redirect</code>로 Deno 모듈인 <a href="https://deno.land/x/fresh@1.1.2/init.ts">https://deno.land/x/fresh@1.1.2/init.ts</a>로 리다이렉트를 시키는 것을 알 수 있다. 프로젝트를 초기화할 때 대표 도메인을 그대로 쓰게 하면서 최신 버전으로 리다이렉트하게 하는 목적이 있는데 좀 숨겨진 느낌이라 아주 좋은 방법은 모르겠다.</p>
<pre class="line-numbers"><code class="language-bash">$ curl -i https://fresh.deno.dev
HTTP/2 307
location: https://deno.land/x/fresh@1.1.2/init.ts
content-type: text/plain;charset=UTF-8
vary: Accept-Encoding
date: Thu, 03 Nov 2022 16:45:13 GMT
content-length: 54
server: deno/asia-northeast3-b
Redirecting to https://deno.land/x/fresh@1.1.2/init.ts
</code></pre>
<p>브라우저에서는 웹사이트가 떴는데 아래처럼 <code>accept</code> 헤더를 지정하면 <code>200 OK</code>로 웹사이트가 반환되는 것을 볼 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ curl -i -H 'accept: text/html' https://fresh.deno.dev
HTTP/2 200
content-type: text/html; charset=utf-8
vary: Accept-Encoding
date: Thu, 03 Nov 2022 16:45:44 GMT
content-length: 49218
server: deno/asia-northeast3-b
<!DOCTYPE html><html lang="en"><head><meta charSet="UTF-8" />
...중간 생략...
</html>
</code></pre>
<p>다시 Fresh 프로젝트로 돌아와서 생성된 폴더에 들어가서 <code>deno task start</code>를 실행하면 서버를 띄울 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ cd fresh-example
$ deno task start
Task start deno run -A --watch=static/,routes/ dev.ts
Watcher Process started.
The manifest has been generated for 3 routes and 1 islands.
Listening on http://localhost:8000/
</code></pre>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/8849210501.jpg" width="600" height="286" alt="Fresh 보일러프로젝트 웹사이트" title="" /></p>
<p>예제 프로젝트를 볼 수 있고 +/- 버튼을 누르면 옆의 숫자도 바뀌는 걸 볼 수 있다.<br />
<br></p>
<h1>Fresh 프로젝트 살펴보기</h1>
<p>Fresh 프레임워크는 유연한 프레임워크라기 보다는 opinionated한 프레임워크라서 페이지 로딩 시간을 최소한으로 줄이고 클라이언트에서 수행되는 작업도 최대한 줄이려는 목표를 가지고 있다. 그래서 다른 프레임워크와는 좀 다른 특징이 있다.</p>
<ul>
<li>Deno를 사용하므로 TypeScript을 쓰더라도 별도의 빌드 과정은 없다.</li>
<li>요즘 프론트엔드 추세와는 달리 기본적으로 서버측 렌더링하는 구조다.</li>
<li>아일랜드 아키텍처를 이용해서 선택적인 클라이언트 하이드레이션을 할 수 있다.</li>
<li>React와 같은 API를 쓰지만 가벼운 <a href="https://preactjs.com/">Preact</a>를 사용한다.</li>
</ul>
<p>위 예제에서 생성된 디렉터리 구조는 다음과 같다.</p>
<pre class="line-numbers"><code class="language-bash">├── README.md
├── components
│ └── Button.tsx
├── deno.json
├── dev.ts
├── fresh.gen.ts
├── import_map.json
├── islands
│ └── Counter.tsx
├── main.ts
├── routes
│ ├── [name].tsx
│ ├── api
│ │ └── joke.ts
│ └── index.tsx
└── static
├── favicon.ico
└── logo.svg
</code></pre>
<ul>
<li><code>dev.ts</code>: 개발용으로 사용하는 시작 파일이다. 개발 중에는 이 파일로 서버를 띄우는데 앞에서 실행한 <code>deno task start</code>는 <code>deno run -A --watch=static/,routes/ dev.ts</code>를 실행한다.(<code>deno.json</code> 파일에서 볼 수 있다.) 파일명은 꼭 <code>dev.ts</code>일 필요는 없지만, 관례상 <code>dev.ts</code>를 사용한다.</li>
<li><code>main.ts</code>: 프로젝트의 시작 파일로 배포 시 이 파일을 실행해서 서버를 띄운다.. 이 파일명도 꼭 <code>main.ts</code>일 필요는 없지만, 관례상 <code>main.ts</code>를 사용한다. 앞의 <code>dev.ts</code> 파일로 내부적으로는 <code>main.ts</code>를 바라보고 있다.</li>
<li><code>fresh.gen.ts</code>: 뒤에서 더 설명하겠지만 라우팅과 아일랜드 정보를 담고 있는 manifest 파일이다. <code>dev.ts</code>로 서버를 띄울 때 <code>routes</code>, <code>islands</code> 디렉터리를 기반으로 자동 생성되므로 직접 수정할 일은 없다.(다른 말로 말하면 이 두 디렉터리의 내용을 수정하면 <code>dev.ts</code>를 이용해서 업데이트해 주어야 한다.)</li>
<li><code>routes/</code>: 라우팅을 담고 있는 디렉터리로 파일시스템 기반의 라우팅을 사용하므로 파일 구조가 URL 경로가 된다. 이 디렉터리 내의 코드는 클라이언트에 전송되지 않는다.</li>
<li><code>islands/</code>: 뒤에서 설명하겠지만 Fresh는 아일랜드 아키텍처를 사용하는데 이 아일랜드 컴포넌트는 이 디렉터리에 들어있다. 아일랜드 컴포넌트는 클라이언트와 서버 모두에서 실행될 수 있기 때문에 클라이언트에서 인터렉티브한 기능을 구현할 때 사용한다.</li>
<li><code>static/</code>: 이 디렉터리의 파일은 정적 에셋으로 제공되고 <code>routes/</code>와 겹치더라도 <code>static/</code>이 우선된다. 파일 확장자 기반으로 <code>Content-Type</code> 헤더가 할당되고 <code>ETag</code>도 자동으로 추가한다.</li>
<li><code>components/</code>: <code>islands/</code>에서 사용하는 컴포넌트가 담긴 디렉터리다.<br />
<br></li>
</ul>
<h1>라우팅</h1>
<p>새로운 라우팅을 추가하려면 <code>routes/</code> 디렉터리 아래 파일을 추가해야 한다. <a href="https://nextjs.org/">Next.js</a>를 사용해봤다면 익숙할 것이다. 생성된 프로젝트에 생성된 라우팅이 있지만 이해를 돕기 위해 <code>routes/about.tsx</code> 파일을 추가해보자.</p>
<pre class="line-numbers"><code class="language-tsx">// routes/about.tsx
export default function AboutPage() {
return (
<main>
<h1>About</h1>
<p>This is the about page.</p>
</main>
);
}
</code></pre>
<p>HTML 페이지를 렌더링 하려면 <code>export default</code>로 JSX 컴포넌트를 익스포트하면 된다. 다시 말하지만, Fresh는 Preact를 사용한다.</p>
<p>여기서는 <code>routes/about.tsx</code>을 추가했으니 <code>/about</code> 경로로 위 페이지가 서빙된다. 파일시스템으로 라우팅할 때 몇 가지 패턴이 있다.</p>
<ul>
<li>파일 확장자는 무시되고 <code>index</code> 파일은 해당 경로로 취급된다. </li>
<li><code>[</code>, <code>]</code>로 감싼 파일은 패스 파라미터로 사용되어 동적인 이름과 매칭된다.</li>
<li>경로 마지막에 <code>[...path].ts</code> 같은 파일이 있으면 와일드카드로 처리되어서 하위 경로 모두와 매칭된다.</li>
</ul>
<pre class="line-numbers"><code class="language-bash">$ deno task start
Task start deno run -A --watch=static/,routes/ dev.ts
Watcher Process started.
The manifest has been generated for 4 routes and 1 islands.
Listening on http://localhost:8000/
</code></pre>
<p>다시 서버를 시작하면 아까와 달리 4 routes(아까는 3개였다)라고 나오는걸 볼 수 있고 <code>fresh.gen.ts</code> 파일에도 <code>./routes/about.tsx</code>에 한 라우팅이 자동으로 추가된 것을 볼 수 있다. <a href="http://localhost:8000/about">http://localhost:8000/about</a>에 접속하면 작성한 페이지를 볼 수 있다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/9970028866.jpg" width="300" height="140" alt="About 페이지" title="" /></p>
<p>좀 더 동적인 페이지를 위해 보일러플레이트 코드에 생성된 <code>routes/[name].tsx</code> 파일을 보자. <code>[name].tsx</code> 패턴을 사용했으므로 <code>/Outsider</code>, <code>/Wayland</code> 같은 경로와 매핑되고 이 경로는 <code>name</code> 파라미터로 들어온다. 이 코드를 보면 <code>PageProps</code>을 이용해서 <code>props.params.name</code>으로 패스 파라미터를 받은 걸 알 수 있다.</p>
<pre class="line-numbers"><code class="language-tsx">// routes/[name].tsx
import { PageProps } from "$fresh/server.ts";
export default function Greet(props: PageProps) {
return <div>Hello {props.params.name}</div>;
}
</code></pre>
<p><a href="http://localhost:8000/outsider">http://localhost:8000/outsider</a>로 접속하면 아래 화면을 볼 수 있다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/5875481547.jpg" width="320" height="74" alt="outsider 경로로 접속한 페이지" title="" /></p>
<p>간단한 라우팅을 살펴봤는데 라우팅은 실제로는 핸들러와 페이지 컴포넌트로 이루어져 있고 지금까지 본 것은 페이지 컴포넌트다.</p>
<p>핸들로는 <code>Request => Response</code>나 <code>Request => Promise<Response></code> 형식의 함수로 해당 경로로 요청이 들어오면 호출된다. 응답 객체는 직접 생성할 수도 있고 페이지 컴포넌트가 렌더링 되면서 생성될 수도 있는데 앞에 봤듯이 커스텀 핸들러를 정의하지 않았다면 페이지 컴포넌트를 렌더링하는 기본 핸들러가 사용된 것이다.</p>
<p>커스텀 핸들러를 정의하려면 <code>handler</code>라는 이름으로 익스포트 해야 한다. <code>routes/[name].tsx</code>에 커스텀 핸들러를 추가해보자.</p>
<pre class="line-numbers"><code class="language-tsx">// routes/[name].tsx
import { Handlers, PageProps } from "$fresh/server.ts";
export const handler: Handlers = {
async GET(req, ctx) {
const resp = await ctx.render();
resp.headers.set("X-Custom-Header", "Hello");
return resp;
}
};
export default function Greet(props: PageProps) {
return <div>Hello {props.params.name}</div>;
}
</code></pre>
<p>이 페이지에 접근하면 아까와 동일하게 페이지는 표시되지만 응답 헤더에 <code>X-Custom-Header</code>가 추가된 것을 볼 수 있다. <code>handler</code>에서 <code>GET</code> 요청에 대한 처리를 추가했고 <code>ctx.render()</code>는 아래 <code>Greet</code> 페이지 컴포넌트를 렌더링하는 부분이다. 렌더링한 응답에 헤더를 추가하고 반환했다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/6224793535.jpg" width="600" height="215" alt="HTTP 응답헤더에 커스텀 헤더가 추가된 화면" title="" /></p>
<p>핸들러의 사용 방법을 알기 위해 API로 데이터를 가져와서 렌더링하도록 이 페이지를 수정해 보자. 페이지 컴포넌트의 렌더링은 동기로 수행되기 때문에 데이터를 가져오려면 핸들러에서 비동기로 데이터를 가져온 뒤에 <code>ctx.render()</code>에 인자로 전달해 주어야 한다. <code>routes/[name].tsx</code> 페이지를 GitHub의 사용자 정보를 가져와서 보여주도록 수정해 보자.</p>
<pre class="line-numbers"><code class="language-tsx">// routes/[name].tsx
import { Handlers, PageProps } from "$fresh/server.ts";
interface User {
login: string;
name: string;
avatar_url: string;
}
export const handler: Handlers<User | null> = {
async GET(req, ctx) {
const { name } = ctx.params;
const resp = await fetch(`https://api.github.com/users/${name}`);
if (resp.status === 404) {
return ctx.render(null);
}
const user: User = await resp.json();
return ctx.render(user);
}
};
export default function Greet({ data }: PageProps<User | null>) {
if (!data) {
return <h1>User not found</h1>
}
return (<div>
<img src={data.avatar_url} width={64} height={64} />
<h1>{data.name}</h1>
<p>{data.login}</p>
</div>);
}
</code></pre>
<p>경로의 <code>name</code> 파라미터의 값으로 <code>https://api.github.com/users/${name}</code>에서 사용자 정보를 가져온 뒤 <code>ctx.render(user)</code>에 전달해 주었다. 이렇게 전달한 값은 <code>PageProps</code>의 <code>data</code>로 가져올 수 있다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/3354742374.jpg" width="330" height="224" alt="GitHub에서 정보를 불러온 화면" title="" /></p>
<p><a href="http://localhost:8000/outsideris">http://localhost:8000/outsideris</a>로 접근하면 GitHub 사용자 정보가 잘 표시된 것을 볼 수 있다.<br />
<br></p>
<h1>아일랜드 아키텍처</h1>
<p>지금까지 본 페이지는 클라이언트에 자바스크립트가 전혀 없고 모두 서버에서 렌더링해서 내려주었다. 요즘은 프론트엔드가 클라이언트 측 렌더링(CSR)하고 있는데 Fresh를 서버 측 렌더링(SSR)을 더 중점적으로 선택하고 있는 게 다른 프레임워크와는 다른 점이다. 하지만 클라이언트에도 상호작용을 위해서 자바스크립트가 필요한데 Fresh는 <a href="https://jasonformat.com/islands-architecture">아일랜드 아키텍처</a>라는 것을 채택했다. 페이지는 서버에서 렌더링해서 내려주지만, 상호작용이 필요한 부분을 섬처럼 별도 취급해서 이 부분에는 JavaScript를 내려주는 형태라고 생각할 수 있다. 그래서 앞에서 <code>islands</code>라는 폴더가 있었다.</p>
<p>클라이언트에서도 렌더링 되는 "아일랜드 컴포넌트"는 <code>islands/</code> 디렉터리에 있어야 하고 파스칼 케이스(예: <code>ConvertUnit</code>)나 케밥 케이스(예: <code>convert-unit</code>)로 정의해야 한다.</p>
<p>예시로 만들어진 프로젝트에도 버튼으로 카운터를 조정하는 <code>islands/Counter.tsx</code>가 있었다.</p>
<pre class="line-numbers"><code class="language-tsx">import { useState } from "preact/hooks";
import { Button } from "../components/Button.tsx";
interface CounterProps {
start: number;
}
export default function Counter(props: CounterProps) {
const [count, setCount] = useState(props.start);
return (
<div>
<p>{count}</p>
<Button onClick={() => setCount(count - 1)}>-1</Button>
<Button onClick={() => setCount(count + 1)}>+1</Button>
</div>
);
}
</code></pre>
<p>일반적으로 볼 수 있는 React 컴포넌트다.(물론 여기서는 preact다.)</p>
<pre class="line-numbers"><code class="language-tsx">// routes/index.tsx
import Counter from "../islands/Counter.tsx";
export default function Home() {
return (
<>
{/*생략*/}
<Counter start={3} />
</>
);
}
</code></pre>
<p>사용할 때도 아일랜드 컴포넌트를 불러와서 사용하면 되고 이 아일랜드 컴포넌트는 클라이언트에서 다시 하이드레이션 된다. 대신 서버에서 렌더링 되는 페이지에서 아일랜드 컴포넌트를 사용하는 것이기 때문에 컴포넌트에 전달되는 <code>props</code>는 반드시 JSON으로 직렬화할 수 있어야 한다. 위에서는 <code>start</code>로 숫자를 넘겨서 상관없지만 <code>Date</code> 객체를 넘긴다고 하더라도 문자열이 되고 함수나 커스텀 클래스 등은 전달할 수 없다. <code>children</code>도 직렬화할 수 없는 VNodes이므로 아일랜드 컴포넌트에 전달할 수 없다.</p>
<p>이제 이 아일랜드 컴포넌트로 버튼을 누를때 마다 숫자가 바뀌는 것을 확인할 수 있다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1632?commentInput=true#entry1632WriteComment">댓글 쓰기</a></strong></p>Deno의 Node.js 호환 기능Outsiderhttps://blog.outsider.ne.kr/16252022-09-26T04:05:09+09:002022-09-26T04:05:09+09:00<p>Node.js를 만든 Ryan Dahl이 Deno도 만들었지만 둘은 다른 프로젝트라서 완전히 호환되지는 않는 런타임이다. 그런데도 JavaScript 풍부한 생태계(브라우저, Node.js 포함)를 무시할 수는 없으므로 Deno 1.15부터 Node.js 호환성 모드를 추가하기 시작했다. Deno가 브라우저에서 사용하는 Web Platform API를 따르고 있고 Node.js처럼 <a href="https://v8.dev/">V8 엔진</a>을 이용하기 때문에 상당수의 Node.js 코드는 Deno에서도 동작하지만 두 런타임의 차이가 있기 때문에 호환성 문제가 있다.</p>
<ul>
<li>Node.js는 그 발전과정의 흐름을 보면 CommonJS와 ES Modules를 모두 지원한다.(Node.js가 만들어질 때는 ES Modules가 없었다.) 하지만 Deno는 ES Modules만 지원하기 때문에 CommonJS(<code>require</code>)로 작성된 코드는 Deno에서는 기본적으로 동작하지 않는다. </li>
<li>Node.js가 내장 모듈을 많이 제공하고 있기 때문에 이러한 모듈은 Deno와는 다르다. Deno는 웹 표준을 따르려고 하고 있으며 브라우저 외의 기능은 전역 변수 <code>Deno</code>에서 API를 제공하고 있다.</li>
<li>Node.js는 <code>package.json</code>이 꼭 필요하지만, Deno는 별도의 메타데이터 파일을 필요로 하지 않는다.</li>
<li>Node.js는 자신만의 모듈 처리 알고리즘을 가지고 있어서 상대 경로나 절대 경로를 사용하지 않고 모듈의 이름을 지정하는 베어 지정자(bare specifier)를 지원한다. 그리고 <code>package.json</code>을 검사해서 이름을 확인하고 <code>node_modules</code> 폴더에서 모듈을 찾는다. 하지만 Deno는 브라우저와 같은 방식으로 모듈을 처리하기 때문에 확장자를 포함한 전체 이름으로 임포트하고 원격 파일을 임포트한다면 웹서버가 처리 후 올바른 미디어 타입을 제공하면 이에 따라 Deno가 처리하게 된다.</li>
<li>Node.js는 HTTP로 원격 임포트를 지원하지 않고 <code>npm</code>같은 패키지 매니저로 서드파티 모듈을 로컬에 설치해야 하지만 Deno는 HTTP로 원격 임포트(<code>data</code>나 <code>blob</code> URL도 지원한다)를 지원하고 원격 코드를 로컬에 캐시하는데 이는 브라우저의 동작과 비슷하다.</li>
</ul>
<p>이러한 차이점이 있지만 Deno는 Node.js 생태계와 호환되기 위해 다양한 기능을 제공하고 있다. 물론 호환이 전혀 되지 않은 부분도 있다</p>
<ul>
<li>Node.js에는 애드온 시스템이 있는데 Deno는 이를 지원하지 않고 앞으로도 지원할 계획이 없다. 사용할 Node.js 코드가 <a href="https://nodejs.org/dist/latest-v16.x/docs/api/addons.html">네이티브 애드온</a>을 사용한다면 Deno에서는 사용할 수 없다.</li>
<li>Node.js에서는 Deno의 지원범위와는 다른 <a href="https://nodejs.org/dist/latest-v16.x/docs/api/vm.html">VM</a>같은 내장 모듈도 있는데 이러한 모듈을 Deno에서 쉽게 폴리필 할 수 없다.<br />
<br></li>
</ul>
<h1><code>std/node</code></h1>
<p>Deno는 Node.js의 내장 모듈을 지원하기 위해 표준 라이브러리 <a href="https://deno.land/manual@v1.25.4/node/std_node"><code>std/node</code></a>를 제공하고 있다. 100% 대체하는 것은 아니므로 <a href="https://deno.land/manual@v1.25.4/node/std_node"><code>std/node</code>의 문서</a>를 참고해야 하지만 해당 문서는 최신 버전과 좀 안 맞는 부분이 있어서 <a href="https://deno.land/std@0.157.0/node?doc">표준 라이브러리</a>를 참고해서 봐야 한다. 이 <code>std/node</code>를 이용해서 Node.js의 내장 모듈을 대체할 수 있다.</p>
<p>Node.js에서 HTTP를 실행하는 다음 코드를 살펴보자.</p>
<pre class="line-numbers"><code class="language-javascript">// nodejs-server.js
const http = require('http');
const server = http.createServer((req, res) => {
res.end('hello world');
});
</code></pre>
<p>이 코드는 CommonJS 형식이라 <code>require</code>를 <code>std/node</code>의 <code>module</code>을 사용해서 폴리필할 수 있다. 아래 코드처럼 <code>require</code>를 정의하면 Deno에서 이 코드를 그래도 실행할 수 있다.</p>
<pre class="line-numbers"><code class="language-javascript">// nodejs-server.js
import { createRequire } from "https://deno.land/std@0.157.0/node/module.ts";
const require = createRequire(import.meta.url);
// 여기서 부터 nodejs http 서버
const http = require('http');
const server = http.createServer((req, res) => {
res.end('hello world');
});
server.listen(8000);
</code></pre>
<p>이는 <a href="https://deno.land/std@0.157.0/node"><code>std.node</code></a>에 폴리필이 있어서 npm 모듈(<code>require</code>를 포함해서)을 처리해 준다. 앞에서 말한 제약 사항 외에는 괜찮긴 하지만 실제로 동작하는 건 모듈별로 봐야 한다.</p>
<p>아직 <a href="https://deno.land/manual@v1.25.4/node/compatibility_mode">공식 문서</a>가 <code>--compat</code> 호환성 모드라 동작 방식에 대한 설명은 어디까지가 지금과 맞는지 확실치 않다.</p>
<p><a href="https://deno.land/std@0.157.0/node"><code>std.node</code></a>에는 Node.js의 내장 모듈을 대체하는 모듈이 포함되어 있다. 100% 호환되는 것은 아니고 부분적으로 구현된 모듈이 있기 때문에 <a href="https://deno.land/manual@v1.25.4/node/std_node">문서</a>를 참고해 봐야 한다.</p>
<p>CommonJS의 <code>require</code>를 사용해야 하는 경우에는 ``std.node<code>의</code>module<code>을 사용할 수 있다. 아래처럼</code>require`를 정의한 뒤에 Node.js의 HTTP 서버 코드를 그대로 사용할 수 있다.</p>
<pre class="line-numbers"><code class="language-javascript">// nodejs-server.js
import { createRequire } from "https://deno.land/std@0.157.0/node/module.ts";
const require = createRequire(import.meta.url);
// 여기서 부터 nodejs http 서버
const http = require('http');
const server = http.createServer((req, res) => {
res.end('hello world');
});
server.listen(8000);
</code></pre>
<p>아래처럼 이 파일을 실행하면 잘 실행되는 것을 알 수 있다. 여기서 <code>http</code> 모듈은 당연히 Node.js가 아니라 <code>std/node</code>의 <code>http</code> 모듈이다.</p>
<pre class="line-numbers"><code class="language-bash">$ deno run --allow-net --allow-env nodejs-server.js
</code></pre>
<p>제대로 서버가 실행되었는지 터미널을 하나 더 열어서 요청을 보내보면 응답이 잘 오는 것을 볼 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ curl http://localhost:8000
hello world
</code></pre>
<p><br></p>
<h1>npm 지원</h1>
<p>Deno 1.15부터 Node.js용으로 만들어진 모듈과 호환성을 제공하기 위해 <code>--compat</code> 플래그를 제공했지만 <a href="https://deno.com/blog/v1.25#experimental-npm-support">1.25.0에서 실험적으로 npm 지원을 추가</a>하면서 <a href="https://github.com/denoland/deno/releases/tag/v1.25.2">1.25.2 버전에서 <code>--compat</code> 플래그를 제거</a>했다. 그래서 아직 공식 문서에도 <code>--compat</code>이 나와 있지만 Deno 최신 버전(이 글을 쓰는 시점에서는 1.25.4)을 쓴다면 무시해도 좋고 npm 지원도 실험적인 지원이므로 나중에는 동작이 달라질 수도 있다.</p>
<p>npm 지원은 모듈을 사용할 때 <code>npm:<package-name>[@<version-requirement>][/<binary-name>]</code>같은 형식으로 앞에 <code>npm:</code> 지정자를 붙여주면 이를 Deno가 처리해 준다. 이 지정자는 현재 <code>deno run</code>, <code>deno test</code>, <code>deno bench</code>에서 동작하고 아직 타입검사는 하지 않는다. 지금은 <code>npm</code>을 쓸 때 환경변수와 파일시스템 읽기 기능이 필요한데 나중에는 필요없게 될 것이라고 한다.</p>
<p>간단한 예제를 살펴보자. 아래에서 <a href="https://www.npmjs.com/package/cowsay">cowsay</a> 모듈을 Deno에서 사용하기 위해 <code>npm:cowsay@1.5.0</code>를 실행했다.(이 예제들은 1.25.0 릴리스 공지에 있는 예제이다.)</p>
<pre class="line-numbers"><code class="language-bash">$ deno run --unstable --allow-env --allow-read npm:cowsay@1.5.0 Hello there!
Download https://registry.npmjs.org/cowsay
Download https://registry.npmjs.org/get-stdin
Download https://registry.npmjs.org/string-width
Download https://registry.npmjs.org/strip-final-newline
Download https://registry.npmjs.org/yargs
...중략...
Download https://deno.land/std@0.157.0/node/module_all.ts
Download https://deno.land/std@0.157.0/node/_http_agent.mjs
...중략...
Download https://deno.land/std@0.157.0/node/_crypto/crypto_browserify/asn1.js/encoders/der.js
Download https://deno.land/std@0.157.0/node/_crypto/crypto_browserify/asn1.js/encoders/pem.js
______________
< Hello there! >
--------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
</code></pre>
<p>자연스럽게 <code>npm</code> 모듈을 Deno에서 사용한 것을 볼 수 있다. 아직 실험적 기능이므로 <code>--unstable</code> 플래그가 필요하다. 지정하지 않으면 <code>error: Unstable use of npm specifiers. The --unstable flag must be provided.</code> 오류가 발생한다. 일부러 캐시를 초기화하고 다운로드받는 로그도 넣어놨는데 Deno의 표준 라이브러리뿐 아니라 <a href="https://registry.npmjs.org">https://registry.npmjs.org</a>에서 필요한 모듈을 알아서 받아오는 것을 볼 수 있다.</p>
<p>이번엔 <a href="https://expressjs.com/">Express</a> 서버를 실행해 보자.</p>
<pre class="line-numbers"><code class="language-typescript">// npm-express.ts
import express from "npm:express";
const app = express();
app.get("/", function (req, res) {
res.send("Hello World");
});
app.listen(3000);
console.log("listening on http://localhost:3000/");
</code></pre>
<p>Express 서버에서 많이 보던 코드 그대로다. 이를 실행해보자.</p>
<pre class="line-numbers"><code class="language-bash">$ deno run --unstable --allow-read \
--allow-env --allow-net npm-express.ts
listening on http://localhost:3000/
</code></pre>
<p>터미널을 새로 열어서 요청을 보내보면 응답을 잘 주는 것을 볼 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ curl http://localhost:3000
Hello World
</code></pre>
<p>npm 지원 기능은 Node.js에서 사용할 때와 유사하게 자연스럽게 동작하는 것을 알 수 있다. 이는 기존 Deno에서 아쉬웠던 부분을 크게 해결할 것으로 생각한다. 물론 npm 모듈이라고 하더라도 앞에서 얘기한 Deno와 호환되지 않는 코드는 제대로 실행되지 않을 것이므로 사용할 때 확인해 보고 사용해야 한다.<br />
<br></p>
<h1>CDN을 이용한 모듈</h1>
<p>Node.js 생태계가 브라우저 JavaScript 생태계도 같이 지원하고 있으므로 Deno도 이를 이용할 수 있다. 이는 정확히 Node.js 호환을 위한 기능은 아니지만, 브라우저에서 ES Modules를 이용하기 위해 CDN을 통해 제공하는 레지스트리가 있고 Deno는 원격 HTTP 모듈을 지원하므로 이러한 CDN을 통해서 Deno에서도 npm 레지스트리의 모듈을 사용할 수 있다.</p>
<p><a href="https://deno.land/x">deno.land/x</a>는 Deno의 공개 레지스트리로 Node.js의 npm 레지스트리라고 생각하면 된다. Deno 모듈을 등록해서 사용할 수 있지만 아직 생태계가 크지 않아서 각 모듈이 잘 관리되고 있다고 하기는 어려운 상황이다.</p>
<p>대신 Deno를 지원하는 CDN이 있어서 Node.js용으로 작성된 모듈을 Deno에서 사용할 수 있도록 지원해 주고 있다. 이러한 CDN은 다음과 같은 지원을 해준다.(모든 CDN이 아래 기능을 다 지원하는 것은 아니다.)</p>
<ul>
<li>npm에 배포된 방식과 상관없이 ES Modules 형식으로 제공한다.</li>
<li>Node.js의 모듈 처리 로직은 CDN이 대신 모든 의존성을 해결해서 제공한다.</li>
<li>모듈의 타입 정의를 Deno에 알려주어 타입 검사를 할 수 있게 해준다.</li>
<li>Node.js 내장 모듈을 CDN이 폴리필해주어 Node.js 내장 모듈도 Deno에서 동작하게 해준다.</li>
<li>CDN이 npm처럼 버전 처리를 해주어 URL에서 버전을 지정할 수 있게 해준다.<br />
<br></li>
</ul>
<h2>esm.sh</h2>
<p><a href="https://esm.sh/">esm.sh</a>는 Deno를 위해 설계된 CDN으로 npm 패키지를 ES Modules 번들로 접근하게 해준다. esm.sh는 <a href="https://esbuild.github.io/">esbuild</a>를 사용해서 npm 패키지를 ES Modules로 변환해서 Deno 코드에서 임포트할 수 있다.</p>
<p>예를 들어 어서트 라이브러리인 <a href="https://www.chaijs.com/">chai</a>를 사용한다고 하면 <a href="https://esm.sh/chai">https://esm.sh/chai</a>를 주소창에 입력하면 다음과 같이 최신 버전으로 리다이렉트가 된다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/3254954677.jpg" width="550" height="105" alt="esm.js의 모듈 안내 페이지" title="" /></p>
<p>이 주소를 아래와 같이 사용하면 <code>chai</code>를 마치 Deno용으로 만들어진 모듈처럼 사용할 수 있다.</p>
<pre class="line-numbers"><code class="language-typescript">// esmsh.ts
import * as chai from "https://esm.sh/v95/chai@4.3.6/es2022/chai.js";
const expect = chai.expect;
const foo = 'foo';
expect(foo).to.equal('bar');
</code></pre>
<p><code>chai</code>가 <code>NODE_ENV</code> 환경 변수에 접근하기 때문에 <code>--allow-env</code> 플래그를 주어서 실행하면 잘 실행되는 것을 알 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ deno run --allow-env esmsh.ts
error: Uncaught AssertionError: expected 'foo' to equal 'bar'
expect(foo).to.equal('bar');
^
at file:///Users/outsider/deno-example/esmsh.ts:6:16
</code></pre>
<p>물론 위처럼 아주 구체적으로 적지 않고 <code>import * as chai from "https://esm.sh/chai@4.3.6";</code>같은 식으로 적는 것도 가능하다. 이외에도 많은 기능이 있긴 한데 <a href="https://esm.sh/">esm.sh 사이트</a>를 참고하면 안내되어 있다. <code>esm.sh</code>도 <code>std/node</code>를 사용해서 Node의 내장 모듈을 교체하므로 Deno가 가진 제약 사항도 동일하게 적용된다.<br />
<br></p>
<h2>Skypack</h2>
<p><a href="https://www.skypack.dev/">Skypack</a>도 npm 레지스트리에 올라온 모듈을 Deno를 위해 지원해 준다. <a href="https://dev.to/pika/introducing-pika-cdn-deno-p8b">2020년 초에 Deno를 지원하기 시작</a>했고 <a href="https://www.skypack.dev/blog/2021/02/skypack-npm-packages-in-deno/">21년 초에도 Deno 호환성을 개선</a>했고 타입 정보를 위한 <code>?dts</code> 쿼리 스트링도 이때 추가되었다.</p>
<p>esm.sh는 모듈 검색도 어렵고 사이트도 개인이 만들었나 싶어질 정도로 설명도 불친절하고 정보 찾기도 어려웠는데 Skypack은 사이트가 훨씬 깔끔하게 되어 있다. 모듈 검색도 할 수 있고 사용법과 함께 README와 통계(이건 npm에서 긁어 온 것 같다.)도 나와 있다.</p>
<p>Skypack으로도 <code>chai</code>를 사용해보자.</p>
<pre class="line-numbers"><code class="language-typescript">// skypack.ts
import * as chai from "https://cdn.skypack.dev/chai";
const expect = chai.expect;
const foo = 'foo';
expect(foo).to.equal('bar');
</code></pre>
<p>모듈을 가져오는 주소 빼고는 esm.sh의 코드와 다른 부분은 없다. 하지만 이를 실행하면 아래와 같이 오류가 난다.</p>
<pre class="line-numbers"><code class="language-bash">$ deno run skypack.ts
[Package Error] "chai@v4.3.6" could not be built.
[1/5] Verifying package is valid…
[2/5] Installing dependencies from npm…
[3/5] Building package using esinstall…
Running esinstall...
chai/karma.sauce.js
Import "./test/auth/index" could not be resolved from file.
/tmp/cdn/_jxKTOH1FZOrZ5Cu9nlya/node_modules/chai/test/auth/index?commonjs-external
Module "/tmp/cdn/_jxKTOH1FZOrZ5Cu9nlya/node_modules/chai/test/auth/index" could not be resolved by Skypack (Is it installed?).
Install failed for one of chai, chai/chai, chai/index, chai/karma.conf, chai/karma.sauce, chai/register-assert, chai/register-expect, chai/register-should, chai/sauce.browsers.
Install failed for one of chai, chai/chai, chai/index, chai/karma.conf, chai/karma.sauce, chai/register-assert, chai/register-expect, chai/register-should, chai/sauce.browsers.
error: Uncaught Error: [Package Error] "chai@v4.3.6" could not be built.
throw new Error("[Package Error] \"chai@v4.3.6\" could not be built. ");
^
at https://cdn.skypack.dev/error/build:chai@v4.3.6-jxKTOH1FZOrZ5Cu9nlya:22:7
</code></pre>
<p>Deno 지원 처리를 하면서 빌드 오류가 발생하는데 정확한 이유는 모르겠다. CDN을 다양하게 사용해 보면서 처리 방식을 좀 더 공부해봐야 할 것 같다.</p>
<p>Skypack의 동작을 확인하기 위해 <code>debug</code> 모듈을 사용해 보자.</p>
<pre class="line-numbers"><code class="language-typescript">// skypack-debug.ts
import debug from "https://cdn.skypack.dev/debug";
const log = debug('mylog');
debug.enable('mylog');
log('hello world');
</code></pre>
<p><a href="https://www.npmjs.com/package/debug">debug</a>는 애플리케이션에서 디버그 로그를 선택적으로 출력할 수 있게 하는 모듈이다. 원래는 <code>DEBUG</code> 환경변수로 제어하는데 ESM으로 빌드되면서 그렇게 된 건지 Deno에서는 환경변수를 읽지 않아서(브라우저 용이 된 게 아닐까 추측해 본다.) 코드로 활성화했다. 실행하면 아래처럼 로그가 출력된다.</p>
<pre class="line-numbers"><code class="language-bash">$ deno run skypack-debug.ts
mylog hello world +0ms
</code></pre>
<p>이 두 CDN 외에도 <a href="https://unpkg.com/">UNPKG</a>나 <a href="https://jspm.io/">JSPM</a>도 공식 문서에서는 추천하고 있는데 이는 Deno를 위해서 뭔가 해준다기보다는 브라우저에서 동작하는 npm 패키지가 ES Modules로 배포되었다면 Deno에서도 동작할 가능성이 아주 높기 때문에 이를 권장해 주는 것으로 보인다.</p>
<p>Deno를 지원하는 CDN 서비스의 동작을 아직 다 몰라서이기도 하지만 <code>npm:</code> 지정자를 지원하면서 쉽게 사용할 수 있기 때문에 전보다 CDN의 활용도는 좀 떨어지지 않나 생각한다. 하지만 아직 npm 지원은 실험적인 기능임을 염두에 두고 사용해야 할 것 같다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1625?commentInput=true#entry1625WriteComment">댓글 쓰기</a></strong></p>deno task 사용하기Outsiderhttps://blog.outsider.ne.kr/16242022-09-20T01:39:33+09:002022-09-20T01:39:33+09:00<p>Node.js에서 <a href="https://docs.npmjs.com/cli/v8/using-npm/scripts">npm scripts</a>처럼 Deno도 커스텀 명령어를 정의해서 사용할 수 있는 <a href="https://deno.land/manual@v1.25.3/tools/task_runner"><code>deno task</code></a>를 지원한다. 이 글에서 사용하는 버전은 v1.25.2인데 <code>deno task</code>는 v1.20에서 도입되었고 아직은 unstable 상태라서 나중에는 사용 방법이 달라질 수 있다.</p>
<p><code>deno task</code>를 사용하려면 <a href="https://deno.land/manual@v1.25.3/getting_started/configuration_file">Deno의 설정 파일</a>인 <code>deno.json</code>에서 <code>tasks</code>를 정의해 주면 된다. 프로젝트에서 자주 사용하는 명령어를 코드로 정의해 두는 걸 좋아하는 편이라 npm scripts도 잘 사용하고 있었고 Deno에서도 마찬가지로 필요해서 정리했다.</p>
<pre class="line-numbers"><code class="language-json">{
"tasks": {
"hello": "echo 'Hello World'"
}
}
</code></pre>
<p><a href="https://docs.npmjs.com/cli/v8/using-npm/scripts">npm scripts</a>에 익숙하다면 이해하기는 어렵지 않다. <code>tasks</code>에 이름을 지정하고 사용할 명령어를 작성하면 된다.</p>
<pre class="line-numbers"><code class="language-bash">$ deno task hello
Warning deno task is unstable and may drastically change in the future
Task hello echo 'Hello World'
Hello World
</code></pre>
<p>위처럼 <code>deno task <TASK_NAME></code> 형식으로 실행하면 정의한 명령어를 실행할 수 있다. 앞에서 얘기한 대로 아직 unstable 상태라는 경고 메시지가 나온다.</p>
<p><code>deno task</code>만 입력하면 정의된 태스크의 목록을 볼 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ deno task
Warning deno task is unstable and may drastically change in the future
Available tasks:
- hello
echo 'Hello World'
</code></pre>
<p><code>deno task</code>는 sh/bash 서브 셋이지만 크로스 플랫폼에서 동작하는 셸이라고 생각하면 된다. 다시 말하면 모든 명령어가 되진 않겠지만 셸을 작성하듯이 사용하면 Windows, Linux, macOS에서 다 동작하도록 Deno가 지원한다. 이는 크로스 플랫폼을 지원하기 위해 다양한 트릭을 써야 했던 npm scripts에 비해 좋은 점이다.</p>
<h2>불리언 리스트</h2>
<p>두 명령어를 <code>&&</code>로 연결하면 앞의 명령어가 exit code <code>0</code>으로 성공하면 두 번째 명령어도 실행된다.</p>
<p>다음과 같이 <code>success.sh</code>라는 셸 파일을 만들어 보자.(<code>chmod +x success.sh</code>로 실행권한을 추가한다.)</p>
<pre class="line-numbers"><code class="language-bash">#!/bin/bash
echo 'Success'
exit 0
</code></pre>
<p>이 파일을 이용해서 다음과 같이 태스크를 추가해보자.</p>
<pre class="line-numbers"><code class="language-json">{
"tasks": {
"and": "./success.sh && echo 'And'",
}
}
</code></pre>
<p>이를 실행하면 <code>./success.sh</code>가 성공했기 때문에 <code>echo 'And'</code>까지 실행된 것을 볼 수 있다. 앞의 명령어가 실패했다면(exit code가 <code>0</code>이 아닌 경우) <code>And</code>가 출력되지 않았을 것이다.</p>
<pre class="line-numbers"><code class="language-bash">$ deno task and
Warning deno task is unstable and may drastically change in the future
Task and ./success.sh && echo 'And'
Success
And
</code></pre>
<p>반대로 <code>||</code>를 사용하면 앞의 명령어가 성공하면 거기서 끝나고 실패하면 두번째 명령어도 실행된다.</p>
<pre class="line-numbers"><code class="language-json">{
"tasks": {
"or": "./success.sh || echo 'OR'"
}
}
</code></pre>
<p>이를 실행하면 앞의 명령어가 성공했으므로 <code>OR</code>은 출력되지 않는다.</p>
<pre class="line-numbers"><code class="language-bash">deno task or
Warning deno task is unstable and may drastically change in the future
Task or ./success.sh || echo 'OR'
Success
</code></pre>
<p><br></p>
<h2>순차 리스트</h2>
<p>앞의 명령어의 성공/실패 여부와 상관없이 여러 명령어를 이어서 실행하고 싶다면 <code>;</code>를 사용하면 된다.</p>
<pre class="line-numbers"><code class="language-json">{
"tasks": {
"sequential": "./fail.sh ; echo 'Finally'",
}
}
</code></pre>
<p>앞의 <code>fail.sh</code>는 일부러 exit code <code>1</code>로 끝나도록 한 쉘 스크립트인데 순차적으로 둘다 실행된 것을 볼 수 있다. 앞의 <code>fail.sh</code>는 일부러 exit code <code>1</code>로 끝나도록 한 셸 스크립트인데 순차적으로 둘 다 실행된 것을 볼 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ deno task sequential
Warning deno task is unstable and may drastically change in the future
Task sequential ./fail.sh ; echo 'Finally'
Failed
Finally
</code></pre>
<p><br></p>
<h1>내장 명령어</h1>
<p><code>deno task</code>는 <code>cp</code>, <code>mv</code>, <code>rm</code>, <code>mkdir</code>, <code>pwd</code>, <code>sleep</code>, <code>echo</code>, <code>cat</code>, <code>exit</code>, <code>xargs</code> 명령어를 내장하고 있다. 일반적인 Unix 명령어와 같아서 따로 용도를 설명할 필요는 없지만, 이 명령어는 사용하는 OS의 셸 명령어가 아니라 Deno가 지원하는 명령어라서 Windows, Mac, Linux에서도 사용할 수 있다.</p>
<pre class="line-numbers"><code class="language-json">{
"tasks": {
"copy": "cp hello.ts hello2.ts"
}
}
</code></pre>
<p>위와 같이 <code>cp</code> 명령어를 사용해서 파일을 복사하는 태스크를 추가했다. 그리고 위 명령어가 OS의 <code>cp</code>가 아닌지 확인하기 위해 <code>cp</code> 명령어를 <code>alias</code>로 교체했다. 아래처럼 <code>cp</code> 명령어를 사용하면 복사 대신 <code>echo</code>가 실행되는 것을 볼 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ alias cp='echo'
$ cp a.txt b.txt
a.txt b.txt
</code></pre>
<p>이제 <code>copy</code>라는 태스크를 실행하면 <code>cp</code> 명령어를 교체했음에도 <code>hello2.ts</code>라는 파일이 복사된 것을 볼 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ deno task copy
Warning deno task is unstable and may drastically change in the future
Task copy cp hello.ts hello2.ts
</code></pre>
<p><strong><a href="https://blog.outsider.ne.kr/1624?commentInput=true#entry1624WriteComment">댓글 쓰기</a></strong></p>Deno 살펴보기Outsiderhttps://blog.outsider.ne.kr/16232022-09-20T01:39:03+09:002022-09-17T15:20:19+09:00<p>Deno(dee-no라고 읽는다)는 JavaScript, TypeScript, Webassembly 런타임으로 <a href="https://v8.dev/">V8</a>, <a href="https://www.rust-lang.org/">Rust</a>, <a href="https://github.com/tokio-rs/tokio">Tokio</a>에 기반을 두고 있다. 나는 TypeScript도 안 쓰고 yarn도 안 쓰고 CommonJS로 모듈을 주로 쓰는 이제는 좀 구식이라고 할 수 있는 예전 Node.js 방식에 익숙한 개발자다. 개발할 때 트랜스파일이나 컴파일하는 걸 좋아하지 않는 터라 보통 개발환경도 가능하면 이런 과정 없이 구성하는 편인데 더 이상 TypeScript를 거부할 수는 없기에 TypeScript를 바로 사용할 수 있는 Deno에도 관심을 가지게 되어 사이드 프로젝트에서 써보고 있다. 바로 사용하려고 하니 이해 못하고 있는 부분이 많아서 이해할 겸 조금씩 정리해본다.</p>
<p>Deno는 2018년 10월 JSConf EU에서 <a href="https://github.com/ry">Ryan Dahl</a>이 <a href="https://www.youtube.com/watch?v=M3BM9TB-8yA">10 Things I Regret About Node.js</a>라는 발표에서 공개되었다. Ryan Dahl은 Node.js의 창시자로 <a href="https://www.jsconf.eu/2009/speaker/speakers_selected.html">2009년 JSConf EU에서 처음으로 Node.js를 공개</a>했다.(<a href="https://www.youtube.com/watch?v=ztspvPYybIY">발표 영상</a>) 계속 Node.js 프로젝트를 이끌다가 2012년 돌연 떠나서 보이지 않다가(개발 말고 다른 일을 한 거로 알고 있다) 2018년 돌아와서 저 발표를 하면서 Deno를 공개하게 된다.(<a href="https://medium.com/tech-confs-in-korean/ryan-dahl-%EB%82%B4%EA%B0%80-node-js%EC%97%90-%EB%8C%80%ED%95%B4-%ED%9B%84%ED%9A%8C%ED%95%98%EB%8A%94-%EA%B2%83-10%EA%B0%80%EC%A7%80-jsconf-eu-2018-c4c3cbb6b5ef">이 영상의 내용을 한국어로 정리해 주신 분이 있어서 링크</a>한다.)</p>
<p>발표 후 4년이 지났지만, Ryan Dahl이 JSConf에서 Deno로 하고 싶다고 한 걸 아직도 잘 유지하고 있다고 생각한다.(깊게까지 본 건 아니고 Deno가 내세우는 특징을 보고 하는 얘기다.) JSConf EU에서 발표하기 위한 프로토타이핑은 Ryan Dahl이 <a href="https://go.dev/">Go</a>로 <a href="https://github.com/denoland/deno/blob/ee208c1b20d5e51fa35583fb698930d70f1e5e8b/Releases.md#v010--20180823--rust-rewrite-and-v8-snapshot">v0.1.0</a>부터는 <a href="https://www.rust-lang.org/">Rust</a>를 선택했다.</p>
<p>Deno는 다음과 같은 특징을 가지고 있다.</p>
<ul>
<li>웹 플랫폼 기능을 제공하고 웹 플랫폼 표준을 따른다. 그래서 ES modules, web workers, <code>fetch()</code>등을 그대로 사용한다.</li>
<li>기본적으로 안전해서 파일 접근, 네트워크, 환경변수 등을 명시적으로 지정했을 때만 접근할 수 있다.</li>
<li>TypeScript를 기본으로 지원한다.</li>
<li>하나의 실행파일로 배포할 수 있다.</li>
<li>코드 포매터나 린터, 테스트 러너 등의 개발 도구를 내장하고 있다.<br />
<br></li>
</ul>
<h1>Deno 설치</h1>
<p><a href="https://deno.land/manual@v1.25.2/getting_started/installation#installation">문서에 따라</a> 사용하는 OS에 맞게 설치할 수 있다. 나는 <a href="https://github.com/denoland/deno/releases">릴리스 노트</a>에서 배포 파일을 다운받아서 <code>PATH</code>에 넣어서 사용하고 있다. 이 글을 쓸 때는 <a href="https://github.com/denoland/deno/releases/tag/v1.25.2">1.25.2 버전</a>을 사용했다.</p>
<pre class="line-numbers"><code class="language-bash">$ deno --version
deno 1.25.2 (release, aarch64-apple-darwin)
v8 10.6.194.5
typescript 4.7.4
</code></pre>
<p><br></p>
<h1>Deno 시작하기</h1>
<p>처음은 항상 Hello World니까 아래와 같은 간단한 TypeScript 파일을 작성해 보자.</p>
<pre class="line-numbers"><code class="language-typescript">// hello.ts
console.log('Hello World');
</code></pre>
<p>이 파일을 실행하려면 <code>deno run</code> 명령어를 사용한다. TypeScript가 아니라 JavaScript 파일이어도 당연히 잘 동작한다.</p>
<pre class="line-numbers"><code class="language-typescript">$ deno run hello.ts
Hello World
</code></pre>
<p><code>deno run</code>은 온라인의 원격 파일도 실행할 수 있는데 <a href="https://deno.land/std">Deno의 표준 라이브러리인 <code>std</code></a>에서 <a href="https://github.com/denoland/deno_std/tree/main/examples">예제 파일도 제공</a>하고 있으므로 가장 간단한 <a href="https://github.com/denoland/deno_std/blob/main/examples/welcome.ts"><code>welcome.ts</code></a>을 실행해 보자. 다양한 예제는 <a href="https://github.com/denoland/deno_std/tree/main/examples">std의 예제 폴더</a>에서 더 확인할 수 있다.</p>
<pre class="line-numbers"><code class="language-typescript">$ deno run https://deno.land/std@0.155.0/examples/welcome.ts
Welcome to Deno!
</code></pre>
<p>여기선 TypeScript나 JavaScript를 설명할 목적은 아니므로 간단한 예제를 하나 더 보자. <code>fetch</code>로 GitHub Status를 조회하는 코드다.</p>
<pre class="line-numbers"><code class="language-typescript">// fetch.ts
const res = await fetch('https://www.githubstatus.com/api/v2/status.json');
const json = await res.json()
console.log(json);
</code></pre>
<p>이 파일을 <code>deno run</code>으로 실행하면 아까와는 달리 경고가 나오는 것을 볼 수 있다. 전에는 플래그를 다 붙여줬어야 했는데 이젠 개발할 때 편하게 프롬프트가 나와서 <code>y</code>만 눌러줘도 된다.</p>
<pre class="line-numbers"><code class="language-bash">$ 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" }
}
</code></pre>
<p>앞에서 Deno는 기본적으로 안전하다고 했는데 이 예제가 네트워크에 접근해야 하는데 기본적으로는 네트워크에 접근할 권한이 없기 때문에 경고가 나온 것이다. 위처럼 경고 메시지를 보지 않으려면 <code>deno run</code>에 <code>--allow-net</code>(혹은 <code>--allow-net=www.githubstatus.com</code>)을 추가하면 된다.</p>
<pre class="line-numbers"><code class="language-bash">$ 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" }
}
</code></pre>
<p><code>deno run</code>에서는 다음과 같은 <a href="https://deno.land/manual@v1.25.2/getting_started/permissions#permissions-list">플래그</a>로 권한을 허용해 줄 수 있다.</p>
<ul>
<li><code>--allow-all</code>: 모든 권한 허용</li>
<li><code>--allow-env[=<allow-env>...]</code>: 환경변수 접근 허용</li>
<li><code>--allow-ffi[=<allow-ffi>...]</code>: 라이브러리 동적 로딩 허용</li>
<li><code>--allow-hrtime</code>: 고해상도 시간 측정 허용</li>
<li><code>--allow-net[=<allow-net>...]</code>: 네트워크 접근 허용</li>
<li><code>--allow-run[=<allow-run>...]</code>: 서브 프로세스 실행 허용</li>
<li><code>--allow-read[=<allow-read>...]</code>: 파일시스템 읽기 허용</li>
<li><code>--allow-write[=<allow-write>...]</code>: 파일시스템 쓰기 허용<br />
<br></li>
</ul>
<h2><code>deno</code> CLI</h2>
<p>앞에서 보았던 JavaScript나 TypeScript를 실행하는 <code>deno run</code> 외에도 <code>deno</code> CLI에서는 꽤 많은 기능을 제공하고 있다.</p>
<ul>
<li><code>deno init</code>으로 프로젝트를 초기화할 수 있다.</li>
<li><code>deno repl</code>로 REPL을 실행해서 JavaScript나 TypeScript를 실행해 볼 수 있다. 대신 타입 검사는 하지 않는다.</li>
<li><code>deno fmt</code>로 코드를 포매팅한다. 따로 코드 관례를 논의할 필요가 없다.</li>
<li><code>deno lint</code>로 코드를 린트할 수 있다. 사용된 린트 규칙은 <a href="https://lint.deno.land/">문서</a>에서 볼 수 있다.</li>
<li><code>deno test</code>로 테스트를 실행할 수 있다.</li>
</ul>
<p><code>--watch</code> 플래그도 지원해서 <code>deno run</code>이나 <code>deno test</code>, <code>deno fmt</code>등과 같이 사용할 수 있다. 이 외에도 많은 명령어가 있는데 각각은 사용해 보면서 필요하면 또 정리해보도록 하겠다. 더 많은 명령어는 <code>deno -h</code>나 <code>deno help</code>로 볼 수 있다.<br />
<br></p>
<h2>설정 파일</h2>
<p>Deno에는 TypeScript 컴파일러나 포매터, 린터에 옵션을 줄 수 있는 <a href="https://deno.land/manual@v1.25.2/getting_started/configuration_file">설정파일</a>을 지원한다. <code>deno.json</code>이나 <code>deno.jsonc</code>를 자동으로 찾아서 사용한다.<br />
<br></p>
<h2>API</h2>
<p>Deno 1.0.0부터 Deno <a href="https://doc.deno.land/deno/stable/~/Deno">네임스페이스의 API</a>는 Stable 상태가 되었지만 <a href="https://deno.land/std@0.155.0">Deno 표준 라이브러리</a>는 아직 Stable 상태는 아니다.(그렇지만 표준 라이브러리 없이 개발은 못 할 것 같다.)</p>
<p>Deno의 특징을 설명할 때 얘기했던 것처럼 Deno는 <a href="https://developer.mozilla.org/ko/docs/Web/API">Web Platform API</a>를 지원한다. 앞의 예제에서 <code>fetch()</code>를 아무런 import 없이 사용했던 걸 볼 수 있는데 이게 가능했던 이유가 Deno가 Web Platform API를 지원하기 때문이다. 현재 Deno에서 지원하는 Web API는 <a href="https://deno.land/manual@v1.25.2/runtime/web_platform_apis">문서</a>에서 볼 수 있다. 이러한 API는 브라우저에서도 동일한 API라서 이미 사용해 봤다면 익숙한 장점이 있다.<br />
<br></p>
<h1>외부 의존성</h1>
<p>아래는 Deno로 작성한 간단한 HTTP 서버다. Deno에서는 상대 경로로 다른 파일을 임포트할 수도 있지만 아래처럼 URL에서 바로 가져와서 외부 모듈을 사용하게 된다. Node.js에서 npm으로 로컬에 설치한 뒤에 로컬의 파일을 불러오는 것과는 다른 구조이고 아직도 나한테 좀 어색한 형식인데 ES Modules를 따르면서 다른 의존성 도구를 사용하지 않기로 하면서 한 결정인 것 같다.</p>
<pre class="line-numbers"><code class="language-typescript">// 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);
</code></pre>
<p>이 외부 모듈을 사용한 코드를 실행하면 다음과 같이 모듈을 다운로드하는 것을 알 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ 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/
</code></pre>
<p>다운로드받은 모듈은 캐싱되었기 때문에 두 번째 실행 때부터는 다시 다운로드하지 않고 바로 실행한다.</p>
<pre class="line-numbers"><code class="language-bash">$ deno run --allow-net http-server.ts
Listening on http://localhost:8000/
</code></pre>
<p>이는 <code>DENO_DIR</code> 환경변수에 지정된 위치를 캐시 디렉터리로 사용하는데 <code>DENO_DIR</code>를 지정하지 않았다면 다음과 같은 위치를 사용하게 된다.</p>
<ul>
<li>macOS: <code>$HOME/Library/Caches/deno</code></li>
<li>Linux: <code>$XDG_CACHE_HOME/deno</code> 혹은 <code>$HOME/.cache/deno</code></li>
<li>Windows: <code>%LOCALAPPDATA%/deno</code></li>
</ul>
<p>찾기가 어렵다면 <code>deno info</code> 명령어로 현재 사용되는 캐시 디렉터리 위치를 확인할 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ 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
</code></pre>
<p><code>$HOME/Library/Caches/deno</code>의 내용을 보면 다음과 같이 되어 있다.</p>
<pre class="line-numbers"><code class="language-bash">├── 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
</code></pre>
<ul>
<li><code>gen</code> 폴더는 TypeScript를 컴파일한 JavaScript 파일을 보관해 두는 곳으로 파일 변경이 없으면 다시 컴파일하지 않고 캐시 된 파일을 사용한다.</li>
<li><code>deps</code> 폴더는 원격 URL에서 임포트한 파일을 저장해 두는 곳으로 프로토콜에 따라 <code>https</code>와 <code>http</code> 하위 폴더를 가진다.</li>
</ul>
<p>코드를 실행하지 않고 외부 의존성만 캐싱하고 싶다면 <code>deno cache</code>를 실행하면 된다.(아래는 캐시 디렉터리를 삭제 후 다시 실행한 것이다.) 이미 캐시 되어 있는데 다시 불러오고 싶다면 <code>deno cache --reload</code>를 사용하면 된다.</p>
<pre class="line-numbers"><code class="language-bash">$ 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
</code></pre>
<p>여기서 Deno가 TypeScript를 다루는 방식도 알 수가 있는데 앞에서도 얘기했듯이 Deno는 TypeScript를 퍼스트 클래스 언어로 다룬다. Web Assembly를 포함해서 JavaScript도 사용할 수 있지만 Deno를 쓴다면 아마 TypeScript를 사용할 가능성이 높다고 생각한다. JavaScript는 큰 생태계를 가진 Node.js가 있기도 하므로 더욱 그렇다.(내가 Node.js에 익숙해서 그렇게 느낄 수도 있다.) 이 캐시구조에서 보듯이 Deno는 TypeScript를 JavaScript로 변환해서 실행하지만 이를 느낄 수 없도록 사용경험을 제공하고 있다. Deno는 <a href="https://github.com/microsoft/TypeScript">TypeScript 컴파일러</a>와 <a href="https://swc.rs/">SWC</a>를 조합해서 사용하고 있다.<br />
<br></p>
<h2>의존성 관리</h2>
<p>앞에서 보았듯이 Deno는 의존성을 불러올 때 URL을 사용한다. 간단한 예제에서는 문제가 없지만 Deno에는 Node.js와 같은 패키지 관리자가 없기 때문에 프로젝트가 커지면 많은 의존성을 어떻게 관리할 것인가 하는 문제가 생긴다. <a href="https://deno.land/manual@v1.25.2/examples/manage_dependencies">문서</a>에 따르면 이를 해결하려고 <code>deps.ts</code>를 사용하는 것이 일반적이라고 권하고 있다. 이는 패키지 관리자라기보다는 ES Modules를 이용해서 원격 의존성을 하나의 파일에 모아놓은 관례에 불과하긴 하다.</p>
<p>아래 두 파일 <code>deps.ts</code>와 이를 사용하는 <code>uuid.ts</code>를 살펴보자.</p>
<pre class="line-numbers"><code class="language-typescript">// deps.ts
export { v4 } from "https://deno.land/std@0.155.0/uuid/mod.ts";
</code></pre>
<pre class="line-numbers"><code class="language-typescript">// uuid.ts
import { v4 } from "./deps.ts";
console.log(v4.generate());
</code></pre>
<p><code>deps.ts</code>라는 파일에서 외부 의존성을 모아두고 다른 파일은 모두 이 파일을 임포트해서 사용하는 방식이다.</p>
<pre class="line-numbers"><code class="language-bash">$ 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
</code></pre>
<p>당연히 동작은 잘 하지만 아직 큰 프로젝트를 해본 경험이 없음에도 그리 편해 보이진 않는다. 위 예제에서도 <code>uuid</code>에서 <code>v1</code>이나 <code>v5</code>이 필요하다면 버전을 바꾸지 않아도 <code>deps.ts</code>를 수정해야 한다. 익스포트 약간 수정하는 거고 사용하는 값도 명시적으로 지정하니까 크게 문제는 없지만, 그냥 너무 날것의 느낌이 들었다. 그래도 여러 파일에 외부 모듈이 흩어져 있는것 보다는 의존성 관리하기는 좋아 보인다.<br />
<br></p>
<h2>임포트맵</h2>
<p>추가로 Deno는 <a href="https://wicg.io/">Web Incubator CG</a>의 <a href="https://github.com/WICG/import-maps">import-maps</a>를 지원하는데 이는 JavaScript의 <code>import</code>가 가져올 URL을 제어할 수 있게 해주는 프로토콜이다. <code>deps.ts</code> 대신 <code>import_map.json</code>을 다음과 같이 작성할 수 있다.</p>
<pre class="line-numbers"><code class="language-json">{
"imports": {
"http/": "https://deno.land/std@0.155.0/http/"
}
}
</code></pre>
<p>이제 앞에서 처럼 전체 URL을 적는 대신 임포트맵으로 매핑한 <code>http/server.ts</code> 같은 식으로 작성할 수 있다.</p>
<pre class="line-numbers"><code class="language-typescript">// 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/
</code></pre>
<p>아래처럼 상대경로에 대해 매핑을 해서 프로젝트 내 파일을 임포트할 때도 절대 경로로 사용하는 것이 가능하다.</p>
<pre class="line-numbers"><code class="language-json">{
"imports": {
"/": "./",
"./": "./"
}
}
</code></pre>
<p>매번 설정에 넣기가 힘들다면 <code>deno.json</code> 파일에 아래와 같이 임포트맵의 경로를 지정해 줄 수 있다.</p>
<pre class="line-numbers"><code class="language-json">{
"importMap": "import_map.json"
}
</code></pre>
<p>개인적으로는 <code>deps.ts</code>보다는 임포트맵이 더 좋아 보인다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1623?commentInput=true#entry1623WriteComment">댓글 쓰기</a></strong></p>