본문 바로가기

제네릭 클래스와 인터페이스

클래스와 인터페이스에 타입 매개변수를 적용해 저장소·응답·컴포넌트 같은 재사용 가능한 타입 안전 구조를 설계합니다.

3장 4절에서 제네릭 함수를 통해 다양한 타입에서 동작하는 재사용 가능한 함수를 만드는 방법을 배웠습니다.

제네릭은 함수뿐만 아니라 클래스(Classes)와 인터페이스(Interfaces)에도 적용될 수 있으며, 이를 통해 더욱 강력하고 유연하며 타입 안전한 데이터 구조와 컴포넌트를 설계할 수 있습니다.

이번 절에서는 제네릭 클래스와 인터페이스의 선언 및 활용 방법을 자세히 알아보겠습니다.

제네릭 타입 매개변수 전파 — 상태 추적

Box<number>의 생성·setValue·getValue에서 같은 _value 저장 값이 바뀌는 실제 코드가 있다. 타입 관계 표를 남기고 number/string의 서로 독립적인 인스턴스 상태를 보강할 수 있다.

Box<number>는 _value:number, setValue(number), getValue():number를 연결한다. 제네릭 인터페이스는 속성과 콜백의 입력 및 출력을 연결한다.

TYPE / RUNTIME / CONTRACT

한 타입 인자가 저장 값과 입출력의 타입을 연결한다

Box의 T를 number로 선택하면 저장 값, setValue의 인자, getValue의 결과가 모두 number입니다. 서로 다른 타입 인자가 별도 JavaScript 클래스를 만들지는 않습니다.

한 타입 인자가 저장 값과 입출력의 타입을 연결한다Box<number>는 _value:number, setValue(number), getValue():number를 연결한다. 제네릭 인터페이스는 속성과 콜백의 입력 및 출력을 연결한다.Box<number>T = number_value저장 값: numbersetValue인자: numbergetValue반환: number
한 타입 인자가 저장 값과 입출력의 타입을 연결한다Box<number>는 _value:number, setValue(number), getValue():number를 연결한다. 제네릭 인터페이스는 속성과 콜백의 입력 및 출력을 연결한다.Box<number>T = number_value저장 값: numbersetValue인자: numbergetValue반환: number

화살표는 number라는 타입 인자가 저장 값과 두 메서드의 타입을 연결함을 나타냅니다. setValue와 getValue가 서로를 호출한다는 실행 순서 그림은 아닙니다.

BOX

상태 · 입력 · 반환

numberBox는 123을 저장하고 setValue(456) 뒤 isSameAs(456)가 true입니다. setValue("abc")는 이 인스턴스의 타입 계약에 맞지 않습니다.

KEYVALUEPAIR

두 타입의 역할을 분리

KeyValuePair<K, V>는 key:K와 value:V입니다. DataStore<T>는 KeyValuePair<string, T>를 구현해 키를 문자열로 고정합니다.

PROCESSCALLBACK

입력과 출력을 따로 정한다

ProcessCallback<string, string>은 "hello" → "HELLO"입니다. <number[], number>는 [1,2,3,4,5] → 15입니다.

JAVASCRIPT

타입 인자는 실행 시 소거

Box의 생성자 구현은 하나입니다. 클래스의 T는 인스턴스 쪽 타입이며 static 멤버는 이 T를 직접 사용할 수 없습니다.

Box<number>는 _value:number, setValue(number), getValue():number를 연결한다. 제네릭 인터페이스는 속성과 콜백의 입력 및 출력을 연결한다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
numberBox 생성_value: 123 / T: number123을 같은 numberBox 객체에 저장한다타입 인자는 넣고 꺼낼 값의 관계를 검사한다
setValue(456)_value: 456 / stringBox: 독립 상태현재 _value를 456으로 교체한다같은 Box 정의의 인스턴스들이 한 저장값을 공유하지 않는다
현재 값과 동등성getValue: 456 / isSameAs(456): truegetValue는 456이고 isSameAs(456)는 true다다른 확장 객체 사례는 score 10→12의 공유 참조로 별도 비교한다

제네릭 타입 매개변수 전파 — 상태 추적

Box<number>의 생성·setValue·getValue에서 같은 _value 저장 값이 바뀌는 실제 코드가 있다. 타입 관계 표를 남기고 number/string의 서로 독립적인 인스턴스 상태를 보강할 수 있다.

Box<number>는 _value:number, setValue(number), getValue():number를 연결한다. 제네릭 인터페이스는 속성과 콜백의 입력 및 출력을 연결한다.

TYPE / RUNTIME / CONTRACT

한 타입 인자가 저장 값과 입출력의 타입을 연결한다

Box의 T를 number로 선택하면 저장 값, setValue의 인자, getValue의 결과가 모두 number입니다. 서로 다른 타입 인자가 별도 JavaScript 클래스를 만들지는 않습니다.

한 타입 인자가 저장 값과 입출력의 타입을 연결한다Box<number>는 _value:number, setValue(number), getValue():number를 연결한다. 제네릭 인터페이스는 속성과 콜백의 입력 및 출력을 연결한다.Box<number>T = number_value저장 값: numbersetValue인자: numbergetValue반환: number
한 타입 인자가 저장 값과 입출력의 타입을 연결한다Box<number>는 _value:number, setValue(number), getValue():number를 연결한다. 제네릭 인터페이스는 속성과 콜백의 입력 및 출력을 연결한다.Box<number>T = number_value저장 값: numbersetValue인자: numbergetValue반환: number

화살표는 number라는 타입 인자가 저장 값과 두 메서드의 타입을 연결함을 나타냅니다. setValue와 getValue가 서로를 호출한다는 실행 순서 그림은 아닙니다.

BOX

상태 · 입력 · 반환

numberBox는 123을 저장하고 setValue(456) 뒤 isSameAs(456)가 true입니다. setValue("abc")는 이 인스턴스의 타입 계약에 맞지 않습니다.

KEYVALUEPAIR

두 타입의 역할을 분리

KeyValuePair<K, V>는 key:K와 value:V입니다. DataStore<T>는 KeyValuePair<string, T>를 구현해 키를 문자열로 고정합니다.

PROCESSCALLBACK

입력과 출력을 따로 정한다

ProcessCallback<string, string>은 "hello" → "HELLO"입니다. <number[], number>는 [1,2,3,4,5] → 15입니다.

JAVASCRIPT

타입 인자는 실행 시 소거

Box의 생성자 구현은 하나입니다. 클래스의 T는 인스턴스 쪽 타입이며 static 멤버는 이 T를 직접 사용할 수 없습니다.

Box<number>는 _value:number, setValue(number), getValue():number를 연결한다. 제네릭 인터페이스는 속성과 콜백의 입력 및 출력을 연결한다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
numberBox 생성_value: 123 / T: number123을 같은 numberBox 객체에 저장한다타입 인자는 넣고 꺼낼 값의 관계를 검사한다
setValue(456)_value: 456 / stringBox: 독립 상태현재 _value를 456으로 교체한다같은 Box 정의의 인스턴스들이 한 저장값을 공유하지 않는다
현재 값과 동등성getValue: 456 / isSameAs(456): truegetValue는 456이고 isSameAs(456)는 true다다른 확장 객체 사례는 score 10→12의 공유 참조로 별도 비교한다

제네릭 클래스

제네릭 클래스(Generic Classes)는 클래스 자체에 타입 매개변수를 적용하여, 클래스 내부의 속성이나 메서드가 해당 타입 매개변수에 따라 타입을 결정하도록 합니다.

이는 특정 데이터 타입을 담는 컬렉션 클래스나, 타입에 따라 동작이 달라질 수 있는 유틸리티 클래스를 만들 때 매우 유용합니다.

클래스 이름 뒤에 <T>와 같은 타입 매개변수를 선언하여 제네릭 클래스를 정의합니다.

// Box 클래스는 어떤 타입이든 담을 수 있는 제네릭 클래스입니다.
class Box<T> {
  private _value: T; // _value 속성의 타입은 T에 의해 결정됩니다.

  constructor(value: T) {
    this._value = value;
  }

  // T 타입의 값을 반환하는 getter 메서드
  getValue(): T {
    return this._value;
  }

  // T 타입의 값을 설정하는 setter 메서드
  setValue(newValue: T): void {
    this._value = newValue;
  }

  // 같은 T 타입의 다른 값을 받아 엄격한 동등성으로 비교하는 메서드
  isSameAs(otherValue: T): boolean {
    return this._value === otherValue;
  }
}

// 인스턴스 생성 시 타입 인자를 명시하여 T의 타입을 결정합니다.
const stringBox = new Box<string>("Hello TypeScript!");
console.log(stringBox.getValue());     // "Hello TypeScript!"
stringBox.setValue("New String");
console.log(stringBox.isSameAs("New String")); // true

const numberBox = new Box<number>(123);
console.log(numberBox.getValue());     // 123
numberBox.setValue(456);
console.log(numberBox.isSameAs(456)); // true

// numberBox.setValue("abc"); // Error: '"abc"' 형식은 'number' 형식에 할당할 수 없습니다.

const booleanBox = new Box<boolean>(true);
console.log(booleanBox.getValue()); // true

// 타입 추론: 생성자 매개변수를 통해 제네릭 타입을 자동으로 추론할 수도 있습니다.
const inferredBox = new Box(789); // inferredBox의 타입은 Box<number>로 추론됩니다.
console.log(inferredBox.getValue()); // 789

제네릭 클래스를 사용하면 Box<string>, Box<number> 등과 같이 특정 타입에 특화된 여러 클래스를 별도로 정의할 필요 없이 하나의 제네릭 Box 클래스로 모든 경우를 처리할 수 있습니다.

이는 코드의 재사용성을 극대화합니다.

같은 타입과 같은 객체는 다른 질문

Box가 선택한 T는 값을 넣고 꺼내는 계약이고, isSameAs의 ===는 실행 중 동등성을 검사합니다. 객체를 넣었다면 같은 속성 값만으로 같다고 판단하지 않습니다. 위 Box 선언이 있는 파일에 다음을 이어서 추가합니다.

const originalItem = { score: 10 };
const itemBox = new Box(originalItem);

console.log(itemBox.isSameAs(originalItem));  // true
console.log(itemBox.isSameAs({ score: 10 })); // false
originalItem.score = 12;
console.log(itemBox.getValue().score);       // 12

두 객체 모두 { score: number }라는 구조는 맞습니다. 첫 비교는 같은 객체를 참조하고 두 번째 비교는 새 객체를 만들므로 결과가 다릅니다. 생성자가 복사하지 않고 원래 객체를 저장했기에 원래 참조로 바꾼 score도 상자에서 보입니다. 제네릭 클래스가 복사·깊은 비교·불변성 정책을 만들어 주지는 않습니다.


제네릭 인터페이스

제네릭 인터페이스(Generic Interfaces)는 인터페이스 자체에 타입 매개변수를 적용하여, 인터페이스가 정의하는 속성이나 메서드의 타입을 동적으로 결정하도록 합니다.

이는 다양한 타입의 데이터를 다루는 데이터 구조나 콜백 함수의 시그니처를 유연하게 정의할 때 매우 유용합니다.

인터페이스 이름 뒤에 <T>와 같은 타입 매개변수를 선언하여 제네릭 인터페이스를 정의합니다.

// KeyValuePair 인터페이스는 어떤 타입이든 key와 value로 가질 수 있습니다.
interface KeyValuePair<K, V> {
  key: K;
  value: V;
}

// StringValuePair는 K가 string, V가 string인 KeyValuePair를 나타냅니다.
type StringValuePair = KeyValuePair<string, string>;
const pair1: StringValuePair = { key: "name", value: "Alice" };

// NumberAndBooleanPair는 K가 number, V가 boolean인 KeyValuePair를 나타냅니다.
type NumberAndBooleanPair = KeyValuePair<number, boolean>;
const pair2: NumberAndBooleanPair = { key: 1, value: true };

// 인터페이스를 클래스에서 구현할 때 제네릭 타입 인자를 지정할 수 있습니다.
class DataStore<T> implements KeyValuePair<string, T> {
  key: string;
  value: T;

  constructor(key: string, value: T) {
    this.key = key;
    this.value = value;
  }

  print(): void {
    console.log(`Key: ${this.key}, Value: ${this.value}`);
  }
}

const userStore = new DataStore<string>("user_id", "u12345");
userStore.print(); // Key: user_id, Value: u12345

const productStore = new DataStore<number>("product_code", 98765);
productStore.print(); // Key: product_code, Value: 98765

제네릭 인터페이스는 특히 콜백 함수나 함수 시그니처를 정의할 때 그 진가를 발휘합니다.

// ProcessCallback 인터페이스는 T 타입의 데이터를 받고 V 타입의 결과를 반환하는 함수를 정의합니다.
interface ProcessCallback<T, V> {
  (data: T): V;
}

// StringProcessor는 string을 받아 string을 반환하는 함수를 의미합니다.
const stringProcessor: ProcessCallback<string, string> = (input) => input.toUpperCase();
console.log(stringProcessor("hello")); // "HELLO"

// NumberProcessor는 number 배열을 받아 number를 반환하는 함수를 의미합니다.
const numberProcessor: ProcessCallback<number[], number> = (numbers) => numbers.reduce((sum, num) => sum + num, 0);
console.log(numberProcessor([1, 2, 3, 4, 5])); // 15

제네릭 인터페이스는 실제 애플리케이션에서 저장소(Repository)처럼 여러 도메인 타입이 같은 동작 계약을 공유해야 할 때 특히 유용합니다.

그림은 아래 실제 IdentifiableRepository<T>의 타입 계약입니다. HasId는 id: string | number를 요구하며, 저장소 클래스는 add와 findById를 직접 구현합니다.

제네릭 저장소 계약 — 상태 추적

add로 받은 참조를 배열에 저장하고 findById가 ===로 첫 일치 참조 또는 undefined를 반환한다. 추가/검색 상태와 숫자·문자열 ID 차이를 실제 repository 배열로 추적할 수 있다.

HasId는 id:string|number를 요구한다. IdentifiableRepository<T>는 add(T)와 findById(string|number):T|undefined를 구현하며 User와 Product를 저장한다.

TYPE / RUNTIME / CONTRACT

저장소의 T가 추가할 값과 검색 결과를 연결한다

실제 IdentifiableRepository는 id가 있는 T를 저장합니다. 같은 클래스에 User 또는 Product를 선택하면 add의 인자와 findById의 결과가 그 모델로 연결됩니다.

저장소의 T가 추가할 값과 검색 결과를 연결한다HasId는 id:string|number를 요구한다. IdentifiableRepository<T>는 add(T)와 findById(string|number):T|undefined를 구현하며 User와 Product를 저장한다.있음없음Repository<T>T extends HasIdadd(item: T)받은 참조를 저장items: T[]추가 순서의 배열findById(id)===로 ID 비교일치 항목?첫 번째 T찾은 참조 반환undefined해당 항목 없음
저장소의 T가 추가할 값과 검색 결과를 연결한다HasId는 id:string|number를 요구한다. IdentifiableRepository<T>는 add(T)와 findById(string|number):T|undefined를 구현하며 User와 Product를 저장한다.있음없음Repository<T>T extends HasIdadd(item: T)받은 참조를 저장items: T[]추가 순서의 배열findById(id)===로 ID 비교일치 항목?첫 번째 T찾은 참조 반환undefined해당 항목 없음

그림은 add 뒤 findById를 호출한 한 번의 조회입니다. 모든 항목 중 엄격한 ID 비교로 일치하는 첫 항목을 찾거나 undefined를 반환하며, 항목 하나의 불일치만으로 검색을 끝내지 않습니다.

USER

숫자 식별자를 저장

User는 id:1, name:"Alice", email을 가진 객체입니다. add 뒤 findById(1)은 이 객체를 찾습니다.

PRODUCT

문자열 식별자를 저장

Product는 id:"P001", name:"Laptop", price:1200입니다. findById("P001")은 Product | undefined입니다.

MISSING

찾지 못한 경우를 포함

findById는 배열의 find와 엄격한 동등성 ===를 사용합니다. 값이 없으면 undefined이며 숫자 1과 문자열 "1"은 같은 식별자로 취급하지 않습니다.

STATIC CONTRACT

저장소 구현은 직접 작성

T extends HasId는 string 같은 잘못된 모델을 제한합니다. 실제 클래스의 add와 findById가 실행 동작을 담당하며 제약이 검증기나 영속 저장소를 생성하지 않습니다.

이 코드에는 별도 Repository 인터페이스나 MemoryRepository 구현체가 없습니다. HasId는 식별자 모양의 계약입니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
additems: 추가 순서 T 배열 / 참조: 받은 객체 유지repository가 받은 T의 참조를 items 배열에 넣는다add는 객체의 깊은 복사가 아니다
ID 검색비교: string/number 구별 / 현재: 첫 일치 후보findById가 ===로 순서대로 비교한다숫자 1과 문자열 1은 자동으로 같은 ID가 되지 않는다
반환있음: 저장한 같은 참조 / 없음: undefined첫 일치 참조 또는 undefined를 얻는다검색이 새 객체를 만들거나 모든 중복 항목을 반환하지 않는다

제네릭 저장소 계약 — 상태 추적

add로 받은 참조를 배열에 저장하고 findById가 ===로 첫 일치 참조 또는 undefined를 반환한다. 추가/검색 상태와 숫자·문자열 ID 차이를 실제 repository 배열로 추적할 수 있다.

HasId는 id:string|number를 요구한다. IdentifiableRepository<T>는 add(T)와 findById(string|number):T|undefined를 구현하며 User와 Product를 저장한다.

TYPE / RUNTIME / CONTRACT

저장소의 T가 추가할 값과 검색 결과를 연결한다

실제 IdentifiableRepository는 id가 있는 T를 저장합니다. 같은 클래스에 User 또는 Product를 선택하면 add의 인자와 findById의 결과가 그 모델로 연결됩니다.

저장소의 T가 추가할 값과 검색 결과를 연결한다HasId는 id:string|number를 요구한다. IdentifiableRepository<T>는 add(T)와 findById(string|number):T|undefined를 구현하며 User와 Product를 저장한다.있음없음Repository<T>T extends HasIdadd(item: T)받은 참조를 저장items: T[]추가 순서의 배열findById(id)===로 ID 비교일치 항목?첫 번째 T찾은 참조 반환undefined해당 항목 없음
저장소의 T가 추가할 값과 검색 결과를 연결한다HasId는 id:string|number를 요구한다. IdentifiableRepository<T>는 add(T)와 findById(string|number):T|undefined를 구현하며 User와 Product를 저장한다.있음없음Repository<T>T extends HasIdadd(item: T)받은 참조를 저장items: T[]추가 순서의 배열findById(id)===로 ID 비교일치 항목?첫 번째 T찾은 참조 반환undefined해당 항목 없음

그림은 add 뒤 findById를 호출한 한 번의 조회입니다. 모든 항목 중 엄격한 ID 비교로 일치하는 첫 항목을 찾거나 undefined를 반환하며, 항목 하나의 불일치만으로 검색을 끝내지 않습니다.

USER

숫자 식별자를 저장

User는 id:1, name:"Alice", email을 가진 객체입니다. add 뒤 findById(1)은 이 객체를 찾습니다.

PRODUCT

문자열 식별자를 저장

Product는 id:"P001", name:"Laptop", price:1200입니다. findById("P001")은 Product | undefined입니다.

MISSING

찾지 못한 경우를 포함

findById는 배열의 find와 엄격한 동등성 ===를 사용합니다. 값이 없으면 undefined이며 숫자 1과 문자열 "1"은 같은 식별자로 취급하지 않습니다.

STATIC CONTRACT

저장소 구현은 직접 작성

T extends HasId는 string 같은 잘못된 모델을 제한합니다. 실제 클래스의 add와 findById가 실행 동작을 담당하며 제약이 검증기나 영속 저장소를 생성하지 않습니다.

이 코드에는 별도 Repository 인터페이스나 MemoryRepository 구현체가 없습니다. HasId는 식별자 모양의 계약입니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
additems: 추가 순서 T 배열 / 참조: 받은 객체 유지repository가 받은 T의 참조를 items 배열에 넣는다add는 객체의 깊은 복사가 아니다
ID 검색비교: string/number 구별 / 현재: 첫 일치 후보findById가 ===로 순서대로 비교한다숫자 1과 문자열 1은 자동으로 같은 ID가 되지 않는다
반환있음: 저장한 같은 참조 / 없음: undefined첫 일치 참조 또는 undefined를 얻는다검색이 새 객체를 만들거나 모든 중복 항목을 반환하지 않는다

제네릭 제약 조건과 함께 사용하기 (복습)

제네릭 클래스와 인터페이스에서도 제네릭 함수와 마찬가지로 제약 조건(Constraints)을 사용할 수 있습니다.

이는 특정 조건을 만족하는 타입만 제네릭 타입 인자로 받을 수 있도록 제한할 때 사용됩니다.

interface HasId {
  id: string | number;
}

// IdentifiableRepository 클래스는 HasId 인터페이스를 만족하는 T 타입만 다룰 수 있습니다.
class IdentifiableRepository<T extends HasId> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  findById(id: string | number): T | undefined {
    return this.items.find(item => item.id === id);
  }
}

interface User extends HasId {
  name: string;
  email: string;
}

interface Product extends HasId {
  name: string;
  price: number;
}

const userRepository = new IdentifiableRepository<User>();
userRepository.add({ id: 1, name: "Alice", email: "[email protected]" });
console.log(userRepository.findById(1)); // { id: 1, name: 'Alice', email: '[email protected]' }

const productRepository = new IdentifiableRepository<Product>();
productRepository.add({ id: "P001", name: "Laptop", price: 1200 });
console.log(productRepository.findById("P001")); // { id: 'P001', name: 'Laptop', price: 1200 }

// const invalidRepository = new IdentifiableRepository<string>();
// Error: 'string' 형식은 'HasId' 형식에 할당할 수 없습니다.

T extends HasId 제약 조건 덕분에 IdentifiableRepository는 id 속성을 가진 객체만 저장하고 검색할 수 있도록 타입 안전성이 보장됩니다.

조회 실패와 저장 정책을 실행으로 확인하기

위 클래스의 구현은 push로 받은 객체를 그대로 저장하고 find로 처음 일치한 객체를 돌려줍니다. T extends HasId는 ID의 모양을 요구하며 ID 중복을 막거나 조회 성공을 보장하지 않습니다. 원래 예제와 분리해 같은 선언을 사용하고, 다음 순서를 먼저 따라가 보세요.

const practiceRepository = new IdentifiableRepository<User>();
const firstUser: User = { id: 2, name: '민지', email: '[email protected]' };
practiceRepository.add(firstUser);
practiceRepository.add({ id: 2, name: '지수', email: '[email protected]' });

console.log(practiceRepository.findById(2)?.name);   // 민지
console.log(practiceRepository.findById('2'));      // undefined
firstUser.name = '민';
console.log(practiceRepository.findById(2)?.name);   // 민

첫 조회는 첫 번째 일치 항목을 반환합니다. 숫자 2와 문자열 '2'는 ===로 같지 않아서 두 번째 조회는 실패합니다. 저장한 객체를 복사하지 않았으므로 원래 참조를 통해 값을 바꾼 뒤에는 이름이 민입니다. 실패 결과를 사용할 때는 undefined를 처리해야 하며, ?.name은 객체를 찾지 못하면 이름을 읽지 않습니다. 이 클래스는 메모리 안의 간단한 예제이고 영속 저장·유일성 검사는 따로 설계해야 합니다.

구현을 바꾸지 않고 결과 예측하기

위 코드를 실행한 다음 { id: '2', name: '유나', email: '[email protected]' }를 추가했다고 가정합니다. findById(2)와 findById('2')의 이름은 각각 무엇일까요? 이어서 practiceRepository.findById(99).name을 직접 읽어도 되는지 반환 타입과 연결해 설명해 보세요.

검색 순서와 실패 처리 확인

숫자 조회는 첫 숫자 ID 항목인 민, 문자열 조회는 새 문자열 ID 항목인 유나를 찾습니다. 서로 다른 ID 값으로 취급하며 두 번째 숫자 항목 지수를 선택하지 않습니다. 99는 저장하지 않았으므로 결과는 undefined입니다. strict 설정에서는 곧바로 .name을 읽는 코드가 진단됩니다. 먼저 결과를 변수에 담아 없는 경우를 처리하거나 ?.name처럼 선택적으로 읽어야 합니다.

공식 제네릭 클래스 설명에서 타입 인자가 인스턴스의 계약을 연결하는 범위를, 타입 좁히기 설명에서 값이 없을 때의 분기를 대조할 수 있습니다.


제네릭의 활용 시나리오

제네릭 클래스와 인터페이스는 현대 웹 애플리케이션 개발에서 광범위하게 사용됩니다.

  • 데이터 구조: 스택, 큐, 연결 리스트, 트리 등 다양한 타입의 요소를 담을 수 있는 재사용 가능한 데이터 구조를 구현할 때.
  • 컴포넌트 라이브러리: UI 컴포넌트(예: Dropdown, Table)가 다양한 타입의 데이터를 표시하거나 다룰 수 있도록 할 때.
  • API 클라이언트: 서버로부터 받아오는 데이터의 형태가 다양할 때, 응답 데이터를 타입 안전하게 처리할 수 있는 클라이언트 모듈을 만들 때.
  • 상태 관리: Redux, Vuex 등의 상태 관리 라이브러리에서 상태의 타입을 제네릭으로 정의하여 유연하게 사용할 때.
  • 공통 유틸리티: 로깅, 캐싱, 데이터 변환 등 특정 타입에 얽매이지 않고 범용적으로 사용될 수 있는 유틸리티를 만들 때.

제네릭 클래스와 인터페이스는 데이터 구조, 컴포넌트, 유틸리티처럼 여러 타입을 받아야 하는 구조에서 타입 매개변수를 명시하는 방법입니다.

저장되는 값, 외부 계약, 콜백 입출력 중 무엇을 일반화할지 먼저 구분해야 합니다.

제네릭 클래스와 인터페이스를 설계할 때는 타입 매개변수가 저장소의 상태를 대표하는지, 외부 계약을 대표하는지, 콜백의 입출력을 대표하는지부터 나누면 구조가 선명해집니다.

제네릭 설계가 커질수록 타입 매개변수의 이름, 제약 조건, 공개 메서드의 반환 타입이 같은 도메인 언어를 가리키는지 함께 점검해야 합니다.