TypeScript 강의 정리: 클래스 & 인터페이스

zeroequaltwo·2022년 10월 3일
0

TS

목록 보기
5/8
post-thumbnail

1. 클래스 기초

1. 클래스 & 인스턴스란?

1) 클래스: 객채의 청사진으로, 객체의 형태 및 포함되어야 할 메소드를 정의한다.

  • 관례상 클래스를 정의할 때 맨 첫글자를 대문자로 작성한다.
  • 생성자(constructor): 인스턴스를 생성할 때 초기화하기 위한 메소드이다.
    -> 인스턴스를 생성할 때 받은 인자는 생성자를 통해 클래스의 필드에 저장되며 이는 결국 프로퍼터(클래스 내에서 사용하는 변수)로 변환된다.

2) 인스턴스: 클래스의 구조에 따라 생성된 객체

// 클래스
class Department {
  // 프로퍼티(key-value와 헷갈리지 말기)
  name: string;
  //생성자
  constructor(n: string) {
    this.name = n;
  }
}

// 인스턴스
const accounting = new Department("Accounting");	
console.log(accounting);  // Department {name: 'Accounting'}

2. 클래스의 this

  • 클래스를 사용하면 this를 자주 볼 수 있는데 여기서 this는 개별 인스턴스를 의미한다. 따라서 this를 쓰지 않고 그냥 변수처럼 불러올 경우 클래스 외부에 있는 변수를 찾아올 수 있다.
class Department {
  name: string;
  
  constructor(n: string) {
    this.name = n;
  }
  
  describe() {
    console.log(`Department: ${name}`); // 이상한 값이 뜬다.
    console.log(`Department: ${this.name}`);  // Department: Accounting
  }
}

const accounting = new Department("Accounting");	
accounting.describe();

3. public & private

  • private은 클래스 내부에서 지정한 메소드를 제외하고는 외부에서 해당 프로퍼티의 값을 변화시키지 못하게 한다. private을 쓰는 이유는 큰 프로젝트일수록 통일되고 일관적인 코드를 쓰는 것이 좋기도 하고, 메소드 안에 validation이라든지 여러 조건을 체크하고 값을 변화시키는 과정이 있을 수도 있는데 외부에서 이를 무시하고 값을 추가하지 못하게 막을 수 있기 때문이다.
  • public은 private의 반대 개념으로, 클래스 내부와 외부를 가리지 않고 접근할 수 있는 프로퍼티다. 클래스 프로퍼티는 디폴트가 public으로 설정되어 있다.
  • 프로퍼티 앞에 private/public이라고 쓰면 된다.
  • JS에서는 public과 private의 개념이 꽤나 최근까지도 없었기 때문에 컴파일 버전에 따라 private으로 설정하고 외부에서 접근했어도 문제로 인식하지 않는 경우가 있다.
class Department {
  // private & public
  public name: string;  // public은 굳이 안써도 된다.
  private employees: string[] = [];

  constructor(n: string) {
    this.name = n;
  }

  addEmployee(employee: string) {
    this.employees.push(employee);
  }

  printEmployeeInfo() {
    console.log(this.employees.length);
    console.log(this.employees);
  }
}

const accounting = new Department("Accounting");

// 클래스 내부 메소드로 접근
accounting.addEmployee("Max");
accounting.addEmployee("Anna");
accounting.printEmployeeInfo();

// 클래스 외부 메소드로 접근
// accounting.employees.push("Tom");  // employees는 클래스 내에서만 엑세스할 수 있습니다.
// accounting.printEmployeeInfo();

4. 약식 초기화

  • 일일이 프로퍼티값과 초기값을 매치해줄 필요없이 아래와 같이 약식으로 작성 가능하다.
class Department {
  // 이중으로 작성할 필요 X
  // private id: string;
  // public name: string;
  private employees: string[] = [];

  constructor(private id: string, public name: string) {  // 약식으로 한 번만 작성
    // 이중으로 작성할 필요 X
    // this.id = id;
    // this.name = name;
  }
}

5. Readonly

  • readonly는 초기화 이후 값이 변해서는 안되는 프로퍼티를 의미한다.
class Department {
  private employees: string[] = [];

  constructor(private readonly id: string, public name: string) {}

  addEmployee(employee: string) {
    this.id = "D0002";  // 읽기 전용이므로 새 값을 할당할 수 없습니다.
    this.employees.push(employee);
  }
}


2. 상속

1. 상속이란?

  • 클래스를 정의할 때 이미 정의된 클래스의 내용을 그대로 활용할 수 있는데 이를 상속이라고 한다. 클래스 명 옆에 extends 상속받을 클래스명을 입력하면 된다.
  • 자식 클래스에서 생성자를 작성할 때 부모 클래스로부터 물려받은 것은 super를 통해 호출한다. 생성자 내에서 super는 하나만 사용되고, this 키워드가 사용되기 이전에 작성되어야 한다.
  • 자식 클래스는 한 클래스로부터만 상속받을 수 있다.
/*----- 부모 클래스 -----*/
class Department {
  private employees: string[] = [];

  constructor(private readonly id: string, public name: string) {}

  describe() {
    console.log(`Department ID: ${this.id}`);
    console.log(`Department NAME: ${this.name}`);
  }

  addEmployee(employee: string) {
    this.employees.push(employee);
  }

  printEmployeeInfo() {
    console.log(this.employees.length);
    console.log(this.employees);
  }
}

const accounting = new Department("D0001", "Accounting");

/*----- 자식 클래스 -----*/
class ITDepartment extends Department {
  admins: string[];

  constructor(id: string, admins: string[]) {
    super(id, "IT");  // 부모 클래스로부터 id, name 프로퍼티를 물려받았다.
    this.admins = admins;  // this는 super 다음에!
  }
}

const it = new ITDepartment("D0002", ["Kim"]);
console.log(it);  // ITDepartment {id: 'D0002', name: 'IT', employees: Array(0), admins: Array(1)}

2. 메소드 재정의와 protected

  • 자식 클래스는 부모 클래스에서 정의한 메소드를 재정의할 수 있다. 만약 메소드 재정의 시 부모 클래스에서 상속받은 private 프로퍼티를 수정해야 하는 경우가 있다면 protected를 활용할 수 있다.
  • protected는 private과 유사하게 클래스 외부에서 속성에 함부로 접근할 수 없게 한다는 공통점이 있지만, private은 딱 그 클래스를 사용한 인스턴스에서만 프로퍼티에 접근할 수 있는 반면 protected는 그 클래스를 상속받은 자식 클래스에서도 해당 프로퍼티에 접근할 수 있게 한다는 차이가 있다.
/*----- 부모 클래스 -----*/
class Department {
  // private -> protected
  protected employees: string[] = [];

  constructor(private readonly id: string, public name: string) {}

  addEmployee(employee: string) {
    this.employees.push(employee);
  }
}

const accounting = new Department("D0001", "Accounting");

/*----- 자식 클래스 -----*/
class ITDepartment extends Department {
  admins: string[];

  constructor(id: string, admins: string[]) {
    super(id, "IT");
    this.admins = admins;
  }

  // 메소드 재정의 
  addEmployee(employee: string) {
    if(employee === "Max"){
      return;
    }
    this.employees.push(employee);  // private일 땐 에러나지만 protected일 땐 에러 안 난다.
  }
}

const it = new ITDepartment("D0002", ["Kim"]);
console.log(it);

3. getter & setter

1) 획득자(getter):

  • getter는 프로퍼티가 함수나 메소드를 사용하여 값을 가져올 수 있게 하는 기능이다. 생긴 건 함수지만 본질은 프로퍼티라는 말이다.
  • getter는 궁극적으로 값이기 때문에 반드시 무언가를 return해야 하며, 이를 호출할 때는 메소드가 아닌 프로퍼티에 접근하듯 중괄호{} 없이 호출해야 한다.

2) 설정자(setter):

  • getter는 말 그대로 받아오는 역할만을 하기 때문에 값을 할당할 수는 없다. 따라서 값을 할당하려면 setter를 사용해야 한다.
const thisYear: number = new Date().getFullYear();

class Student {
  constructor(private name: string, private bornYear: number = thisYear) {}

  // getter
  get koreanAge() {
    return thisYear - this.bornYear + 1;
  }
  // setter
  set koreanAge(val) {
    if (thisYear - val + 1 >= 20) {
      throw new Error("어른은 빠지세요~~!");
    }
    this.bornYear = val;
  }
}

let kim = new Student("Kim");
// setter
kim.koreanAge = 2006;
// getter
console.log(kim.koreanAge);  // 17

4. 정적 프로퍼티 & 메소드(Static property & method)

  • 정적 프로퍼티 & 메소드는 클래스를 인스턴스화시키지 않고 사용할 수 있는 프로퍼티와 메소드를 의미한다. 인스턴스에서는 호출할 수 없다.
  • 정적 프로퍼티 & 메소드는 static 설정을 해두지 않은 부분들에서는 접근할 수 없다. 또한 클래스 내에서 this로 호출할 수 없고, 호출하고자 한다면 클래스명을 대신 써야 한다.
  • static을 공부하다 보면 한가지 의문점이 드는데 인스턴스화 안하고 그냥 쓸 수 있게 만들 거면 전역변수와 함수로 만들어버리면 되지 왜 static을 쓰냐는 점이다. 우리는 이미 static을 배우기 전부터 이를 수도 없이 써왔는데 너무나도 친숙한 Math 클래스가 바로 그것이다. Math.floor(), Math.ceil(), Math.pow() 등 Math 클래스 아래 많은 static 메소드들이 예쁘게 묶여 보기좋게(?) 사용되어져 왔다. 이처럼 인스턴스 프로퍼티 & 메소드 없이 static으로 구성된 클래스를 유틸리티 클래스라고 하고, static 이런 경우에 자주 사용된다고 한다.
class Student {
  // 정적 프로퍼티
  static thisYear: number = new Date().getFullYear();
  
  constructor(
    private name: string,
    private bornYear: number = Student.thisYear  // 클래스 내에서 사용하려면 클래스명을 붙여야 한다.
  ) {}

  get koreanAge() {
    return Student.thisYear - this.bornYear + 1;
  }

  set koreanAge(val) {
    if (Student.thisYear - val + 1 >= 20) {
      throw new Error("어른은 빠지세요~~!");
    }
    this.bornYear = val;
  }
}

let kim = new Student("Kim");
kim.koreanAge = 2006;
console.log(kim.koreanAge);

5. 추상 클래스 & 메소드(Abstract class & method)

  • 추상 메소드는 클래스 내에 정의된 메소드의 구조 정도만 정의해두고 정확한 내용 구현은 자식 클래스에게 일임하는 메소드를 의미한다.
  • 추상 메소드를 하나라도 포함한 클래스를 추상 클래스라고 한다. 추상 클래스는 그 자체로 인스턴스를 만들 수는 없다. 오로지 상속을 위해 존재하는 클래스이다.
abstract class Student {
  static thisYear: number = new Date().getFullYear();
  constructor(
    protected name: string,
    protected bornYear: number = Student.thisYear
  ) {}

  abstract describe(this: Student): void;
}

// let Lee = new Student("Lee", 2005);  // 추상 클래스는 인스턴스를 만들 수 없습니다.

class SmartStudent extends Student {
  private scores: number[] = [];

  constructor(name: string, bornYear: number) {
    super(name, bornYear);
  }

  addScore(score: number) {
    this.scores.push(score);
  }

  // 추상 메소드를 구현하지 않으면 오류가 난다.
  describe() {
    console.log(this.scores);
  }
}

let kim = new SmartStudent("Kim", 2006);
kim.addScore(97);
kim.addScore(100);
kim.addScore(87);
kim.describe(); // [97, 100, 87]

6. Singleton & private constructor

  • 싱글튼 패턴은 특정 클래스의 인스턴스가 하나만 생성될 수 있도록 제한한다. 이 패턴은 정적 메소드 & 프로퍼티를 사용할 수 없다.
  • private consturctor는 클래스 밖에서 인스턴스를 생성할 수 없게 한다. 싱글튼 패턴을 구현할 때 사용하지만 클래스를 정의하고 클래스 밖에서 new Instance()를 하지 못하게 하는 것은 싱글은커녕 제로(?)로 만들어버리는 것이므로 매우 이상하다. 이때 static 프로퍼티를 사용하면 이를 해결할 수 있다. static instance: 클래스 자기자신; 으로 설정해버리면 인스턴스 없이도 불러낼 수 있게 된다.
/*----- 부모 클래스 -----*/
class Department {
  constructor(
    private readonly id: string,
    public name: string,
    protected employees: string[]
  ) {}
}

/*----- 자식 클래스 -----*/
class ITDepartment extends Department {
  admins: string[] = [];
  private static instance: ITDepartment;

  private constructor(id: string, admins: string[], employees: string[]) {
    super(id, "IT", employees);
    this.admins = admins;
  }

  static getInstance() {
    if (ITDepartment.instance) {
      return this.instance;
    }
    this.instance = new ITDepartment("D0002", ["Kim"], ["Kim", "Lee", "Park"]);
    return this.instance;
  }
}

// const it = new ITDepartment("D0002", []);  // private constructor라서 그냥 인스턴스화 못 한다.

const it = ITDepartment.getInstance();
console.log(it);  // ITDepartment {id: 'D0002', name: 'IT', employees: Array(3), admins: Array(1)}


3. 인터페이스

1. 인터페이스란?

  • 객체의 구조를 설명만 한다. 즉, 클래스는 인스턴스화를 통해 거푸집으로 찍어내듯이 객체를 만들어낼 수 있지만 인터페이스는 틀만 정해주기 때문에 초기값을 지정해주거나(에러 발생) 구체적인 내용을 할당하진 못한다.
  • 인터페이스 내에도 메소드도 넣을 수 있지만 역시 메소드의 구체적인 내용을 넣는 것이 아니라 구조만 짠다.
  • 인터페이스는 JS에는 없고 TS에만 있는 기능이다. 따라서 컴파일을 한다고 해도 JS 파일엔 아무런 변화도 일어나지 않는 순수한 개발 전용 기능이다.
  • 클래스와 마찬가지로 맨 앞글자를 대문자로 쓰는 것이 관례이다.
interface Person {
  name: string;
  age: number;
  
  greet(phrase: string): void;
}

// 인터페이스를 적용해 객체 직접 구현
let user1: Person;
user1 = {
  name: "Max",
  age: 30,
  
  greet(phrase: string) {
    console.log(phrase + ' ' + this.name);
  }
};

user1.greet("Hello,");

2. 인터페이스를 사용하는 이유(클래스와 인터페이스)

  • interface는 type과 기능적으로 별 차이 없어 보이지만 기술적으로 약간의 차이점을 보인다. 인터페이스는 객체의 구조만을 설명하기 위해 쓰고, 확장이 가능하며, TS의 고급타입 중 일부를 적용할 수 없다.
    참고: 타입 vs 인터페이스 비교
  • 인터페이스는 주로 서로 다른 클래스 간에 기능을 공유하기 위해 사용되며, 상속과는 다르게 클래스에 다수의 인터페이스를 적용할 수 있다.
  • 추상 클래스와도 비슷하게 느껴지지만 상속을 통해서만 구현할 수 있는 추상클래스와 여러 클래스들이 공유할 수 있는 인터페이스는 탄생 목적(?)이 다르게 느껴진다.
  • 인터페이스를 적용해 직접 객체를 만들 때는 인터페이스의 룰을 엄격하게 지켜야하지만(인터페이스에서 지정한 내용만 넣어야 하며 마음대로 뺄 수 없음) 클래스를 통해 인터페이스를 적용할 경우 인터페이스에서 지정한 내용 이외에도 내용을 추가할 수 있다.
interface Greetable {
  name: string;
  greet(phrase: string): void;
}

// 클래스에 인터페이스를 적용해 객체 구현
class Person implements Greetable {
  name: string;
  age = 30;  // 인터페이스 외의 내용
  
  constructor(n:string) {
    this.name = n;
  }
  
  greet(phrase: string) {
    console.log(phrase + ' ' + this.name);
  }
}

let user1: Greetable;
user1 = new Person("Max");
console.log(user1);  // Person {age: 30, name: 'Max'}

3. 인터페이스의 readonly

  • 인터페이스 프로퍼티에도 readonly 속성을 추가할 수 있다.(public, private은 X)
    cf) type도 readonly 속성 사용 가능하다.
  • 인터페이스에서 readonly 설정된 프로퍼티는 클래스에 적용될 때 별다른 설정이 없더라도 변경 불가능하다.
interface Greetable {
  readonly name: string;
  greet(phrase: string): void;
}

class Person implements Greetable {
  name: string;
  age = 30;

  constructor(n: string) {
    this.name = n;
  }

  greet(phrase: string) {
    console.log(phrase + " " + this.name);
  }
}
let user1: Greetable;
user1 = new Person("Max");
// user1.name = "Anna";  // 읽기 전용이므로 할당 불가능

3. 인터페이스 확장하기

  • 인터페이스는 extends를 통해 확장이 가능하다.
interface Person {
  name: string;
  age: number;
}
interface Greetable extends Person {
  greet(): void;
}

class GreetWithWho implements Greetable {
  name: string;
  age: number;

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

  greet() {
    if (this.age < 10) {
      console.log("안녕, " + this.name);
    } else {
      console.log("안녕하세요, " + this.name);
    }
  }
}

let user1: Greetable;
user1 = new GreetWithWho("Max", 30);
user1.greet();  // 안녕하세요, Max

let user2: Greetable;
user2 = new GreetWithWho("Anna", 5);
user2.greet();  // 안녕, Anna

4. 인터페이스로 함수구조 정하기

  • 인터페이스로 함수의 구조도 정의가 가능하다.
// 타입
type AddFnType = (a: number, b: number) => number;
let addType: AddFnType;
addType = (n1: number, n2: number) => {
  return n1 + n2;
};
console.log(addType(3, 5));  // 8

// 인터페이스
interface AddFnInterface {
  (a: number, b: number): number;
}
let addInterface: AddFnInterface;
addInterface = (n1: number, n2: number) => {
  return n1 + n2;
};
console.log(addInterface(7, 4));  // 11

5. 선택적 매개변수와 프로퍼티(Optional parameter & property)

  • 선택적 매개변수와 프로퍼티는 없어도 문제가 안되는 매개변수와 프로퍼티를 의미하고, 물음표(?)를 통해 지정한다.
  • 선택적 프로퍼티는 인터페이스는 물론 클래스에서도 적용 가능하다. 클래스에서 프로퍼티를 옵셔널로 설정하면 constructor도 그에 맞게 조건부로 바꿔야 하며, 클래스에 적용된 인터페이스에도 옵셔널을 적용해야 한다.
interface Greetable {
  name?: string;  // 클래스에서 옵셔널로 설정했으므로 인터페이스도 변경
  nickname?: string;  // 인터페이스에서만 옵셔널로 설정
  age: number;
  greet(): void;
}

class GreetWithWho implements Greetable {
  name?: string;  // 클래스에서 옵셔널로 설정
  age: number;

  constructor(age: number, name?: string) {  // 생성자도 옵셔널로 설정, 옵셔널 변수는 항상 뒤에 있어야 한다.
    // 조건부 초기값 할당
    if (name) {
      this.name = name;
    }

    this.age = age;
  }

  greet() {
    console.log("안녕하세요, " + this.name);
  }
}

let user1: Greetable;
user1 = new GreetWithWho(30);  // 파라미터가 모자르다고 에러나지 않는다.
profile
나로 인해 0=2가 성립한다.

0개의 댓글