ghlee.dev

my profile picture

Deno의 내부 속으로

2024. 8. 25.

Deno는 널리 알려진 Node.js, Chromium과 같은 JavaScript 런타임으로 알려져있으며 2018년 JSConf Eu에서 Node.js의 창시자인 Ryan Dahl에 의해 처음으로 세상에 공개되었다.

Deno는 Node의 애너그램이다.

이 글을 작성하는 시점으로는 1.45.5 버전이 릴리즈되어 있는데 비록 Node.js에 비하면 점유율이 높지 않지만 서버 측 JavaScript 런타임의 트렌드를 이끌고 있다 해도 과언이 아니다.

Ryan Dahl이 컨퍼런스에 나와서 Node.js를 만들고 난 후에 후회한 것을 몇 가지 이야기했는데 그중 하나는 권한 모델이 없다는 것이었다. Deno에는 파일 및 네트워크 접근 같은 중요한 권한을 설정할 수 있는 플래그(Flag)가 존재하며 중요한 기능의 모든 권한은 기본적으로 '접근 거부'로 설정되어 있다. Node.js에는 이러한 권한 모델이 존재하지 않았으나 버전 20.x 이후로 권한 모델을 새롭게 도입하였다.

또한 Deno는 Rust로 작성된 SWC 컴파일러를 활용하여 기본적으로 TypeScript를 지원하고 있었는데, 이 글을 쓰고 있는 시점을 기준으로 Node.js에도 실험 기능에 SWC 컴파일러가 포함되면서 TypeScript를 지원하려는 모습을 보이고 있다. 이렇듯 다양한 면에서 Node.js가 Deno의 장점을 흡수하는 듯한 정황(?)이 속속 드러나고 있다.

여담으로 Ryan Dahl은 Node.js가 패키지 매니저인 npm에 사실상 종속되어 있다는 정치적인(?) 문제도 간접적으로 지적했다.

(2024.08 기준) 실제로 Node.js의 Corepack을 기본 기능으로 활성화하자는 논의가 있었는데 사실상 npm 측의 강력한 반대로 무산되는 분위기이다. 이를 보더라도 Node.js에 대한 npm의 영향력은 무시할 수 없어 보인다.

내가 보기에 Deno는 단순히 JavaScript 런타임 환경일 뿐 아니라 TypeScript 툴체인(Tool Chain)으로서 면모를 보인다. 이 점에 대해서는 길게 말하기 보다 스크린샷이 이해하는데 더 도움이 될 듯하다.

deno cheatsheet 1

deno cheatsheet 2

Source: https://docs.deno.com/runtime/manual/node/cheatsheet/

JavaScript의 개발 환경을 이해하고 있는 사람이라면, 개발을 시작하기 위해 여러 가지 설치할 필요 없이 Deno 만으로 대부분을 해결할 수 있다는 것에 놀랄 것이다. 물론 저 기능 중에는 아직 프로덕션 레벨에서 사용하기 힘든 도구도 있을 수 있지만, 적어도 Deno가 파편화되어있는 현재의 JavaScript 개발 환경을 개선하는 것에 열정적인 모습을 볼 수 있다.

여담으로 비교적 현대적인 언어인 Go와 Rust는 이러한 툴체인이 상당히 편리하다. 그리고 Deno 개발 멤버들의 SNS와 컨퍼런스에서의 언급을 미루어볼 때 실제로 두 언어의 영향을 받았을 것이라 짐작해 볼 수 있다.

서론이 길었다. 이 글은 Deno의 사용 방법을 이야기하는 글은 아니고 제목처럼 Deno의 내부를 파헤쳐 보고자 한다. Deno는 다른 런타임에 비해 상대적으로 파헤치기 쉬운 편이다. 왜냐하면 코드는 마이크로 아키텍처로 분리되어 Github에 친절하게 공개되어 있으며, 유튜브 검색으로 핵심 개발 멤버가 Deno 내부에 대해 직접 설명하는 것을 찾아볼 수 있기 때문이다.

들어가기 앞서 이 글을 이해하기 위해서는 기본적으로 JavaScript 개발 환경에 대한 이해가 있어야 한다. 또한 Deno의 핵심 코드는 Rust로 작성되어 있으므로 이에 대한 이해가 어느 정도 필요하다. Deno의 기본적인 특징과 사용 방법은 따로 설명하지 않으니 공식 문서를 읽어보기를 권한다.

Deno의 핵심 요소

모놀리식 아키텍처인 Node.js와 달리 Deno는 마이크로 아키텍처로 설계되어 있으며 핵심 기능 별로 크레이트(Crate)를 만들어 리포지토리(Repository)를 구분해 놓았다. 또한 하나의 리포지토리 안에서도 기능 별로 또다시 크레이트가 구분되어 있다.

Rust의 크레이트(Crate)는 JavaScript에서 하나의 모듈(Module) 혹은 라이브러리, 컴포넌트(Component) 정도로 이해하면 편하다.

deno architecture

Deno의 아키텍처를 대략적으로 그려보았다.

Deno에서 작성한 다양한 크레이트가 있지만, 내가 생각하기에 Deno를 이해하는 데에 필요한 주요 크레이트 혹은 컴포넌트 몇 가지를 짧게 소개하면 아래와 같다.

Cli(Command Line Interface)

Deno의 커맨드로 접근할 수 있는 인터페이스이며 터미널에서 deno <command>로 실행하는 익숙한 그 녀석이다. 대부분의 기능을 사용자에게 제공하는 게이트웨이 역할을 담당한다.

Permissions

Deno의 권한 모델을 담당한다. 플래그와 함께 파일 및 네트워크 접근과 같은 중요한 접근 권한을 허용하거나 차단한다. 두 가지 타입의 권한이 있는데 bool 타입으로 허용과 차단만 있는 권한과 Vector 자료구조를 이용하여 한 개 혹은 복수의 String을 허용하거나 차단하는 식의 권한이 있다.

FileFetcher

Deno는 다양한 경로의 모듈을 지원하는데 HTTP, Local 모듈, NPM 또는 JSR를 통해 모듈에 접근할 수 있다. FileFetcher는 어떤 경로인지 상관없이 모듈에 접근하여 코드를 가져오는 역할을 담당하는 구조체이다.

JsRuntime

JavaScript 코드를 읽고 실행하는데 핵심이 되는 구조체(Struct)이다. 초기화할 때 ModuleLoaderExtensions등 런타임을 실행하는데 필요한 요소를 인자로 받는다.

ModuleLoader

ES Module을 처리하는데 중요한 트레이트(Trait)이다. 소스 코드로부터 모듈을 가져오고 필요하다면 컴파일을 진행하며 최적화를 위해 캐싱 할 수도 있다. JsRuntime 코드를 초기화(Init) 할 때 필요한 로더(Loader)를 인자로 넘겨주어야 한다.

Ext(Extensions)

WHATWG와 W3C에서 발표하는 Web API와 Deno 네임스페이스로 접근하는 자체적인 API를 구현한다. ops 매크로와 함께 다양한 API를 구현하고 있으며 실제 코드는 JavaScript와 Rust가 함께 구성되어 있다.

rusty_v8 & V8

Deno의 핵심 코드는 Rust로 작성되어 있으며 C++로 작성된 V8을 활용하려면 서로 다른 언어 간의 호환이 필요하다. rusty_v8은 Rust의 FFI(Foreign Function Interface)를 이용하여 V8 API에 대한 Rust 바인딩(Binding) 함수를 제공한다.

V8은 Google에서 개발한 JavaScript 엔진으로 JavaScript 코드를 바이트 코드로 변환 후 평가(Evaluate) 하거나 필요하다면 최적화 및 기계어 컴파일을 담당한다. 앞서 이야기했듯 C++로 작성되어 있다.

Tokio

서드 파티 크레이트로 Deno의 이벤트 루프(Event Loop) 및 비동기 처리를 담당한다. Rust 생태계의 비동기 라이브러리에서 가장 영향력 있는 크레이트이며 Node.js의 libuv와 유사한 역할이다.

SWC

서드 파티 크레이트로 TypeScript를 JavaScript로 변환하는 컴파일러이다. Rust로 작성되었으며 컴파일 속도가 빠르기로 유명하다. Deno와 Next.js, Node.js 등 다양한 프로덕트에서 사용되고 있다.

여담으로 SWC은 한국의 엔지니어가 개발한 컴파일러로 국내에선 더욱 화제가 되었었다.

Deno의 모든 요소를 설명하는 것은 글 하나로는 지면이 부족할뿐더러 나 또한 모든 코드를 이해하고 있지 않기 때문에 사실상 불가능하다. 그래서 매우 중요한 요소만 몇 가지만 이야기해 볼 생각이다. 우선 JavaScript 런타임에서 가장 중요한 요소인 엔진에 해당하는 rusty_v8부터 알아가 보자.

rusty_v8

Deno의 핵심 코드는 Rust로 작성되어 있다고 이야기했었다. 그리고 Deno는 C++로 작성된 V8 엔진을 사용한다. 그렇다면 서로 다른 언어로 작성된 둘은 어떻게 소통하는 것일까? 답은 rusty_v8에 있다. rusty_v8은 Deno에서 자체적으로 작성한 크레이트로 Github 리포지토리도 별도로 분리되어 있다.

Rust에는 C와 C++를 호출할 수 있는 FFI를 제공하는데 extern 키워드를 사용하여 다른 언어로 된 코드의 함수 시그니처를 정의할 수 있으며 호출할 때는 unsafe 블록 내부에서 호출해야 한다. FFI(Foreign Function Interface)는 어느 한 언어에서 다른 언어 코드를 호출하기 위한 인터페이스이며 unsafe 는 Rust 컴파일러에게 그 블록의 코드는 일부 컴파일러 검사를 생략하라는 명령과 같다.

rusty_v8은 FFI를 적극 활용하여 C++로 작성된 V8의 함수를 바인딩 하여 Rust 함수로 제공한다.

Rust
/// https://github.com/denoland/rusty_v8/blob/main/src/string.rs#L43
extern "C" {
	fn v8__String__Length(this: *const String) -> int;
}

rusty_v8에 실제 있는 간단한 코드를 보자. extern 키워드와 함께 "C"라고 선언하여 C 코드임을 Rust 컴파일러에게 알려주며 실제로 사용할 C 코드의 함수 시그니처를 선언한다.

extern 키워드에 선언해 준 함수를 호출하려면 아래 코드처럼 C 코드 파일에 v8__String__Length 함수가 실제로 있어야 한다.

C++
// https://github.com/denoland/rusty_v8/blob/main/src/binding.cc#L1051
int v8__String__Length(const v8::String& self) {
	return self.Length();
}

참고로 extern 블록에 있는 String은 V8 엔진에서 제공하는 문자열 타입으로 ECMA-262 규격의 JavaScript String을 C++로 구현한 것이다. extern 키워드에 있는 String과 C++ 코드의 v8::String은 동일한 타입으로 보면 된다.

src/binding.cc 파일은 Deno에서 작성한 코드로 V8에서 실제로 필요한 함수만을 가져와서 한번 래핑(Wrapping)하여 모아놓았다.

v8__String__Length 함수를 실제로 호출하는 코드는 아래와 같다.

Rust
/// https://github.com/denoland/rusty_v8/blob/main/src/data.rs#L1570
pub struct String(Opaque);
Rust
/// https://github.com/denoland/rusty_v8/blob/main/src/string.rs#L409
impl String {
	pub fn length(&self) -> usize {
		unsafe { v8__String__Length(self) as usize }
	}
}

V8이나 binding.cc 등 외부 C++ 코드는 링크(Link) 단계에서 컴파일된 Rust 코드와 결합된다.

Rust의 컴파일 과정에 대해 짧게 설명하면, 우리가 작성한 Rust 코드는 Rust 컴파일러(rustc)에 의해 두 가지 중간 표현(HIR과 MIR)을 거쳐서 LLVM IR로 변환된다. 이때부터는 LLVM 컴파일러가 관할하는 영역으로 들어가게 되는데, LLVM IR은 최종적으로 기계어 코드 및 오브젝트 파일로 변환되어 링크 단계에서 외부 C++ 코드와 결합된다. 참고로 링크 단계에서 외부 코드와 결합되는 과정이 궁금하다면 오브젝트 파일의 구조에 대해 분석해보면 알 수 있다.

rusty_v8이 Rust 코드와 외부 C++ 코드(V8 등)를 결합하는 과정은 루트 경로의 build.rs를 살펴보면 자세히 알 수 있다. 이 파일에는 V8을 컴파일하는 과정도 포함되어 있는데 V8은 컴파일 자체로 과정이 매우 복잡하고 컴파일 시간만 30분 넘게 소요된다. 그래서 Deno에서는 rusty_v8의 사용자를 위해 V8을 사전에 컴파일한 소스코드를 Github의 releases에 게시하고 있으며 cargo build할 때 기본적으로 다운로드하게끔 되어있다.

이번엔 다른 바인딩을 살펴보자.

C++
// https://github.com/denoland/rusty_v8/blob/main/src/binding.cc#L1789
const v8::Number* v8__Number__New(v8::Isolate* isolate, double value) {
	return *v8::Number::New(isolate, value);
}
Rust
/// https://github.com/denoland/rusty_v8/blob/main/src/number.rs
extern "C" {
	fn v8__Number__New(isolate: *mut Isolate, value: f64) -> *const Number;
}
 
impl Number {
	#[inline(always)]
	pub fn new<'s>(
		scope: &mut HandleScope<'s, ()>,
		value: f64,
	) -> Local<'s, Number> {
		unsafe {
			scope.cast_local(|sd| v8__Number__New(sd.get_isolate_ptr(), value))
		}
		.unwrap()
	}
}

JavaScript의 Number 객체를 구현한 코드이다. MDN 문서를 보면 JavaScript의 number 리터럴(Literal)은 부동소수점이라는 설명을 볼 수 있는데, Rust의 Number 생성자(new) 함수를 보면 MDN의 설명대로 value 값으로 64비트 부동소수점을 받고 있는 것을 확인할 수 있다.

또한 value와 함께 scope를 매개변수(Parameter)로 받고 있는 것을 볼 수 있는데, 현재 실행하고 있는 컨텍스트(Context)를 이야기할 때 나오는 그 스코프(Scope)가 맞다. 이는 String, Object, Symbol 등 다른 리터럴에서도 마찬가지이며, 각각의 리터럴은 자기 자신이 위치한 스코프에 대한 정보를 가지고 있다. Rust 바인딩을 통해 아주 조금이지만 V8의 구조에 대해 엿볼 수 있다.

JavaScript에서 자주 사용하는 JSON 직렬화(Serialize) 함수도 존재한다.

C++
// https://github.com/denoland/rusty_v8/blob/main/src/binding.cc#L2659
const v8::Value* v8__JSON__Parse(const v8::Context& context, const v8::String& json_string) {
	return maybe_local_to_ptr(v8::JSON::Parse(ptr_to_local(&context), ptr_to_local(&json_string)));
}
Rust
/// https://github.com/denoland/rusty_v8/blob/main/src/json.rs
extern "C" {
	fn v8__JSON__Parse(context: *const Context, json_string: *const String,) -> *const Value;
}
 
pub fn parse<'s>(scope: &mut HandleScope<'s>, json_string: Local<'_, String>,) -> Option<Local<'s, Value>> {
	unsafe {
		scope
		.cast_local(|sd| v8__JSON__Parse(sd.get_current_context(), &*json_string))
	}
}

이 코드는 직접 한번 살펴보기를 바란다.

여담으로 Deno는 rusty_v8 이전에 libdeno라고 하는 C++ 라이브러리를 통해서 V8 엔진과 Rust 코드와 연결하고 있었다. C++로 작성된 libdeno를 사용할 때는 추가적인 기능 개발에 리소스가 많이 들어갔다는 언급을 보면 Deno의 코드 베이스가 Rust인 만큼 C++ 코드는 가급적 최소화하고 Rust의 비중을 높인 rusty_v8 크레이트를 만들게 된 것인가하고 재미 삼아 추측해 본다.

deno_core

다음 중요한 요소로 deno_core 레포지토리를 살펴보자. 굳이 레포지토리로 표현한 이유는 deno_core 하나에 여러 크레이트가 존재하기 때문인데, 대표적으로 core(JsRuntime)와 ops, serde_v8이 있다. 우선 ops부터 살펴보자.

ops

JavaScript 런타임을 사용하는 가장 큰 이유는 무엇일까? V8 같은 JavaScript 엔진이 할 수 없는 기능을 수행할 수 있기 때문이라고 생각한다.

예를 들어 현대적인 브라우저에서는 window 객체의 API를 통해 브라우저 내의 동작을 제어할 수 있으며 Node.js와 Deno에서는 globalThis 객체를 통해서 운영체제의 파일 시스템에 접근하거나 TCP 및 HTTP 통신을 가능하게 한다. 이는 JavaScript 엔진만 있어서는 불가능하며 엔진 바깥의 함수를 실행할 수 있도록 무언가를 제공해야 하는데 이에 해당하는 것이 바로 ops이다.

ops는 V8이 제공하는 ECMAScript 스펙을 넘어 Deno의 기능을 확장하기 위해 사용된다. 예를 들어 Deno.envDeno.readFileSync, Deno.connect 같은 Deno의 API를 예시로 볼 수 있다. 브라우저에서 볼 수 있는 localStorage처럼 Web API에 해당하는 것도 있고 Deno.dlopen처럼 Deno만의 API도 있다. 심지어 호환을 위해 Node.js의 API도 일부 제공한다.

ops에는 op2라는 Rust의 속성 매크로(Attribute Macros)가 있다. 이는 V8에 건네주기 위한 오퍼레이션(Operation) 정의에 필요한 코드를 만들어준다. op2 매크로는 extension!라는 선언형 매크로(Declarative Macro)와 함께 사용되는데 op2 매크로를 사용하여 만든 오퍼레이션 함수를 extension! 매크로의 인자로 건네준다.

실제 사용하는 예제를 보자.

Rust
/// https://github.com/denoland/deno_core/blob/main/core/examples/op2.rs
 
#[op2]
fn op_use_state(
	state: &mut OpState,
	callback: v8::Global<v8::Function>,
) -> Result<(), Error> {
	state.put(callback);
	Ok(())
}
 
extension!(
	op2_sample,
	ops = [op_use_state],
);
 
fn main() {
	let mut js_runtime = JsRuntime::new(deno_core::RuntimeOptions {
		module_loader: Some(Rc::new(FsModuleLoader)),
		extensions: vec![op2_sample::init_ops_and_esm()],
		..Default::default()
	});
}

링크로 들어가서 실제 코드를 보면 살짝 다르다. 설명하는데 불필요한 코드는 생략했다.

이 코드는 Deno에서 직접 작성한 예제 코드이다. #[op2] 매크로를 이용해서 op_use_state라는 함수를 정의하고 있는데 이를 배열에 넣어서 extension! 매크로의 ops 인자에 건네주고 있다. 또한 이렇게 만든 매크로를 통해 만든 Extension 구조체를 Vector에 담아서 JsRuntime 구조체를 구성할 때 인자로 사용한다.

또한 Rust로 작성한 op_use_state를 Deno 런타임 환경에서 실행하려면 JavaScript의 전역 컨텍스트에 정의해야 한다. 아래 코드는 정말 간단한 예시이다.

JavaScript
// https://github.com/denoland/deno_core/blob/main/core/examples/op2.js
globalThis.op2_sample = {
  use_state: (f) => Deno.core.ops.op_use_state(f),
};

사실 Deno에서 실제로 정의할 때는 이렇게 단순하지 않다. Deno의 핵심 코드는 Rust로 작성되었지만 실제로는 JavaScript 코드량도 상당한데, 그 이유는 WebAPI와 Deno API 같은 V8 바깥의 함수를 정의해 주기 위함이다.

런타임 실행 준비 시에만 사용되는 globalThis.__bootstrap 같은 JavaScript 객체부터 Deno 네임스페이스(Namespace)에 숨겨진 내부 함수도 존재한다. 실제로 Deno[Deno.internal].core로 접근할 수 있지만 Deno에서 타입 정의를 제공하지 않으므로 접근은 가능하지만 타입 에러가 발생한다.

여담으로 Deno 팀에서 2021년 초에 WebGPU API를 처음 도입할 때 JavaScript 번들이 너무 커져서 런타임 실행이 늦어지는 이슈가 있었다. 그래서 WebGPU API를 삭제하고 해당 이슈를 해결한 후 2023년 12월 재도입하였는데, 이러한 점에서 볼 때 JavaScript 번들이 Deno에 미치는 영향은 Rust로 작성된 코드만큼이나 상당하다고 추측해 볼 수 있다.

WebGPU is back

이제 이렇게 정의한 JavaScript 코드를 JsRuntime에 넘겨주어야 하는데 extension! 매크로를 사용할 때 인자로 넘긴 후 JsRuntime 방법과 JsRuntime에서 제공하는 함수를 사용하는 방법이 있다.

실제로 Deno에서 Fetch API를 정의하는 extension! 매크로는 다음과 같다.

Rust
/// https://github.com/denoland/deno/blob/main/ext/fetch/lib.rs#L124
deno_core::extension!(
	deno_fetch,
	deps = [ deno_webidl, deno_web, deno_url, deno_console ],
	parameters = [FP: FetchPermissions],
	ops = [
		op_fetch<FP>,
		op_fetch_send,
		op_fetch_response_upgrade,
		op_utf8_to_byte_string,
		op_fetch_custom_client<FP>,
	],
	esm = [
		"20_headers.js",
		"21_formdata.js",
		"22_body.js",
		"22_http_client.js",
		"23_request.js",
		"23_response.js",
		"26_fetch.js",
		"27_eventsource.js"
	],
	options = {
		options: Options,
	},
	state = |state, options| {
		state.put::<Options>(options.options);
	},
);

EcmaScript Module을 의미하는 esm인자로 JavaScript 파일을 넘겨주는 것을 확인해 볼 수 있다.

아까 #[op2]를 사용했던 예제에서 JsRuntime을 초기화하는 코드를 다시 보자.

Title
fn main() {
	let mut js_runtime = JsRuntime::new(deno_core::RuntimeOptions {
		module_loader: Some(Rc::new(FsModuleLoader)),
		extensions: vec![op2_sample::init_ops_and_esm()],
		..Default::default()
	});
}

코드를 자세히 보면 RuntimeOptions 구조체를 통해 우리가 매크로를 이용하여 선언한 Extension 구조체인 op2_sample을 넘겨주고 있다. 이렇게 넘겨진 op2_sample은 다양한 처리를 거쳐서 최종적으로 V8 엔진이 참조하는 외부 요소로서 사용된다. 아래는 그 내용에 대한 코드의 일부이다.

Rust
/// https://github.com/denoland/deno_core/blob/main/core/runtime/jsruntime.rs#L775
impl JsRuntime {
	fn new_inner(
		mut options: RuntimeOptions,
		will_snapshot: bool,
	) -> Result<JsRuntime, Error> {
	let init_mode = InitMode::from_options(&options);
	let mut extensions = std::mem::take(&mut options.extensions);
	let mut isolate_allocations = IsolateAllocations::default();
 
	(중략)
 
	isolate_allocations.external_refs =
		Some(Box::new(bindings::create_external_references(
			&op_ctxs,
			&additional_references,
			&isolate_allocations.externalized_sources,
			ops_in_snapshot,
			sources_in_snapshot,
		)));
	let external_refs: &v8::ExternalReferences = isolate_allocations.external_refs.as_ref().unwrap();
	let external_refs_static = unsafe { &*(external_refs as *const _) };
 
	let mut isolate = setup::create_isolate(
			will_snapshot,
			options.create_params.take(),
			maybe_startup_snapshot,
			external_refs_static,
		);
	}
 
	(중략)
 
	Ok(js_runtime)
}

코드 중간 부분을 보면 create_external_references 함수에 op_ctxs를 참조로 건네주고 있는 것이 보이는가? 우리가 매크로를 이용하여 만들었던 오퍼레이션 함수들은 최종적으로 op_ctxs를 통해 V8 엔진에 전달된다.

또한 isolate는 V8의 격리된 환경 또는 인스턴스를 의미한다. 크롬 브라우저에는 여러 개의 탭(Tab)을 만들 수 있는 기능이 있지 않은가? 그 탭들은 각각의 isolate를 가지고 있다. 당연하게도 isolate은 격리 환경이기 때문에 사용자가 실행한 JavaScript 코드를 기본적으로 서로 공유할 수 없다. 다만 크롬이나 Deno에서 지원하는 Broadcast Channel API를 이용하면 제한적으로 코드를 공유할 수 있게 된다.

그리고 우리가 본 예제를 응용하면 각각의 isolate마다 다른 API를 지원하도록 만들어볼 수도 있다. 브라우저로 예를 들면 각각의 탭마다 지원하는 API가 다른 셈이다. 실제로 Deno에서는 실행 시 받는 플래그를 통해 지원되는 API를 변경할 수 있다.

Bash
$ deno run --unstable-webgpu main.ts

To be continued...

이 글의 초반부는 Deno에 관한 히스토리와 전반적인 구성 요소에 대해 이야기했다. 그런 다음 rusty_v8부터 deno_core의 내부에 있는 ops까지 우리는 크게 두 요소에 대해 살펴보았다. 사실 deno_core를 더 살펴보고 난 후 커맨드 라인 인터페이스인 deno_cli까지도 다루어야 하고 서드 파티 크레이트인 TokioSWC도 설명해야 하는데, 글이 길어지는 바람에 여기서 끊고 시리즈물(?)로 작성해야 할 것 같다. 아직 할 이야기가 정말 많이 남아있다.

우리가 살펴보았던 두 요소를 한 줄 요약하면서 이 글을 마무리하겠다.

  • rusty_v8: Rust 바인딩 함수로 사용하는 V8 엔진
  • ops: Web API와 Deno API, Node.js API를 구현한 코드