함수
타입스크립트의 함수는 자바스크립트의 함수와 비슷하지만, 인자와 반환값에 타입을 지정할 수 있다는 차이가 있습니다.
function calcTicket(ageGroup, price, total) { if( ageGroup === 'ADULT' ) { return price * total; } else if( ageGroup === 'CHILD' ) { return price * total * 0.5; } }
앞의 함수는 연령대를 체크하여 어른의 경우와 어린이의 경우를 구분하여 인원수 만큼 티켓 가격을 계산하는 기능을 구현한 것으로, 어린이가 두 명인 경우라면 티켓 가격을 다음과 같이 계산할 수 있습니다.
let ticket = calcTicket('CHILD', 10000, 2); // 10000
함수에서는 total
의 인자를 number
타입으로 받기 때문에 올바른 결과가 나오지만, 다음과 같이 number
타입이 아닌 string
타입의 인자를 넣게 되면 잘못된 데이터가 나올 수 있습니다.
let ticket = calcTicket('CHILD', 10000, '2인'); // 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인'); // 오류 출력
let ticket:string = calcTicket('CHILD', 10000, 2); // 오류 출력
인자의 기본값 지정
타입스크립트는 함수를 선언할 때 함수의 인자가 전달되지 않는 경우에 사용할 기본 값을 지정할 수 있는데, ‘기본 값을 지정하는 인자는 마지막 인자부터 채워야 한다’는 규칙이 있기 때문에 주의해야 합니다.
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);
let ticket:number = calcTicket(10000, 2, 'CHILD');
인자를 옵션으로 지정하기
타입스크립트에서는 생략이 가능한 함수의 인자 뒤에 물음표를 붙여 옵션으로 지정할 수 있는데, 인자를 옵션으로 지정하는 경우에도 기본 값과 같이 함수를 선언할 때는 마지막 인자부터 채워야 합니다.
또 인자를 옵션으로 지정하는 경우에는 해당 옵션을 처리하는 로직도 추가해야 하는데, 앞의 함수를 인자를 옵션으로 지정하는 방식으로 바꾸면 다음과 같습니다.
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) + '원 입니다.' ); // 티켓 가격은 10000원 입니다. console.log('어른 티켓 가격은 ' + calcTicket(10000, 'ADULT') + '원 입니다.'); // 어른 티켓 가격은 10000원 입니다. console.log('어린이 2인 티켓 가격은 ' + calcTicket(10000, 'CHILD', 2) + '원 입니다.'); // 어린이 2인 티켓 가격은 10000원 입니다.
위의 함수에서 인수 total
에는 ? 기호가 사용되었는데, 이것이 바로 인자를 옵션으로 지정하는 방법입니다.
함수에서는 이 옵션을 처리하기 위해 total
에 인자가 전달되면 해당 인자를 티켓 수로 변환하여 변수 tickets
에 담고, total
에 인자가 전달되지 않으면 tickets
에 1
을 담는 로직을 추가했습니다. 그래서 최종 계산식에서는 해당 로직에 의해 계산된 티켓의 가격을 반환합니다.
화살표 함수
화살표 함수 표현식은 익명 함수를 간단하게 사용할 수 있는 문법인데, 다음과 같이 function
을 사용하지 않고 단순한 형태로 함수를 선언하는 방식입니다. 원래는 ES6에서 도입된 방식으로 람다 함수 또는 람다 표현식이라고도 합니다.
let getName = () => 'Hong Gil-dong'; console.log( getName() ); // Hong Gil-dong
빈 괄호는 함수에 전달되는 인자가 아무것도 없다는 것을 의미하는데, 코드와 같이 함수를 한 줄로 나타내는 경우에는 함수의 컨텐스트를 지정하는 중괄호{}가 필요없고, return
키워드를 사용할 필요도 없습니다.
코드에 명시적으로 작성되지는 않았지만 getName
함수가 “Hong Gil-dong”을 반환값으로 출력한다는 것을 나타내고 있습니다. 즉 getName
함수가 “Hong Gil-dong”을 반환하기 때문에 console.log()
함수에서는 “Hong Gil-dong”이라는 문자열이 출력되는 것이죠.
위 화살표 함수를 예전의 방식으로 표현하면 다음 처럼 표현할 수 있는데, 같은 동작을 하는 코드임에도 뭔가 더 복잡함이 느껴지지 않나요? ^^;
let getName = function() { return 'Hong Gil-dong'; }; console.log( getName() );
물론 화살표 함수도 여러 줄의 로직을 작성해야하는 경우에는 중괄호{}를 반드시 사용해야 하고, 반환 값이 있을 경우에도 return
키워드를 꼭 사용해야 합니다.
let getNameUpper = () => { let name = 'Hong Gil-dong'.toUpperCase(); return name; } console.log( getNameUpper() ); // HONG GIL-DONG
화살표 함수의 특징
자바스크립트는 함수 안에서 this
키워드를 사용할 때, 함수가 실행되는 컨텍스트 대신 다른 객체를 가리키는 경우가 종종 발생합니다. 이로 인해 수많은 런타임 버그가 발생하기도 하고, 이 문제를 인지하지 못한채 버그를 해결하기 위해 불필요하게 많은 시간을 소비하는 경우도 생깁니다.
다음의 두 코드는 각각 화살표 함수와 익명 함수를 사용하여 구현한 StockQuoteArrow()
와 StockQuoteAnonymous()
함수로, 모두 Math.random()
함수를 호출하여 이 값을 임의의 주가로 사용하고 있습니다.
두 함수는 모두 this
객체의 symbol
이라는 변수에 ‘삼성전자’를 전달하고 있지만, 두 함수의 결과는 서로 다르게 나오는 것을 확인할 수 있습니다.
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()
는 정상적으로 함수의 컨텍스트를 가리키는 변수가 따로 저장되어 있다가 화살표 함수 안에서 this
를 참조할 때 가져와서 사용되기 때문에 this.symbol
에는 정상적인 값이 담기지만, 익명함수를 사용한 StockQuoteAnonymous()
에서는 익명 함수의 this
가 전역 window
객체를 가리키기 때문에 this.symbol
의 값이 undefined
가 되는 것을 확인할 수 있습니다. 이는, StockQuoteAnonymous()
함수에서는 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 인자 처리 }
위와 같이 구현된 함수 오버로딩 코드를 타입스크립트 컴파일러를 사용하여 자바스크립트로 변환하면, 변환된 코드에서는 마지막 함수만 남는 것을 확인할 수 있습니다.
즉 마지막 함수 외의 나머지 함수들은 가장 마지막에 선언한 함수를 다른 형태로 표현한 것에 불과한 거라고 볼 수 있는데, 이렇게 마지막에 선언한 함수만 남는 ‘함수 오버로딩’을 구현하는 이유는 함수가 어떻게 동작하는지를 보다 명확하게 추론할 수 있게 해주기 때문입니다.
클래스
클래스는 정의를 미리 만들어 두고 인스턴스를 생성할 때마다 이 클래스를 불러와서 사용할 수 있는데, 이 때 생성한 클래스에 부모 클래스가 존재한다면 부모와 자식 클래스에 대한 정의를 모두 사용하여 인스턴스를 생성하게 됩니다.
클래스는 객체지향 프로그래밍 언어의 특징이라고 할 수 있지만, 객체지향 언어가 아닌 자바스크립트에서는 지원하지 않는 기능이었습니다. 하지만 타입스크립트에서는 클래스를 생성할 수 있는데, 순수한 객체지향 언어가 아닌 자바스크립트의 상위 집합인 만큼 진정한 객체지향 프로그래밍이라고는 할 수 없습니다.
타입스크립트에서 작성한 클래스를 자바스크립트로 변환해보면 프로토타입을 활용한 상속으로 구현되는 것을 확인할 수 있는데, 프로토타입을 활용한 상속은 어떤 객체의 프로토타입에 있는 프로퍼티에 부모 객체를 지정하여 상속 관계를 동적으로 만드는 방식이라고 할 수 있습니다.
타입스크립트에서 class
키워드로 클래스를 선언하면 new
라는 키워드로 해당 클래스의 인스턴스를 생성할 수 있는데, 이 new
라는 키워드는 자바스크립트에서도 지원하는 키워드로 자바스크립트에서도 함수로 클래스를 구현하고, new
키워드를 사용하여 새로운 인스턴스를 만들 수 있습니다.
결국 타입스크립트에서 지원하는 class
와 extends
키워드는 기존의 방식을 깔끔하고 명확하게 만들어주는 역할을 하는 “문법적 설탕”이라고 볼 수 있습니다.
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 ); // 24 console.log( person.id ); // 000000-0000000
위의 코드는 이름과 나이, 주민등록번호를 저장하는 3개의 프로퍼티를 가지고 있는 Person
이라는 클래스인데, 이 클래스를 자바스크립트로 변환하면 다음과 같은 형태가 됩니다.
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
클래스의 항목은 타입스크립트에서 구현된 코드에 따라 외부로 공개하거나 내부에 감추는 항목이 달라지게 됩니다.
자바스크립트로 클래스를 구현하는 방법 중에서는 클로저를 사용하는 방법이 비교적 간단한 편이지만, 타입스크립트에서는 클래스 문법으로 더 간단하게 클래스를 정의할 수 있습니다.
참고로 클래스의 구성 요소에는 생성자와 프로퍼티, 메소드가 있는데 이 중 프로퍼티와 메소드는 클래스 멤버라고 하고, 클래스 멤버는 클래스의 인스턴스를 생성할 때 생성자를 사용하여 프로퍼티를 초기화할 수 있습니다. 그리고 이 생성자는 인스턴스가 생성될 때 한 번만 실행이 됩니다.
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');
위와 같이 Person
클래스에 생성자를 추가하면, 클래스는 인스턴스를 생성하면서 전달받은 인자들을 해당 클래스의 인자로 전달하여 프로퍼티 값을 초기화하는데, 이렇게 생성자가 추가된 타입스크립트 클래스는 다음과 같은 자바스크립트로 변환이 됩니다.
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
으로 지정했는지 헷갈릴 수 도 있기 때문입니다.
코드를 컴파일 해보면 컴파일러에서 에러가 발생되는 것을 확인할 수 있는데, 그 이유는 바로 마지막의 console.log
에서 Person
클래스의 private
프로퍼티인 _id
에 직접 접근하고 있기 때문입니다.
error TS2341: Property '_id' is private and only accessible within class 'Person'. console.log('name: ' + person.name + ', id: ' + person._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
키워드를 사용한 프로퍼티와 메소드는 해당 클래스로 만들어진 모든 인스턴스에 공유되기 때문에 따로 인스턴스를 만들지 않아도 클래스의 멤버에 직접 접근할 수 있는데, 다음의 경우에는 doSomething
메소드를 static
으로 지정했기 때문에 인스턴스를 참조하지 않고 클래스를 직접 참조해서 사용할 수 있습니다.
class MyClass { static doSomething(times: number):void { // code } } MyClass.doSometing(10);
타입스크립트에서는 클래스의 인스턴스를 만든 후 어떤 메소드에서 같은 클래스에 있는 다른 메소드를 호출하기 위해서는 this
키워드를 반드시 사용해야 하는데, 그렇지 않으면 에러가 발생되기 때문에 주의해야 합니다.
getter와 setter
클래스의 외부에서 private으로 지정된 멤버에 직접 접근을 하면 에러가 발생되는데, 이런 경우에는 get과 set 키워드를 사용해 해당 멤버에 접근할 수 있습니다.
다음은 _id
값은 생략할 수 있도록 ? 기호를 사용하여 옵션으로 지정하고, getter
와 setter
메소드를 추가한 코드로, 클래스의 getter
와 setter
메소드는 객체의 프로퍼티에 접근하기 위해 this
를 사용하고 있습니다.
타입스크립트에서는 클래스의 다른 멤버에 접근할 때 this 키워드를 반드시 사용해야 하는데, 만약 this
키워드를 생략하게 되면 에러가 발생하기 때문입니다.
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
프로퍼티에 직접 접근하는 대신 setter
메소드를 통해 접근하기 때문에 오류가 발생되지 않습니다.
상속
자바스크립트는 프로토타입을 사용한 객체 기반 상속을 지원하는데, 이는 어떤 객체가 다른 객체의 프로토타입이 되는 방식이라고 할 수 있습니다.
타입스크립트에서는 ES6와 객체 기반 언어에서 일반적으로 제공하는 키워드인 extends
를 사용하여 클래스 상속을 구현하고 있는데, 물론 컴파일을 하면 앞에서 이야기한 프로토타입을 사용하는 방식으로 변환되는 것을 확인할 수 있습니다.
class Person { constructor(public name: string, public age: number, private _id: string) {} } class Employee extends Person {}
위와 같이 Person
클래스를 상속하는 Worker
클래스를 정의하는 코드를 작성했을 때, 타입스크립트 컴파일러는 다음과 같은 자바스크립트 코드로 변환합니다.
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));
자바스크립트로 변환된 코드를 보면 클래스의 상속이 프로토타입을 사용하는 방식으로 변환된 것을 확인할 수 있는데, 같은 동작을 하는 코드이지만 타입스크립트로 작성한 코드가 훨씬 간단하고 읽기 편한 것을 알 수 있습니다.
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(); ... }