# deno 첫 외주 후기



최근에 재밌는 외주를 하나 했다. 외주는 여러 번 했지만 이런 종류의 외주는 처음이기도 하고 재미있었기에 후기를 남긴다.

파트타임으로 일하던 것도 있고 이것저것 만드느라 바빠서 한동안 [swc][] 프로젝트에 신경을 못 쓰고 있었다. 그런데 [deno][] 프로젝트 팀한테서 연락이 왔다. [swc]의 crate 중 `swc_ecma_transforms` 를 쓰고 싶은데 stable rustc 에서 컴파일 돼야 deno에서 쓸 수 있으니 그렇게 바꾸는 작업을 나한테 외주 형태로 맡기고 싶다고 했다. 그리고 화상 채팅으로 swc의 설계를 포함해서 이런 저런 얘기를 하다가 외주 비용 지급 방식에 대해 얘기했는데, 좀 특이했다.

## 시급제

기본적으로 시급제였다. 외주인데? 싶었지만 딱히 태클을 걸거나 그러지는 않았다. 시급이 나쁘지는 않았다. 컨설턴트 형태의 외주로 계약을 했고, 당연히 100% 재택근무였다. 시간은 시작할 때 / 끝낼 때 내가 기록하라고 했다. 관련해서 페이스북 생활코딩에 질문도 했었는데 북미에서는 흔한 계약 방식이라고 했다. 시간 재주는 IDE 플러그인을 쓰거나 스탑워치를 이용해서 시간을 잰 뒤 분 단위로 청구한다고 하더라. 난 그렇게까진 하지 않았고 그냥 대충 30분 단위로 반올림했다.

## 예상 기간

처음에 예상 기간을 물어봐서 대충 하루에 10 ~ 12 시간씩 쓴다고 생각하고 10일을 불렀다. 그쪽은 10일 = 80시간 이렇게 생각한 것 같지만 그 대신 내가 얘기한 10일보다 한참 더 걸릴 것이라고 예상했던 것 같다.

## 작업

### 계획

작업을 시작하기 전에 할 일을 생각하며 정리했다. nightly rust 에서만 쓸 수 있는, 다시 말해 내가 제거해야할 것들 중 큼직한 것은 `specialization`, `box_syntax`, `box_patterns` 이렇게 3개였다. `box_syntax`를 제거하는 건 쉽고, `box_patterns`를 제거하는 건 어렵지도 쉽지도 않고,`specialization`을 제거하는 건 매우 어려울 것이라 생각했다. `specialization` -> `box_syntax` -> `box_patterns` 이 순서대로 제거하는 게 가장 빠를 것 같아서 순서를 저렇게 정했다.

### specialization 제거

처음에 `#[feature(specialization)]` 을 제거하니까 에러가 1000개가 **넘었다**. 이딴 짓을 내가 해야 하나 이런 생각이 들었지만 내 프로젝트 쓰고 싶다고 돈까지 준다는데 당연히 해야지... 근데 10일 안에 못 끝낼 것 같이서 Ryan Dahl 에게 '이거 내가 처음에 예상한 것보다 오래 걸릴 것 같다'고 메일을 보냈는데, 그쪽도 오래 걸릴 걸 예상하고 있었는지 계약서에 적힌 한계 바용을 적용하지 않겠다고 했다. 참고로, [swc][]는 러스트 코드만 10만줄이 넘는 작지 않은 프로젝트이고, 코드를 트랜스컴파일하기 위한 코드는 대부분 `swc_ecma_transforms`에 있으므로 작지 않은 작업이었다.

이 작업을 하기 전의 [swc][]는 타입 시스템과 procedural macro를 절묘하게 활용해서 작성해야하는 코드의 양을 획기적으로 줄인 상태였다. [작업 전 swc 소스코드](https://github.com/swc-project/swc/tree/68799d74efc10261c4224bf992cc8db353584c2d)를 보면 알 수 있는데, `swc_common::Fold`, `ast_node::Fold`가 핵심이었다.

```rust
/// Folder based on a type system.
///
/// This trait requires `#![feature(specialization)]`.
pub trait Fold<T> {
    /// By default, this folds fields of `node`
    ///  and reconstruct `node` with folded fields
    fn fold(&mut self, node: T) -> T;
}


/// Trait implemented for types which know how to fold itself.
///
///
/// # Derive
///
/// This trait can be derived with `#[derive(Fold)]`.
pub trait FoldWith<F>: Sized {
    /// This is used by default implementation of `Fold<Self>::fold`.
    fn fold_children(self, f: &mut F) -> Self;

    /// Call `f.fold(self)`.
    ///
    /// This bypasses a type inference bug which is caused by specialization.

    fn fold_with(self, f: &mut F) -> Self
    where
        F: Fold<Self>,
    {
        f.fold(self)
    }
}
```

`swc_common`에 이 2개의 trait이 정의되어 있었고, `FoldWith` 구현은 `#[ast_node]` 매크로에 의해 자동 생성되었다. 이렇게 하면 코드가 매우 간결해진다. 예를 들어 `Span`을 무시해야 할 일이 생기면

```rust

struct DropSpan;

impl Fold<Span> for DropSpan {
    fn fold(&mut self, _: Span) {
        Default::default()
    }
}

module.fold_with(&mut DropSpan)
```

이러면 끝이었다. 유닛 테스트를 하다보면 `Span`을 없애야 할 일이 많이 생기기 때문에, 위에 있는 `DropSpan`은 실제로 `testing` crate에 있었다. 그런데 저 `Fold`와 `FoldWith`는 `specialization`이 반드시 필요했다. 저 2개를 없앤다는 건 `module.fold_with(&mut compiler())`을 `compiler().fold_module(module)` 이런 식으로 바꿔야한다는 걸 의미했다. 컴파일러답게 [swc][]는 `.fold_with()`와 `.fold_children()`로 가득했다. 저 코드를 전부 저렇게 길게 바꾸는 건 좋은 생각이 아닌 것 같아서 고민을 좀 했다. 금방 좋은 생각이 떠올랐다. 그게 뭐냐면 procedural 매크로를 잘 활용하면 `FoldWith`는 거의 기존 방식과 비슷하게 사용할 수 있다는 것이다. 내가 생각해낸 새로운 `Fold`과 `FoldWith`는 다음과 같다.

```rust
// swc_visit 매크로를 이용해서 생성된 trait
pub trait Fold {
    fn fold_module(&mut self, n: Module) -> Module {
        swc_ecma_visit::fold_module(n)
    }

    fn fold_stmt(&mut self, n: Stmt) -> Stmt {
        swc_ecma_visit::fold_script(n)
    }
    // ... 각 ast node 마다 메소드가 있다.
    // 당연하지만 Vec, Option도 처리 가능하다
}

// swc_visit 매크로를 이용해서 생성된 trait
pub trait FoldWith<V> where V: Fold {
    fn fold_with(self, v: &mut V) -> Self;

    fn fold_children_with(self, v: &mut V) -> Self;
}

```

이렇게 정의를 한 다음에, procedural 매크로를 이용해서 아래와 같은 코드를 만든다.

```rust

impl<V> FoldWith<V> for Module where V: Fold {
    fn fold_with(self, v: &mut V) -> Self {
        v.fold_module(self)
    }

    fn fold_children_with(self, v: &mut V) -> Self {
        // swc_ecma_visit::fold* 는 자식 노드를 방문하기 위한 함수이다
        swc_ecma_visit::fold_module(v, self)
    }
}

```

이러한 코드를 많이 생성해 모든 AST 노드가 `FoldWith`를 구현하게 했다. `.fold_with()` 와 `.fold_children()` 문제는 해결되었고, 이제 매크로에서 `Fold<T>`를 구현하는 코드와, 아래 코드처럼 `Fold<T>` 구현에서 `T`가 제너릭인 경우만 처리하면 됐다.

```rust
impl<T> Fold<Vec<T>> for Classes where T: StmtLike {
    fn fold(&mut self, stmts: Vec<T>) -> Vec<T> {
        // .. 구현
    }
}
```

근데 저 경우를 처리하는 건 그리 어렵지 않았다. 저런 코드는 전부 `ModuleItem`과 `Stmt`를 동시에 처리하고 싶은데 귀찮았던 경우라서

```rust

fn fold_stmt_like<T>(&mut self, stmts: Vec<T>) where T: StmtLike;

```

같은 함수를 만들어주고 `Fold`의 `fold_module_items`와 `fold_stmts`에서 `fold_stmt_like`을 호출하는 걸로 충분했다. 제일 오래 걸릴 작업이 순식간에 끝났다.

### box_syntax 제거

에러가 750개 정도 됐던 것으로 기억하는데, `box Expr::Call(call)`을 `Box::new(Expr::Call(call))`으로 바꾸기만 하면 되는 단순 노가다성 작업이었다. 딱히 트릭을 쓸 필요도 없었고, 손목은 좀 아팠지만 그래도 금방 끝냈다.

### box_patterns 제거

에러가 180개 정도였다. 이건 좀 골치 아픈 경우가 있었는데, swc 개발 기간 동안 rust가 match guard에서 변수 사용하는 걸 허용하도록 바뀌었기 때문에 할만했다. 가끔 destructuring 했는데 return하면 안 되는 경우 다시 그 구조체를 만들어줘야하는 경우가 있어서 좀 귀찮기는 했다. 실수로 조건을 바꿔버린 경우가 조금 있는데, 유닛 테스트가 워낙 많아서 테스트에서 걸렸고, 크롬에 기존 코드 띄워놓고 동작이 정확히 같은지 확인하면서 고쳤다.

## 기타

예상보다 너무 빨리 끝났다. 일주일도 안 걸렸는데, 시급제로 계약을 해서 이거 시간 오래 걸린 척 해야하나 이런 생각도 했지만 그냥 빨리 끝났다고 다음엔 시급을 좀 올리면 좋겠다고 하고 작업을 마무리했다. 일 끝나고 돈은 바로 보내줬다. 근데 송금했다는 이메일은 바로 왔는데 실제 입금까지는 일주일 정도 걸린 것 같다.

이건 여담인데 여전히 개발할 떈 nightly를 사용한다. test 모듈이 필요하기도 하고 stable 툴체인에 포함된 `rustfmt`가 지원하지 않는 옵션이 있어서 별다른 수가 없었다. 그냥 하나씩 해보면서 적당히 리눅스, mac os x에서 rustfmt 있는 버전 골라서 개발용 nightly 버전을 고정시켜놨다.

## 그 이후

그리고 다시 다른 외주를 맡기고 싶다고 했는데, 시급을 얼마로 하면 좋을지 제안해보라고 해서 나름 세게 불렀는데 흔쾌히 ok 해서 그렇게 하기로 했다. 근데 이번 것도 지나치게 일찍 끝날 것 같은 느낌이다. 최대 3주짜리 계약이고 오늘이 3일짼데 작업 목록이 반도 안 남았다. 그래도 남은 작업 중에 코드 수정할 때마다 release 빌드를 해야만 결과를 확인할 수 있는 것도 있고, 크롤러를 짜야하는 작업도 있어서 첫번째 외주 때만큼 시간 걱정을 하진 않아도 될 것 같다.

[swc]: https://github.com/swc-project/swc
[deno]: https://deno.land/

