코딩/Typescript

타입스크립트 제네릭 - 타입스크립트 TypeScript 강좌 7편

드리프트 2021. 11. 8. 17:09
728x170

 

 

 

안녕하세요?

 

오늘은 타입스크립트에 제공하는 제네릭(Generics)에 대해 알아보겠습니다.

 

C++, Java 같은 고급 언어에서는 제네릭을 기본 제공하고 있는데요.

 

타입스크립트에서도 제네릭을 쓸 수 있습니다.

 

어떻게 하는지 함께 알아보겠습니다.

 

일단 지난 시간에 만들었던 코드를 볼까요?

 

function simpleState(initial: string): [() => string, (v: string) => void] {
    let str: string = initial;
    return [
        () => str,
        (v: string) => {
            str = v;
        }
    ]
}

 

simpleState라는 클로저를 이용한 함수입니다.

 

그런데 잘 보시면 string 이란 타입이 여러 군데 보이는데요.

 

제네릭은 특정 타입을 이용해서 템플릿처럼 함수를 만들 수 있습니다.

 

number 타입, string 타입 등 특정 타입으로의 함수 템플릿을 만들 수 있어서 똑같은 이름의 함수이지만 받아들이는 타입이 달라지는 경우죠.

 

그럼 위 코드에서 string 부분을 제네릭의 T로 바꿔 보겠습니다.

 

function simpleState<T>(initial: T): [() => T, (v: T) => void] {
    let val: T = initial;
    return [
        () => val,
        (v: T) => {
            val = v;
        }
    ]
}

 

string이라고 타입을 지정했었는데요. 이걸 전부 T로 바꿨습니다.

 

꼭 T라는 이름으로 쓸 필요는 없고요. V나 K 등 원하는 이름으로 쓸 수 있습니다.

 

그리고 제네릭이라고 표시하기 위해 simpleState 함수 이름 뒤에 <T>라고 제네릭 표시를 넣었습니다.

 

우리가 앞에서 배열 제네릭 표시를 잠깐 배웠었는데요.

 

Array<string> 이라고 말입니다.

 

<T>처럼 T는 string이 될 수 있고 number가 될 수 있습니다.

 

그럼 실행해 볼까요?

 

다음과 같이 simpleState 함수를 호출하려고 할 때 VS Code의 인텔리 센스가 보여주고 있는 걸 보시면,

 

위 그림처럼 unknown이라고 나옵니다.

 

왜냐하면 T는 타입인데 아직 모른다는 얘기죠.

 

그런데 여기서 숫자 1을 넣으면 아래와 같이 바뀝니다.

 

 

number 타입이 T로 대응되기 때문에 simpleState라는 함수의 형식이 위 그림처럼 number 타입으로 바뀐 겁니다.

 

이제 아래 코드를 실행해 볼까요?

 

const [st1getter, st1setter] = simpleState(1)
console.log(st1getter());
st1setter(2);
console.log(st1getter());

 

위 그림처럼 number 타입으로 아주 잘 실행되고 있습니다.

 

이처럼 제네릭은 그때그때 타입이 바뀔 수 있는 함수를 만들 수 있는 아주 막강한 기능입니다.

 

여기서 더 나아가서 만약에 초기 타입이 null인 경우 즉, simpleState의 initial 값을 null로 주고 그다음에 다시 string 타입으로 줄려고 하려면 어떻게 할까요?

 

const [st1getter, st1setter] = simpleState(null)
console.log(st1getter());
st1setter("str");
console.log(st1getter());

 

 

위에서 VS Code에서 보시면 에러가 생겼습니다.

 

simpleState(null)로 null 타입의 제네릭을 실행시켰는데 그 다음에 "str"처럼 string 타입으로 바꾸면 에러가 나는 건 당연합니다.

 

그러면 어떻게 해야 할까요?

 

이럴 때는 simpleState 제네릭을 오버라이드 해야 하는데요. 다음과 같이 하시면 됩니다.

 

const [st1getter, st1setter] = simpleState<string | null>(null)

 

simpleState함수를 실행할 때 <string | null>을 추가해서 제네릭의 타입을 오버라이드 했습니다.

 

그래서 여기서 실행한 simpleState는 string 또는 null을 받을 수 있는 거죠.

 

이렇게 바꾸니까 VS Code의 에러가 사라졌습니다.

 

실행해 볼까요?

 

우리가 원한 방식으로 잘 실행되었습니다.

 

제네릭의 어려운 예제를 한번 더 알아보겠습니다.

 

ranker라는 함수를 만들어 보겠습니다.

 

ranker는 배열 아이템을 특정 특정 함수로 랭킹을 메기는 함수인데요.

 

어떻게 만들까요?

 

function ranker(items: unknown[], rank: (v: unknown) => number): unknown[] {

}

 

위 코드처럼 일단 items는 unknown 타입의 배열로 볼 수 있고요.

 

특정 함수인 rank 함수는 (v: unknown) => number 형식이고

 

그리고 최종적으로 unknown[]을 리턴하는 함수입니다.

 

이제 이걸 제네릭 타입으로 만들어 볼까요?

 

일단 unknown 이 마음에 안 드네요.

 

function ranker<RankItem>(items: RankItem[], rank: (v: RankItem) => number): RankItem[] {

}

 

위 코드처럼 unknown을 RankItem으로 바꿨고 그다음 ranker 함수 뒤에 <RankItem>이라고 제네릭 타입을 주었습니다.

 

이제 마저 코드를 작성해 볼까요?

 

function ranker<RankItem>(
    items: RankItem[],
    rank: (v: RankItem) => number
): RankItem[] {
    const ranks = items.map((item) => ({
        item,
        rank: rank(item)
    }));

    ranks.sort((a, b) => a.rank - b.rank);

    return ranks.map((rank) => rank.item);
}

 

코드가 뭔가 어려운데요.

 

일단 제네릭 부분만 유의해서 보겠습니다.

 

위 코드에서 ranks 변수에 대한 타입이 없는 데요. 마우스를 슬쩍 갖다 놓으면 아래와 같이 나옵니다.

 

 

VS Code가 알려주기를 ranks는 객체의 배열인데 item과 rank를 가지고 있다고 하네요.

 

그럼 ranks의 타입을 만들어 볼까요?

 

function ranker<RankItem>(
    items: RankItem[],
    rank: (v: RankItem) => number
): RankItem[] {
    interface Rank {
        item: RankItem;
        rank: number;
    }
    const ranks: Rank[] = items.map((item) => ({
        item,
        rank: rank(item)
    }));

    ranks.sort((a, b) => a.rank - b.rank);

    return ranks.map((rank) => rank.item);
}

 

위 코드에서 볼 수 있듯이 interface로 Rank를 만들고 ranks의 타입을 Rank[]처럼 지정했습니다.

 

그런데 interface Rank처럼 Rank 인터페이스를 함수 안에 선언했는데요.

 

만약에 interface Rank 부분을 함수 밖으로 옮기면 어떻게 될까요?

 

 

interface Rank를 함수 밖으로 넘기니까 이제는 RankItem 이 모르는 이름이라고 나오네요.

 

맞습니다. RankItem은 함수 안에서 쓰는 제네릭 타입이거든요.

 

그럼 어떻게 해야 할까요?

 

interface에도 제네릭을 추가할 수 있습니다.

 

interface Rank<RankItem> {
    item: RankItem;
    rank: number;
}

function ranker<RankItem>(
    items: RankItem[],
    rank: (v: RankItem) => number
): RankItem[] {

    const ranks: Rank<RankItem>[] = items.map((item) => ({
        item,
        rank: rank(item)
    }));

    ranks.sort((a, b) => a.rank - b.rank);

    return ranks.map((rank) => rank.item);
}

 

interface 에도 Rank<RankItem> 처럼 제네릭 타입을 넣었습니다.

 

그리고 const ranks: Rank<RankItem>[] 처럼 여기에서 제네릭 타입을 넣었습니다.

 

이제 VS Code가 에러를 뿜어내지 않는데요.

 

이처럼 제네릭은 타입스크립트에서 있어 클래스, 함수, 인터페이스 등 아무 곳에 적용할 수 있습니다.

 

이제 ranker 함수를 사용해 볼까요?

 

interface Squid {
    name: string;
    hp: number;
}

const squidGame: Squid[] = [
    {
        name: "Player456",
        hp: 100,
    },
    {
        name: "Player218",
        hp: 90,
    }
]

 

위 코드처럼 squidGame 배열을 만들었는데요.

 

이제 이 배열을 ranker 함수로 랭킹을 정해 보겠습니다.

 

const ranks = ranker(squidGame, ({ hp }) => hp);
console.log(ranks);

 

위 코드를 입력할 대 아래처럼 ranker 함수의 두 번째 파라미터가 나옵니다. Squid를 넣고 number가 나옵다는 얘기죠

 

그래서 {hp}라는 객체를 넣은 겁니다.

 

 

실행 결과입니다.

 

 

sort 함수의 특성상 작은 거부터 나왔네요.

 

제네릭 부분이 잘 작동되고 있다는 증거입니다.

 

지금까지 타입스크립트의 제네릭에 대해 알아보았습니다.

 

 

 

그리드형