01
제네릭이란?
제네릭은 간단하게 "함수나 클래스, 객체 등이 다양한 타입을 처리할 수 있도록 도와주는 도구"라고 할 수 있습니다.
예를 들어, 우리가 숫자나 문자를 다루는 함수가 필요할 때, 각기 다른 타입을 처리하려면, 그때마다 함수를 따로 만들어야 할 수도 있죠. 그런데 제네릭을 사용하면, 한 번만 작성한 함수로 다양한 타입을 처리할 수 있게 됩니다.
1. 제네릭이 필요한 이유
예를 들어, 우리가 받은 값에 대해 그 값을 그대로 반환하는 함수가 있다고 생각해 볼게요.
처음에는 이런 방식으로 작성할 수 있죠:
function func(value) {
return value;
}
이 함수는 어떤 타입이든 받을 수 있지만, 타입스크립트에서 any나 unknown을 사용하게 되면 그 타입이 추론되지 않아서 문제가 생길 수 있어요. 예를 들어, 숫자와 문자가 섞여 있는 데이터를 처리할 때, any 타입은 문제가 될 수 있죠.
2. 제네릭 함수 만들기
그럼 이제 제네릭을 사용하면 이런 문제를 해결할 수 있어요.
제네릭을 사용해서 타입을 유연하게 처리할 수 있는 함수를 만들 수 있거든요. 예를 들어:
function func<T>(value: T): T {
return value;
}
여기서 T는 타입 변수입니다. 우리가 어떤 값을 넣을지 모르기 때문에,
T라는 변수를 사용해 두고, 실제로 함수를 사용할 때 T가 어떤 타입인지 자동으로 결정되도록 해줍니다.
(제네릭함수는 함수가 호출될 때 인수에 따라 타입이 정해집니다.)
let num = func(10); // Number 타입
let str = func("hello"); // String 타입
위처럼 func 함수는 T가 숫자일 수도 있고 문자열일 수도 있는 범용적인 함수로 바뀌었습니다.
func(10)을 호출하면 T는 숫자 타입으로, func("hello")를 호출하면 T는 문자열 타입으로 자동으로 설정됩니다.
제네릭 함수를 호출할 때 다음과 같이 타입 변수에 할당할 타입을 직접 명시하는 것도 가능합니다.
function func<T>(value: T): T {
return value;
}
let arr = func<[number, number, number]>([1, 2, 3]);
02
타입 변수 응용하기
1. 2개의 타입 변수가 필요한 상황
가끔 두 개의 타입을 다뤄야 하는 경우가 있습니다.
예를 들어, 두 개의 값을 받아서 그 순서를 바꾸는 함수(swap)가 필요할 때, 두 개의 타입 변수를 사용할 수 있습니다.
function swap<T, U>(a: T, b: U): [U, T] {
return [b, a];
}
const [a, b] = swap("hello", 42);
console.log(a); // number
console.log(b); // string
- T는 첫 번째 값의 타입, U는 두 번째 값의 타입입니다.
- swap 함수는 두 값을 바꾸고, a와 b는 각각 number와 string 타입으로 추론됩니다.
2. 다양한 배열 타입을 인수로 받는 제네릭 함수 만들기
다양한 타입의 배열을 인수로 받아 처리하는 함수도 쉽게 만들 수 있습니다.
예를 들어, 배열에서 첫 번째 값을 반환하는 함수는 다음과 같이 제네릭을 사용할 수 있습니다.
function returnFirstValue<T>(data: T[]): T {
return data[0];
}
let num = returnFirstValue([1, 2, 3]); // number 타입
let str = returnFirstValue(["apple", "banana"]); // string 타입
- T []는 배열의 타입을 의미합니다. T는 배열의 요소 타입으로 추론됩니다.
- returnFirstValue 함수는 배열의 첫 번째 요소를 반환하며, 그 타입은 T입니다.
배열이 number []이면 반환값도 number 타입이고, string []이면 반환값도 string 타입입니다.
3. 배열의 첫 번째 요소의 타입을 반환값으로 사용하는 상황
배열의 첫 번째 요소의 타입을 반환값으로 사용하는 경우는, 특히 배열을 다루는 함수에서 유용합니다.
튜플 타입을 사용하면 첫 번째 요소의 타입을 기반으로 반환값의 타입을 결정할 수 있습니다.
function returnFirstValue<T>(data: [T, ...unknown[]]): T {
return data[0];
}
let num = returnFirstValue([1, 2, 3]); // number 타입
let str = returnFirstValue(["apple", "banana"]); // string 타입
let mixed = returnFirstValue([1, "hello", true]); // number 타입
- [T,... unknown []]는 튜플입니다.
✅ T: 첫 번째 요소의 타입을 나타냅니다.
✅... unknown []: 나머지 요소들은 unknown []로 처리합니다.
✅ 즉, 첫 번째 요소의 타입만 중요하고, 나머지 값은 어떤 타입이든 상관없습니다.
왜 unknown []를 사용하는가?
unknown []는 타입을 유추할 수 없으므로, 배열의 나머지 요소에 대해서는 추가적인 제약을 두지 않기 위함입니다.
첫 번째 요소의 타입만 유의미하고, 그 이후에 올 수 있는 다양한 타입들은 자유롭게 다룰 수 있도록 합니다.
4. 타입 변수를 제한하는 사례
타입 변수를 제한하는 것은 함수가 처리할 수 있는 타입을 제한하는 것입니다.
예를 들어, 길이를 가지는 객체만 허용하고 싶을 때, extends 키워드를 사용하여 제한을 걸 수 있습니다.
function getLength<T extends { length: number }>(data: T): number {
return data.length;
}
console.log(getLength("hello")); // 5
console.log(getLength([1, 2, 3])); // 3
console.log(getLength({ length: 10 })); // 10
// console.log(getLength(undefined)); // 오류
// console.log(getLength(null)); // 오류
- T extends { length: number }는 length 프로퍼티가 있는 객체만 허용하도록 제한하는 부분입니다.
- 문자열이나 배열 같은 length 프로퍼티를 가진 객체만 이 함수에 전달할 수 있습니다.
- undefined나 null은 length 프로퍼티가 없기 때문에 오류가 발생합니다.
03
제네릭을 사용한 map과 forEach 메서드
자바스크립트의 map과 forEach 같은 메서드를 제네릭으로 정의할 수 있습니다.
map은 각 요소에 대해 어떤 작업을 수행하고 새로운 배열을 만들어 반환하죠.
function map<T, U>(arr: T[], callback: (item: T) => U): U[] {
let result: U[] = [];
for (let i = 0; i < arr.length; i++) {
result.push(callback(arr[i]));
}
return result;
}
이 함수는 원본 배열의 타입과 새로운 배열의 타입을 다르게 할 수 있어요.
예를 들어, 숫자 배열을 받아서 각 요소를 문자열로 변환하는 경우도 가능합니다.
let newArr = map([1, 2, 3], (it) => it.toString()); // string[] 타입
04
제네릭 인터페이스
1. 제네릭 인터페이스 (Generic Interface)
제네릭 인터페이스는 제네릭 함수와 비슷하게, 타입을 동적으로 설정할 수 있습니다.
다만, 인터페이스를 사용할 때는 변수를 정의할 때 반드시 꺾쇠괄호(<>)를 열고, 타입을 직접 할당해야 합니다.
interface KeyPair<K, V> {
key: K;
value: V;
}
let keyPair: KeyPair<string, number> = {
key: "key",
value: 0,
};
- KeyPair <K, V>는 제네릭 타입을 두 개 받는 인터페이스입니다. K는 key의 타입을, V는 value의 타입을 나타냅니다.
- 변수를 선언할 때, 타입을 <string, number>처럼 직접 명시해야 합니다.
2. 제네릭 인덱스 시그니처 (Generic Index Signature)
객체의 동적 키를 제네릭으로 처리할 수 있게 해 줍니다.
즉, 객체의 키를 문자열로 지정하고, 그에 맞는 값을 제네릭 타입으로 설정할 수 있습니다.
interface Map<V> {
[key: string]: V;
}
let stringMap: Map<string> = {
key: "value",
};
let booleanMap: Map<boolean> = {
key: true,
};
3. 제네릭 타입 별칭 (Generic Type Alias)
제네릭 타입 별칭은 인터페이스와 비슷하지만 문법적으로 타입 별칭을 사용하여 타입을 정의합니다.
제네릭 타입 별칭을 사용하면, 제네릭 인터페이스와 동일한 방식으로 다양한 타입을 처리할 수 있습니다.
type Map2<V> = {
[key: string]: V;
};
let stringMap2: Map2<string> = {
key: "string",
};
- Map2 <V>는 key가 string이고, value가 제네릭 타입 V인 객체 타입입니다.
- 이 타입을 Map2 <string>으로 선언하면 value가 string 타입인 객체로 설정됩니다.
05
제네릭 인터페이스 활용 예시
여기서는 학생과 개발자를 구분하여 처리하는 예시를 제시합니다.
이 예시는 제네릭 인터페이스를 사용하여 코드가 더 간결하고 유연하게 동작하도록 만듭니다.
✅ 학생과 개발자 타입 정의
먼저, 학생과 개발자를 각각 타입으로 정의합니다.
interface Student {
type: "student";
school: string;
}
interface Developer {
type: "developer";
skill: string;
}
- Student는 type이 "student"이고, school 속성이 있습니다.
- Developer는 type이 "developer"이고, skill 속성이 있습니다.
✅ User 인터페이스
User 인터페이스는 name과 profile을 가지고 있으며, profile의 타입을 Student나 Developer로 정의할 수 있습니다.
interface User {
name: string;
profile: Student | Developer;
}
- profile은 학생 또는 개발자 타입을 가질 수 있습니다.
✅ 구체적인 사용자 객체 예시
const developerUser: User = {
name: "아무개",
profile: {
type: "developer",
skill: "typescript",
},
};
const studentUser: User = {
name: "홍길동",
profile: {
type: "student",
school: "한국대학교",
},
};
- developerUser는 개발자로, studentUser는 학생으로 설정되었습니다.
✅ 학생만 이용할 수 있는 함수 (goToSchool)
학생만 사용할 수 있는 함수 goToSchool을 만들고,
user.profile이 "student"일 때만 실행되도록 제한하려면 타입을 좁히는 조건문을 작성해야 합니다.
function goToSchool(user: User) {
if (user.profile.type !== "student") {
console.log("잘못 오셨습니다");
return;
}
const school = user.profile.school;
console.log(`${school}로 등교 완료`);
}
- 여기서는 user.profile.type이 "student"가 아니라면 오류 메시지를 출력합니다.
✅ 제네릭을 사용한 개선
매번 조건문으로 type을 좁히는 대신 제네릭 인터페이스를 사용하여 User 타입을 구체적으로 지정할 수 있습니다.
이를 통해 코드가 더 간결해지죠.
interface User<T> {
name: string;
profile: T;
}
function goToSchool(user: User<Student>) {
const school = user.profile.school;
console.log(`${school}로 등교 완료`);
}
- goToSchool 함수는 User <Student> 타입으로만 호출할 수 있으므로, 매번 타입을 좁힐 필요가 없어집니다.
✅ 구체적인 객체 예시 (제네릭 적용)
const studentUser: User<Student> = {
name: "홍길동",
profile: {
type: "student",
school: "한국대학교",
},
};
goToSchool(studentUser); // 정상 작동
- goToSchool 함수는 이제 학생만 받을 수 있습니다.
06
제네릭 클래스 (Generic Class)
제네릭 클래스는 다양한 타입을 다룰 수 있는 클래스입니다.
타입을 동적으로 지정할 수 있기 때문에, 같은 클래스를 여러 타입에 대해 재사용할 수 있어 유용합니다.
📌 언제 쓰면 좋을까요?
: 같은 작업을 여러 타입에 대해 반복하려면 제네릭 클래스를 사용하면 코드가 깔끔해집니다.
✅ 예시 1: 여러 타입을 다루는 박스 클래스
타입을 동적으로 지정하여 하나의 클래스로 다양한 타입을 처리할 수 있습니다.
class Box<T> {
value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
// 문자열과 숫자 타입을 다룰 수 있음
const stringBox = new Box("Hello");
const numberBox = new Box(123);
✅ 예시 2: 타입 제한이 있는 컨테이너 클래스
타입 제한을 두어, 오직 string 타입만 사용할 수 있도록 할 수 있습니다.
class Container<T extends string> {
value: T;
constructor(value: T) {
this.value = value;
}
}
const stringContainer = new Container("Hello");
// const numberContainer = new Container(123); // 오류 발생!
07
프로미스와 제네릭
프로미스(Promise)는 비동기 작업을 처리할 때, 그 작업이 끝나면 결괏값이나 에러를 반환하는 객체입니다.
예를 들어, 비동기 작업(서버에서 데이터 받아오기)이 끝나면 그 결과를 프로미스가 알려주는 역할을 합니다.
📌프로미스(Promise)를 까먹었다면 아래의 포스팅을 봐주세요.
[JS] 프로미스 (Promise)
01Promise자바스크립트에서 비동기 작업이 끝날 때, 그 결과를 알려주는 "약속"과 같은 역할의 오브젝트입니다.예를 들어, 어떤 일이 잘 처리되면 결과를 알려주고, 문제가 생기면 에러를 알려주는
dev-watnu.tistory.com
하지만 프로미스는 결괏값의 타입을 자동으로 추론하지 못하기 때문에,
이를 제네릭을 사용하여 명시적으로 지정할 수 있습니다.
✅ 기본 프로미스 사용
const promise = new Promise<number>((resolve, reject) => {
setTimeout(() => {
resolve(20); // 결과값: 20 (number 타입)
}, 3000);
});
promise.then((response) => {
console.log(response); // response는 number 타입
});
- Promise <number>는 비동기 작업의 결과가 number 타입임을 명시합니다.
- then에서 response는 자동으로 number 타입으로 추론됩니다.
하지만 resolve를 사용할 때, 반환 타입을 추론하지 못해 오류가 생길 수 있습니다.
제네릭을 사용하여 이를 방지할 수 있습니다.
✅ 에러 처리와 제네릭
const promiseWithError = new Promise<number>((resolve, reject) => {
setTimeout(() => {
reject("Something went wrong");
}, 3000);
});
promiseWithError
.then((response) => {
console.log(response); // response는 number 타입
})
.catch((error) => {
if (typeof error === "string") {
console.log(error); // error는 string 타입
}
});
- catch: 에러가 발생하면 error의 타입을 확인할 수 있습니다. 이 경우, error는 string 타입임을 명시적으로 체크합니다.
08
마무리
✅ 제네릭 간단 개념 요약
제네릭은 함수나 클래스, 객체가 다양한 타입을 처리할 수 있도록 도와주는 타입 변수입니다.
- 유연성: 같은 코드로 여러 타입을 처리할 수 있게 해 줍니다.
- 타입 안전성: 타입 오류를 줄이고, 타입을 명확히 지정하여 안전한 코드 작성이 가능합니다.
- 재사용성: 여러 타입에 대해 동일한 함수나 클래스를 재사용할 수 있습니다.
즉, 제네릭은 타입을 동적으로 처리하면서 코드의 유연성과 안전성을 높여주는 개념입니다.
이미지출처: 한 입 크기로 잘라먹는 타입스크립트(TypeScript)
'DEVELOPMENT > Typescript' 카테고리의 다른 글
[TS] 조건부 타입 (Conditional Types) (0) | 2025.03.05 |
---|---|
[TS] 타입 조작하기 (0) | 2025.03.04 |
[TS] 클래스 (class) (1) | 2025.03.01 |
[TS] 인터페이스 (Interface) (0) | 2025.02.28 |
[TS] 함수와 타입 (1) | 2025.02.27 |