자바스크립트에 타입 입히기
Typescript 승
자바스크립트의 타입 없는 자유로움에서 비롯된 여러 버그들을 마주치다 보면, 정적 타입이 있는 언어들이 부러워질 때가 가끔 있다. 그런 우리 자바스크립트 개발자에겐 Flow와 Typescript가 있다. 그래서 하나씩 시도해보았다. 해보지 않고는 제대로 느끼지 못하기 때문에.
참고) 아래 내용은 러프한 회고로 되어있으므로, 도움이 되는 이야기를 원한다면 스크롤을 내려 결론 부분만 확인하길 추천.
Flow 시도해보기
처음엔 Flow를 시도해보았다. 타입스크립트가 ‘언어'의 포지션을 취하고 있는 것도 부담스러웠고, 단순한 몇몇 표시만으로 타입 있는 언어와 비슷한 효과를 누릴 수 있다는 사실이 긍정적으로 보였다.
일단 시도를 해 봤다. (사실 지난 1년간 네번쯤 시도해봤다. 세번의 설정 실패 끝에 네 번째 도전에 돌아가는 설정을 잡은 거다..) 전체 프로젝트 중에서 재사용 가능하도록 분리된 컴포넌트 몇몇만 타입을 입혀봤다. 그럭저럭 괜찮아보였다. 조금씩 조금씩 바꾸어가며 전체 프로젝트에 Flow를 적용했다.
몇가지 문제가 보였다. 문제 아닌 문제로 파일 앞에 // @flow
를 붙이는 행위와 타입을 불러오는 import type {...}
이 귀찮았다.
두번째로, ImmutableJS와 상성이 잘 안 맞았다. ImmutableJS가 내부에 어떤 데이터가 들어있는지를 꽁꽁 싸매고 있었기 때문에, 어떤 Immutable.Map 객체에 ‘user’라는 키가 들어있는지, 없는지 타입 체크를 할 수가 없었다. 때문에 get, getIn 함수를 사용할 때에는, 항상 not set value를 설정해주어야 했다. 게다가 babel-plugin-extensible-destructing
을 사용해 immutable 객체를 일반 object처럼 destructing해서 사용하기 위해서는 꼼수가 필요했다.
// Immutable.Map이면서도 destruct 가능한 척
type User = Immutable.Map & {
userId: number,
...,
}
마지막으로 조금 느렸다. 주 사용 IDE로 웹스톰을 사용하고 있었는데, 웹스톰의 Flow는 타입 체크용 서버를 뒤에서 돌리고 있었고, 이 서버의 동작이 느린건지 코딩을 느리게 했다. 웹스톰 2017.3 버전부터였던가, 이런 부분이 개선되어서 더이상 버벅이고 코딩에 방해가 될 정도는 아니었지만, 코드의 변화를 감지하고 에러를 표시해주는 데에는 여전히 딜레이가 있었다.
Typescript 시도해보기
Flow의 마지막 문제가 해결된 것으로, 그냥 이쯤에서 정착해볼까 하던 차에, 친한 개발자가 타입스크립트가 꽤 괜찮다고 추천을 하기에 타입스크립트로도 시도해보기로 했다. Flow 타입을 적용했던 것과 같은 순서로, 일단 설정을 잡아두고, 파일 하나씩 // @flow
를 떼어내고, .ts, .tsx
파일로 변경해보기 시작했다.
첫 인상은 괜찮았다. Flow와 타입 정의도 비슷해서 변경해야 할 지점도 많지 않았고, 동작도 비슷했다. 타입스크립트 컴파일러가 트랜스파일링을 맡을 테니, 이렇게만 진행된다면 Babel 없는 프로젝트를 해 볼수 있겠다는 생각도 들었다. 한 번 그렇게 해보자-는 생각으로 편하게 쓰던 Babel 플러그인 몇몇과 작별을 고했다.
여전히 Immutable과의 불협화음은 계속해서 났다. Flow에서 통하던 꼼수도 더이상 통하지 않던 데다가, redux를 배우고 적용하면서 Immutable이 없더라도 Immutable한 코드를 짜는 습관이 들었다고 생각했고, 타입스크립트가 타입 체크까지 해 줄텐데 내가 왜 이걸 유지하고 있지- 라는 생각이 들어 Immutable을 버렸다. 복잡한 객체의 일부분만 변경하고자 할 때 조금 아쉬울 때가 있지만, 지금도 가끔 떠오르는 아쉬움을 참으며 코딩하고 있다.
DefinitelyTyped는 축복이었다. 거의 모든 라이브러리에 대한 타입을 제공했다. luxon이 공개되고 며칠 지나지 않아 DefinitelyTyped에 luxon의 타입 정의가 올라왔다. Flow보다 훨씬 낫다고 느낀 부분이었다. 하지만 가끔, 라이브러리의 타입 정의를 들여다보며 내가 뭘 잘못 넣었나 고민하게 되는 일도 생겼다.
styled-components와 같은 라이브러리는 template literal을 아주 잘 활용하는데, 타입스크립트에서 이 template literal을 함수로 사용할 때에 대한 타입 지원이 되지 않아 타입 사용에 문제가 있을 때가 있었다. 물론 여러 사람들이 이를 극복해낼 방법을 이리저리 찾아냈고, 그런 방식을 사용하고 있긴 하지만, 타입스크립트의 직접적인 지원이 있다면 훨씬 매끄러운 해결책이 나올텐데 싶어 아쉽다.
TSlint는 감점 요인이다. ESLint에 비해 문서도 빈약하고, 옵션도 빈약하다. 게다가 unused variable을 확인하기 위해서는 타입 체크(타입 표시는 used variable로 판별되지 않기 때문에…)옵션을 켜야 하는데, 이 경우 lint가 (느낌상)20배 가량 느려진다. 때문에 이런 옵션들은 1–2주에 한번 확인하고 처리하는 방식으로 일하게 되었다.
TS로의 변환이 충분히 진행되고 다른 도구를 사용하려고 했더니 Babel Plugin이 필요해졌다. TS는 충분한 역할을 제공하고 있었지만, Babel의 플러그인 생태계는 이미 너무 컸고 피할 수 없었다. 프로젝트에 다시 Babel을 넣었다. TS 컴파일러의 역할 일부를 바벨에게 넘기면서 plugin 일부를 포함시켰다. Babel 7의 typescript preset이 잘 돌아간다고는 하던 것 같던데, 조만간 시도해볼까 한다.
결론
Flow나 Typescript나, 모두 Typed code를 작성하는데 충분한 도움을 제공한다. 어떤 도구를 쓰더라도, 다른 도구로 옮겨가는 데에 약간의 노가다 이외의 큰 문제는 없을 정도로 문법도 비슷하다. 타입을 사용하고 싶다면, 길게 고민하지 않고 둘 중 아무 것이든 시도해보는게 좋다.
- 간편하게 시작하기엔 Flow가 좋지만, 라이브러리의 타입까지 다 들고와 확인하기엔 Typescript가 좋더라. 이 점에서 Typescript가 낫다고 본다.
- 타입을 막상 적용해보면, 잘 돌아가는 코드였지만 타입 상으로는 문제 있는 코드가 발견되기도 한다. 이런걸 확인할 때마다 타입 도입하길 잘 했다는 생각이 든다.
- 타입을 사용하다 보면, 결국 내가 사용하는 외부 라이브러리들의 타입 정보 역시 들여다볼 수 밖에 없다. React의
Node
,Element
,Component
와ComponentType
의 차이를 알게 된다. - 타입 정의가 제대로 되지 않아 답답함을 느끼게 된다면, 당신은 결국 해당 라이브러리의 index.js.flow 혹은 index.d.ts, 또는 DefinitelyTyped 프로젝트에 기여를 하고 있을 것이다.
- 가끔 코드보다 타입 선언을 어떻게 해야 할 지 더 깊은 고민을 하게 될 때가 있다. 당장은 낭비처럼 느껴질 때도 있지만, 잘 선언한 타입 하나가 많은 버그를 막는다. 맘에 드는 해답이 나올 때까지 고민해보되, 이슈도 잘 찾아보자. 아직 이슈가 열려있다면, 고민은 멈추는게 낫다.
- 타입스크립트를 사용하더라도, Babel은 빼놓을 수 없는 JS 개발의 핵심이 되어버린 듯 하다.
- Immutable과 같은 내부 정보를 모두 감춰버리는 도구는 타입을 사용하는 코드와는 전혀 조화롭지 못하다.