let을 사용하면 0, 1, 2, 3, 4가 약 1초 뒤 한번에 출력되는 것을 확인할 수 있고 var를 사용하면 5가 다섯 번 출력되는 것을 확인할 수 있습니다.
우선 var는 왜 5를 다섯 번 출력할까요?
이는 이벤트 루프의 처리에 따라 setTimeout 메서드가 호출 스택에서 백그라운드를 거쳐 태스크 큐로 이동했다가 다시 콜 스택으로 돌아오는 동안 for 문이 모두 종료(콜 스택이 비어 있어야 태스크 큐에서 콜 스택으로 이동)되어 출력할 데이터는 i=5인 클로저를 참조하기 때문입니다.
그렇다면 var와 달리 let은 왜 순서대로 숫자가 출력될까요?
이는 스코프와 관련이 있습니다.
var는 함수 스코프를 가지므로 for 루프마다 같은 참조를 바인딩하고 let은 블록 스코프이므로 for 루프마다 새로운 참조를 바인딩하게 됩니다.
그렇다면 var를 사용하더라도 for 루프마다 새로운 참조를 바인딩하면 결과가 달라지지 않을까요?
타입스크립트는 변수나 파라미터 등에 데이터 타입을 명시하여 코드 작성 단계에서 오류를 확인할 수 있고 미리 타입을 결정하여 실행 속도가 빠르다는 장점이 있습니다.
그러나 매번 타입을 지정하는 것은 분명 번거로운 일이므로 타입스크립트의 단점으로 꼽힙니다.
더욱이 똑같은 기능을 하는 함수지만 상황에 따라 다른 데이터 타입을 전달받는 함수는 매번 타입에 맞는 함수를 생성해야 합니다.
그렇다면 데이터를 전달할 때 데이터 타입 정보도 함께 전달해서 함수에게 알려주면 어떨까요?
전달하는 데이터의 타입 정보를 함께 전달하여 선언 시점이 아닌 생성 시점에 타입 정보도 변수처럼 변경할 수 있도록 하는 기능이 바로 제네릭입니다.
제네릭은 오래 전 C++부터 사용되어 왔는데요.
제네릭 함수의 모양새는 다음과 같습니다.
//함수형
const thisGeneric = <T>(value: T):T => {
return value;
}
//클래스형
function thisGeneric2<T>(value: T) : T {
return value;
}
const myVariable = thisGeneric<string>('my generic');
const myVariable2 = thisGeneric2('my generic2'); //< >생략 시 타입 추론
< >를 사용해 제네릭을 표현하고 안의 T는 정해진 것이 아니므로 다른 글자를 사용해도 되지만 관습적으로 T(type의 t)를 많이 사용합니다. 만약 두 개의 제네릭 정보를 받는 경우에는 <T, U>와 같이 표기합니다.
많이 사용되는 알파벳의 의미는 다음과 같습니다.
T : type E : element K : key V : value
어떤 타입을 전달해도 사용이 가능한 any도 제네릭으로 볼 수 있지만 any는 타입을 체크하지 않으므로 전달받은 데이터의 타입을 알 수 없고 리턴 시에도 타입 정보를 반환하지 않습니다. 그러나 제네릭은 전달받은 타입을 확인하고 정보를 함께 반환할 수 있으며 세부적인 제한을 둘 수도 있습니다.
제네릭 인터페이스도 사용 방법은 제네릭 함수와 동일합니다.
interface ValueInterface<T> {
value : T
}
let myValue : ValueInterface<string> {
value : 'data'
}
그렇다면 or의 연산자 | 를 사용하여 여러 타입을 지정할 수 있는 유니온 타입과는 어떤 차이가 있을까?
유니온은 | 로 지정한 여러 타입의 공통 메서드만 사용할 수 있으며 리턴 데이터의 타입도 하나가 아닌 유니온 타입이 반환됩니다.
function myUnion(val:number|string){
return val;
}
const test = myUnion('string');
console.log(test.length); //에러 (length는 number와 string의 공통 메서드가 아니므로)
유사한 기능이 존재하지만 제네릭이 갖는 장점이 명확합니다.
제네릭을 사용하면 깔끔하게 코드를 줄이면서도 코드의 재활용이 가능해 매우 유용합니다.
제네릭을 사용한 오픈 소스들이 매우 많으므로 제네릭을 익혀두면 다른 고급 개발자들의 코드를 살펴볼 때도 큰 도움이 될 것 같습니다.