1장에서 살펴본 바와 같이 타입스크립트는 자바스크립트보다 개선된 언어이다. 새로운 프로젝트를 시작한다면 처음부터 타입스크립트로 작성하면 되지만, 이미 자바스크립트로 작성되어 있는 프로젝트는 규모가 매우 클 수 있기 때문에 점진적으로 타입스크립트로 전환하는 과정이 필요하다.
규모가 큰 프로젝트를 타입스크립트로 마이그레이션 하는 과정은 많은 시간을 소모할 수 있으나, 결과적으로 자바스크립트에 타입을 추가해 줌으로써 코드의 퀄리티와 가독성을 높일 수 있고 프로젝트 과정에서 발생할 수 있는 런타임 에러들(예를 들어 동일 연산자의 인수 강제 변환, 존재하지 않는 프로퍼티의 접근 허용 등으로 인한 에러)을 방지할 수 있다. 프로젝트의 유지 및 보수에 많은 이점이 된다.
현재 구글, 마이크로소프트, 메타(구 페이스북)와 같은 회사들은 타입스크립트를 사용하고 있다. 또한, 자바스크립트 개발자를 대상으로 한 ‘2021년 JS 현황’ 설문조사에서 타입스크립트를 사용하고 있는 개발자의 비율이 2016년 21%에서 5년 만에 69%로 상승한 것으로 나타났다.
이를 통해 향후 타입스크립트의 점유율이 지속적으로 상승할 것을 예측할 수 있다. 결과적으로 자바스크립트를 타입스크립트로 변환하는 것은 프로젝트를 장기적으로 관리하는 데 있어 좋은 선택지가 될 것이다.
자바스크립트에서 타입스크립트로의 변환은 다양한 에러를 수반할 수 있다. 이 장에서는 타입스크립트로의 마이그레이션 방법과, 마이그레이션 과정에서 발생할 수 있는 에러들을 해결할 수 있는 방법을 살펴 볼 것이다.
8.2 마이그레이션 가이드
8.2.1 디렉토리 설정하기
마이그레이션 가이드 파트에서는 번들러 없이 자바스크립트로 구현한 프로젝트를 마이그레이션 한다고 가정한다.
먼저 초기 프로젝트 폴더의 구조는 아래와 같다.
루트 디렉토리에 기본 문서 파일인 index.html이 있고, src 폴더에 script.js, style.css 파일이 존재한다. 여기서 우리가 타입스크립트로 마이그레이션 할 파일이 바로 script.js다.
❓
번들러란?
웹 애플리케이션을 구성하는 리소스나 모듈을 하나로 합쳐주는 도구.대표적으로 webpack이 있다.
컴파일한 파일이 들어갈 폴더 생성
먼저 컴파일한 자바스크립트 파일이 들어갈 ‘dist’ 폴더를 하나 생성해보자. 이 폴더에는 타입스크립트에서 자바스크립트로 컴파일한 파일이 들어간다. 마이그레이션을 진행하는 동안 컴파일된 자바스크립트와 파일이 중복되는 일이 없도록 폴더를 분리하는 것이다.
8.2.2 tsconfig.json 생성과 기본 설정
tsconfig.json은 타입스크립트를 자바스크립트로 컴파일할 때 경로와 옵션에 관한 설정 파일이다. 이 파일이 있는 위치가 프로젝트의 루트 디렉토리가 된다. 생성 후 tsc 파일명.ts 명령어를 입력할 때 tsconfig.json 파일에서 설정한 옵션에 따라 컴파일이 진행된다.
8.2.2.1 tsconfig.json 파일 생성하기
프로젝트 루트 디렉토리에 tsconfig.json 파일을 생성해 보자. 파일을 직접 생성할 수도 있지만 tsc 명령어를 이용할 수도 있다.
터미널에 아래와 같은 명령어를 입력하면 tsconfig.json 파일이 생성된다.
tsc --init
8.2.2.2 tsconfig.json 커스터마이징
tsc 명령어로 생성한 tsconfig.json 파일에는 기본적인 옵션과 주석이 포함되어있다. 필요에 따라 내용을 수정하여 옵션을 커스터마이징 할 수 있다.
타입스크립트에는 다양한 컴파일 옵션이 존재하는데, 이번 목차에서는 그중에서도 기본적으로 많이 사용되는 옵션을 위주로 살펴 볼 것이다.
프로젝트가 동작할 플랫폼에 따라 그에 맞는 모듈 방식을 선택할 필요가 있다. 예를 들어 ES6 문법으로 컴파일 시 import 문법을 사용하면 import가 그대로 컴파일된다. import 문법은 node 환경에서 실행이 불가능하기 때문에 모듈 옵션을 변경해주어야 하는데, "module": "CommonJS" 를 추가하면 import 를 require 문법으로 컴파일이 가능하다.
import example from './example.js';
example();
"module": "CommonJS" 인 경우 위 코드는 아래와 같이 컴파일 된다.
allowJS
설명
컴파일 시 JS와 JSX 확장자 파일의 허용 여부를 결정한다.
기본 값
false
타입스크립트는 기본적으로 컴파일 시 타입스크립트 파일 외에는 에러가 발생한다.
"allowJs : true" 인 경우에는 자바스크립트 파일을 프로젝트에 포함할 수 있다. 이 옵션은 주로 jQuery 같은 자바스크립트 기반 라이브러리를 사용하거나, 점진적으로 마이그레이션을 진행할 때 활용한다.
💡
VSCode 에디터에서 jsconfig.json 속성에 마우스 오버를 하면 설명과 함께 공식 사이트 링크를 제공한다.
8.2.3 에러 방지
타입스크립트로 파일을 변환하기 전, 앞서 생성한 tsconfig.json 파일에 컴파일 옵션을 추가해 줌으로써 초기 에러를 방지할 수 있다.
타입스크립트에는 다양한 컴파일 옵션이 존재한다. 컴파일 옵션에 대해서는 8.3 마이그레이션 이후 파트에서 더 자세하게 설명되어 있으며, 8.2.3에서는 마이그레이션의 초기 과정에 도움을 줄 수 있는 두 가지 옵션에 대해서 간단하게 살펴 볼 것이다. 다음의 두 가지 옵션을 적용하면 가장 기본적인 에러를 확인하기 편리하다.
함수의 마지막에 return을 빠뜨리는 것을 방지하는 noImplicitReturns와, 타입 추론이 불가능하고 타입 선언이 되지 않은 값이 any로 평가되는 것을 방지하는 noImplicitAny에 대해 알아볼 것이다.
.ts 파일로 변환 후 해당 옵션을 추가하고 에러를 해결해 나가도 무방하며, 지금은 .ts파일로 전환 전 가벼운 설정이라 여기면 된다.
8.2.3.1 noImplicitReturns 설정하기
🔷
다음과 같이 tsconfig.json 파일에 "noImplicitReturns": true 값을 추가해 준다.
타입스크립트에서는 타입이 명시되지 않았거나 타입추론이 불가능한 값을 any 타입으로 평가한다. noImplicitAny 옵션은 이러한 상황에서 any 타입으로 평가된 값에 대한 에러 반환 여부를 결정한다.
타입스크립트는 해당 옵션을 활성화 시키지 않아도 경고를 나타내긴 하나, 옵션을 활성화 시키면 컴파일 시 에러를 발생시켜 컴파일을 막아 더 안전하게 코드를 작성할 수 있다.
위 형태와 같이 매개변수에 어떤 타입도 정해주지 않았을 때 다음과 같은 에러를 발생시킨다.
해당 에러를 해결할 수 있는 방법에 대해서는 8.3 마이그레이션 이후에서 더 자세하게 알아볼 것이다.
8.2.4 TypeScript 파일로 변환하기
8.2.3 까지의 과정이 완료되었다면, 타입스크립트 파일로 변환하기만 하면 된다. .js 파일은 .ts 파일로, .jsx 파일이라면 .tsx로 확장자만 변경해주면 마이그레이션이 완료된다.
🔷
파일의 확장자 명을 변경하는 것으로 마이그레이션은 완료된다.
확장자를 변경한 타입스크립트 파일을 vscode와 같은 에디터로 열어보면 군데군데 발생해 있는 에러들을 확인할 수 있을 것이다. 해당 에러들은 자바스크립트 파일로 작성할 때 에러가 발생하지 않았던 부분들이 타입스크립트에서 엄격한 체킹을 거치며 발생한 에러들이다.
또한 적용되지 않은 컴파일 옵션들을 하나씩 활성화 하였을 때, 새롭게 발생하는 에러들이 있 것이다. 따라서 타입스크립트로 마이그레이션 한 후, tsconfig.json 파일에 컴파일 옵션들을 적용하며 점진적으로 에러를 해결해 나가는 과정이 필요하다.
8.3 마이그레이션 이후파트에서는 해당 에러들을 점진적으로 해결할 수 있는 방안에 대해 살펴 볼 것이다.
8.3 마이그레이션 이후
타입스크립트로 확장자 변환을 마친 후 해당 파일에서 나타나는 에러들을 점진적으로 해결해 나가야 한다.
먼저, 8.2.3 에러 방지 파트에서 적용해 주었던 noImplicitAny, noImplicitReturns 옵션으로부터 발생한 에러의 해결 방법부터 알아 볼 것이다. 이후 컴파일 옵션들을 하나씩 적용하며 타입스크립트 코드를 더욱 엄격하게 작성할 수 있다.
8.3.1 암묵적 타입 추론 금지! noImplicit
noImplicit 옵션들은 타입이 암묵적으로 평가되는 상황을 방지하는 목적을 가지고 있다. 암묵적으로 평가되는 상황은 개발자의 의도가 반영된 것이 아니기 때문에 예상치 못한 문제를 발생시킬 수 있다. noImplicit 옵션을 활용하면 문제를 사전에 방지하여 더욱 안정성 있는 코드를 작성할 수 있다.
8.3.1.1 noImplicitAny
값
설명
true
타입이 명시되지 않았거나 타입추론이 불가능한 값을 any 타입으로 사용하는 것을 금지한다.
false (default)
타입이 명시되지 않았거나 타입추론이 불가능한 값을 any 타입으로 사용하는 것을 허용한다.
function sum(a, b) {
return a + b;
}
"noImplicitAny: true" 인 경우 위 코드에서 에러가 발생한다. 타입스크립트가 sum 함수의 파라미터 a, b 를 any로 암묵적으로 타입 추론하기 때문이다.
any로 평가된 값에 타입을 명시함으로써 에러를 해결할 수 있다. 암묵적으로 타입이 추론된 것이 아니라면 타입을 any로 명시하는 것도 가능하다. 그러나 타입스크립트에서 any 타입을 사용하는 것을 지양하고 있으므로 상황에 따라 적절하게 사용하여야 한다.
// 타입을 명시
function sum1(a: number, b: number) {
return a + b;
}
// 타입을 명시
function sum2(a: any, b: any) {
return a + b;
}
자주 발생할 수 있는 상황으로, 변수 선언 후에 값을 할당하지 않은 let 키워드에 대한 에러를 확인할 수 있다.
let count;
setTimeout(function () {
count++;
}, 1000);
타입을 명시해주어도 되지만, 타입스크립트가 타입을 추론할 수 있도록 초기 값을 할당해주어도 에러가 해결된다.
function checkEmpty(item): boolean {
if (typeof item== 'undefined' || item== null || item== '') {
return true;
}
}
"noImplicitReturns: true" 인 경우 위 코드에서 에러가 발생한다. 함수가 일부 조건에서 암묵적으로 undefined를 반환하기 때문이다. checkEmpty 함수에서 if 문을 만족하는 경우에는 true를 반환하지만, if 문을 만족하지 않는 경우에는 따로 return문을 명시하지 않아서 함수가 암묵적으로 undefined를 반환한다.
어떤 조건에서든 함수가 값을 return 할 수 있도록 return문을 명시하면 에러가 해결된다.
function checkEmpty(item): boolean {
if (typeof item == 'undefined' || item == null || item == '') {
return true;
} else {
return false; // return문 추가
}
}
8.3.1.3 noImplicitOverride
값
설명
true
메서드가 암묵적으로 오버라이딩 되는 것을 금지한다.
false (default)
메서드가 암묵적으로 오버라이딩 되는 것을 허용한다.
noImplicitOverride는 클래스를 사용하는 상황에서 적용되는 옵션이다. 값이 true 일 때 오버라이딩된 메서드에 override 키워드를 적어주지 않으면 에러가 발생한다. 이 옵션을 활용하면 부모 클래스의 메서드를 실수로 오버라이딩 하는 것을 사전에 방지할 수 있으며, override 키워드를 명시함으로써 가독성을 높일 수 있다.
class Person {
public name: string;
public nickname: string;
constructor(name: string, nickname: string) {
this.name = name;
this.nickname = nickname;
}
sayHello() {
console.log('Hello!');
}
get getNickname(): string {
return this.nickname;
}
set setNickname(nickname: string) {
this.nickname = nickname;
}
}
class Student extends Person {
sayHello() {}
get getNickname(): string {
return this.nickname;
}
set setNickname(nickname: string) {
this.nickname = nickname;
}
}
"noImplicitOverride: true" 인 경우 다음과 같은 에러가 발생한다.
에러를 해결하기 위해서는 자식 클래스의 오버라이딩된 메서드 앞에 override 키워드를 추가하면 된다. 만약 오버라이딩을 원하지 않는다면 에러가 발생한 메서드의 이름을 부모 클래스의 메서드와 중복되지 않게 바꿔 준다.
class Person {
public name: string;
public nickname: string;
constructor(name: string, nickname: string) {
this.name = name;
this.nickname = nickname;
}
sayHello() {
console.log(`Hello ${this.name}!`);
}
get getNickname(): string {
return this.nickname;
}
set setNickname(nickname: string) {
this.nickname = nickname;
}
}
class Student extends Person {
override sayHello() {
console.log(`Hello ${this.name}!`);
}
override get getNickname(): string {
return this.nickname;
}
override set setNickname(nickname: string) {
this.nickname = nickname;
}
}
8.3.1.4 noImplicitThis
값
설명
true
this 키워드가 암묵적으로 any 타입으로 추론되는 것을 금지한다.
false (default)
this 키워드가 암묵적으로 any 타입으로 추론되는 것을 허용한다.
자바스크립트의 this는 함수가 호출되는 방식에 따라 동적으로 바인딩 된다. 이와 같은 이유로 타입스크립트에서 this의 타입을 추론할 수 없는 상황이 생기는데, noImplicitThis 옵션은 이와 같은 상황에서 암묵적으로 any로 타입을 추론하는 경우를 방지하는 옵션이다.
"noImplicitThis: true" 인 경우 아래 코드에서 에러가 발생한다. 중첩 함수가 일반 함수로 호출되면 this는 전역 객체를 가리키게 된다. 이 때 클래스 내부에서는 this가 undefined를 반환하는데, 타입스크립트가 이를 any로 타입을 추론하면서 에러가 발생한다.
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
getName() {
return function () {
return this.name; // this 는 undefined
};
}
}
const user = new Person('Son');
user.getName()();
중첩 함수의 this를 전역 객체가 아닌 메서드의 this와 일치시키는 방법은 다음과 같다.
일반 함수 대신 화살표 함수를 사용한다. 화살표 함수의 this는 상위 스코프의 this를 가리키므로 중첩 함수의 this가 메서드의 this와 동일해지면서 에러가 해결된다.
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
getName() {
return () => { // 화살표 함수
return this.name;
};
}
}
const user = new Person('Son');
user.getName()(); // Son
다른 방법으로는 this를 변수 that에 할당하여 중첩 함수 내부에서 this 대신 that을 참조하도록 할 수 있다. 중첩 함수의 this 대신 메서드의 this를 참조하면서 에러가 해결된다.
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
getName() {
const that = this; // 변수 that에 this를 할당
return function () {
return that.name;
};
}
}
const user = new Person('Son');
user.getName()(); // Son
8.3.2 조금 더 엄격하게! strict
strict 옵션들은 타입스크립트의 타입을 더욱 엄격하게 평가하여 프로그램의 정확성을 높여준다. 타입스크립트 팀은 strict 옵션을 strict mode family라고 지칭하고 있으며, 자바스크립트의 use strict와 비슷한 옵션이라고 볼 수 있다. 개발자의 필요에 따라 각각의 strict 옵션을 활성화 할 수 있다. 이 장에서는 alwaysStrict와 strictNullCheck에 대해 살펴 볼 것이다.
8.3.2.1 alwaysStrict
값
설명
true
소스 코드에서 strict 룰을 위반한다면 에러를 발생시킨다.
false (default)
소스 코드에서 strict 룰을 위반하더라도 허용한다.
해당 옵션을 활성화 할 경우, 각 소스 파일에 ‘use strict’가 적용되어 있는 것처럼 작동하며 파일을 ECMAScript 엄격 모드(Strict modee)로 분석 하고 strict룰에 위반되는 경우 에러를 발생시킨다.
8.3.2.2 strictNullChecks
값
설명
true
구체적인 값이 필요한 상황에서 값이 null 및 undefined일 경우 에러를 발생시킨다.
false (default)
구체적인 값이 필요한 상황에서 값이 null 및 undefined인 경우무시된다.
다음과 같은 타입스크립트 코드를 사용하는 경우, find 메서드는 조건을 만족하는 첫 번째 요소의 값을 반환한다. 혹은 그러한 요소가 없다면 undefined를 반환하도록 되어있으므로 loggedInUser는 undefined가 될 수 있다.