제네릭(Generic)이란?
제네릭은 타입스크립트에서 함수, 클래스, 인터페이스, 타입 등을 다양한 타입과 함께 동작하도록 만드는 기능입니다. 즉, 특정 타입에 종속되지 않고, 여러 타입에 대해 범용적으로 동작할 수 있는 코드를 작성하는 것을 목표로 합니다.
제네릭이 필요한 상황
함수에서 다양한 타입의 매개변수를 받아 그대로 반환하는 경우를 예로 들어 설명할 수 있습니다.
function func(value: any) {
return value;
}
let num = func(10);
let str = func("string");
- 위 코드에서는 매개변수 value의 타입을 **any**로 설정했습니다. 이로 인해 함수 func가 어떤 타입을 받든 받아들일 수 있습니다.
- 그러나 num과 str 모두 any 타입으로 추론되므로 타입스크립트가 런타임 오류를 감지하지 못합니다. 예를 들어 num.toUpperCase()와 같은 부적절한 메서드 호출도 컴파일 오류 없이 통과되어, 결국 실행 시 오류가 발생하게 됩니다.
이를 해결하기 위해 unknown 타입을 사용할 수 있지만, 이 경우 각 변수의 실제 타입을 명시적으로 확인하고 타입 좁히기(type narrowing)를 해야 하는 번거로움이 발생합니다.
제네릭을 통한 해결
제네릭을 사용하면 이러한 문제를 쉽게 해결할 수 있습니다.
function func<T>(value: T): T {
return value;
}
let num = func(10); // num은 number 타입으로 추론됨
let str = func("string"); // str은 string 타입으로 추론됨
- <T>: 여기서 T는 타입 변수로, 매개변수의 타입을 함수가 호출될 때 결정하도록 합니다.
- 함수가 호출될 때 전달된 인수에 따라 T가 결정되고, 반환 타입도 동일하게 T로 설정됩니다.
- func(10)을 호출하면 T는 **number**로 추론되고, 반환 타입도 number가 됩니다. 마찬가지로 func("string")을 호출하면 T는 **string**으로 추론됩니다.
이렇게 하면 함수의 매개변수로 전달된 타입을 반환 타입으로 유지할 수 있어 타입 안정성을 보장하게 됩니다.
제네릭 함수의 호출 시 타입 명시
제네릭 함수 호출 시 타입을 직접 명시하는 것도 가능합니다.
function func<T>(value: T): T {
return value;
}
let arr = func<[number, number, number]>([1, 2, 3]);
- 여기서 T는 [number, number, number] (튜플 타입)으로 명시적으로 설정되었습니다.
- 이로 인해 매개변수와 반환값 모두 튜플 타입이 되며, 타입스크립트는 더 구체적인 타입을 사용하게 됩니다.
타입스크립트는 일반적으로 타입을 추론하며, 더 범용적인 타입으로 설정하려고 합니다. 따라서 특별히 구체적인 타입을 원할 때는 타입 변수를 직접 명시할 수 있습니다.
제네릭의 장점
- 타입 안정성: 제네릭은 컴파일 시 타입을 확인하므로, 런타임 오류를 줄이고 코드의 타입 안전성을 보장합니다.
- 재사용성: 타입에 독립적이므로 여러 타입에 대해 재사용 가능한 코드를 작성할 수 있습니다.
- 유연성: 제네릭을 사용하면 다양한 타입을 받아들일 수 있어 코드의 유연성이 높아집니다.
제네릭의 요약
- 제네릭은 다양한 타입에 대해 동작하는 범용적인 함수를 만들기 위한 타입스크립트의 기능입니다.
- any 타입을 사용하면 타입 안정성을 잃게 되고, unknown 타입을 사용하면 타입 좁히기가 번거로워지지만, 제네릭을 사용하면 호출 시 전달된 타입에 맞춰 반환 타입을 결정하여 이러한 문제를 해결합니다.
- 제네릭을 사용할 때는 함수 뒤에 **<T>**와 같은 타입 변수를 선언하고, 이를 매개변수와 반환 타입에 활용합니다.
제네릭은 코드의 유연성, 안정성, 재사용성을 모두 높일 수 있는 강력한 기능이므로, 타입에 구애받지 않으면서도 타입 안전성을 유지하고자 할 때 활용하면 좋습니다.
제네릭(Generic) 사례 정리
**제네릭(Generic)**은 다양한 타입을 유연하게 다루기 위한 타입스크립트의 강력한 기능입니다. 이 기능을 통해 여러 타입에 대해 안전하게 작업하면서 코드 재사용성을 극대화할 수 있습니다.
아래에서는 다양한 제네릭의 사례를 통해 어떻게 제네릭을 활용할 수 있는지, 각 사례의 목적과 결과를 살펴보겠습니다.
사례 1: 두 개의 타입 변수가 필요한 경우
- 제네릭 타입 변수를 여러 개 사용하면, 함수나 클래스에서 서로 다른 타입을 유연하게 다룰 수 있습니다.
- 예제:
- 이 함수는 두 개의 타입 변수 T와 U를 사용하여 두 값의 순서를 뒤바꿉니다.
- **"1"**은 T로 string 타입, **2**는 U로 number 타입으로 추론됩니다.
- 함수의 반환 타입은 [U, T]로 [number, string]이 됩니다.
-
function swap<T, U>(a: T, b: U): [U, T] { return [b, a]; } const [a, b] = swap("1", 2);
사례 2: 다양한 배열 타입을 인수로 받는 제네릭 함수
- 제네릭 타입을 사용하여 다양한 배열 타입을 처리할 수 있습니다.
- 예제:
- 함수 매개변수의 타입을 **T[]**로 설정하여 배열 타입만 받을 수 있게 제한했습니다.
- **num**은 [0, 1, 2] 배열의 첫 번째 요소 타입인 **number**로 추론됩니다.
- **str**은 [1, "hello", "world"] 배열의 첫 번째 요소인 **number | string**으로 추론됩니다.
-
function returnFirstValue<T>(data: T[]): T { return data[0]; } let num = returnFirstValue([0, 1, 2]); // number let str = returnFirstValue([1, "hello", "world"]); // number | string
사례 3: 배열의 첫 번째 요소의 타입을 반환하는 제네릭 함수
- 배열의 첫 번째 요소의 타입만 반환하고 싶을 때는 튜플 타입과 **나머지 매개변수(...)**를 사용하여 타입을 정의할 수 있습니다.
- 예제:
- 여기서는 매개변수를 튜플 타입으로 지정하여 첫 번째 요소만 특정한 타입(T)을 가집니다.
- T는 배열의 첫 번째 요소의 타입으로 결정되며, 따라서 반환 타입도 그 첫 번째 요소의 타입이 됩니다.
- 위 코드에서는 T가 number로 추론되어 반환값의 타입도 number가 됩니다.
-
function returnFirstValue<T>(data: [T, ...unknown[]]): T { return data[0]; } let str = returnFirstValue([1, "hello", "world"]); // number
사례 4: 타입 변수를 제한 (extends 사용)
- 특정 타입에 제한을 두고 싶을 때 **extends**를 사용하여 제네릭 타입을 제한할 수 있습니다.
- 예제:
- T extends { length: number }: 여기서 T는 length 프로퍼티를 가진 객체 타입으로 제한됩니다.
- 이를 통해 T는 반드시 length 프로퍼티를 가진 타입이어야 합니다.
- 문자열(string), 배열(Array), 그리고 length 프로퍼티를 가진 객체는 모두 허용됩니다.
- 반면, **undefined**와 **null**은 length 프로퍼티가 없으므로 오류가 발생합니다.
-
function getLength<T extends { length: number }>(data: T): number { return data.length; } getLength("123"); // ✅ 허용 (string 타입은 length 프로퍼티가 있음) getLength([1, 2, 3]); // ✅ 허용 (배열은 length 프로퍼티가 있음) getLength({ length: 1 }); // ✅ 허용 (length 프로퍼티가 있는 객체) getLength(undefined); // ❌ 오류 (length 프로퍼티가 없음) getLength(null); // ❌ 오류 (length 프로퍼티가 없음)
제네릭 사용의 이점 요약
- 유연한 타입 지원: 제네릭을 사용하면 특정한 타입에 종속되지 않고 다양한 타입을 받아들일 수 있습니다.
- 타입 안전성: 제네릭은 함수나 클래스가 다룰 타입을 호출 시점에 결정하므로, 타입 안전성을 확보하면서 다양한 경우에 대응할 수 있습니다.
- 재사용성: 제네릭은 같은 함수나 클래스를 여러 타입에 대해 재사용 가능하게 만들어 줍니다.
- 제약 조건 설정: extends를 사용해 제네릭 타입에 제한을 설정할 수 있으며, 이를 통해 특정 조건을 만족하는 타입만 사용할 수 있습니다.
사례의 요약
- 사례 1: 두 개의 타입 변수가 필요한 경우, 제네릭 타입 변수를 여러 개 사용하여 서로 다른 타입을 유연하게 다룰 수 있습니다.
- 사례 2: 배열 타입을 인수로 받는 제네릭 함수에서, 배열의 첫 번째 요소 타입에 맞춰 반환값의 타입을 추론할 수 있습니다.
- 사례 3: 튜플 타입과 나머지 매개변수를 이용해 첫 번째 요소의 타입만 추론하고 반환할 수 있습니다.
- 사례 4: 타입 변수를 제한하여 특정 조건(예: length 프로퍼티가 존재)을 만족하는 타입만 허용하는 제네릭 함수를 만들 수 있습니다.
제네릭은 타입스크립트에서 코드를 재사용하고 타입 안정성을 유지하는 데 매우 유용한 기능입니다. 이를 잘 활용하면 다양한 타입에 대해 안전하고 확장 가능한 코드를 작성할 수 있습니다.
제네릭을 활용한 배열 메서드 구현 및 타입 정의
자바스크립트의 배열 메서드인 **map**과 **forEach**는 배열을 조작하는 데 매우 유용한 함수입니다. 이들을 제네릭을 이용하여 직접 구현하고 타입 정의까지 어떻게 하는지 단계별로 설명합니다.
1. Map 메서드 구현과 타입 정의
map 메서드는 배열의 각 요소에 콜백 함수를 적용한 결과로 새로운 배열을 반환하는 메서드입니다.
const arr = [1, 2, 3];
const newArr = arr.map((it) => it * 2);
// [2, 4, 6]
- 일반적인 함수 형태로 구현:
- arr: 배열의 각 요소에 작업을 수행할 원본 배열 (unknown[] 타입).
- callback: 각 배열 요소에 적용할 함수 ((item: unknown) => unknown).
- 이 함수는 모든 타입의 배열에 적용할 수 있어야 하므로 unknown 타입을 사용했지만, 이후 제네릭으로 수정합니다.
-
function map(arr: unknown[], callback: (item: unknown) => unknown): unknown[] {}
- 제네릭 함수로 수정:
- 타입 변수 T를 사용해 원본 배열의 타입을 추론하고, 콜백 함수의 매개변수 및 반환값 타입도 T로 설정하여 타입 안전성을 확보합니다.
-
function map<T>(arr: T[], callback: (item: T) => T): T[] { let result = []; for (let i = 0; i < arr.length; i++) { result.push(callback(arr[i])); } return result; }
- 타입 변수 추가하기:
- map 함수는 원본 배열과 다른 타입으로 변환도 가능합니다. 이를 위해 두 개의 타입 변수를 추가합니다.
-
function map<T, U>(arr: T[], callback: (item: T) => U): U[] { let result = []; for (let i = 0; i < arr.length; i++) { result.push(callback(arr[i])); } return result; }
- T는 원본 배열 요소의 타입이고, U는 콜백 함수 반환값의 타입입니다.
- 이렇게 하면 배열 요소의 타입과 변환된 배열의 타입이 다를 수 있습니다.
- 사용 예시:
-
const arr = [1, 2, 3]; const stringArr = map(arr, (it) => it.toString()); // string[] 타입의 배열 반환 // 결과 : ["1", "2", "3"]
- 이 코드에서는 number[] 타입의 배열을 문자열로 변환하여 string[] 타입의 배열로 만들었습니다.
- 이렇게 제네릭을 통해 타입 변환이 유연해졌습니다.
-
2. ForEach 메서드 구현과 타입 정의
forEach 메서드는 배열의 모든 요소에 대해 콜백 함수를 수행하는 메서드로, 새로운 배열을 반환하지 않습니다.
const arr2 = [1, 2, 3];
arr2.forEach((it) => console.log(it));
// 출력 : 1, 2, 3
직접 구현하기
- forEach 메서드 구현:
-
function forEach<T>(arr: T[], callback: (item: T) => void): void { for (let i = 0; i < arr.length; i++) { callback(arr[i]); } }
- 매개변수:
- arr: 순회할 배열 (T[] 타입).
- callback: 배열의 각 요소에 대해 수행할 함수 ((item: T) => void).
- 반환 타입은 void이며, 새로운 배열을 반환하지 않고 단지 콜백 함수를 배열의 각 요소에 대해 호출만 합니다.
-
- 사용 예시:
-
const arr2 = [1, 2, 3]; forEach(arr2, (it) => console.log(it)); // 출력 : 1, 2, 3
- 배열의 각 요소를 순회하면서 콜백 함수를 호출하여 출력만 수행합니다.
-
제네릭 메서드 타입 정의 요약
- Map 메서드:
- 원본 배열의 각 요소에 대해 콜백 함수를 수행하고, 그 결과를 모아 새로운 배열을 반환합니다.
- 제네릭을 이용해 두 개의 타입 변수 T와 U를 사용하여 원본 배열의 타입과 변환된 배열의 타입을 유연하게 정의합니다.
-
function map<T, U>(arr: T[], callback: (item: T) => U): U[] { let result = []; for (let i = 0; i < arr.length; i++) { result.push(callback(arr[i])); } return result; }
- ForEach 메서드:
- 배열의 각 요소에 대해 콜백 함수를 수행하지만, 새로운 배열을 반환하지 않습니다.
- 제네릭을 사용하여 배열의 타입을 유지하고, 콜백 함수의 반환 타입을 **void**로 설정합니다.
-
function forEach<T>(arr: T[], callback: (item: T) => void): void { for (let i = 0; i < arr.length; i++) { callback(arr[i]); } }
제네릭 메서드를 사용하는 이유
- 타입 유연성: 제네릭을 사용하면 다양한 타입의 데이터를 처리할 수 있으면서도, 타입 안전성을 유지할 수 있습니다.
- 코드 재사용성: 제네릭을 통해 같은 함수나 메서드를 여러 타입에 대해 재사용 가능하게 만들 수 있습니다.
- 타입 안전성: 제네릭은 타입스크립트의 강력한 타입 추론 기능을 사용하여 런타임 오류를 줄이고 컴파일 시점에 오류를 미리 잡을 수 있습니다.
위의 설명을 바탕으로 제네릭을 사용하여 구현한 map과 forEach 메서드는 타입 안전성을 유지하면서도 유연한 타입 변환을 가능하게 합니다. 이를 통해 더욱 안정적이고 재사용 가능한 코드를 작성할 수 있습니다.
'TypeScript' 카테고리의 다른 글
TypeScript 타입조작하기 (0) | 2024.11.02 |
---|---|
TypeScript 제네릭2 (0) | 2024.10.29 |
TypeScript 인터페이스로 구현하는 클래스 (0) | 2024.10.29 |
TypeScript 클래스 (1) | 2024.10.29 |
JavaScript this정리 (0) | 2024.10.29 |