Flow vs TypeScript - 왜 나는 아직 타입스크립트를 쓰는가

나는 개인적으로 동적 타입 언어보다 정적 타입 언어를 선호한다. 아니 동적 타입 언어를 싫어한다. 정확히 말하면 동적 타입 언어이기에 생기는 실수에 의한 버그들과 그로 인해 사고에 걸리는 부하를 싫어한다. 그래서 웹 개발을 몇 년을 했지만 여전히 동적 타입 언어인 자바스크립트로 코딩하는 것은 싫어한다. 그 때문에 어지간히 간단한 일이 아니라면, 자바스크립트를 사용할 때 반드시 다른 툴을 붙여 정적분석을 한다.

자바스크립트의 정적 분석을 위해 사용되는 도구는 크게 2가지로 나뉜다. 마이크로소프트에서 만든 타입스크립트와 페이스북에서 만든 플로우다. 한때는 구글이 타입스크립트를 기반으로 만든 AtScript라는 것도 있었지만, 이는 다시 TypeScript로 합쳐지면서 사라지게 되었다.

타입스크립트와 플로우는 둘 다 자바스크립트를 정적 타입 검사가 가능하도록 만들어주었다. 하지만 접근하는 방향은 완전 다른 방향으로 접근하였다. 우선 타입스크립트는 2012년에 첫 버전이 나왔다. 타입스크립트의 목표는 자바스크립트로 변환되는 더 쓰기 편하고 안전한 언어를 만드는 것이었다. 어디까지나 자바스크립트로 변환되는 언어를 만드는 것이었기에 문법적 기반을 자바스크립트에 두었다. 다시 말해서 타입스크립트의 문법은 자바스크립트 문법의 슈퍼 셋이다. 자바스크립트의 문법을 기반으로 하여 그 위에 추가적인 기능을 더했다. 추가된 기능에는 classenum처럼 사용성을 올리기 위한 기능도 있고, private이나 타입 어노테이션 처럼 안전한 코드를 만들기 위해 추가된 기능도 있다.

반면에 페이스북의 플로우는 새로운 언어를 정의하지 않는다. 플로우는 ECMAScript 6(a.k.a. ES6)의 타입 검증 도구일 뿐이다. 이미 타입스크립트의 많은 기능이 ES6에 표준으로 들어왔기 때문에 새로운 언어를 만들 필요가 없었다. 플로우는 새로운 언어를 정의하지 않기 때문에 이미 자바스크립트로 작성된 프로그램을 바로 분석할 수 있다. 타입이 모호한 경우에는 어노테이션을 추가하여 타입을 선언할 수도 있다. 이런 경우 바벨 등을 이용해서 어노테이션을 없애야 올바른 자바스크립트 코드가 되는 불편함이 있었지만, 0.4.0 이후부터는 어노테이션을 주석으로 처리할 수도 있다.

2016년인 지금은 타입스크립트의 기능 대부분이 ES6에 추가되었다. 오히려 지금은 타입스크립트가 ES6나 ES7의 기능을 타입스크립트로 가지고 오고 있다. 게다가 ES6를 ES5로 변환시켜주는 바벨이 널리 사용되고 있고, 플로우가 ES6의 중요한 기능들은 대부분 처리할 수 있다.1) 따라서 더는 ES6 + 플로우를 사용하는 것에 비해 타입스크립트를 사용해서 얻는 장점은 없다.

게다가 플로우가 타입 추론을 더 잘해준다. 물론 타입스크립트도 타입 추론을 한다. 하지만 함수형 언어들이 많이 사용하는 힌들리-밀러 타입 시스템처럼 추론이 강력하지 않다. 변수를 선언하는 시점에서 타입을 추론할 수 있어야 한다. 추론할 수 없는 변수는 any 타입으로 선언된다. 반면에 플로우는 그 값이 사용되는 부분의 타입까지 고려하여 타입을 추론해준다. 그를 보여주는 좋은 예가 아래의 코드이다.

위와 같이 함수를 선언한 경우, 플로우에서는 코드를 사용하는 부분에서 타입 에러를 검사한다. 따라서 length 함수의 인자로 numberboolean을 넣으면, numberbooleanx가 되기 적절한지, 다시 말해 length 프로퍼티를 가지는지를 확인하기 때문에 잘못된 타입을 사용했다는 것을 알 수 있다.

하지만 타입스크립트는 length 함수를 선언하는 순간 이 함수의 타입이 any => any로 결정되기 때문에 length 프로퍼티가 없는 값을 넘겨도 타입 에러인지 알 수 없다. 이런 경우를 막기 위해 타입스크립트에서는 함수를 선언할 때 타입 어노테이션을 붙이는 것을 추천한다. 즉, 위의 length 함수는 아래와 같이 선언하여 사용하는 것이 좋다.

지금까지 말한 것만 보면 ES6가 나온 지금은 플로우를 쓰는 것이 더 올바른 방식으로 보인다. 하지만 그럼에도 나는 아직 타입스크립트를 사용한다. 단순히 쓰던 것이 익숙해서가 아니다. 그저 플로우가 아직 미완성이기 때문이다. 플로우가 미완성이라는 것은 단순히 아직 1.0이 나오지 않았다는 것만을 말하지 않는다. 플로우는 사용하는 데 중요한 몇 가지 부분이 아직 미완성이다.

우선 기본 API에 대한 타입 정의가 완전하지 못하다. 우선 DOM API의 타입 선언이 어떻게 되었는지 보자. 타입스크립트는 13,813줄로 선언되어 있지만 플로우의 경우 1,306줄의 파일로 선언되어 있다. bom.jscssdom.js를 합쳐도 2천 줄밖에 되지 않는다. ES6와 관련된 타입은 플로우는 614줄밖에 정의되지 않지만, 타입스크립트는 5,174줄로 정의되어 있다. node.js API는 타입스크립트의 타겟이 아니라서 DefinitelyTyped에 정의된 선언을 사용해야 해서 공정한 게임은 아니지만, 이 경우에도 플로우보다 타입스크립트가 더 상세하게 타입을 정의하고 있다. 플로우도 해당 이슈를 알고 있고 해결하려고 하지만 아직 해결되지 않았고, 내가 보기에 1.0이 나오기 전에 해결되면 다행일 것으로 보인다.

게다가 아직 써드파티 라이브러리 지원이 완전하지 않다. 플로우는 타입 어노테이션이 없는 함수도 타입을 추론해준다. 따라서 플로우의 타입어노테이션을 사용하지 않은 라이브러리를 사용해도 플로우가 자동으로 타입 추론을 해준다. 하지만 자바스크립트처럼 오버로딩이 섞여 있는 언어를 분석하여 추론하는 경우 타입 추론은 쉽게 틀린다. 그래서 멀쩡한 라이브러리를 타입 에러라고 검사하기도 하고, 라이브러리를 잘못 사용한 경우를 잡지 못하기도 한다. 이런 경우는 라이브러리의 타입을 선언해놓은 인터페이스 파일을 사용하면 된다. 하지만 아직 플로우의 인터페이스 파일은 만들어진 것을 찾기 힘들어서 대부분의 경우 직접 만들어서 써야 한다. 2012년부터 기록을 쌓아서 이제는 1천 개가 넘는 라이브러리의 타입선언을 보유하고 있는 타입스크립트와는 상대되지 않는다. 이에 대해서 타입스크립트의 타입선언을 읽을 수 있게 하는 방향으로 진행되고 있지만, 아직 완성되지 않았다.

마지막 문제는 타입 추론의 완성도에 대한 문제다. 위의 자바스크립트에서 많이 사용되는 object에 프로퍼티를 추가하는 코드이다. 새로운 프로퍼티를 추가하는 것이기 때문에 문제없어야 할 것 같지만, 플로우는 이를 { bar: number } 타입으로 추론하기 때문에 해당 코드를 타입 에러로 처리한다.

위의 코드는 정반대의 경우라서 문제가 되는 경우이다. {}는 어떤 값이라도 키로 받을 수 있도록 해석하기 때문에 실제로는 obj.foo가 nullable한 ?number 타입이 되어야 하지만 number 타입으로 받아도 타입 에러라고 검사하지 못한다.

다음 문제는 더 심각하다. 플로우는 리터럴 한 값 그 자체를 타입으로 사용할 수 있다. 이를 이용해서 쉽게 다른 언어의 enum에 해당하는 타입을 만들거나 tagged union을 만들 수 있다. 이는 자바스크립트의 관습적인 사용법에 맞기 때문에 enum타입을 새로 추가한 타입스크립트에 비해서 올바른 접근이라고 생각한다. 하지만 이에 대한 타입 추론이 완벽하지 않다는 것이 문제다.

위와 같은 코드를 보면 인자로 올 수 있는 "red", "green", "blue"를 전부 처리하였기 때문에 모든 경우를 처리한 것이다. 하지만 플로우는 모든 경우를 처리하였다고 생각하지 않고, undefined가 반환될 수 있다는 에러를 출력한다.

더 심각한 문제는 위와 같은 경우다. 위의 코드의 무엇이 잘못되었을까? case 문에서 "green"의 스펠링을 "greeen"으로 잘못 사용하였다. color 타입은 "red", "green", "blue"만 가능하므로 이는 있을 수 없는 경우다. 하지만 플로우는 이것을 타입 에러로 검사해주지 못한다. 심지어 작년 10월에 리포트되었는데 아직 아무런 피드백조차 없다.

이런 예제들은 하나하나 들자면 끝이 없다. 하나하나는 사소한 사례로 보일 수 있지만, 타입 추론이 핵심인 툴에서 타입 추론을 못 하는 부분이 있다는 것은 플로우에 대한 신뢰도와 사용성을 떨어뜨린다. 그렇다고 앞으로 계속 타입스크립트만을 사용하겠다는 것은 아니다. 앞에서도 말했듯이 설계 자체만을 놓고 보면 더는 타입스크립트가 ES6 + 플로우에비해 얻는 이득은 없다. 따라서 플로우가 기본적인 도구들을 갖추고, 많이 사용되는 패턴들에 대해 안전하게 타입 추론을 하게 된다면 플로우로 넘어갈 것이다. 일단 지금은 이것들이 1.0 릴리즈에는 해결될 것으로 보고 기다리고 있다.


1) ES6의 let/const를 플로우가 처리할 수 있게된게 2015년 9월의 일이다. 2014년 11월 플로우가 처음 공개된 이후 거의 1년 가까이 플로우는 ES6의 기본적인 기능인 스코프 단위 변수 할당을 하지 못했던 것이다.

댓글

이 블로그의 인기 게시물

[C++] enum class - 안전하고 쓰기 쉬운 enum

Log Aggregator 비교 - Scribe, Flume, Fluentd, logstash

RAII는 무엇인가

[Python] cache 데코레이터로 최적화하기

[Web] SpeechSynthesis - TTS API