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 소스코드를 보면 알 수 있는데, 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에 있었다. 그런데 저 Fold
와 FoldWith
는 specialization
이 반드시 필요했다. 저 2개를 없앤다는 건 module.fold_with(&mut compiler())
을 compiler().fold_module(module)
이런 식으로 바꿔야한다는 걸 의미했다. 컴파일러답게 swc는 .fold_with()
와 .fold_children()
로 가득했다. 저 코드를 전부 저렇게 길게 바꾸는 건 좋은 생각이 아닌 것 같아서 고민을 좀 했다. 금방 좋은 생각이 떠올랐다. 그게 뭐냐면 procedural 매크로를 잘 활용하면 FoldWith
는 거의 기존 방식과 비슷하게 사용할 수 있다는 것이다. 내가 생각해낸 새로운 Fold
과 FoldWith
는 다음과 같다.
// 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> {
// .. 구현
}
}
근데 저 경우를 처리하는 건 그리 어렵지 않았다. 저런 코드는 전부 ModuleItem
과 Stmt
를 동시에 처리하고 싶은데 귀찮았던 경우라서
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 빌드를 해야만 결과를 확인할 수 있는 것도 있고, 크롤러를 짜야하는 작업도 있어서 첫번째 외주 때만큼 시간 걱정을 하진 않아도 될 것 같다.