[Typescript] Typescript 의 Generic Type

Generic type 이란?

Generic 은 정적 타입 언어를 사용하던 사람에게는 매우 친숙한 단어이다. 여기에서 Generic 은 함수나 클래스를 선언할 때 타입을 고정하지 않고, 사용 때 명시해 타입을 유연하게 사용할 수 있게 해준다. 예를 들어 선언과 동시에 타입을 선언한다고 하면 다음과 같은 것이다.

function sum (str1: string, str2: string): string {
    return str1 + str2;
}

위의 코드를 보면 str1이라는 string 타입의 인자와 str2 라는 string 타입의 인자를 받아서 합쳐준 후, string 으로 반환해주는 sum 이라는 함수가 있다. 이 것만 봤을 때는 크게 문제는 없어 보인다. 그런데 만약 number 형의 인자를 받아서 처리를 해야 하는 경우가 생겼다면 어떻게 변화가 되어야 할까?

function sumString (str1: string, str2: string): string {
    return str1 + str2;
}
function sumNumber (num1: number, num2: number): number {
    return num1 + num2;
}

아마도 위처럼 처리되는 것에 따라 함수 자체를 나눠주거나 타입을 아래 처럼 any 로 처리를 해야할 수 있다.

function sum (str1: any, str2: any): any {
    return str1 + str2;
}

하지만 이렇게 되는 경우의 문제점이 있다. 위처럼 단순한 코드에서는 타입이 무엇이든 신경을 안 쓸순 있다. (물론 사실 이렇게 쓴다면 타입스크립트 보단 그냥 자바스크립트만 쓰는 게 더 나은 것 같다) 하지만 만약 그 규모가 커진다고 한다면 정확한 타입 추정이 가능해야 한다. 또한 그 타입에 따라 사용할 수 있는 고유의 메소드들 역시 다를 수 있는데, 타입이 명확하지 않으면 일단 런타임이든 아니면 그 이전이든 에러를 뿜어낼 것이다. 예를 들어 sum 이라는 함수에서 return 해준 결과가 string 이라고 생각하고 그 string 의 특정 문자열을 구하려고 한다면 런타임 에러가 뜨는 것을 볼 수 있다.

function sum (str1: any, str2: any): any {
    return str1 + str2;
}
sum(1, 23).indexOf('2');    // Error!

혹은 여러개의 함수가 유기적으로 얽혀있는 모듈 중에 하나의 기능이 예상치 못한 결과를 반환해주어 예상치 못한 결과를 가져올 수 있을 것이다.

function sum (str1: any, str2: any): any {
    return str1 + str2;
}
sum(1, 23).length;  // undefined

물론 위와 같이 예상치 못한 결과, 에러 등도 그 이유겠지만, 무엇보다 타입스크립트에서 지원해주는 타입 추론에 대한 안정성이 떨어진다는 것이 제일 큰 단점이다. 이때 generic 타입을 사용하게 되면 타입 추론의 안정성과 위 예제와 같은 유연성을 동시에 가져갈 수 있다.

Generic type 의 장점

일단 Generic 타입의 장점으로는 실행 시 타입을 선언할 수 있다는 점에서 오는 재사용성이 있다. 위처럼 string, number 와 같이 명시적으로 써줄 필요 없이 실행하는 곳에서 원하는 타입을 선언해주면 된다. 또한 그렇게 타입을 선언함으로써 타입의 안정성을 보장 받을 수 있다.

// 여기에서 캐스팅 코드에 대해서는 밑에서 다시 설명한다. 
function sum<T> (str1: T, str2: T): T {
    return String(str1) + String(str2);
}

위와 같이 선언함과 동시에 타입을 선언해주는 것이 아니라 유연하게 사용할 수 있게 실행과 동시에 타입을 선언할 수 있도록 했다. 위의 코드는 단순하다. sum 이라는 함수는 무엇인지 모를 타입 T 를 받아 T라는 타입을 가진 인자를 2개 받은 후, 더해서 T 라는 타입으로 반환시켜준다. 여기에서 T 는 타입 매개변수(type parameter) 또는 제네릭 타입 변수(generic type variables) 라 부르며 타입이 정해져 있지 않은 가상의 타입으로서 임의의 알바벳 혹은 단어를 선언해도 된다.

위와 같은 함수 선언을 제네릭 함수 선언이라고 표현하다.

function sum<T> (str1: T, str2: T) {
    return String(str1) + String(str2);
}

sum<string>('abc', 'efg');

코드를 살펴보면 sum 이라는 함수는 무언지 모를 익명의 타입 T 를 받아 함수의 인자 str1str2 에게 전달을 해준다. 그 후, 함수를 실행할 때, 익명타입 T 에 대해 string 타입을 넘겨준다.

function sum<T> (str1: T, str2: T) {
    return String(str1) + String(str2);
}

sum<string>(123, 'efg');

만약 인자로 string 이 아닌 number 타입을 넘기려고 시도하는 순간 Argument type 123 is no assignableto parameter type string 이라는 문구를 볼 수 있다. 이렇게 타입의 안정성과 재사용할 수 있는 코드를 만들 수 있다. 하지만 아직도 함수 안에서는 타입 캐스팅이 이뤄져 불필요한 성능을 낭비하고 있습니다.

그 부분을 해결하기 위해 오버로드 함수(overload function) 을 이용하며 된다.

function sum<T>(str1: T, str2: T): T;
function sum(str1: any, str2: any) {
    return str1 + str2;
}

sum<string>('123', 'efg');

위와 같이 함수명은 overload 함으로써, 기존의 함수는 익명 타입 T 를 받아 인자들도 동일하게 T 타입을 받는다. 그리고 실제 작성된 함수에서는 각 인자는 any 타입의 인자를 받음으로써, 어떠한 인자가 들어와도 허용할 수 있도록 한다. 여기에서 어떠한 인자가 들어와도 라고 표현했지만, 실제 타입이 T 와 같지 않으면 위와 동일하게 Argument type 123 is no assignableto parameter type string 에러가 난다.

function sum<T>(str1: T, str2: T): T;
function sum(str1: any, str2: any) {
    return str1 + str2;
}

sum<string>(123, 'efg');

반대로 만약 파라미터의 갯수가 일치하지 않거나 타입이 일치하지 않으면 overload signature is not compatible with function implementation 라는 에러가 나는 것은 볼 수 있다.

function sum<T>(str1: T, str2: T): T;
function sum(str1: any, str2: any, str3: string) {
    return str1 + str2;
}

sum<string>('123', 'efg');

이와 같이 오버로드 함수를 이용하게 되면 좀 더 유연한 제네릭 타입을 사용할 수 있다.

Comments