본문 바로가기
개발공부

#7. 제네릭

by 반류연 2025. 2. 28.

※ 이 글은 이정환 강사님의 '한 입 크기로 잘라먹는 타입스크립트' 강의를 본 뒤 정리한 글입니다.

 

 

제네릭(Generic) 함수

  • 함수나 인터페이스, 타입 별칭, 클래스 등을 다양한 타입과 함께 동작하도록 만들어주는 TS 기능
  • 두루두루 모든 타입의 값을 다 적용할 수 있는 범용적인 함수
  • 형태: 함수이름<T> (value:T):T

※ 어떨 때 필요할까?

[예시] 다양한 타입의 매개변수를 받고 해당 매개변수를 그대로 반환하는 함수가 필요한 상황

function func(value: any){
  return value;
}

let num = func(10)
let str = func("string")

num.toUpperCase() // 오류이나 감지하지 못함
  • 위의 함수의 경우, 어떤 값을 넣어도 any 타입으로 반환되기 때문에 잘못된 메서드를 적용해도 TS가 오류를 감지하지 못함 -> 나중에 런타임 오류 발생.

[해결책] 제네릭 함수 사용

function func<T>(value:T): T {
  return value;
}

let num = func(10) // number 타입으로 출력

 

  • T에 어떤 타입이 할당되는지는 함수가 호출될 때 결정됨.
  • 제네릭 함수 호출 시, 타입변수에 할당할 타입을 직접 명시하는 것도 가능.
function func<T>(value: T): T {
  return value;
}

// 제네릭 함수 호출
let arr = func<[number, number, number]>([1,2,3]);
  • 2개 이상의 타입변수도 사용 가능
function swap<T, U>(a: T, b: U){
  return [b,a]
}

const [a,b] = swap("1",2);
// T : string 타입, U는 number 타입으로 추론

 

제네릭 함수 응용

1. Map 메서트 타입 정의하기

  • map(): 원본 배열의 각 요소에 콜백함수를 수행하여 반환된 값을 새로운 배열로 반환하는 메소드.
    • 형태: arr.map((it) => it*2)
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(arr, (i) => i*2); // [2,4,6]

 

2. ForEach 메서드 타입 정의하기

  • forEach(): 배열의 모든 요소에 콜백함수를 한 번씩 수행하는 메소드.
function forEach<T>(arr: T[], callback: (item: T) => void){
  for(let i = 0; i < arr.length; i++){
    callback(arr[i]);
  }
}

 

제네릭 인터페이스 / 타입별칭 / 클래스

  • 제네릭은 인터페이스, 타입별칭, 클래스에도 적용할 수 있다!

 

제네릭 인터페이스

// 인터페이스 정의
interface KeyPair<K,V>{
  key: K;
  value: V;
}

// 사용
let keyPair: KeyPair<string, number> = {
  key: "key",
  value: 0
}

let keyPair2: KeyPair<boolen, string[]> = {
  key: true,
  value: ["1"]
}
  • 인터페이스에 제네릭을 쓸 때는, 변수타입 정의시 반드시 꺽쇠와 함께 타입 변수에 할당할 타입을 명시해야 한다.
  • 함수는 매개변수의 값을 기준으로 타입변수의 값을 추론할 수 있지만 인터페이스는 추론할 값이 없기 때문!
  • 인터페이스와 인덱스 시그니처를 함께 사용하면 더 유연한 객체타입 정의가 가능함.
interface Map<V>{
  // 인덱스 시그니처 - [key: type] : value 요 형태!
  [key: string]: V;
}

let stringMap: Map<string> = {
  key: "value"
}

let boolenMap: Map<boolean> = {
  key: true;
}

 

제네릭 타입별칭

  • 인터페이스와 똑같이 타입별칭에도 제네릭을 적용할 수 있다!
  • 인터페이스와 마찬가지로, 반드시 타입 변수에 설정할 타입을 명시해야함.
type Map<V> = {
  [key: string]: V
}

let stringMap2: Map2<string> = {
  key: "안녕하세요";
}

 

제네릭 클래스

  • 제네릭을 이용해 범용적으로 사용 가능한 클래스를 만들 수 있다!(타입별로 따로따로 만들 필요 X)
class List<T> {
  constructor(private list: T[]){}
  
  push(data: T){
    this.list.push(data)
  }
  
  pop(){
    return this.list.pop(data)
  }
  
  print(){
    console.log(this.list)
  }
}

const numberList = new List([1,2,3]);
const stringList = new List(["1","2"])

 

 

프로미스와 제네릭

※ 프로미스란?

  • 자바스크립트에서 비동기 작업을 처리할 때 사용하는 객체.
  • '비동기 처리'현재 실행하는 작업과 별도로 다른 작업을 수행하는 것. A 작업이 끝나면 B를 처리하라고 명령하기 위해 전통적으로는 callback 함수를 썼으나 작업량이 많아질수록 코드가 깊어지는 '콜백 지옥' 현상이 발생해 가독성을 떨어뜨림. => 프로미스의 등장
  • 비동기 작업이 끝날때까지 기다리는게 아니라, 결과를 제공하겠다는 '약속'을 반환한다는 의미에서 Promise라고 명명되었다 함.
  • resolve, reject / .then(), .catch() 메서드 체이닝을 이용하여 성공과 실패에 대한 후속 처리를 진행.
// 프로미스 객체를 반환하는 함수 생성
const myPromise = new Promise((resolve, reject) => {
  // 비동기 작업 수행
  const data = fetch('서버로부터 요청할 때');
  
  if(data) {
    resolve(data) // 성공시 실행
  } eles {
    reject("error!")  // 실패시 실행
  }
})


// 프로미스 객체를 반환하는 함수 사용
myPromise()
  .then((result) => {
    console.log(`성공! 데이터: ${result}`)
  })
  .catch((err) => {
    console.log(`실패! 에러메세지: ${err}`)
  })

 

프로미스 객체를 함수로 만드는 이유(프로미스 팩토리 함수)

  • 재사용성: 필요할 때 마다 호출하여 사용 가능. 반복되는 비동기 작업을 효율적으로 처리할 수 있다. 
  • 가독성: 코드의 가독성이 높아진다.
  • 확장성: 프로미스 객체를 함수로 만들면 인자를 전달하며 동적으로 비동기 작업을 수행할 수 있다. 또한, 여러개의 프로미스 객체 반환 함수를 연결해 복잡한 비동기 로직을 구현할 수 있다.

프로미스의 상태

  • Pending(대기)
    • 아직 비동기 처리 로직이 완료되지 않은 상태
  • Fulfilled(이행)
    • 비동기 처리 로직이 성공적으로 완료된 상태
    • resolve()의 결과
    • 체이닝된 .then() 메서드를 호출해 처리결과 값을 받을 수 있다
  • Rejected(거부)
    • 비동기 처리 로직이 실패한 상태
    • reject()의 결과
    • 체이닝된 .catch() 메서드를 호출해 실패에 대한 행동을 수행

 

프로미스 핸들러

  • 프로미스의 성공/실패 여부에 따라 실행되는 콜백 함수
  • then(), catch(), finally()
  • then()을 반복해 사용하면 프로미스 지옥에 빠질 수 있음
    => 이를 극복하기 위해 나온 것: async() / await! << 추후 정리

 

제네릭에서 프로미스 사용하기

  • promise는 제네릭 클래스로 구현되어 있음
  • Promise를 생성할 때 타입 변수에 할당할 타입을 직접 설정해주면 그 타입이 resolve 결과값의 타입이 됨.
  • reject 값의 타입은 정의할 수 없음. 그냥 unknown 타입으로 고정 => 타입좁히기 사용하는게 안전
const promise = new Promise<number>(() => {
  setTimeout(() => {
    resolve(20);
  }, 3000)
})

promise
.then((res)=> {
  console.log(res) // res는 number 타입
})
.catch((err) => {
  if(typeof err === "string"){
    console.log(err)
  }
})

'개발공부' 카테고리의 다른 글

[공식문서 읽기/API] LocalStorage / Geolocation  (0) 2025.03.17
#8. 타입 조작하기  (0) 2025.03.05
#6. 클래스  (0) 2025.02.27
#5. 인터페이스  (0) 2025.02.26
#4. 함수와 타입  (0) 2025.02.25