[TypeScript] 2. 클래스, 인터페이스

rin·2021년 2월 11일
0
post-thumbnail

https://poiemaweb.com/typescript-class

클래스

정의

ES6에서 새롭게 도입된 클래스는 기존 프로토타입 기반 객체지향 언어보다 클래스 기반 언어에 익숙한 개발자가 빠르게 학습할 수 있는 단순명료한 새로운 문법을 제시한다.

사실 클래스도 함수이고, 기존 프로토타입 기반 패턴의 Syntactic sugar일 뿐이다.

ES6에서 제공하는 클래스에서는

  • 몸체에 메소드만 포함가능하며,
  • 클래스 프로퍼티는 생성자 내부에서 선언하고 초기화한다.

하지만 TypeScript 클래스는 클래스 몸체에 클래스 프로퍼티를 사전 선언해야 한다.

class Person {
  // 클래스 프로퍼티를 사전 선언
  name: string;
  
  constructor(name) {
    this.name = name;
  }
  
  walk() {
    console.log(`${this.name} is walking`);
  }
}

const person = new Person('Lee');
person.walk(); // Lee is walking

접근 제한자

public, private, protected를 지원하며 기본적으로 의미 또한 클래스 기반 객체 지향 언어와 동일하다.

단, 접근 제한자를 명시하지 않은 경우, 암묵적으로 public이 선언된다.

접근 가능성publicprotectedprivate
클래스내부OOO
자식 클래스 내부OOX
클래스 인스턴스OXX

생성자 파라미터에 접근 제한자 선언

접근 제한자는 생성자 파라미터에도 선언할 수 있다.

이 때 접근 제한자가 사용된 생성자 파라미터는 암묵적으로 클래스 프로퍼티로 선언되고, 생성자 내부에서 별도 초기화가 없어도 암묵적으로 초기화가 수행된다.

class Foo {
  // 접근 제한자가 사용된 파라미터 x, y
  constructor(public x: string, private y: string) {}
}

// 이는 아래와 동일하다
class Foo {
  x: string;
  private y: string;
  
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

만약 생성자 파라미터에 접근 제한자를 선언하지 않으면 생성자 파라미터는 생성자 내부에서만 유효한 지역 변수가 된다.

class Foo {
  constructor(x: string) {
    console.log(x);
  }
  
  prt() {
    console.log(x); // x는 클래스 프로퍼티로 선언되지 않았기 때문에 에러가 발생한다.
  }
}

readonly

readonly가 선언된 클래스 프로퍼티는 선언 시 또는 생성자 내부에서만 값을 할당할 수 있다. 즉, 그 외의 경우에는 오직 읽기만 가능한 상태가 된다. 이를 이용하여 상수의 선언에 사용한다.

class Foo {
  private readonly MAX_LEN: number = 5;
  private readonly MSG: string;
  
  constructor() {
    this.MSG = 'hello';
  }
  
  log() {
    this.MAX_LEN = 10; // error
    this.MSG = 'Hi'; // error
  }
}

new Foo().log(); // log 내부에서 readonly로 선언된 프로퍼티를 변경하려고 하고 있으므로 에러가 발생한다.

static

정적 메소드를 정의한다.
정적 메소드는 클래스의 인스턴스가 아닌 클래스 이름으로 호출한다. (클래스 기반 객체 지향 언어와 동일하게 작동한다.)

class Foo {
  staic instanceCount = 0;
  
  constructor(prop) {
    this.prop = prop;
    Foo.instanceCount++; // 생성자가 호출될 때마다 1씩 증가
  }
  
  static staticMethod() {
    return 'static method';
  }
  
  prototypeMethod() {
    return this.prop;
  }
}

console.log(Foo.staticMethod()); // static method

const foo = new Foo(123);
const boo = new Foo(100);
console.log(foo.prototypeMethod()); // 123
console.log(Foo.instanceCount); // 2

console.log(boo.staticMethod()); // error
console.log(boo.instanceCount); // error

단 static으로 선언된 메소드나 프로퍼티는 인스턴스로 호출 할 시 에러가 발생한다.

추상 클래스 (abstract class)

하나 이상의 추상 메소드를 포함하며 일반 메소드도 포함할 수 있다.

❗️ NOTE
추상 메소드란 내용없이 메소드 이름과 타입만이 선언된 메소드를 의미한다.

추상 클래스는 직접 인스턴스 생성이 불가하며 오로지 상속만을 위해 사용된다. 추상 클래스를 상속한 클래스는 추상 클래스의 추상 메소드를 반드시 구현하여야 한다.

abstract class Animal {
  abstract makeSound(): void;
  
  move(): void {
    console.log('roaming the earth ....');
  }
}

const animal = new Animal(); // 추상 클래스는 직접 인스턴스를 생성할 수 없으므로 에러가 발생한다.

class Dog extends Animal {
  // 추상 클래스를 상속받은 클래스는 반드시 추상 메소드를 구현한다.
  makeSound() {
    console.log('bowwow~~');
  }
}

const dog = new Dog();
dog.move(); // roaming the earth ....
dog.makeSound(); // bowwow~~

인터페이스

정의

인터페이스는 모든 메소드가 추상 메소드인 추상 클래스이다. (단, abstract class가 아닌 interface로 선언한다)

여러가지 타입을 갖는 프로퍼티로 이뤄진 새로운 타입을 정의하는 것과 유사하며, 인터페이스에 선언된 프로퍼티/메소드는 구현이 강제되며 이를 통해 일관성을 유지할 수 있다.

일반적으로 타입 체크를 위해 사용되며 변수, 함수, 클래스에 사용할 수 있다. 📌

변수, 함수 그리고 인터페이스

인터페이스는 변수의 타입으로 사용할 수 있다. 아래 예제를 먼저 보도록 하자.

interface Todo {
  id: number;
  content: string;
  completed: boolean;
}

let todo: Todo;

todo = { id: 1, content: 'typescript', completed: false };

Todo 라는 인터페이스를 선언하였고, 해당 타입의 todo 인스턴스를 선언하였다.
todo 인스턴스는 Todo 인터페이스를 준수하여야한다.

뿐만아니라 함수의 타입으로도 사용될 수 있다.

interface SquareFunc {
  (num: number): number; // 이 함수는 number 타입의 num 파라미터를 받고, number 타입의 리턴 값을 가져야한다.
}

const squareFunc: SquareFunc = (num: number): number => {
  return num*num;
}

console.log(squareFunc(10)); // 100;

인터페이스를 구현하는 클래스

클래스 선언문에 implements ${interface_name}를 포함시키면 해당 클래스는 지정된 인터페이스를 반드시 구현하여야한다.

interface IPerson {
  name: string;
  sayHello(): void;
}

class Person implements IPerson {
  // 인터페이스에서 정의한 프로퍼티 구현
  constructor(public name: string) {}
  // 인터페이스에서 정의한 추상 메소드 구현
  sayHello() {
    console.log(`Hello ${this.name}`);}
}

🐤 Duck typing

❗️ 주의 할 점은 타입 체크 시 인터페이스를 구현했는지에 대한 여부가 아닌 실제로 인터페이스에서 요구하는 값을 가지고 있는지를 확인한다는 것이다.

interface IDuck {
  quack(): void;
}

class MallardDuck implements IDuck {
  quack() {
    console.log('Quack!');
  }
}

function makeNoise(duck: IDuck): void {
  duck.quack();
}

위와 같은 IDuck 인터페이스, 인터페이스를 구현한 클래스, IDuck 인터페이스 타입을 파라미터로 받는 함수가 있다고 생각해보자.

MallardDuck이 생성한 인스턴스는 makeNoise의 파라미터로 당연히 사용할 수 있을 것이다.

makeNoise(new MallardDuck());

그렇다면 IDuck 인터페이스를 구현하지 않았지만, 해당 인터페이스가 가지고 있는 추상메소드인 quack() 메소드와 동일한 이름의 메소드를 가진 클래스의 경우에는 어떠할까? 🤔

class RedheadDuck {
  quack() {
    console.log('Quack!');
  }
}

makeNoise(new RedheadDuck());

이 경우에도 makeNoise 함수는 문제없이 실행된다.

프로퍼티의 경우에도 마찬가지다.
특정 인터페이스가 포함하는 프로퍼티를 동일하게 가지고 있는 경우, 인터페이스를 상속받은 클래스와 동일하게 타입 체크를 통과한다.

선택적 프로퍼티

선택적으로 필요한 프로퍼티의 경우, 프로퍼티 명 뒤에 ?를 붙임으로써 이를 생략할 수 있도록 정의한다.

interface UserInfo {
  username: string;
  password: string;
  age?: number;
}

const userInfo: UserInfo = {
  username: 'robin',
  password: '1234'
} // age는 생략가능하다.

인터페이스 상속

인터페이스도 클래스처럼 extends 키워드를 사용해 인터페이스 혹은 클래스를 상속받을 수 있다.

  • ,을 이용하여 한 번에 복수개의 인터페이스를 상속받을 수 있다.
  • 클래스를 상속받는 경우, (접근제한자에 상관없이) 클래스의 모든 멤버가 상속되지만 구현은 상속되지 않는다.
profile
🌱 😈💻 🌱

0개의 댓글