타입스크립트. 지금은 나도 대세.

타입스크립트 한 방에 끝내기

타입스크립트를 써야할까요?

인기있는 자바스크립트 프레임워크 중에 하나인 앵귤러는 타입스크립트를 사용합니다. 또 현재 주요 업그레이드가 진행 중인 Vue 3.0도 타입스크립트로 제공이 된다고 하죠. 그런데 왜 앵귤러나 뷰와 같은 자바스크립트 프레임워크는 자바스크립트를 사용하지 않고 굳이 타입스크립트를 사용할까요?

우선 타입스크립트에 대해 간단히 알아보겠습니다. 타입스크립트는 마이크로소프트에서 개발하고 유지/관리하는 Apache 라이센스가 부여되어 있는 오픈 소스이며, 일반 자바스크립트로 컴파일이 되는 자바스크립트의 상위 호환언어라고 할 수 있습니다.

typescript.org

타입스크립트는 이름에서도 알 수 있듯이 강한 타입 시스템을 사용합니다. C#Java같은 체계적이고 정제된 언어들에서 사용하는 강한 타입 시스템은 높은 가독성과 코드 품질 등을 제공할 수 있고, 치명적인 오류는 런타임이 아닌 컴파일 환경에서 발생되기 때문에 이런 종류의 에러를 더 쉽게 잡아낼 수 있습니다.

반면 우리가 사용하는 보통의 자바스크립트는 타입 시스템이 없는 동적 프로그래밍 언어로, 자바스크립트에서 변수는 타입을 지정하지 않아도 문자열, 숫자, 불린 등 여러 타입의 값을 가질 수 있습니다. 이를 약한 타입 시스템이라고 표현할 수 있는데 비교적 유연하게 개발할 수 있는 환경을 제공하는 장점이 있지만, 런타임 환경에서 쉽게 에러가 발생할 수 있는 단점이 있습니다.

강한 타입 시스템을 사용하는 타입스크립트는 이런 자바스크립트의 단점을 보완하여 대부분의 에러를 컴파일 환경에서 코드를 입력하는 동안 체크할 수 있습니다.

그럼 다시 처음으로 돌아가서 앵귤러는 타입스크립트로 작성된 프레임워크입니다. 앵귤러가 타입스크립트를 사용하는 이유는 생산성 때문이라고 할 수 있는데 현재 업데이트 되고 있는 Vue3.0의 경우에도 그런 이유로 타입스크립트를 지원하는 것이 아닐까 하는 추측을 해봅니다.

예를 들어 어떤 함수가 ‘문자열’을 인자로 받아야 하는데 개발자가 이 함수에 실수로 ‘숫자’를 인자로 전달한다면 어떻게 될까요? 이 문자열을 인자로 받아야 하는 함수는 문자열이 아닌 숫자가 인자 전달 되었기 때문에 에러가 발생할 수 있습니다.

그런데 이런 상황이 보통의 자바스크립트에서 발생한다면 컴파일 과정이 없기 때문에 런타임에서 에러가 발생됩니다. 즉 사소한 실수 하나로도 운영 중인 서비스에 문제가 발생하게 되어 애플리케이션이 종료되어 서비스가 멈출 수도 있다는 의미입니다.

컴파일을 하는 과정이 있다면 어떨까요? 앞의 사례와 같은 사소한 실수는 컴파일 과정에서 에러를 발생시키기 때문에 문제를 미리 발견하고 처리할 수 있겠죠? 

대표적인 컴파일 언어인 JavaC#에서는 타입이 맞지 않으면 컴파일 자체를 하지 않습니다. 하지만 자바스크립트는 동적으로 타입이 결정되고 컴파일 과정이 없기 때문에 이런 에러에 대해 관대한 편이죠.

사실 타입의 문제는 작은 애플리케이션에서는 큰 문제가 아닐 수도 있습니다. 하지만 애플리케이션의 크기가 커지고 구조가 복잡해져 사용해야 할 API가 많아지면개발자의 생산성은 점점 더 떨어질 수 밖에 없습니다.

큰 프로젝트일수록 코드를 리팩토링할 때 IDE가 문맥에 맞는 도움말을 제공하거나 오류가 발생할 수 있는 코드를 미리 검사해주는 기능이 중요하기 때문이죠. 또 타입이 정해진 프로그래밍 언어는 변수나 함수의 이름을 한 번에 바꾸는 작업을 순식간에 변경할 수 있는 기능을 IDE가 지원해주기도 합니다. 하지만 타입을 지원하지 않는 자바스크립트는 이런 기능을 사용할 수 없습니다.

타입스크립트의 장점

지금까지 타입스크립트를 써야하는 이유에 대해 알아보았습니다. 타입스크립트가 어떤 언어인지, 왜 써야 하는지 조금은 감을 잡으셨나요? 여기서는 타입스크립트의 장점을 간단히 정리해보겠습니다.

  • 타입스크립트는 타입을 지원합니다. 에러가 발생할 수 있는 코드는 컴파일 단계에서 미리 검출되어 런타임에서 발생할 수 있는 에러를 방지할 수 있습니다.
  • IDE가 자바스크립트보다 더 많은 기능을 지원해 줄 수 있습니다. 인자의 개수가 잘못되거나 다른 타입의 인자를 전달할 때 에러를 표시해주고, 코드를 수정할 때 컨텍스트에 어울리는 도움말을 제공하여 리팩토링이 편해지는 등 더 높은 생산성을 발휘할 수 있습니다.
  • 앵귤러 프레임워크를 사용한다면 IDE에서 앵귤러의 API를 사용할 때 타입 체크를 수행하고, 작업하고 있는 컨텍스트에 적합한 도움말을 제공해줍니다.
  • 타입스크립트는 ECMAScript 6과 7 표준을 따르고, 여기에 타입, 인터페이스, 데코레이터, 클래스 멤버 변수, 제네릭, public, private과 같은 키워드를 추가로 제공합니다.
  • 타입스크립트에서 제공하는 인터페이스 기능을 사용하면 애플리케이션에 적합한 커스텀 타입을 직접 정의할 수 있어 애플리케이션을 구조적으로 더 견고하게 만들 수 있습니다.
  • 타입스크립트로 코드를 작성하고 컴파일을 한 자바스크립트 코드는 가독성이 좋습니다. 즉 사람이 작성한 것처럼 쉽게 읽을 수 있습니다.

타입스크립트시작하기

타입스크립트는 마이크로소프트가 오픈 소스로 공개한 후 GitHub 저장소를 통해 소스를 관리하고 있습니다.

앞에서도 살펴보았듯이 타입스크립트를 사용하기 위해서는 컴파일러가 필요합니다. 타입스크립트의 컴파일러는 npm을 통해 설치하거나 타입스크립트 웹사이트에서 내려받을 수 있습니다.

참고로 타입스크립트 웹사이트에서는 웹에서 직접 컴파일을 할 수 있도록 타입스크립트 트래스파일러도 제공하고 있어, 간단한 타입스크립트 코드를 직접 웹에서 변환하여 테스트해 볼 수도 있습니다.

typescript.org의 웹 트랜스파일러

타입스크립트 직접 설치하고 실행해보기

여담이지만 타입스크립트를 자바스크립트로 변환해주는 타입스크립트 컴파일러의 작성 언어는 타입스크립트입니다. 즉 타입스크립트 컴파일러 자체도 타입스크립트를 이용해서 작성되었다는 얘기죠.

우선 타입스크립트 컴파일러는 노드를 이용하여 설치하도록 하겠습니다. 혹시 노드에 대해 아직 잘 모르신다면 다음의 포스트에서 노드와 설치 방법에 대해 학습하실 수 있습니다.

노드와 NPM, Yarn 설치 가이드

윈도우의 커맨드 창을 열고 타입스크립트 컴파일러를 전역 환경으로 설치해주기 위해 다음과 같이 명령어를 입력하고 실행합니다.

npm i -g typescript

npm 패키지 관리자를 사용하여 노드의 패키지를 설치할 때 위의 명령어와 같이 -g옵션을 붙여주면 전역 환경으로 패키지를 설치해주기 때문에 컴퓨터에 있는 모든 프로젝트에서 해당 패키지를 사용할 수 있습니다.

타입스크립트 컴파일러도 모든 프로젝트에서 사용할 수 있도록 전역으로 설치해줍니다. 설치를 하고 난 후 설치 된 버전을 확인하기 위해 아래의 명령어를 입력해줍니다. 현재의 작성일 기준으로 최신 버전은 4.0.3입니다.

tsc -v
Version 4.0.3

컴파일하기

타입스크립트로 코드를 작성하여 저장할 때는 .ts라는 확장자로 저장하는데 이 .ts라는 확장자로 저장된 파일은 웹 브라우저에서 실행할 수 있도록 자바스크립트 코드로 변환해주는 컴파일 과정을 거쳐야 합니다. 바로 이런 과정을 타입스크립트 컴파일러가 해주는 것이죠.

예를 들어 type.ts라는 이름으로 저장된 파일이 있을 경우 다음과 같은 명령어로 컴파일을 실행할 수 있습니다.

tsc type.ts

소스맵 생성하기

컴파일이 생각보다 간단하죠? 그런데 원래 작성했던 타입스크립트의 소스 코드를 디버깅해야 하는 경우도 있겠죠? 이런 경우에는 소스맵을 함께 생성해주면 됩니다. 소스맵을 생성하면 브라우저에서 자바스크립트 코드를 실행하더라도 TypeScript 중단점을 설정한 후 원하는 시점에서 코드의 실행을 멈추고 디버깅을 실시할 수도 있습니다.

소스맵은 다음의 명령어로 함께 생성할 수 있습니다. 생성된 소스맵은 type.map과 같이 .map이라는 확장자로 저장됩니다.

tsc type.ts --sourcemap

소스맵을 이용한 디버깅은 다음과 같이 크롬 개발자 도구로 할 수 있습니다. 크롬 개발자 도구를 열고 소스탭을 선택하면 타입스크립트로 작성한 코드를 확인 할 수 있고 코드의 원하는 곳에 중단점을 지정하면 편리하게 디버깅을 할 수 있습니다.

크롬 개발자도구 타입스크립트 디버깅

참고로 타입스크립트에서 지정한 모든 타입의 정보인터페이스, 타입스크립트에서만 사용할 수 있는 키워드들은 타입스크립트 코드가 컴파일되고 나면 모두 제거됩니다.

자바스크립트 코드 버전 지정하기

타입스크립트 파일을 컴파일 할 때 자바스크립트 코드의 버전을 지정할 수도 있습니다. 기본 값은 ES3이지만 다음과 같은 명령어로 ES5나 ES6로 지정할 수 있습니다.

tsc type.ts --t ES6

파일 변경 자동으로 감지하기

–watch또는 -w옵션은 타입스크립트 컴파일러를 watch 모드로 실행합니다. 이 모드로 실행을 하게 되면 코드를 변경하고 저장할 때 컴파일러가 파일의 변경을 감지하고 자동으로 컴파일을 해줍니다. 프로젝트의 모든 .ts 파일에 대해 워치 모드를 적용하고 싶은 경우에는 다음과 같은 명령어를 사용합니다.

tsc --watch *.ts

타입스크립트 설정파일

일반적으로 프로젝트에서는 타입스크립트 컴파일 옵션을 매번 일일이 지정해주지는 않습니다. 그 이유는 바로 타입스크립트의 컴파일 옵션을 미리 지정하여 파일로 만들어 둘 수 있기 때문이죠.

타입스크립트의 컴파일 옵션을 설정하는 파일은 보통 프로젝트 루트 폴더tsconfig.json이라는 이름으로 저장됩니다. 이 파일이 있을 경우 커맨드라인에서 tsc를 실행할 때 이 파일에 있는 설정을 반영합니다.

{
    "compilerOptions" : {
        "target" : "es6",
        "module" : "commonjs",
        "emitDecoratorMetadata" : true,
        "experimentalDecorators" : true,
        "rootDir" : ".",
        "outDir" : "./build"
    }
}

위와 같은 내용의 설정 파일이 프로젝트 루트에 있을 경우 tsc는 루트 폴더로 지정된 폴더이 모든 ts 파일을 컴파일하여 ./build 폴더에 ES6 문법으로 컴파일된 자바스크립트 파일을 생성하여 저장합니다.

그 외에 만약 대상 파일을 따로 지정하고 싶은 경우에는 files라는 프로퍼티를 사용하고, 특정한 파일을 컴파일 대상에서 제외하고 싶은 경우에는 exclude프로퍼티를 사용합니다. 더 많은 옵션을 알고 싶으시면 타입스크립트 웹사이트에서 제공하는 컴파일러 옵션 문서를 참고해주세요. 해당 문서를 방문하시면 생각보다 방대한(?) 옵션 수에 깜짝 놀라실 수도 있습니다. 😅

상위 집합(Superset)

타입스크립트는 ES5와 ES6 문법의 대부분을 지원합니다. 즉 순수한 자바스크립트 파일의 확장자를 .js에서 .ts로 바꾸기만해도 그 파일은 타입스크립트 파일로 정상적으로 컴파일이 된다는 뜻입니다.

이것은 타입스크립트가 자바스크립트의 상위 집합이기 때문에, 타입스크립트는 결국 자바스크립트에서 제공하는 기능에 생산성을 위한 몇 가지 유용한 기능을 추가한 것 뿐이라는 결론을 내릴 수 있습니다.

이제 타입스크립트가 완전히 새로운 언어가 아니라 자바스크립트와 크게 다르지 않은, 그저 유용한 몇 가지 기능이 추가된 언어라는 것을 아시겠죠?

js에서 ts로 변환하기

프로젝트의 코드를 자바스크립트에서 타입스크립트로 바꿔야 한다면 tsc 컴파일러를 실행할 때 –allowJs옵션을 사용하세요.

–allowJs옵션을 사용하면 함께 사용하는 –target옵션과 –module옵션에 따라 js 파일의 문법 에러를 검사하고 결과물을 만들 수 있습니다. 즉 이렇게 만든 자바스크립트 파일은 타입스크립트 파일을 변환한 자바스크립트 파일과 같이 사용해도 문제가 생기지 않습니다.

그럼 이제부터 본격적으로 타입스크립트만의 추가 기능을 살펴보도록 하겠습니다.

타입 지정하기

타입스크립트는 변수를 선언할 때 타입을 지정할 수 있습니다. 다음과 같이 기존의 자바스크립트 문법을 사용하는 경우와 타입스크립트의 변수 타입을 지정하는 경우 모두 에러가 발생되지 않습니다.

let firstName = '길동';
let lastName:string = '홍';

그런데 모두 에러가 발생되지 않는 다는 것은 알겠는데 왜 굳이 변수의 타입을 지정해서 사용할까요? 그 이유는 타입을 지정하면 타입스크립트 컴파일러가 컴파일을 할 때 잘못된 코드를 잡아낼 수 있고, IDE와 같은 프로그램에서는 자동 완성 기능이나 리팩토링기능을 제공할 수 있어 생산성을 높일 수 있기 때문입니다.

또 타입스크립트를 사용하면 컴파일러가 타입을 지정하지 않은 변수에 할당되는 값을 기준으로 타입을 예측하여 자동으로 타입 체크를 수행하는 타입 추론이라는 기능을 적용합니다.

위와 같이 변수에 타입을 지정하게 되면 해당 변수에는 지정된 타입의 값만 할당할 수 있습니다. 만약 지정된 타입이 아닌 다른 타입의 값을 사용하면 컴파일시에 에러를 발생시킵니다.

즉 string 타입으로 지정한 변수는 string 타입의 값만을 할당할 수 있고, number 타입이라면 number 타입으로만 값을 할당해야 합니다. 그리고 타입을 지정하지 않은 경우라도 처음에 해당 변수에 할당된 값을 기준으로 타입을 인식하기 때문에 이후에라도 해당 변수에는 다른 타입의 값을 할당할 수 없습니다.

타입스크립트는 타입에 대해 강한 규칙을 적용하기 때문에 아래의 두 가지 경우 모두 에러가 발생됩니다.

let name1 = '홍길동';
name1 = 12345;
example_01.ts:2:1 - error TS2322: Type 'number' is not assignable to type 'string'.
let name2:string = '임꺽정';
name2 = 12345;
example_02.ts:2:1 - error TS2322: Type 'number' is not assignable to type 'string'.

타입스크립트에서 변수만 타입을 지정할 수 있는 것은 아닙니다. 변수 외에도 함수의 인자, 함수의 반환값에도 타입을 지정할 수 있습니다.

참고로 타입스크립트에서 제공하는 자료형은 아래와 같이 매우 다양합니다. 타입스크립트에서 제공하는 자료형에 대한 자세한 설명은 타입스크립트 웹사이트의 basic-types 문서를 참고하시기 바랍니다.

  • Boolean
  • Number
  • String
  • Array
  • Tuple
  • Enum
  • Unknown
  • Any
  • Void
  • Null and Undefined
  • Never
  • Object

타입을 지정해서 변수를 선언하는 방법은 다음과 같이 변수명 뒤에 콜론(:)을 붙이고 사용할 타입을 적어주면 됩니다.

let account:number;
let name:string = '홍길동';
let isOpen:boolean;
let whoAreYou:string = null;

위와 같이 변수에 적용한 number, string, boolean 타입은 모두 any 타입하위 타입입니다.

만약 변수나 함수의 인자에 타입을 명시하지 않은 경우에는 컴파일러가 해당 변수나 인자에 any 타입을 지정한 것으로 간주하고 처음에 할당되는 값에 모든 타입을 허용합니다. 즉 타입을 지정하지 않은 경우는 아래와 같은 상태라고 생각할 수 있는 거죠.

let account:any;
let name:any = '홍길동';
let isOpen:any;
let whoAreYou:any = null;

그런데 중요한 차이점이 하나 있습니다. 아래와 같이 첫번째의 아무 타입도 지정하지 않은 상태에서 값을 할당한 경우에는 다른 타입의 값을 할당 할 수 없지만, any 타입으로 지정된 변수에는 값을 할당한 경우라도 다른 타입의 값을 할당하는 것이 아무런 문제가 되지 않습니다. 즉 정상적으로 컴파일이 되는 것이죠.

let name = '홍길동';
name = 12345;
example_03.ts:2:1 - error TS2322: Type 'number' is not assignable to type 'string'.
let myName:any = '홍길동';
myName = 12345;

앞에서 살펴본 바와 같이 변수에 명시적으로 타입을 지정하면 컴파일러가 변수의 타입을 체크하고 오류를 잡아냅니다.

타입스크립트는 웹 브라우저에서 사용하는 HTML ElementDocument와 같은 타입도 지원하고, 클래스나 인터페이스도 새롭게 정의하여 커스텀 타입으로 변수의 선언에 사용할 수 있습니다. 이렇게 다양하게 타입을 지정하여 사용하는 것은 강력한 디버깅을 할 수 있게 해주고, 쉽게 오류를 찾아낼 수 있게 해줍니다.

함수 사용하기

타입스크립트의 함수는 자바스크립트의 함수와 비슷합니다. 하지만 타입스크립트는 인자와 반환값에 타입을 지정할 수 있다는 차이가 있죠. 우선 다음과 같이 어른과 어린이의 티켓 가격을 계산해주는 간단한 함수를 자바스크립트로 작성해보겠습니다.

function calcTicket(ageGroup, price, total) {
    if( ageGroup === 'ADULT' ) {
        return price * total;
    } else if( ageGroup === 'CHILD' ) {
        return price * total * 0.5;
    }
}

위의 함수는 연령대를 체크하여 어른일 경우 인원수 만큼의 티켓 가격을 계산하고, 어린이의 경우 인원수 만큼의 티켓 가격에서 50% 할인된 가격을 표시해주는 계산기입니다. 이제 이 계산기를 사용하여 어린이가 두 명일 때의 티켓 가격을 다음과 같이 계산할 수 있습니다.

let ticket = calcTicket('CHILD', 10000, 2);
10000

함수의 인자에 올바른 타입의 데이터를 넣었다면 티켓의 가격은 10,000원이 됩니다. 위의 경우에는 올바른 인자를 대입하였기 때문에 올바른 결과 값이 나왔습니다. 하지만 아래의 경우는 어떨까요?

let ticket = calcTicket('CHILD', 10000, '2인');
NaN

이 경우 함수는 실행이 되지만 정상적인 값을 반환하지는 않습니다. 변수 ticket의 값을 확인해보면 ticket의 값은 정상적인 값이 아닌 NaN데이터임을 알 수 있습니다. 변수의 타입을 체크하지 않으면 이렇게 정상적인 값이 아님에도 함수를 런타임에서 실행해도 오류를 출력하지 않기 때문에 버그를 찾아내는 것이 어려울 수 있습니다.

만약 이와 동일한 함수를 타입스크립트를 이용하여 인자와 반환 값에 타입을 지정해 주었다면 오류를 출력하여 쉽게 버그를 잡아낼 수 있었을 것입니다.

function calcTicket(ageGroup:string, price:number, total:number):number {
	if( ageGroup == 'ADULT' ) {
		return price * total;
	} else if( ageGroup == 'CHILD' ) {
		return price * total * 0.5;
	}
}
let ticket = calcTicket('CHILD', 10000, '2인');
function_03.ts:9:41 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

타입스크립트로 작성한 위의 함수는 반환값도 타입을 따로 지정해주었기 때문에 다음과 같이 변수 ticket를 잘못된 타입으로 지정하는 경우에도 오류를 출력합니다.

let ticket:string = calcTicket('CHILD', 10000, 2);
function_04.ts:9:5 - error TS2322: Type 'number' is not assignable to type 'string'.

이렇게 타입스크립트를 사용하면 컴파일러에서 타입을 미리 체크하기 때문에 개발 단계에서 발생할 수 있는 사소한 오류(?)들을 미리 파악할 수 있어 개발의 생산성을 향상 시킬 수 있습니다.

인자의 기본값 지정하기

타입스크립트는 함수를 선언할 때 함수의 인자가 전달되지 않는 경우에 사용할 기본 값을 지정할 수 있습니다. 그런데 이 때 주의해야 할 점이 하나 있는데 ‘기본 값을 지정하는 인자는 마지막 인자부터 채워야 한다’는 규칙입니다.

다음과 같이 이 규칙을 지키지 않은 코드는 제대로 동작하지 않습니다.

function calcTicket(ageGroup:string = 'ADULT', price:number, total:number):number {
	if( ageGroup == 'ADULT' ) {
		return price * total;
	} else if( ageGroup == 'CHILD' ) {
		return price * total * 0.5;
	}
}

위의 코드를 제대로 동작하는 코드로 만들기 위해서는 기본 값을 지정하는 인자를 가장 마지막으로이동 시키면 됩니다.

function calcTicket(price:number, total:number, ageGroup:string = 'ADULT'):number {
	if( ageGroup == 'ADULT' ) {
		return price * total;
	} else if( ageGroup == 'CHILD' ) {
		return price * total * 0.5;
	}
}

이렇게 규칙에 따라 올바른 기본 값을 지정하여 함수를 작성하고 나면 기본 값으로 지정된 성인의 티켓 요금을 계산하는 경우 다음과 같이 두 개의 인자만 사용해도 함수는 정상적으로 실행이 됩니다.

let ticket:number = calcTicket(10000, 2);
20000

또는 어린이의 티켓 요금을 계산하는 경우에는 아래와 같이 해당 인자의 위치에 ageGroup 데이터를 넣어주면 됩니다. 인자가 지정된 함수를 사용할 때는 인자의 순서에 유의해야 합니다.

let ticket:number = calcTicket(10000, 2, 'CHILD');
10000

인자를 옵션으로 지정하기

타입스크립트에서는 생략이 가능한 함수의 인자 뒤에 물음표(?)를 붙여 옵션으로 지정할 수 있습니다. 인자를 옵션으로 지정하는 경우에도 기본 값과 같이 함수를 선언할 때 마지막 인자부터채워야 합니다. 또 인자를 옵션으로 지정하는 경우에는 해당 옵션을 처리하는 로직도 추가해야 합니다.

앞에서 작성했던 함수를 수정하여 인원수를 필수 인자로 지정하지 않고, 해당 인원에 대한 인자가 전달 될 때만 추가되는 티켓의 가격을 계산하도록 로직을 변경하겠습니다.

function calcTicket(price:number, ageGroup:string = 'ADULT', total?:number):number {
	let tickets:number;

	if(total) {
		tickets = total;
	} else {
		tickets = 1;
	}

	if( ageGroup == 'ADULT' ) {
		return price * tickets;
	} else if( ageGroup == 'CHILD' ) {
		return price * tickets * 0.5;
	}
}

console.log('티켓 가격은 ' + calcTicket(10000) + '원 입니다.' );
console.log('어른 티켓 가격은 ' + calcTicket(10000, 'ADULT') + '원 입니다.');
console.log('어린이 2인 티켓 가격은 ' + calcTicket(10000, 'CHILD', 2) + '원 입니다.');
티켓 가격은 10000원 입니다.
어른 티켓 가격은 10000원 입니다.
어린이 2인 티켓 가격은 10000원 입니다.

위의 함수에서는 인수 total에 물음표(?)기호가 사용되었는데 이것이 바로 인자를 옵션으로 지정하는 방법입니다. 함수에는 이 옵션을 처리하기 위해 total에 인자가 전달되면 해당 인자를 티켓 수로 변환하여 변수 tickets에 담고 total에 인자가 전달되지 않으면 tickets에 1을 담는 로직을 추가하여 최종 계산식에서 해당 로직에 의해 계산된 티켓의 가격을 반환하도록 하였습니다.

화살표 함수 사용하기

화살표 함수 표현식은 익명 함수를 간단하게 사용할 수 있는 문법입니다. 다음과 같이 function을 사용하지 않고 단순한 형태로 함수를 선언하는 화살표 함수는 ES6의 문법인데 이를 람다 표현식이라고도 합니다.

let getName = () => 'Hong Gil-dong';
console.log( getName() );
Hong Gil-dong

위의 코드에서 빈 괄호는 함수에 전달되는 인자가 아무것도 없다는 것을 의미합니다. 또 함수를 한 줄로 나타내는 경우에는 함수의 컨텐스트를 지정하는 중괄호가 필요없고, return 키워드를 사용할 필요도 없습니다.

위 코드의 경우에는 명시적으로 작성되지는 않았지만 getName 함수가 ‘홍길동’을 반환값으로 출력해주는 것을 나타냅니다. 즉 getName 함수는 ‘홍길동’을 반환하기 때문에 console.log() 함수에 ‘홍길동’이라는 문자열이 출력되는 것이죠.

만약 화살표 함수가 아닌 이전의 코드라면 어떨까요?

let getName = function() { 
	return 'Hong Gil-dong'; 
};
console.log( getName() );
Hong Gil-dong

같은 동작을 하는 코드임에도 불구하고 뭔가 더 복잡하지 않나요? 😅

물론 화살표 함수도 다음과 같이 여러 줄의 로직을 작성해야하는 경우에는 중괄호({})를 반드시 시용해야 합니다. 물론 이 때 반환 값이 있을 경우에도 return 키워드를 생략하면 안됩니다.

let getNameUpper = () => {
    let name = 'Hong Gil-dong'.toUpperCase();
    return name;
}
















console.log( getNameUpper() );
HONG GIL-DONG

화살표 함수의 장점

기존의 자바스크립트에서는 함수 안에서 this키워드를 사용할 때, 함수가 실행되는 컨텍스트 대신 다른 객체를 가리키는 경우가 종종 발생합니다. 이로 인해 수많은 런타임 버그가 발생하기도 하고, 이 문제를 인지하지 못해 버그를 해결하기 위해 불필요하게 많은 시간을 소비하는 경우도 생깁니다.

이 문제를 확인하기 위해 다음과 같이 두 가지 경우의 함수를 작성하여 테스트해보겠습니다.

function StockQuoteArrow( symbol:string ) {
	this.symbol = symbol;

	setInterval(() => {
		console.log( this.symbol + '의 주식 시세는 ' + Math.random() + '입니다.' );
	}, 1000);
}

let stockQuote = new StockQuoteArrow('삼성전자');
삼성전자의 주식 시세는 0.9172685188976275입니다.
function StockQuoteAnonymous( symbol:string ) {
	this.symbol = symbol;

	setInterval(function(){
		console.log( this.symbol + '의 주식 시세는 ' + Math.random() + '입니다.' );
	}, 1000);
}

let stockQuote = new StockQuoteAnonymous('삼성전자');
undefined의 주식 시세는 0.9556074019272924입니다.

위 코드에서 StockQuoteArrow() 함수StockQuoteAnonymous() 함수는 각각 화살표 함수와 익명 함수를 사용하며, Math.random()함수를 호출하여 이 값을 임의의 주가로 사용합니다. 그리고 두 함수 모두 this 객체의 symbol 변수에 주식 종목의 이름으로 ‘삼성전자’를 전달하고 있습니다.

화살표 함수를 사용한 StockQuoteArrow()는 정상적으로 함수의 컨텍스트를 가리키는 변수가 따로 저장되어 있다가 화살표 함수 안에서 this를 참조할 때 가져와서 사용되기 때문에 this.symbol에는 정상적으로 ‘삼성전자’이라는 값이 담깁니다.

하지만 익명함수를 사용한 StockQuoteAnonymous()에서는 익명 함수의 this가 전역 window 객체를 가리키기 때문에 this.symbol의 값이 undefined가 됩니다. 함수에서는 window 객체에 symbol이라는 변수를 선언한 적이 없기 때문이죠.

각각의 결과에서 보듯 화살표 함수를 사용한 경우에는 정상적인 코드가 출력되지만, 익명 함수를 사용한 경우에는 undefined가 출력되는 것을 알 수 있습니다.

참고로 타입스크립트에서는 화살표 함수 표현식 안에 사용한 this를 함수 선언 밖에 있는 this와 같도록 조정을 합니다. 즉 StockQuoteArrow() 함수에서 symbol 프로퍼티를 선언한 this 객체와 화살표 함수 표현식 안에 있는 this는 같은 객체를 가리키는 것이죠.

함수 오버로딩

순수한 자바스크립트는 함수 오버로딩을 지원하지 않습니다. 즉 함수의 이름은 같지만 인자의 갯수는 다른 함수를 만들 수 없는 것이죠. 하지만 타입스크립트는 함수 오버로딩을 지원합니다.

물론 타입스크립트에서 함수 오버로딩을 구현하더라도 최종적으로는 자바스크립트로 변환되어 하나의 함수로 합쳐지기 때문에 완벽한 오버로딩이라고 할 수는 없습니다.

타입스크립트에서 함수를 오버로딩하는 방법은 다음과 같이 함수의 선언을 여러 형태로 만들고 이 함수들을 모두 포괄하는 함수를 따로 선언하면서 함수의 로직을 정의하면 됩니다. 이 때 함수 몸체의 로직에서는 각 인자가 전달되었는지를 확인하는 로직도 필요합니다.

function fn( name:string ):string;
function fn( name:string, value:string ):void;
function fn( map:any ):void;
function fn( nameOrMap:any, value?:string ):any {
	if(nameOrMap && typeof nameOrMap === 'string') {
		// string 타입의 인자가 전달된 경우
	} else {
		// any 타입의 인자가 전달된 경우
	}

	// value 인자 처리
}

사실 위의 함수를 타입스크립트 컴파일러를 사용하여 자바스크립트로 변환하면 변환된 코드에서는 마지막 함수만 남는 것을 확인할 수 있습니다. 즉 마지막 함수 외의 나머지 함수는 가장 마지막에 선언한 함수를 다른 형태로 표현된 것일 뿐이죠.

하지만 함수가 어떻게 동작하는지를 보다 명확하게 추론할 수 있게 해준다는 점에서 타입스크립트에서 지원하는 함수 오버로딩을 사용하는 것도 한번 고려해 볼만 하겠죠?

클래스

여러분은 혹시 자바나 C#이라는 프로그래밍 언어로 개발을 해 본 경험이 있으신가요? 아마 경험이 있다면 클래스와 상속이라는 개념에 대해서는 이미 익숙할 지도 모릅니다.

자바나 C#에서는 클래스에 대한 정의를 미리 만들어 두었다가 인스턴스를 생성할 때마다 이 클래스를 불러와서 사용하죠. 이 때 생성한 클래스의 부모 클래스가 존재한다면 부모와 자식 클래스에 대한 정의를 모두 사용하여 인스턴스를 생성합니다.

타입스크립트는 이런 객체지향 프로그래밍 언어의 특징이라고 할 수 있는 클래스를 지원합니다. 하지만 앞에서 살펴보았듯이 타입스크립트는 순수한 객체지향 언어가 아닌 자바스크립트의 상위 집합이기 때문에 진정한 객체지향 프로그래밍을 할 수 있는 것은 아닙니다.

타입스크립트의 클래스를 자바스크립트로 변환해보면 프로토타입을 활용한 상속으로 구현 되어 있는 것을 확인할 수 있는데, 프로토타입을 활용한 상속은 어떤 객체의 프로토타입에 있는 프로퍼티에 부모 객체를 지정하여 상속 관계를 동적으로 만드는 방식이라고 할 수 있습니다.

결국 타입스크립트에서 지원하는 classextends키워드는 일종의 문법 설탕이라고 할 수 있습니다. 즉 문법을 쉽게 만들어주는 기능이라는 것이죠. 앞에서 이야기한 것처럼 타입스크립트에서 class와 extends 키워드를 사용하여 클래스를 구현하더라도 자바스크립트로 변환하면 결국 프로토타입을 활용한 상속으로 구현이 되기 때문입니다.

타입스크립트에서 class 키워드로 클래스를 선언하면 new라는 키워드로 해당 클래스의 인스턴스를 생성할 수 있습니다. 사실 new 키워드는 자바스크립트에서도 지원하는 키워드입니다. 자바스크립트에서도 함수로 클래스를 구현한 후 new 키워드를 사용하여 새로운 인스턴스를 만들 수 있습니다.

우선 다음과 같이 Person이라는 간단한 클래스를 만들어보겠습니다. Person 클래스는 이름과 나이, 주민등록번호를 저장하는 3개의 프로퍼티를 가지고 있습니다.

class Person {
	name: string;
	age: number;
	id: string;
}

var person = new Person();

person.name = '홍길동';
person.age = 24;
person.id = '000000-0000000';

console.log( person.name );
console.log( person.age );
console.log( person.id );
홍길동
24
000000-0000000

위의 타입스크립트 클래스를 자바스크립트로 변환하면 다음과 같은 형태의 코드로 변환됩니다.

var Person = (function () {
    function Person() {
    }
    return Person;
}());

var person = new Person();

person.name = '홍길동';
person.age = 24;
person.id = '000000-0000000';

console.log(person.name);
console.log(person.age);
console.log(person.id);

자바스크립트로 변환된 코드는 Person 함수를 클로저를 사용하여 클래스를 구성합니다. 자바스크립트로 변환된 Person 클래스의 항목은 타입스크립트에서 구현된 코드에 따라 외부로 공개하거나 내부에 감추는 항목이 달라집니다.

자바스크립트로 클래스를 구현하는 방법 중에서는 클로저를 사용하는 방법이 비교적 간단한 편이지만, 타입스크립트에서는 클래스 문법을 사용하여 클로저를 사용하는 것 보다 더 간단하게 클래스를 정의할 수 있습니다.

참고로 클래스의 구성 요소에는 생성자와 프로퍼티, 메소드가 있는데 이 중 프로퍼티와 메소드는 클래스 멤버라고 하며, 클래스 멤버는 클래스의 인스턴스를 생성할 때 생성자를 사용하여 프로퍼티를 초기화할 수 있습니다. 이 생성자는 인스턴스가 생성될 때 한 번만 실행이 됩니다.

다음과 같이 Person 클래스에 생성자를 추가하면, 클래스는 인스턴스를 생성하면서 전달받은 인자들을 해당 클래스의 인자로 전달하여 프로퍼티 값을 초기화합니다.

class Person {
	name: string;
	age: number;
	id: string;

	constructor(name: string, age: number, id: string) {
		this.name = name;
		this.age = age;
		this.id = id;
		console.log('name: ' + this.name + ', age: ' + this.age + ', id: ' + this.id);
	}
}

var person = new Person('홍길동', 24, '000000-0000000');
name: 홍길동, age: 24, id: 000000-0000000

위와 같이 생성자가 추가된 타입스크립트 클래스는 다음과 같은 자바스크립트로 변환됩니다.

var Person = /** @class */ (function () {
    function Person(name, age, id) {
        this.name = name;
        this.age = age;
        this.id = id;
        console.log('name: ' + this.name + ', age: ' + this.age + ', id: ' + this.id);
    }
    return Person;
}());
var person = new Person('홍길동', 24, '000000-0000000');

접근 제한자

기존의 자바스크립트는 클래스의 변수나 메소드에 접근 범위를 지정할 수 없기 때문에 어떤 클래스의 멤버를 외부에서 접근하지 못하게 하기 위해서는 클로저로 클래스를 정의하면서 프로퍼티를 this 객체에 선언하지 않고 지역 변수로 선언하거나, 클로저에서 return 키워드를 사용하여 외부로 공개할 클래스의 멤버만 지정하는 방법을 사용해야 했습니다.

하지만 타입스크립트에서는 클래스 멤버의 접근 권한을 지정하는 public, protected, private 키워드를 제공하고 클래스를 정의 할 때 접근 제한자를 생략하면 public이 자동으로 지정됩니다. public으로 지정된 클래스 멤버는 클래스의 밖에서도 자유롭게 접근할 수 있는 멤버가 됩니다.

public이 아닌 protected로 지정된 클래스 멤버는 해당 클래스나 자식 클래스에서 접근할 수 있고, private으로 지정된 클래스 멤버는 해당 클래스에서만 접근할 수 있습니다.

접근 제한자를 지정하는 방법은 두 가지가 있는데, 하나는 클래스 멤버 변수를 선언할 때 지정하는 방법이고, 다른 하나는 생성자에서 지정하는 방법입니다. 우선 클래스 멤버 변수를 선언할 때 지정하는 방법을 알아보겠습니다.

class Person {
	public name: string;
	public age: number;
	private _id: string;

	constructor(name: string, age: number, id: string) {
		this.name = name;
		this.age = age;
		this._id = id;
	}
}

var person = new Person('홍길동', 24, '000000-0000000');
console.log('name: ' + person.name + ', id: ' + person._id);

위의 코드와 같이 private을 지정하는 변수는 프로퍼티를 쉽게 구분하기 위해 _id와 같이 밑줄(_)이나 접두사를 붙여서사용하는 것이 좋습니다. 프로퍼티가 몇 개 되지 않을 때는 크게 상관이 없지만 프로퍼티가 많아지게 되면 어떤 변수를 private으로 지정했는지 헷갈릴 수 도 있겠죠?

그런데 위의 코드를 컴파일 해보면 타입스크립트의 컴파일러에서는 다음과 같은 에러가 발생됩니다. 아마 위의 코드를 컴파일 하다가 깜짝 놀라신 분도 있으시겠죠? 놀라지 마세요. 코드가 잘못된 것이 맞으니까요.

error TS2341: Property '_id' is private and only accessible within class 'Person'.
console.log('name: ' + person.name + ', id: ' + person._id);

이유는 바로 마지막의 console.log에서 Person 클래스의 private 프로퍼티인 _id에 직접 접근하고 있기 때문입니다. 이렇게 타입스크립트의 컴파일러는 접근 범위가 아닌 경우에는 에러를 표시하기 때문에, 에러가 난 코드를 확인하고 빠르게 수정할 수 있습니다.

이번에는 두 번째 방법인 접근 제한자를 생성자에서 지정하는 방법을 알아보겠습니다. 다음과 같이 Person 클래스의 생성자에서 인자를 받을 때 접근 제한자를 지정할 수 있습니다.

class Person {
	constructor(public name: string, public age: number, private _id: string) {
	
	}
}

let person = new Person('홍길동', 24, '000000-0000000');

위의 코드와 같이 접근 제한자를 생성자에서 지정하면 타입스크립트 컴파일러는 인자에 지정된 접근 제한자로 클래스 변수를 만들고 해당 변수의 초기값에 전달된 인자를 할당합니다. 즉 별개의 클래스 변수를 선언하거나 초기값을 할당할 필요가 없어 더 간단하게 코드를 작성할 수 있습니다.

물론 앞에서 살펴본 클래스 멤버에 접근 제한자를 직접 지정하는 방법생성자에서 접근 제한자를 지정하는 방법은 모두 자바스크립트로 변환했을 때 동일한 결과를 보여줍니다.

정적 클래스 멤버

다음과 같이 인자가 하나 있고 반환값은 없는 간단한 클래스를 작성해보겠습니다.

class MyClass {
    doSomething(times: number):void {
        // code
    }
}

let mc = new MyClass();
mc.doSomething(10);

위의 코드는 mc라는 클래스의 인스턴스를 먼저 생성하고 이 인스턴스를 사용하여 클래스 멤버에 접근합니다. 일반적으로 많이 사용하는 방법이죠? 그런데 타입스크립트에서는 인스턴스없이 바로 클래스의 멤버에 접근할 수 있는 방법이 있습니다. 바로 static 키워드를 사용하는 것입니다.

다음과 같이 프로퍼티나 메소드에 static 키워드를 사용하면 static 키워드를 사용한 프로퍼티와 메소드는 해당 클래스로 만들어진 모든 인스턴스에 공유되기 때문에 따로 인스턴스를 만들지 않아도 클래스의 멤버에 직접 접근할 수 있습니다.

class MyClass {
	static doSomething(times: number):void {
		// code
	}
}

MyClass.doSometing(10);

위의 코드에서는 doSomething 메소드를 static으로 지정했기 때문에 인스턴스를 참조하지 않고 클래스를 직접 참조해서 사용할 수 있습니다.

참고로 타입스크립트에서는 클래스의 인스턴스를 만든 후 어떤 메소드에서 같은 클래스에 있는 다른 메소드를 호출하기 위해서는 this 키워드를 반드시 사용해야 합니다. 그렇지 않으면 에러가 발생됩니다.

게터세터

앞에서 살펴본 것 처럼 클래스의 외부에서 private으로 지정된 멤버에 직접 접근을 하면 에러가 발생됩니다. 이런 경우에는 게터와 세터, 즉 get과 set 키워드를 사용하여 해당 멤버에 접근할 수 있습니다.

class Person {
	constructor(public name: string, public age: number, private _id?: string) {
	}

	get id(): string {
		return this._id;
	}

	set id(value: string) {
		this._id = value;
	}
}

let person = new Person('홍길동', 24);
person.id = '000000-0000000';

console.log('name: ' + person.name + ', id: ' + person.id);
name: 홍길동, id: 000000-0000000

위의 코드에서는 _id 값은 생략할 수 있도록 물음표(?) 기호를 사용하여 옵션으로 지정하고, 게터 세터메소드를 추가하였습니다.

클래스의 게터와 세터 메소드는 객체의 프로퍼티에 접근하기 위해 this를 사용하고 있는데, 타입스크립트에서는 클래스의 다른 멤버에 접근할 때 this 키워드를 반드시 사용해야 합니다. 만약 this 키워드를 생략하게 되면 에러가 발생됩니다.

Person 클래스의 인스턴스를 만들고 내부 프로퍼티인 id에 접근했을 때 앞에서 살펴본 코드에서는 오류가 발생했지만 위의 코드에서는 내부의 id 프로퍼티에 접근하는 대신 세터 메소드가 실행되어 오류가 발생되지 않습니다.

상속

자바스크립트는 프로토타입을 사용한 객체 기반 상속을 지원합니다. 즉 어떤 객체가 다른 객체의 프로토타입이 되는 방식이라고 할 수 있습니다.

타입스크립트에서는 ES6와 객체 기반 언어에서 일반적으로 제공하는 키워드인 extends를 사용하여 클래스 상속을 구현합니다. 물론 자바스크립트로 컴파일을 하면 extends를 사용하여 구현된 상속 코드는 프로토타입을 사용하는 방식으로 변환됩니다.

다음과 같이 Person 클래스를 상속하는 Worker 클래스를 정의하는 코드를 작성하고 해당 코드를 자바스크립트로 변환해 보겠습니다.

class Person {
	constructor(public name: string, public age: number, private _id: string) {
	}
}

class Employee extends Person {	
}
var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var Person = /** @class */ (function () {
    function Person(name, age, _id) {
        this.name = name;
        this.age = age;
        this._id = _id;
    }
    return Person;
}());
var Employee = /** @class */ (function (_super) {
    __extends(Employee, _super);
    function Employee() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    return Employee;
}(Person));

위와 같이 자바스크립트로 변환된 코드를 보면 클래스의 상속이 프로토타입을 사용하는 방식으로 변환된 것을 확인할 수 있습니다. 같은 동작을 하는 코드이지만 타입스크립트로 작성한 코드가 훨씬 간단하고 읽기 편한 것을 알 수 있습니다.

이번에는 앞에서 만든 Employee 클래스에 part라는 프로퍼티를 추가해보겠습니다.

class Person {
	constructor(public name: string, public age: number, private _id: string) {
	}
}

class Employee extends Person {
	part: string;

	constructor(name: string, age: number, _id: string, part: string) {
		super(name, age, _id);
		this.part = part;
	}
}

위의 코드는 part 프로퍼티를 선언하고 Person 클래스의 생성자에 이 part 프로퍼티를 추가하여 새로운 생성자를 만듭니다.

코드는 super 키워드를 사용하여 부모 클래스의 생성자를 호출하고 있는데, 이와 같이 자식 클래스의 생성자를 정의하는 경우에는 자식 클래스의 생성자에서 부모 클래스의 생성자를 호출해야 합니다.

부모 클래스에 있는 메소드를 자식 클래스에서 사용할 때는 클래스에 구현된 메소드를 참조하는 것처럼 this 키워드를 사용하지만, 명시적으로 부모 클래스를 지정해서 함수를 실행하는 경우에는 super 키워드를 사용합니다.

참고로 super 키워드는 두 가지용도로 사용할 수 있습니다.

첫 번째, 자식 클래스의 생성자에서 부모 클래스의 생성자를 호출할 때 super()와 같이 함수처럼 호출해서 사용할 수 있습니다.

두 번째, 메소드를 오버라이딩한 경우입니다. 메소드를 오버라이딩한 경우에는 다음과 같이 부모 클래스에 있는 메소드를 명시적으로 호출할 때 사용할 수 있습니다.

doSomething() {
	super.doSomething();
	...
}

제네릭

제네릭은 자료형을 정하지 않고 여러가지 타입을 한번에 사용하는 방식입니다. 타입스크립트 컴파일러는 코드에서 잘못 사용된 타입 때문에 발생하는 문제점을 제거하기 위해 제네릭 코드에 대해 강한 타입 체크를 합니다.

즉 제네릭 코드는 런타임에서 타입 에러가 나는 것 보다는 컴파일 시에 미리 타입을 강하게 체크하여 에러를 사전에 방지하는 역할을 하게 되죠.

비제네릭코드의 경우에는 불필요한 타입 변환을 하기 때문에 프로그램의 성능에 악영향을 미치기도 하는데, 제네릭을 사용하면 따로 타입 변환을 할 필요가 없어 프로그램의 성능이 향상되는 장점이 있습니다.

타입스크립트에서는 함수를 정의할 때는 인자의 타입을 신경쓰지 않지만, 함수가 실행되는 시점에는 명확한 타입을 지정해서 사용하고 싶을 때나 배열의 항목을 특정 타입으로 제한할 때 제네릭을 사용할 수 있습니다.

제네릭을 사용하면 특정 타입의 항목으로 제한 된 배열에 정해진 타입에 맞지 않는 타입을 추가하려고 시도하는 경우 컴파일을 할 때 에러가 발생됩니다.

참고로 자바스크립트는 제네릭을 지원하지 않기 때문에 타입스크립트에서 제네릭 코드를 사용하여 컴파일을 해도 자바스크립트에서는 해당 코드를 볼 수 없습니다. 즉 타입스크립트의 제네릭은 어디까지나 컴파일 단계까지만 유효한 코드인 것이죠.

우선 이해하기 쉽도록 간단한 제네릭 배열을 만들어 보겠습니다. 우선 다음과 같이 Person 클래스를 정의하고 이 Person 클래스를 이용해 두 개의 인스턴스를 만듭니다.

만들어진 두 개의 인스턴스는 employees라는 제네릭 배열에 추가합니다. 제네릭은 코드에서와 같이 꺾은 괄호(<>)를 사용하여 정의할 수 있습니다.

class Person {
    name: string;
}

class Employee extends Person {
    part: number;
}

class Animal {
    feed: string;
}

let employees: Array<Person> = [];
employees[0] = new Person();
employees[1] = new Employee();
employees[2] = new Animal();
error TS2741: Property 'name' is missing in type 'Animal' but required in type 'Person'.

위의 코드는 컴파일을 할 때 에러가 발생합니다. 앞에서 설명한 것처럼 코드의 배열은 특정 타입으로 제한된 제네릭 배열이기 때문입니다.

즉 employees 배열에는 Person 클래스와 Person 클래스의 자식 객체만을 저장할 수 있는 <Person> 제네릭 타입을 지정했기 때문에, 이 배열의 타입이 아닌 Animal 클래스의 인스턴스를 배열에 저장하려고 하면 에러가 발생되는 것이죠.

만약 employees 배열에 다른 타입의 배열도 저장하고 싶다면 위 코드의 제네릭 배열 선언을 아래와 같이 바꿔주면 됩니다.

let employees: Array<any> = [];

클래스나 함수에 제네릭을 사용하는 경우에는 아래와 같이 사용할 수 있습니다. 함수를 제네릭 타입으로 정의하고 해당 함수에 문자열을 인자로 전달할 때 타입이 맞지 않으면 컴파일러에서는 에러가 발생됩니다.

function doIntroduce<G>(data: G) {
    console.log( 'my name is ' + data );
}

doIntroduce<string>('Hong Gil-dong');
doIntroduce<string>(12345);
error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
doIntroduce<string>(12345);

위의 코드와 같이 제네릭 타입 G를 string으로 지정하여 실행한 함수는 컴파일이 되지만 string 타입이 아닌 인자값을 넣은 함수에서는 에러가 발생됩니다.

앞에서 자바스크립트로 변환된 코드에서는 제네릭에 대한 정보가 사라진다고 했죠? 위의 코드를 변환한 자바스크립트 코드를 확인해보면 제네릭 코드가 빠져있기 때문에 해당 자바스크립트 코드는 실행해도 에러가 발생되지 않습니다.

function doIntroduce(data) {
    console.log( 'my name is ' + data );
}

doIntroduce('Hong Gil-dong');
doIntroduce(12345);

인터페이스

인터페이스를 사용하면 코드를 견고한 구조로 작성할 수 있는 장점이 있지만 자바스크립트는 아쉽게도 인터페이스를 지원하지 않습니다. 그렇지만 다행스럽게도(?) 타입스크립트에서는 인터페이스를 자체적으로 지원하고 있습니다.

타입스크립트에서는 interfaceimplements키워드를 사용하여 인터페이스 기능을 사용할 수 있습니다. 물론 자바스크립트에서는 해당 키워드를 제공하지 않기 때문에 컴파일 된 자바스크립트 코드에서는 다른 방식으로 구현됩니다.

즉 타입스크립트의 인터페이스 역시 개발자의 생산성 향상을 위해 사용되는 기능이라고 할 수 있습니다. 타입스크립트에서 인터페이스는 주로 다음의 2가지 용도로 사용됩니다.

커스텀 타입으로 사용
커스텀 타입으로 사용된 인터페이스에는 필수 항목으로 사용할 프로퍼티를 선언하여 함수에 전달하는 인자의 형식을 고정할 수 있습니다. 이 경우 인터페이스를 인자로 받는 함수가 실행될 때 컴파일러가 인자의 유효성을 먼저 검사합니다.
추상 클래스로 사용
추상 클래스로 사용되는 경우에는 메소드의 형태만 선언하여 인터페이스를 정의하고, 이후에 클래스를 정의할 때 implements 키워드를 사용하여 인터페이스를 지정합니다. 이 때 해당 클래스는 추상 함수로 선언 된 메소드의 몸체를 모두 정의해야 합니다.

커스텀 타입으로 사용하기

일반적으로 자바스크립트 프레임워크를 사용하는 경우에는 정해진 형태의 객체를 전달해 함수를 실행하는 경우가 많기 때문에 이해당 프레임워크 함수의 인자로 전달되는 객체에 어떤 프로퍼티가 있는지 확인하기 위해서는 가이드 문서를 보거나 소스 코드를 직접 확인해야 했습니다.

하지만 타입스크립트에서는 인터페이스로 필수 항목과 항목의 타입을 정의해서 사용할 수 있어 보다 쉽게 함수를 정의하고 사용할 수 있습니다.

이를 확인하기 위해 앞에서 만들었던 Person 클래스에 인터페이스를 추가하여 Person 클래스의 생성자가 개별 인자 대신 인터페이스를 받도록코드를 수정해보겠습니다.

interface InPerson {
    name: string;
    age: number;
    id?: string;
}

class Person {
    constructor(public config: InPerson) {}
}

let i: InPerson = {
    name: 'Hone Gil-dong',
    age: 24
}

let p = new Person(i);
console.log('name: ' + p.config.name);
name: Hone Gil-dong

위의 코드는 인터페이스의 형태를 InPerson 프로퍼티로 정의한 후 Person 클래스의 생성자가 InPerson 타입을 인자로 받도록수정되었습니다.

인스턴스를 생성할 때는 InPerson 인터페이스의 형태에 맞는 객체를 생성하기 위해 객체 리터럴 문법을 사용하여 프로퍼티를 추가하고 Person 객체의 인스턴스를 생성할 때 InPerson 타입의 i 객체를 인자로 전달합니다. 그런데 코드에서 인터페이스로 정의된 InPerson과 객체 i의 형태가 조금 다름에도 불구하고 컴파일을 할 때 에러가 발생되지 않습니다.

그 이유는 타입스크립트가 두 객체의 형태가 같은 경우에는 호환되는 것으로 판단하기 때문입니다. 즉 타입스크립트가 객체 리터럴 iInPerson이 서로 호환된다고 판단하기 때문에 에러가 발생하지 않는 거죠. 또 객체 i는 명시적으로 InPerson 타입으로 선언되었기 때문에 Person 클래스의 생성자에 전달하는 것도 문제가 되지 않습니다.

인터페이스에는 프로퍼티 외에 메소드의 형태도 정의할 수 있지만 인터페이스에 메소드를 정의하는 경우에는 함수의 몸체는 정의하지 않습니다.

추상 클래스로 사용하기

인터페이스를 추상 클래스로 사용하는 경우에는 implements키워드를 사용합니다. implements 키워드는 어떤 인터페이스를 기반으로 클래스를 정의할 때 사용합니다.

다음의 샘플 코드는 InPayable로 정의된 인터페이스를 기반으로 Employee 클래스를 정의하는 형태를 보여줍니다.

interface InPayable {
    increase_cap: number;
    increasePay(percent: number): boolean
}

class Employee implements InPayable {
    //
}

코드를 클래스 안에 미리 만들어 두지 않고 인터페이스로 만드는 목적은 여러 클래스에서 공통적으로 사용하기 위해서입니다.

다음과 같이 직원들의 연봉을 계산하는 애플리케이션을 만드는 경우를 생각해보겠습니다.

연봉을 계산하는 샐러리앱이라는 가상의 애플리케이션에는 Person이라는 클래스가 있고 이 Person 클래스를 상속받는 Employee라는 클래스가 있습니다.

Person 클래스를 상속받는 Employee 클래스의 내부에는 연봉을 계산하는 calcPay()라는 메소드가 정의되어 있습니다. 그런데 사용자가 이 샐러리앱 애플리케이션에 계약직으로 일하는 직원의 연봉을 계산하는 기능을 추가해달라고 합니다.

이 경우 샐러리앱은 기존 직원의 연봉 계산 뿐만 아니라 계약직 직원의 연봉도 계산해야 하기 때문에 기존의 방식, 즉 클래스를 그대로 사용할 수가 없습니다.

그렇다면 어떻게 해야 할까요? 계약직 직원의 연봉을 위해 일단 Contractor라는 클래스와 필요한 프로퍼티를 추가하고 calcContractorPay()라는 메소드도 추가해야 하겠죠?

이렇게 두 개의 클래스를 만들어 서로 다른 API를 만들어두면 Employee 클래스는 기존 직원의 연봉을 계산할 때 사용하고, Contractor라는 새로운 클래스는 계약직 직원의 연봉을 계산할 때 사용할 수 있습니다.

하지만 위와 같이 기존의 기능을 일부 공유하면서 다른 기능을 제공해야 하는 경우라면 더 나은 방식이 있습니다. 각각의 클래스에 서로 다른 메소드를 만들지 않고 두 클래스의 공통 부분을 뽑아서 인터페이스를 만들고 각 클래스에서 인터페이스 메소드의 몸체를 다르게 구현하는 방식입니다.

위에서 가정한 가상의 샐러리앱 애플리케이션을 인터페이스로 구현하면 다음과 같이 더 나은 방식으로 코드를 작성할 수 있습니다.

interface InPayble {
    calcPay(percent: number): boolean
}

class Person {
    // 구체적인 코드 생략
    constructor() {}
}

class Employee extends Person implements InPayble {
    calcPay(percent: number): boolean {
        console.log('올해의 연봉 인상률은 ' + percent + '% 입니다.');
        return true;
    }
}

class Contractor implements InPayble {
    increaseCap: number = 20;

    calcPay(percent: number): boolean {
        if(percent < this.increaseCap) {
            console.log('올해의 계약직 직원의 연봉 인상률은 ' + percent + '% 입니다.');
            return true;
        } else {
            console.log('죄송합니다. 계약직 직원의 연봉 인상률은 최대 ' + this.increaseCap + '% 입니다.');
            return false;
        }
    }
}

let workers: Array<InPayble> = [];
workers[0] = new Employee();
workers[1] = new Contractor();

workers.forEach(worker => worker.calcPay(30));
올해의 연봉 인상률은 30% 입니다.
죄송합니다. 계약직 직원의 연봉 인상률은 최대 20% 입니다.

타입스크립트에서는 Employee 클래스나 Contractor 클래스와 같이 인터페이스를 정의할 때 implements키워드를 빼도 컴파일 시에 에러가 발생하지 않습니다. 물론 코드도 제대로 동작합니다. 그 이유는 타입스크립트의 컴파일러가 각 클래스에서 해당 인터페이스의 메소드가 정의되어 있는 것을 체크하여 컴파일을 하기 때문입니다. 즉 반드시 명시적으로 implements 키워드를 사용할 필요가 없는 것이죠.

하지만 위의 코드와 같이 배열의 타입을 제네릭으로 설정한 경우에는 implements 키워드를 사용하는 것이 좋습니다. 만약 implements 키워드를 사용하지 않은 상태에서 InPayble 인터페이스의 calcPay() 메소드에 다른 메소드를 인터페이스에 추가하는 경우에는 에러가 발생하기 때문입니다.

그 이유는 앞에서 살펴본 것 처럼 제네릭이 강한 타입 체크를 하기 때문이죠. 즉 implements 키워드를 사용하지 않은 클래스에서 새롭게 추가된 인터페이스의 메소드를 구현하고 있지 않은 경우더 이상 InPayble 타입이 아니기 때문입니다.

동일한 타입이 아닌 인스턴스는 제네릭으로 설정된 workers 배열에 추가할 수 없기 때문에 컴파일 시에 에러가 발생됩니다. 때문에 위와 같이 제네릭을 사용하는 코드라면 반드시 implements 키워드를 사용해주어야 컴파일 시의 에러를 방지할 수 있습니다.

실행할 수 있는 인터페이스

(percent: number): boolean;

인터페이스에서 위의 코드와 같이 익명 함수를 사용하면 타입스크립트에서는 해당 인터페이스를 실행할 수 있는 인터페이스(callable interface)로 정의합니다. 즉 이 익명 함수가 있는 인터페이스로 만든 인스턴스를 실행할 수 있다는 의미이죠.

다음과 같이 앞에서 구현한 샐러리앱을 익명 함수가 정의된 인터페이스로 다시 구현해 보겠습니다.

interface InPayble {
    (percent: number): boolean;
}

class Person {
    constructor(private validator: InPayble) {
    }

    calcPay(percent: number): boolean {
        return this.validator(percent);
    }
}

const employees: InPayble = (percent) => {
    console.log('올해의 연봉 인상률은 ' + percent + '% 입니다.');
    return true;
};

const contractors: InPayble = (percent) => {
    const increaseCap: number = 20;

    if(percent < this.increaseCap) {
        console.log('올해의 계약직 직원의 연봉 인상률은 ' + percent + '% 입니다.');
        return true;
    } else {
        console.log('죄송합니다. 계약직 직원의 연봉 인상률은 최대 ' + this.increaseCap + '% 입니다.');
        return false;
    }
}

const workers: Array<Person> = [];

workers[0] = new Person(employees);
workers[1] = new Person(contractors);

workers.forEach(worker => worker.calcPay(30));
올해의 연봉 인상률은 30% 입니다.
죄송합니다. 계약직 직원의 연봉 인상률은 최대 20% 입니다.

위의 코드는 익명 함수로 실행할 수 있는 인터페이스를 정의하고 Person 클래스는 이 인터페이스를 인자로 받아 validator 프로퍼티로 할당합니다. Person 클래스의 calcPay() 메소드는 this.validator()를 실행하는데 this.validator는 바로 Person 클래스의 생성자에 인자로 전달된 InPayable 인터페이스의 익명 함수를 가리킵니다.

정직원과 계약직 직원의 연봉을 계산하는 함수인 employees와 constractors는 InPayable 인터페이스 기반의 화살표 함수로 정의하고 Person 클래스에 각각의 함수를 인자로 전달하여 인스턴스를 생성하여 다음과 같은 결과를 출력합니다.

올해의 연봉 인상률은 30% 입니다.
죄송합니다. 계약직 직원의 연봉 인상률은 최대 20% 입니다.

인터페이스 상속하기

타입스크립트의 인터페이스도 클래스와 같이 extends 키워드를사용하여 다른 인터페이스를 상속 받을 수 있습니다. 즉 A 인터페이스를 상속하여 B 인터페이스를 만들고, A 인터페이스를 상속한 B 인터페이스를 기반으로 클래스 C를 만들 수 있습니다.

주의해야 할 점은 이렇게 A 인터페이스를 상속한 B 인터페이스를 기반으로 만든 C 클래스는 반드시 A 인터페이스와 B 인터페이스에 선언되어 있는 모든 멤버를 정의해야 합니다.

복잡한가요? 어렵게 설명했지만 한 마디로 정리하자면 ‘클래스 C는 상속받는 모든 인터페이스의 멤버를 반드시 정의해야 한다’는 내용입니다. 어렵지 않죠? 😅

클래스를 인터페이스로 사용하기

타입스크립트는 클래스도 인터페이스의 하나로 간주합니다. 만약 A라는 클래스가 선언되어 있다면 extends 키워드 대신 implements 키워드를 사용하여 클래스를 인터페이스로 사용하는 것이 가능하다는 의미이죠.

코드로 구현하면 다음과 같은 형태가 됩니다.

class A {
    //
}

class B implements A {
    //
}

참고로 타입스크립트에서 만든 인터페이스는 자바스크립트로 변환하면 해당 코드가 사라집니다. 즉 a.ts라는 파일에 인터페이스를 정의하고 컴파일을 하면 a.ts는 코드가 사라지고 빈 파일로 생성되기 때문에 주의해야 합니다.

SystemJs의 경우 아래와 같이 meta 어노테이션을 사용하여 등록할 수 있습니다.

System.config({
    transpiler: 'typescript',
    typescriptOptions: {
        emitDecoratorMetadata: true
    },
    packages: {
        app: {
            defaultExtension: 'ts'
        }
    },
    meta: {
        'app/a.ts': {
            format: 'es6'
        }
    },
    ...
})

타입스크립트의 인터페이스를 사용하면 커스텀 타입을 효율적으로 만들 수 있습니다. 즉 코드가 더 간결해지고 가독성이 좋아지기 때문에타입과 관련된 에러를 방지하는 데에도 도움이 됩니다.

앵귤러를 위한 타입스크립트

클래스 메타데이터 추가하기

메타데이터데이터에 대한 데이터라고 할 수 있습니다. 즉 일종의 코드를 설명하는 데이터인 셈이죠.

타입스크립트는 작성한 코드에 @ 기호를 사용하여 메타데이터를 추가할 수 있는데 이렇게 타입스크립트의 데코레이터 문법인 @ 기호를 사용하여 메타데이터를 추가하는 것을 어노테이션이라고 합니다.

앵귤러에서는 타입스크립트로 작성한 클래스를 컴포넌트로 만들 때 @Component어노테이션을 사용합니다. @Component를 사용하면 앵귤러는 이 어노테이션을 파싱하여 앵귤러 프레임워크에 필요한 코드를 추가로 생성합니다.

@Component에는 HTML 문서에 컴포넌트가 위치할 셀렉터를 지정하거나, 컴포넌트를 렌더링할 템플릿이나 스타일을 지정할 수 있습니다.

@Component({
    // Selector, Template, Style
})

Class HelloWorldComponent {
    // code
}

앵귤러에서 제공하는 어노테이션은 앵귤러의 컴파일러인 ngc를 통해 해당 어노테이션의 내용을 파싱하고 브라우저에서 동작하는 코드로 만들어 주는데, 앵귤러에서 제공하는 어노테이션을 사용하기 위해서는 다음과 같이 애플리케이션의 코드에서 해당 어노테이션을 로드해 주어야 합니다.

import { Component } from '@angular/core';

앵귤러는 미리 구현해둔 어노테이션을 제공하기 때문에 정해진 규칙에 따라 사용하며 되지만, 타입스크립트의 데코레이터 문법을 사용하여 앵귤러 프레임워크에서 제공되지 않는 커스텀 어노테이션을 만들어서 사용할 수도 있습니다.

타입 정의 파일

타입 정의 파일이란 기존에 자바스크립트로 작성된 라이브러리나 프레임워크를 타입스크립트에서 사용하기 위해 만들어 놓은 것으로, 개발자들이 많이 사용하는 자바스크립트 라이브러리를 타입스크립트에서도 사용할 수 있도록 타입스크립트 재단에서 파일을 제공하고 있습니다.

타입스크립트는 컴파일 과정을 거쳐 타입을 검증하는데 검증을 하려면 타입이 정의된 파일이 있어야 합니다. 처음부터 타입스크립트로 작성한 모듈이라면 문제가 없지만 기존에 자바스크립트로 작성된 모듈을 사용할 때는 컴파일러가 타입 검증을 할 수 없겠죠?

모듈의 타입을 알려주기 위해서는 .d.ts파일을 두어 타입을 선언해 주면 되지만 이 파일은 타입스크립트의 컴파일 과정에서만 필요한 파일이기 때문에 npm과 같은 저장소에 공개된 모듈들의 필수 사항은 아닙니다. 그래서 타입스크립트 프로젝트에서는 .d.ts파일이 없는 외부 자바스크립트 모듈을 받아오는 경우문제가 발생할 수 있습니다.

하지만 다행스럽게도 npm 패키지 관리자를 통해 기존의 자바스크립트 라이브러리들의 타입 정의 파일이 배포되고 있어 해당 파일을 설치하면 컴파일 문제를 쉽게 해결할 수 있습니다. 그렇지만 타입 정의 파일은 실제 구현체가 아닌 단순히 정의를 해둔 파일이라 아예 생성하지 않았거나, 잘못 정의되어 있는 경우도 있을 수 있습니다. 이런 경우에는 직접 타입 정의를 해줄 수도 있습니다.

타입 정의 파일은 npmjs의 웹사이트에서 @types로 검색을 하면 현재 기준으로 약 7224개의 npm 패키지를 찾을 수 있는데 타입 정의 파일을 사용하기 위해서는 npm또는 yarn과 같은 패키지 관리자의 설치가 필요합니다.

타입 정의 파일 설치하기

npm install uuid
import { v4 } from 'uuid';
console.log(v4());

만약 위와 같이 uuid 모듈을 가져와 v4로 출력하도록 코드를 작성했다면 타입스크립트에서는 uuid의 타입 정의 파일이 없어 다음과 같이 @type/uuid를 사용하거나, 타입 정의 파일인 .d.ts 파일을 만들어야 한다는 에러가 표시됩니다.

error TS7016: Could not find a declaration file for module 'uuid'.
Try npm install @types/uuid if it exists or add a new declaration (.d.ts) file containing declare module 'uuid';

@types/uuid를 사용하기 위해 다음의 명령어를 입력합니다.

npm install @types/uuid

타입 정의 파일을 설치하면 node_modules 폴더의 아래에 해당 패키지가 설치되고 node_modules 폴더의 아래에 있는 /@types/uuid/ 폴더에서 index.d.ts라는 파일을 찾을 수 있습니다. 이렇게 타입 모듈을 설치하고 다시 실행을 하면 uuid가 잘 출력되는 것을 확인할 수 있습니다.

만약 패키지를 설치하지 않고 직접 .d.ts 파일을 정의하고 싶다면 src 폴더아래에 @types/uuid 폴더를 만들고 index.d.ts 파일을 만들어 다음과 같이 타입을 정의하면 됩니다.

declare module 'uuid' {
    const v4: () => string

    export { v4 }
}

댓글 남기기