JavaScript 프로젝트에 TypeScript 도입하기
TypeScript migration
프로그래밍을 C, Java등의 정적타입 언어로 입문 했던 나는 현재 프론트엔드 개발을 하면서 JavaScript를 주로 사용하고 있는데 이는 위의 언어들과 달리 코드를 작성하는 시점에서 일정한 타입이 정해져있지 않은 동적언어이다.
처음에는 일일이 타입을 지정해주어야 하는 부담없이 자유롭게 작성할 수 있어 코드를 작성하는 속도도 빨라지고 마음에 들었지만 프로그램의 복잡성이 증가하고 개발에 참여하는 인원이 많아질수록 오히려 유지보수가 어려워지면서 TypeScript의 필요성을 느끼게 되었다.
순수 JavaScript로 개발을 하면서 느끼는 어려움들의 예를 들자면
- 의도치 않은 값을 변수에 할당해주거나 또는 할당해주지 않아서 에러가 발생하거나
2. 다른 이들이 작성해놓은 변수가 어떤 형식의 데이터인지 한 눈에 알아보기 어렵다는 등의 문제였다.
앞으로 우리가 운영하게 될 서비스의 복잡성은 더욱 증가할 것이고 좀 더 코드를 엄밀하게 규정하고 관리해줄 필요성이 커졌다. 하지만 그렇다고 기존에 잘 운영되고 있는 일정 규모 이상의 프로젝트를 한 번에 통째로 typescript로 변경하는 것은 안정성 면에서나 인력면에서나 무리가 있다. 따라서 js 파일과 ts파일이 공존하면서도 서로를 import하는 개발환경을 구축할 필요성이 생겼다.
TypeScript는 컴파일을 한 후에는 일반 JavaScript가 되는 JavaScript의 슈퍼셋이다. ts code 자체를 브라우저에서 읽지는 못하기 때문에 ‘ts-loader’, ‘awesome-typescript-loader’ 등으로 컴파일만 해주면 된다.
해당 포스팅에서는 ts-loader도, awsome-typescript-loader도 사용하지 않고 babel plugin을 통해 ts코드를 빌드할 것이다. 이유는 아래에서 설명한다.
js로 작성된 해당 코드를 통해 ts migration을 시도해 보려고 한다. redux를 이용해 간단한 카운터를 만드는 예제이며 webpack을 통해 babel-compiler로 코드를 compiling하고 있다.
먼저 src/components/Counter.js 파일을 Counter.tsx파일로 파일명을 수정했다. 해당 컴포넌트는 리액트 jsx문법이 들어가 있기 때문에 .ts가 아닌 .tsx로 파일명을 설정해주어야 한다.
확장자를 변경하자 다음과 같이 에러가 나타났다. 해당 에러는 컴포넌트의 props와 state에 대한 타입 정의가 되어있지 않았기 때문에 발생하는 에러이다.
따라서 이와 같이 Counter component의 Prop type과 state type을 정의해주고
interface CounterProp { value: number, onIncrease: () => void, onDecrease: () => void,}// State는 당장 사용하는 것이 없기 때문에 빈 interface로 선언만interface CounterState {}
컴포넌트의 generic으로 선언해주면
class Counter extends Component<CounterProp, CounterState> { ... }
에러가 사라진다.
이처럼 TypeScript는 컴포넌트에서 사용할 props와 state의 타입을 명확히 정의하고 사용하게 되어있어서, 이후 미처 정의하지 않은 값을 사용하거나 할당할 경우(에러가 발생할 가능성이 ‘매우’ 높은 경우)를 사전에 발견하고 수정할 수 있게 해준다.
에러를 제거해주었으면 이제 webpack으로 다시 빌드를 해준다. 빌드를 시도하면
ERROR in ./src/index.jsModule not found: Error: Can’t resolve ‘./components/Counter’ in ‘/Users/woohyun/Documents/posting/ts-migration-tutorial/src’@ ./src/index.js 6:0–43 10:59–66
이번에는 빌드과정에서 에러가 발생한다. 현재 webpack config에서는 js | jsx 파일만 compiling하기 때문에 당연하다. 이제는 ts | tsx 파일도 함께 빌드해주어야 한다.
webpack.config.js 파일의 rules 중에 babel-loader의 타겟을
test: /\.(js|jsx)$/,
에서
test: /\.(ts|js)x?$/,
로 변경해준다. 이제 babel-loader는 .ts | .tsx파일도 빌드의 타켓으로 삼을것이다. 또 한가지. index.js 파일에서 Counter component를 import하는 부분은 현재
import Counter from “./components/Counter”;
이렇게 작성되어있다. js파일의 경우 문제가 되지 않지만 ts파일은 webpack에서 확장자 없이 읽어주기 위해서는 webpack.config.js파일에
resolve: { extensions: [ ‘.js’, ‘.json’, ‘.ts’, ‘.tsx’ ] },
해당 설정을 추가해주어야 한다.
다음은 ts 코드를 babel에서 js 코드로 변경할 수 있도록 ‘ @babel/preset-typescript’ 프리셋을 설정하는 단계이다.
$ yarn add @babel/preset-typescript
명령어를 통해 설치해주고 .babelrc 파일에 presets에 추가해준다. 이렇게 js코드를 ts로 이전하는 최소한의 작업이 끝났다. yarn build등의 명령어를 통해 해당 프로젝트를 빌드해보면 이전의 dist/main.js 파일보다 훨씬 커진 빌드파일이 생성될 것이다.
ts code를 위에서 언급했던 ts-loader나 awsome-typescript-loader가 아닌 babel로 빌드하는 이유는 바로 여기에 있다. 현재 예제코드와 같은 작은 규모의 프로젝트는 어떤 loader를 사용하든 크게 상관없으나, 규모가 큰 프로젝트를 ts-loader로 빌드하자 memory size를 초과하여 build 도중 서버가 터져버리는 문제가 발생했다. babel은 ts-loader보다 비교적 안정적으로 build가 되어 babel을 사용하는것을 추천한다.
이렇게 js 코드로 작성된 프로젝트의 일부를 ts 코드로 수정하고도 잘 build되는 개발환경을 구축해보았다. 아직도 우리 프로젝트는 계속 마이그레이션을 진행중이고 부족한 점이 많아 불가피하게 ‘any’ 타입을 사용하며 현실과 타협하기도 한다:(
하지만확실히 코드의 가독성도 좋아지고 재사용성도 높아짐이 느껴지는 가치가 있는 작업이었다고 생각한다.