[TypeScript] 클래스 & 제네릭

Main·2023년 9월 13일
0

TypeScript

목록 보기
4/8
post-thumbnail

타입스크립트 클래스(class) 사용법

클래스에서 타입정의 방법은 함수 정의 방법과 비슷하며, 생성자 메서드에서 사용될 속성들을 미리 정의해주어야합니다.

class User {
   // 사용될 name 속성의 타입을 정의
  name: string;
   // 파라미터 타입 정의
  constructor(name: string) {
    this.name = name;
  }
  // 파라미터 및 리턴 타입 정의
  introduce(text: string): void {
    return console.log(text);
  }
}

클래스 접근 제어자

클래스의 멤버 변수와 메서드에 대한 접근 권한을 제어하는 키워드입니다. 접근 제어자를 사용하여 클래스의 내부 구조를 캡슐화하고, 외부에서의 접근을 제한하거나 허용할 수 있습니다. 타입스크립트에서는 다음 네 가지 접근 제어자(public, private, protedcted, readolny)를 사용할 수 있습니다.

  • public: 멤버 변수나 메서드를 클래스 내부에서만 접근할 수 있도록 합니다. 외부에서 접근 시 컴파일 오류가 발생합니다.
  • private: 멤버 변수나 메서드를 클래스 내부에서만 접근할 수 있도록 합니다. 외부에서 접근 시 컴파일 오류가 발생합니다.
  • protected : 멤버 변수나 메서드를 클래스 내부와 해당 클래스를 상속한 하위 클래스에서만 접근할 수 있도록 합니다.
  • readonly : 멤버 변수를 읽기 전용으로 만듭니다. 즉, 초기화된 후에는 값을 변경할 수 없습니다.

예시 코드로 접근 제어자 알아보기

아래 코드에서 public 접근 제어자를 이용하여 은행 클래스를 구현하였습니다.
class에서 접근 제어자의 키워드를 붙이지 않으면 기본적으로 public 접근 제어자가 부여됩니다,
이 코드에서 발생할 수 있는 문제점은 money 속성을 외부에서 접근가능하도록 public 접근 제어자로 지정하였기 때문에 외부에서 money를 맘대로 조작할 수 있는 문제가 발생합니다.

class Bank {
  public money: number;

  constructor(money: number) {
    this.money = money;
  }

  public withdraw(money: number) {
    const currentMoney = this.money - money;
    if (currentMoney > 0) {
      console.log(`예금 인출이 성공적으로 이루어졌습니다. 현재 남은 예금은 ${currentMoney}원 입니다.`);
    } else {
      console.log(`예금 인출 잔액이 부족합니다. 현재 남은 예금은 ${currentMoney}원 입니다.`)
    }
  }

  public inquiry() {
    console.log(`현재 예금 잔액은 ${this.money}원 입니다.`)
  }
}

const myBank = new Bank(1000);

myBank.withdraw(100);
myBank.withdraw(1000);
// 외부에서 money 조작 가능
myBank.money = 9999999;
// 현재 금액은 9999999원이 됨
myBank.inquiry();

이를 해결하려면 private 접근 제어자를 사용하여 외부에서 money 속성에 접근하지 못하도록 막을 수 있습니다.
이처럼 private 접근 제어자는 외부에서 접근을 할 수 없도록 막는 은닉화의 기능이 있으며, private로 선언한 속성과 메서드의 경우 클래스 인스턴스에서 자동완성을 지원하지 않습니다.

class myBank{
  private money: number
              •
              •
              
            (생략)
// 에러 발생 외부에서 money 조작 불가
myBank.money = 9999999;
// 현재 금액은 900원이 됨
myBank.inquiry();
}

protected 접근 제어자는 private과 비슷하면서도 다릅니다. protected로 선언된 속성이나 메서드는 클래스 코드 외부에서 사용할 수 없는 것은 동일하지만, 상속 받은 클래스에서 사용할 수 있다는 차이점이 있습니다.
아래 코드에서 protected로 선언된 introduce() 메소드는 Me class에 상속시 접근 가능하지만 private로 선언된 secret() 메소드의 경우 접근이 불가 합니다. 또한 protected로 선언된 introduce() 메소드는 외부에서 사용이 불가합니다.

class Person {
  public name: string;
  public age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  protected introduce(): void {
    console.log(`안녕 내 이름은 ${this.name} 나이는 ${this.age}`);
  }
  
  private secret(): void {
  	console.log("secret is...")
  }
}

class Me extends Person {
  constructor(name: string, age: number) {
    super(name, age);
    // 상속 가능 protected 접근 제어자이기 때문에 상속 가능
    this.introduce();
    // 에러 발생 private 접근 제어자로 인해 상속불가
    this.scret();
  }
}

const jon = new Me("Jon", 20);
console.log(jon.name, jon.age)
// protected 접근 제어자로 인해 접근 불가
jon.introduce();

readonly 접근 제어자는 해당 속성을 상수처럼 정의하도록 합니다. 즉, 한번 선언 후 변경이 불가능합니다.
아래 코드에서 name를 readonly 접근제어자로 지정하고, changeName() 메서드에서 이름을 변경하려고 하면 readonly 접근제어자로 인해 타입에러나 나타납니다.

class Person {
  readonly name: string;
  public age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  introduce(): void {
    console.log(`안녕 내 이름은 ${this.name} 나이는 ${this.age}`);
  }

  changeName(name: string): void {
    // 에러 readonly 접근 제어자로 인해 값 변경 불가
    this.name = name;
  }
}

const jon = new Person("Jon", 20);

클래스 접근 제어자 사용시 주의사항

클래스 접근 제어자를 사용할 때는 접근 범위에 따라 실행까지는 막아주지 않습니다.
즉, 타입 에러는 나타내어 주지만 직접 실행시점의 에러까지는 보장해 주지 못합니다.
타입스크립트가 실행시점의 에러까지 보장하지 못하는 이유는 타입 에러가 발생한 코드는 타입스크립트가 자바스크립트로 컴파일 하지 않고 무시하기 때문입니다.

만약, 실행결과 까지도 클래스 접근 제어자와 일치시키고 싶을 경우 ECMA2020에 추가된 private 문법(#)을 사용하면됩니다. # 문법을 사용하려면 tsconfig의 target 속성을 2015이상으로 변경해주어야 합니다.

class Bank {
// private # 문법
  #money: number;

  constructor(money: number) {
    this.money = money;
  }

  public withdraw(money: number) {
    const currentMoney = this.money - money;
    if (currentMoney > 0) {
      console.log(`예금 인출이 성공적으로 이루어졌습니다. 현재 남은 예금은 ${currentMoney}원 입니다.`);
    } else {
      console.log(`예금 인출 잔액이 부족합니다. 현재 남은 예금은 ${currentMoney}원 입니다.`)
    }
  }

  public inquiry() {
    console.log(`현재 예금 잔액은 ${this.money}원 입니다.`)
  }
}

const myBank = new Bank(1000);

myBank.withdraw(100);
myBank.withdraw(1000);
// 실행시점에 에러 발생 
myBank.money = 9999999;
myBank.inquiry();

제네릭(Generic) 이란 ?

제네릭(Generic)은 타입을 미리 정의하지 않고 사용하는 시점에서 원하는 타입을 정의해서 쓸 수 있도록하는 문법입니다.
마치 함수의 파라미터와 같은 역할을 하며, 인자에 넘긴 값을 함수의 파라미터로 받아 함수 내부에서 그대로 사용하는 것처럼 동작하게 됩니다.
제네릭을 사용하면 불필요한 중복되는 타입 코드를 줄이고, 코드 자동완성도 활용할 수 있습니다.

제네릭 사용 예시 코드

제네릭은 먼저 함수 이름 오른쪽에 를 붙인후 파라미터를 닫는 괄호 오른쪽에 콜론(:)을 붙이고 T를 붙입니다.
파라미터 타입을 T로 정의합니다.(T값은 원하는대로 사용해도 됩니다. 관례상 주로 T(type)로 사용됩니다.)
이렇게 정의 제네릭은 함수 실행시점에서 파라미터로 받은 타입을 적용시켜줍니다.
타입은 함수명뒤<>를 이용하여 전달합니다. T로 정의한 부분에 타입이 들어가게됩니다.

function getText<T>(text:T): T {
	return text;
}

// <>로 전달받은 타입을 적용
getText<string>('hi');
getText<number>(10);

제네릭을 사용하는 이유

제네릭을 불필요한 타입 중복사용을 줄이고, 코드 자동완성을 활용할 수 있도록 합니다.

(1) 불필요한 중복타입 사용 줄이기

제네릭을 사용하지 않는 경우에는 아래와 같은 타입마다 함수를 재생성 해야합니다.
제네릭 사용시 이런 불필요한 중복사용을 줄일 수 있습니다.

  function getBoolean(bool:boolean):boolean {
  	retun bool
  }
  
   function getBoolean(str:string):string {
  	retun str
  }
  
   function getBoolean(num:number):number {
  	retun number
  }
  
  // 제네릭을 활용하여 불필요한 중복코드 사용 제거
  function getSomting<T>(something:T):T {
  	retrun something;
  }

(2) 코드 자동완성 활용

제네릭 타입으로 불필요한 코드 사용을 줄일 수 있습니다. 물론 이것을 any 타입을 사용하여 구현할 수 있습니다.
하지만 any 타입의 경우 어떠한 타입 추론이 어렵기 때문에 자동완성 기능이 적용되지 않습니다.
제네릭 타입은 함수 실행시점에 타입을 정하므로 타입스크립트의 자동완성 기능을 활용할 수 있습니다.

   // 자동완성 사용불가
  function getSomtingAny(something: any):any {
  	return something;
  }
   // 제네릭을 활용하여 불필요한 중복코드 사용 제거
  // 자동완성 사용가능
  function getSomtingGeneric<T>(something:T):T {
  	return something;
  }
  // toString() 메소드 자동완성이 되지 않음
  getSomtingAny("10").toString();
  // toString() 메소드 자동완성이 됨
  getSomtingGeneric("10").toString();

인터페이스에서 제네릭 사용

제네릭은 함수뿐만 아니라 인터페이스에서도 사용할 수 있습니다.
아래 코드는 드롭다운 메뉴에 사용되는 인터페이스 코드입니다.
드롭다운 메뉴별로 다른 value 타입을 사용해야되서 새로운 value가 있다면 매번 새로운 interface를 정의해야 한다는 문제가 있습니다.
인터페이스에서도 제네릭을 사용하면 이런 중복사용 코드를 줄일 수 있습니다.

interface ProductDropdown{
  value: string;
  selected: boolean;
}  

interface StockDropDown {
 	value: number;
  	selected: boolean;
 }
  
interface AddressDropDown {
	value: {city: string; zipCode: string};
  	selected: boolean;
}

interface DropDown<T> {
  value: T,
  selected: boolean
}
  
// 드롭다운 유형별로 각각의 인터페이스를 연결
 const product: ProductDropdown;
const Stock: StockDropDown;
const address: AddressDropDown;
  
// 드롭다운 유형별로 하나의 제네릭 인터페이스로 연결
const product: DropDown<string>;
const Stock: DropDown<number>;
const address: DropDown<{city: string; zipCode: string}>

제네릭 타입의 제약

제네릭 타입의 경우 호출하는 시점에서 타입을 정의해서 유연하게 확장할 수 있습니다. 이것은 타입을 별도로 제약하지 않고 아무 타입을 받아서 쓸수 있다는 것을 의미합니다. 만약 별도의 타입만을 사용하고 싶다면 제네릭의 타입 제약 문법을 사용해야합니다.

(1) 상속 extends 키워드를 활용한 타입 제약

// string 타입만 받을 수 있도록 타입 제약
function stringThing <T extends stirng>(thig: T):T {
 	return thing
 } 
 stringThine<string>('jon');

 // length 속성이 존재하는 타입을 받도록 타입 제약(문자열, 배열, length 속성을 가진 객체)
 function lengthOnly<T extends { length: number }>(value: T) {
   return value.length;
 }
 
 stringThing("string");
 // 타입에러
 stringThing(100);
 
 lengthOnly([1,2,3]);
 // 타입에러
 lengthOnly(100);

(2) keyof 키워드를 활용한 타입 제약

keyof 키워드는 특정 타입의 키 값을 추출해서 문자열 유니언 타입으롷 변환해줍니다.

// "name", "skill"만을 받도록 타입 제약
function printKeys<T extends keyof { name: string; skill:string; }>(value: T) {
	console.log(value);
}

printKeys("name");
// 타입 에러
printKeys("jon");

정리

타입스크립트에서의 class는 함수 정의 방법과 비슷하며, 생성자 메서드에서 사용될 속성들을 미리 정의해주어야합니다.
class에서 사용되는 메서드와 속성에 대한 접근 권한을 접근 제어자를 통해 제어할 수 있습니다.
접근자 제어자의 종류로는 public, private, protected, readonly가 있습니다.
class 접근 제어자를 사용할 때는 접근 범위에 따라 실행까지는 막아주지 않습니다.
실행결과 까지도 클래스 접근 제어자와 일치시키고 싶을 경우 ECMA2020에 추가된 private 문법(#)을 사용해야합니다.
generic은 타입을 미리 정하지 않고 실행 시점에서 원하는 타입을 정의해서 쓸 수 있는 문법입니다.
마치 함수의 파라미터와 같은 역할을 합니다.
generic은 중복되는 타입 코드를 줄일 수 있고 자동완성 기능을 활용할 수 있다는 장점이 있습니다.
generic은 extends 키워드와 keyof 키워드를 활용하여 타입 제약을 할 수 있습니다.

profile
함께 개선하는 개발자

0개의 댓글