deno 첫 외주 후기

5 min read

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

파트타임으로 일하던 것도 있고 이것저것 만드느라 바빠서 한동안 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 소스코드를 보면 알 수 있는데, swc_common::Fold, ast_node::Fold가 핵심이었다.

/// 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을 무시해야 할 일이 생기면


struct DropSpan;

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

module.fold_with(&mut DropSpan)

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

// 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 매크로를 이용해서 아래와 같은 코드를 만든다.


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가 제너릭인 경우만 처리하면 됐다.

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

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


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

같은 함수를 만들어주고 Foldfold_module_itemsfold_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 빌드를 해야만 결과를 확인할 수 있는 것도 있고, 크롤러를 짜야하는 작업도 있어서 첫번째 외주 때만큼 시간 걱정을 하진 않아도 될 것 같다.