이번 시간에는 타입스크립트로 객체지향을 작성하는 법을 알아보자. 왜냐면 타스는 객체지향을 좋아하거든!
먼저 OOP를 사용하는데 필요한 기본 지식을 습득하자
this는 컨스트럭터 필드에 쓰던 문법이고 바닐라 JS에선 필수다.
객체 그 자신을 가리키는 키워드로 사용
이렇게만 알고가도 충분하다.
this는 어디까지 참조이고, 문맥에 따라 어디에 속하건 기준만 잡으면 큰 문제가 없다.
어디서든지 접근할 수 있으며 외부 인터페이스를 구성. 키워드를 써주지 않으면 기본값.
클래스 내부에서만 접근할 수 있으며 내부 인터페이스를 구성할 때 쓰임.
#으로 시작, #이 붙으면 클래스 안에서만 접근하다는 얘기
private 필드는 언어 자체에 의해 강제된다는 점이 장점.
프로퍼티 명 설정, 앞엔 밑줄 _이 붙는다.
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: 물의 양은 음수가 될 수 없습니다.
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 | ⭕ | ⭕ | ⭕ |
추가로 메소드는 클래스 안에 존재하는 함수라는 뜻
자스처럼 써주면서 차이점을 알아가보자.
먼저 한마디로 정리하자면 필드에는 어떤 보호 등급인지(접근 제어자), 이름, 타입만 써주면 된다! 그리고 타스에서 알아서 문법 오류나 오용 등을 거르고 컴파일될 때 다 빼고 자스로 넘어가게 된다. 차차 알아가보자.
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
붙이면 못쓰겠지? 저런 식으로 쓰면 된다. 객체지향이니 캡슐화, 추상화가 목적이니까 말이다.
타스의 객체지향 클래스 중 가장 매력적인 포인트 추상클래스다.
다른 클래스가 상속받을 수 있는 클래스, 하지만 생성자로 새로운 인스턴스를 만들 수 없다.
자식을 낳고 은퇴한 부모님 같달까? 다시 말하자면 추상 클래스는 오직 다른 곳에서 상속할 수만 있는 클래스.
자스에서 쓰던 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();
이렇게 말이다!
메소드는 클래스 안에 있는 함수를 뜻한다. 여기에서도 추상화를 쓸 수 있다.
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에서만 제공하는 기능이다. JS로 넘어갈 땐 출력되지 않는다.
구현이 돼있지 않은 메소드
다. 즉, Call Signature만 가지고 있고, 인자를 가질 수도 있는 그런 상태다. private
은 안된다.다시 강조하지만, TS 클래스의 모든 보호 기능은 TS에서만 작동한다.
JS로 컴파일된 코드를 실행했다. 잘된다!
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);
}
}
}
Dict
작성 후 private words;
를 작성해서 기본 내용 추가Words
를 선언해 만들었다. string
타입인 것만 알고있기에, 객체의 타입 설정에서 제한된 양의 Property나 key를 가지는 타입을 정의해주는 방법이다.words
를 선언하고, Constructor
를 수동으로 초기화 시켜줘 words
를 컨스트럭터가 지정해주지 않으면서 선언하게 만듦.Word
클래스를 만들고 음식이름과 설명을 넣을 수 있는 클래스로 선언Dict
는 사전이므로 추가할 메소드-함수를 추가하도록 만들기add
를 넣어 인자 word
를 받으면 Dict
의 words
에 word
가 없다면 추가하기word
에 타입을 설정할 때 클래스 Word
를 넣었다. 이게 무슨 말이냐면 이 파라미터가 설정한 클래스의 인스턴스이기를 원할 때 쓸 수 있다. 즉, Word
로 설정한 변수만 넣을 수 있다는 뜻이다!Word
의 term
를 key로, def
를 value로 받아들이게 설정이런 식으로 했다.
words
는 소중한 데이터를 담는 곳으로 굳이 꺼내 쓰지 않으니 private
를 설정words
의 타입은 대괄호를 써서 이름이 다 다른 key값의 전체 타입을 설정이 포인트들이 이번 실습에 중요한 포인트들이다.
나머지는 알아서 구현하면 된다. 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를 배운단다. 기대된다.
클래스개념도 정확히 자리잡히지 않고 타입스크립트도 몰라서 어렵네요! 공부하면 이 글을 잘 읽을수 있겠죠?
자세한설명과 키포인트! 코드까지 좋은 글이였습니다! 👍