타입스크립트5: Classes

윤뿔소·2023년 2월 13일
3

TS

목록 보기
3/5
post-thumbnail

이번 시간에는 타입스크립트로 객체지향을 작성하는 법을 알아보자. 왜냐면 타스는 객체지향을 좋아하거든!

선행 학습

먼저 OOP를 사용하는데 필요한 기본 지식을 습득하자

this

this는 컨스트럭터 필드에 쓰던 문법이고 바닐라 JS에선 필수다.

객체 그 자신을 가리키는 키워드로 사용

이렇게만 알고가도 충분하다.
this는 어디까지 참조이고, 문맥에 따라 어디에 속하건 기준만 잡으면 큰 문제가 없다.

객체 필드 및 프로퍼티

public

어디서든지 접근할 수 있으며 외부 인터페이스를 구성. 키워드를 써주지 않으면 기본값.

private

클래스 내부에서만 접근할 수 있으며 내부 인터페이스를 구성할 때 쓰임.
#으로 시작, #이 붙으면 클래스 안에서만 접근하다는 얘기
private 필드는 언어 자체에 의해 강제된다는 점이 장점.

protected

프로퍼티 명 설정, 앞엔 밑줄 _이 붙는다.

class CoffeeMachine {
  _waterAmount = 0;

  set waterAmount(value) {
    if (value < 0) throw new Error("물의 양은 음수가 될 수 없습니다.");
    this._waterAmount = value;
  }

  get waterAmount() {
    return this._waterAmount;
  }

  constructor(power) {
    this._power = power;
  }

}

// 커피 머신 생성
let coffeeMachine = new CoffeeMachine(100);

// 물 추가
coffeeMachine.waterAmount = -10; // Error: 물의 양은 음수가 될 수 없습니다.
  • getter, setter를 설정한 모습, useState와 비슷한 개념이다.
class CoffeeMachine {
  #waterLimit = 200;

  #checkWater(value) {
    if (value < 0) throw new Error("물의 양은 음수가 될 수 없습니다.");
    if (value > this.#waterLimit) throw new Error("물이 용량을 초과합니다.");
  }

}

let coffeeMachine = new CoffeeMachine();

// 클래스 외부에서 private에 접근할 수 없음
coffeeMachine.#checkWater(); // Error
coffeeMachine.#waterLimit = 1000; // Error
  • #waterAmount에 접근하려면 waterAmount의 getter와 setter를 통해야 함.
구분선언한 클래스 내상속받은 클래스 내인스턴스
private  ⭕    ❌    ❌  
protected  ⭕    ⭕    ❌  
public  ⭕    ⭕    ⭕  

추가로 메소드는 클래스 안에 존재하는 함수라는 뜻

Classes

자스처럼 써주면서 차이점을 알아가보자.
먼저 한마디로 정리하자면 필드에는 어떤 보호 등급인지(접근 제어자), 이름, 타입만 써주면 된다! 그리고 타스에서 알아서 문법 오류나 오용 등을 거르고 컴파일될 때 다 빼고 자스로 넘어가게 된다. 차차 알아가보자.

class Player {
  constructor(
    protected firstname: string,
    private lastname: string,
    public nickname:string
  ) {}
}

const rhino = new Player("rhino", "Yoon", "윤뿔소")

JS에선 this를 꼭 써줬어야 했는데 타스에선 그냥 써주면 알아서 된다. 이게 🐶꿀

객체필드(private)도 사용용도에 따라 적어준 모습이다. 그런데 컴파일되면서 없어졌다. 이게 무슨말이냐. 타스가 오로지 코드를 보호하기 위해 사용한 키워드고 JS에선 안쓰이는 것이니 편하게 사용하면 된다. 훨씬 직관적이다! public도 똑같이 써주면 된다. 당연히 private 써준 건 객체 밖에서 사용하면 안된다.

사용하고 싶다면?! public을 붙이든가 따로 public으로 된 함수를 써줘야한다.

class Player {
  constructor(
    protected firstName: string,
    private lastName: string,
    public nickname: string
  ) {}
  getFullName(){
    return `${this.firstName} ${this.lastName}`
  }
}
const rhino = new Player("rhino", "Yoon", "윤뿔소");

rhino.getFullName();

이런 식으로 말이다. 당연히 getFullName앞에 private붙이면 못쓰겠지? 저런 식으로 쓰면 된다. 객체지향이니 캡슐화, 추상화가 목적이니까 말이다.

Astract Class

타스의 객체지향 클래스 중 가장 매력적인 포인트 추상클래스다.

다른 클래스가 상속받을 수 있는 클래스, 하지만 생성자로 새로운 인스턴스를 만들 수 없다.
자식을 낳고 은퇴한 부모님 같달까? 다시 말하자면 추상 클래스는 오직 다른 곳에서 상속할 수만 있는 클래스.

자스에서 쓰던 extend랑 비슷하다. 하지만 차이점은 인스턴스로 선언이 불가하다는 얘기다. 즉, 기능 분리를 했다는 얘기. 이러한 점에서 자스완 대비된다. 훨씬 정적!

abstract class User {
  constructor(
    protected firstName: string,
    private lastName: string,
    public nickname: string
  ) {}
  getFullName(){
    return `${this.firstName} ${this.lastName}`
  }
}

class Player extends User {
  // 추상 메서드는 추상 클래스를 상속받는 클래스들이 반드시 구현(implement)해야하는 메서드.
  getNickname() {
    console.log(this.nickname)
  }
}

// 불가
const rhino1 = new User("rhino", "Yoon", "윤뿔소")
// 가능
const rhino2 = new Player("rhino", "Yoon", "윤뿔소")

다시 말하자면, 클래스의 메소드 getFullName가 무엇을 구현해야하는지 단서, 청사진을 주는 것이다.

또한 상속받은 클래스는 당연히 상속을 준 클래스의 메소드도 사용 가능하다.

rhino2.getFullName();
rhino2.getNickname();

이렇게 말이다!

Method in Abstract Class

메소드는 클래스 안에 있는 함수를 뜻한다. 여기에서도 추상화를 쓸 수 있다.

abstract class User {
  constructor(
    protected firstName: string,
    private lastName: string,
    public nickname: string
  ) {}
  abstract getNickname():void
  getFullName() {
    return `${this.firstName} ${this.lastName}`
  }
}

class Player extends User {
  // 추상 메서드는 추상 클래스를 상속받는 클래스들이 반드시 구현(implement)해야하는 메서드.
  // 즉, getNickname을 꼭 구현해야한다.
}

오류를 보면 상속받은 추상클래스 중 추상 멤버인 getNickname을 꼭 구현해야한다고 나와있다. 중요! 그래서 꼭 구현해야한다.

class Player extends User {
  getNickname() {
    console.log(this.firstName)
    console.log(this.nickname)
  }
}

이렇게 말이다 ㅎㅎ. 당연히 안에 써줘야하는 건 접근 제어자 중 public이나 protected을 받은 변수나 함수겠지? 개념을 모르겠다면 사전 학습으로 가서 다시 보자.

TS의 클래스 recap

추상클래스, 추상메소드의 요약을 설명하겠다. 참고로 접근제어자 키워드는 TS에서만 제공하는 기능이다. JS로 넘어갈 땐 출력되지 않는다.

  1. TS의 클래스는 JS와 비슷하지만 constructor 필드에 어떤 보호 등급인지(접근 제어자), 이름, 타입만을 작성한다.
  2. 추상클래스는 extend와 비슷하다. 하지만 생성자를 통한 인스턴스로 못만든다. 상속만 가능한 클래스다.
  3. 추상메소드는 구현이 돼있지 않은 메소드다. 즉, Call Signature만 가지고 있고, 인자를 가질 수도 있는 그런 상태다.
  4. 추상클래스에게 상속 받는 클래스가 있다면 거기에 무조건 추상메소드를 구현해야한다.
  5. 추상메소드를 구현할 때 상속 받은 클래스의 접근제어자에 따라 변수를 사용해야한다. private은 안된다.

다시 강조하지만, TS 클래스의 모든 보호 기능은 TS에서만 작동한다.

JS로 컴파일된 코드를 실행했다. 잘된다!

실습: TS 클래스로 해시맵 만들기

Dictionary같은 걸 만들어보자.

클래스 작성

type Words = {
  // 해시
  [key: string]: string | string[];
  // 객체의 property에 대해 모르지만 타입만을 알 때 유용하다.
};
class Dict {
  private words: Words;
  constructor() {
    this.words = {};
  }
  add(word: Word) {
    // word는 Word 클래스의 인스턴스 타입.
    if (!this.words[word.term]) {
      // 사전에 없는 단어이면
      this.words[word.term] = word.def;
    }
  }
  find(term: string) {
    return this.words[term];
  }
  // 단어를 삭제
  rmv(term: string) {
    delete this.words[term];
  }
  // 단어 이름 업데이트
  update(oldTerm: string, newTerm: string) {
    if (this.words.hasOwnProperty(oldTerm)) {
      this.words[newTerm] = this.words[oldTerm];
      delete this.words[oldTerm];
    }
  }
  // 사전에 저장된 단어의 개수
  size() {
    return Object.keys(this.words).length;
  }
  // 모든 사전의 이름과 뜻 출력
  all() {
    for (let [key, value] of Object.entries(this.words)) {
      console.log(`${key}: ${value}`);
    }
  }
}
// words는 initializer 없이 선언해주고 contructor에서 수동으로 초기화
// constructor에 인자로 넣어 constructor가 지정해주길 바라는 게 아니므로

// 각각의 단어에 대한 클래스
class Word {
  constructor(public term: string, public def: string | string[]) {}
  // 단어 출력하는 메소드
  toString() {
    console.log(`${this.term}: [뜻] ${this.def}`);
  }
  // 단어 정의 추가
  addDef(newDef: string) {
    if (typeof this.def === "string") {
      this.def = [this.def, newDef];
    } else {
      this.def = [...this.def, newDef];
    }
  }
  // 단어 정의 수정
  updateDef(oldDef: string, newDef: string) {
    if (typeof this.def === "string") {
      if (oldDef === this.def) this.def = newDef;
    } else {
      this.def.filter((val) => val !== oldDef);
      this.def.push(newDef);
    }
  }
}
  1. 클래스 Dict 작성 후 private words;를 작성해서 기본 내용 추가
  2. 타입 Words를 선언해 만들었다. string타입인 것만 알고있기에, 객체의 타입 설정에서 제한된 양의 Property나 key를 가지는 타입을 정의해주는 방법이다.
    걍 Property(key)의 이름이 다양할수도 있으니까 타입만 다 공통되게 묶어준 거임.
  3. words를 선언하고, Constructor를 수동으로 초기화 시켜줘 words를 컨스트럭터가 지정해주지 않으면서 선언하게 만듦.
  4. Word 클래스를 만들고 음식이름과 설명을 넣을 수 있는 클래스로 선언
  5. 클래스 Dict는 사전이므로 추가할 메소드-함수를 추가하도록 만들기
    5-1. add를 넣어 인자 word를 받으면 Dictwordsword가 없다면 추가하기
    5-2. 파라미터 word에 타입을 설정할 때 클래스 Word를 넣었다. 이게 무슨 말이냐면 이 파라미터가 설정한 클래스의 인스턴스이기를 원할 때 쓸 수 있다. 즉, Word로 설정한 변수만 넣을 수 있다는 뜻이다!
    5-3. 클래스 Wordterm를 key로, def를 value로 받아들이게 설정
    ...

이런 식으로 했다.

  1. words는 소중한 데이터를 담는 곳으로 굳이 꺼내 쓰지 않으니 private를 설정
  2. words의 타입은 대괄호를 써서 이름이 다 다른 key값의 전체 타입을 설정
  3. 클래스를 인자(파라미터)에 타입을 설정하면 그 클래스의 결과물인 인스턴스와 관련된 변수만 가져올 수 있다.

이 포인트들이 이번 실습에 중요한 포인트들이다.

나머지는 알아서 구현하면 된다. JS 메소드들이 가지고있는 흔한 기능을 생각하여 add 뿐만 아니라 전체길이 size, 키를 찾아 def를 반환하는 find, all을 만들었고, Word에도 addDef 등을 만들었다.

출력 코드 작성

// 출력
const kimchi = new Word("kimchi", "한국의 음식");
const tang = new Word("연근 갈비탕", "중국의 음식");
const sushi = new Word("스시", "일본의 음식");
kimchi.addDef("고춧가루로 배추를 버무려 숙성 및 발효시킨 음식");
kimchi.toString(); // kimchi: 한국의 음식,고춧가루로 배추를 버무려 숙성 및 발효시킨 음식
tang.toString(); // 연근 갈비탕: 중국의 음식
sushi.updateDef("일본의 음식", "밥을 뭉쳐놓고 그 위에 재료를 얹어낸 음식");
sushi.toString(); // 스시: 밥을 뭉쳐놓고 그 위에 재료를 얹어낸 음식
const dict = new Dict();
dict.add(kimchi);
dict.add(tang);
dict.add(sushi);
dict.all();
// kimchi: 한국의 음식,고춧가루로 배추를 버무려 숙성 및 발효시킨 음식
// 연근 갈비탕: 중국의 음식
// 스시: 밥을 뭉쳐놓고 그 위에 재료를 얹어낸 음식
dict.find("kimchi");
// (2) ['한국의 음식', '고춧가루로 배추를 버무려 숙성 및 발효시킨 음식']
dict.size();
// 3
dict.update("kimchi", "김치");
dict.all();
// 연근 갈비탕: 중국의 음식
// 스시: 밥을 뭉쳐놓고 그 위에 재료를 얹어낸 음식
// 김치: 한국의 음식,고춧가루로 배추를 버무려 숙성 및 발효시킨 음식
dict.rmv("연근 갈비탕");
dict.all();
// 스시: 밥을 뭉쳐놓고 그 위에 재료를 얹어낸 음식
// 김치: 한국의 음식,고춧가루로 배추를 버무려 숙성 및 발효시킨 음식

사담

어려운 OOP를 TS로 다시 하니까 뭔가 재밌기도 하고? 점점 메소드를 만드는 것도 재밌어 지기는 한다.
다음엔 오늘 파라미터에 타입을 쓴 Word같이 Concrete와 Generic보다 더 다양하게 타입 설정을 활용할 수 있게끔 Interface를 배운단다. 기대된다.

profile
코뿔소처럼 저돌적으로

6개의 댓글

comment-user-thumbnail
2023년 2월 13일

클래스개념도 정확히 자리잡히지 않고 타입스크립트도 몰라서 어렵네요! 공부하면 이 글을 잘 읽을수 있겠죠?
자세한설명과 키포인트! 코드까지 좋은 글이였습니다! 👍

답글 달기
comment-user-thumbnail
2023년 2월 14일

강의 들으시면서 정리해놓으신건가요? 와..들을 필요 없을 정도로 자세히 설명해 놓으신 거 같아요! 공부 잘하고 갑니다!

답글 달기
comment-user-thumbnail
2023년 2월 15일

자바스크립트와 비교하여 설명하니 읽기 너무 좋았습니다 !

답글 달기
comment-user-thumbnail
2023년 2월 16일

우와 ,,, 저도 지금 타스 공부하는데 정말 깔끔한 설명에 박수치고 갑니다 !

답글 달기
comment-user-thumbnail
2023년 2월 18일

타입스크립트 저도 공부하려하는데 이렇게 야무지게 정리 잘 할 수 있을지 걱정되네요.. 역시 대단하십니당 ㅠㅠ 자바스크립트랑 비교도 해주시고 너무 좋은 글 같아요 최고 !!

답글 달기
comment-user-thumbnail
2023년 2월 19일

며칠 전에 읽고 이해 못해서 타스..? 어려워... 하고 오늘 다시 읽었는데 다시보니 이해가 쏙쏙 되는 설명이네요..👍 자식을 낳고 은퇴한 부모님이라는 비유가 추상 클래스를 한번에 이해하게 해줬습니다ㅋㅋㅋㅋ

답글 달기