Typescript - 7

hoin_lee·2023년 7월 28일
0

TypeScript

목록 보기
9/14

클래스 - javascript를 먼저 확인하기

typescript를 익히기 전에 javascript의 class를 먼저 익히고 가보자
기본적으로 class는 패턴을 생성하는 역할을 하는데 객체를 나타낼 때 프로퍼티뿐 아니라 기능도 나타내준다.
클래스는 TS의 인터페이스나 타입 별칭과는 다르다.

class Player{
  taunt(){
    console.log("FUS RO DAH")
  }
}

const player1 = new Player();
player1.taunt(); // FUS RO DAH
const player2 = new Player();
player2.taunt(); // FUS RO DAH

어떠한 프로퍼티도 없지만 이게 class를 사용하는 방법이다 그냥 매번 어딜가나 듣는 붕어빵 기계 틀 그거와 같다.
class Player라는 붕어빵 틀이 있고 new Playser()가 붕어빵을 만드는 방법이다
그래서 plyaer1player2라는 붕어빵이 나왔다.
둘 다 taunt()라는 속재료를 가지고 있는 것이다.

생성자(constructor)

class 배우면 항상 배우는 생성자인데 사용자가 정의할 수 있는 메서드이다.
정의가 필수는 아니며 정의하지 않아도 문제는 없지만 대부분의 클래스에서는 정의를 한다.
한번만 할 수 있고 이름은 반드시 constructor라고 해야 자동으로 호출된다.

class Player{
  constructor(){
    console.log("IN CONSTRUCTOR1")
  }
  taunt(){
    console.log("FUS RO DAH")
  }
}
const player1 = new Player(); //IN CONSTRUCTOR1
player1.taunt(); // FUS RO DAH
const player2 = new Player(); //IN CONSTRUCTOR1
player2.taunt(); // FUS RO DAH

위와 같이 적을 경우 클래스를 인스턴스화(instantiate)할 때마다 JS가 자동으로 생성자 함수를 호출해준다.
new Player()로 만들때마다 자동으로 호출되는 것!
그럼 처음 만들때 이름과 성이 들어가야한다면

class Player{
  constructor(first, last){
    this.first = first;
    this.last = last;
  }
  taunt(){
    console.log("FUS RO DAH")
  }
}
const player1 = new Player("blue","steele");
player1.taunt(); // FUS RO DAH
console.log(player1.first) //blue
console.log(player1.last)//steele
const player2 = new Player("charlie","brown"); 
player2.taunt(); // FUS RO DAH
console.log(player2.first) //charlie
console.log(player2.last)//brown

생성자 함수에 firstlast란 이름의 파라미터(이름은 어떻게 되도 상관없다! 파라미터일 뿐이니까)로 this.first=first와 같은 구문으로 입력하면
이제 모든 Player 인스턴스에는 firstlast라는 프로퍼티가 있고 엑세스 한다.
따라서 새 플레이어를 만들 땐 그 둘의 값을 꼭 인자로 넘겨줘야 한다.

여기서 this는 해당하는 인스턴스를 가리킨다!

클래스 필드

필드와 프로퍼티를 빠르게 정의하게 해주는 구문으로 클래스 안에서 자유롭게 바꿔가며 사용할 수 있다.
클래스에 플레이어가 생성될 때마다 점수와 목숨등을 넣어보려 하는데

class Player{
  constructor(first, last){
    this.first = first;
    this.last = last;
    this.score = 0;
    this.numLives = 10; 
  }
  taunt(){
    console.log("FUS RO DAH")
  }
}

위와 같이 직접 생성자에 집어넣어 만들수도 있고

class Player{
  score = 0;
  numLives = 10; 
  constructor(first, last){
    this.first = first;
    this.last = last;
  }
  taunt(){
    console.log("FUS RO DAH")
  }
}

이렇게 밖에 설정할 수도 있다. this를 통해 생성자 안에서 인스턴스를 참조할 필요없이 JS는 스스로 파악할 수 있다.
위와 같이 만들경우 모든 Playerscore란 프로퍼티는 0이고 numLives프로퍼티는 10이 된다.

이처럼 클래스 필드의 기능을 통해 프로퍼티를 정의할 때 생성자를 거치지도 this를 참조하지 않고도 하드 코딩된 값을 추가할 수 있다.
하지만 각자 다르고 받아야 한 값이 있다면 사용할 수 없다.
이는 모든 플레이어가 처음 만들어질 땐 0점과 10개의 목숨으로 고정되어 있기 때문에 가능한 것!

아래와 같이 목숨을 잃는 방법도 추가할 수 있다.

class Player{
  score = 0;
  numLives = 10; 
  constructor(first, last){
    this.first = first;
    this.last = last;
  }
  taunt(){
    console.log("FUS RO DAH")
  }
  loseLife(){
    this.numLives -= 1;
  }
}

프라이빗 필드

class Player{
  score = 0;
  numLives = 10; 
  constructor(first, last){
    this.first = first;
    this.last = last;
  }
  taunt(){
    console.log("FUS RO DAH")
  }
  loseLife(){
    this.numLives -= 1;
  }
}

const player1 = new Player("blue","steele");
player1.score = -234234//가능

모든 인스턴스의 score 값은 0에서 시작하는데 점수가 계속 커진다 가정하자.
하지만 0보다 작은 점수는 없고 최소가 0점이다.
그런데 만약 누군가 player1.score = -음수 식으로 실수를 한다고 했을 때 JS는 막아줄 수 있을까?
그대로 실행될 것이다.
모든게 퍼블릭 클래스로 간주돼서 클래스 안이나 밖에서 엑세스 할 수가 있다.
그래서 나온 기능이 프라이빗으로

class Player{
  #score = 0;
  numLives = 10; 
  constructor(first, last){
    this.first = first;
    this.last = last;
  }
  taunt(){
    console.log("FUS RO DAH")
  }
  loseLife(){
    this.numLives -= 1;
  }
}
const player1 = new Player("blue","steele");
player1.#score = -234234 // Private field error 접근 자체가 거부된다.

#으로 시작하는 프로퍼티가 있다면(#score = 0;) JS는 이 프로퍼티가 Player 클래스 안에서만 사용할 수 있다고 인식한다!
그렇기에 밖에서 프로퍼티에 엑세스 할 수 없다.
Private field라는 에러가 발생한다! 하지만 사용자가 score 프로퍼티에 엑세스 하려는 방법은 있는데 래퍼 메서드를 사용한다.

class Player{
  #score = 0;
  numLives = 10; 
  constructor(first, last){
    this.first = first;
    this.last = last;
  }
  getScore(){
    return this.#scroe;
  }
  taunt(){
    console.log("FUS RO DAH")
  }
  loseLife(){
    this.numLives -= 1;
  }
}
const player1 = new Player("blue","steele");
console.log(player1.getScore()) // 0

이처럼 접근할 수 있고 만약 값을 변경하고 싶다면 setScoreupdate..등의 메서드를 추가하면 된다.

class Player{
  #score = 0;
  numLives = 10; 
  constructor(first, last){
    this.first = first;
    this.last = last;
  }
  getScore(){
    return this.#scroe;
  }
  updateScore(newScore){
    this.#score = newScore;
  }
  taunt(){
    console.log("FUS RO DAH")
  }
  loseLife(){
    this.numLives -= 1;
  }
}
const player1 = new Player("blue","steele");
player1.updateScore(2) // #score의 값이 변경됨

물론 생성자 안에서 this.#score이런식으로 적어도 똑같이 동작한다!
또한 같은 구문을 이용해 메서드 또한 프라이빗한 메서드를 만들 수 있다.

class Player{
  #score = 0;
  numLives = 10; 
  constructor(first, last){
    this.first = first;
    this.last = last;
  }
  getScore(){
    return this.#scroe;
  }
  updateScore(newScore){
    this.#score = newScore;
  }
  taunt(){
    console.log("FUS RO DAH")
  }
  loseLife(){
    this.numLives -= 1;
  }
  #privateMethod(){
	console.log("secret")
  }
}
const player1 = new Player("blue","steele");
player1.#privateMethod() //Private field Error

Getter, Setter

객체 접근자라고도 불리는 기능인데 부가적인 로직을 간단히 추가하거나 프로퍼티처럼 보이는 합성 프로퍼티를 쉽게 만들 수 있다!

Getter

class Player{
  #score = 0;
  numLives = 10; 
  constructor(first, last){
    this.first = first;
    this.last = last;
  }
  get fullName() { //get을 이용해 프로퍼티처럼 만들었다
 	return `${this.first} ${this.last}`
  }
  getScore(){
    return this.#scroe;
  }
  updateScore(newScore){
    this.#score = newScore;
  }
  loseLife(){
    this.numLives -= 1;
  }
}
const player1 = new Player("blue","steele");
console.log(playser1.fullName) // 호출시 ()를 적지 않는다. 메서드 취급이 아니기때문에!

이 방법이 getter이다.
위에서 fullName을 통해 이름 전체를 호출하고 싶은데 해당 메서드나 프로퍼티가 없다. 메서드를 만들어도 되지만 앞에 get을 쓰고 뒤에 메서드를 쓸 경우 정상 동작한다.
단, 메서드와 다른 것은 호출시 ()를 통해 실행하지 않고 프로퍼티처럼 작성하여도 뒤에서 동작한다는 것

class Player{
  #score = 0;
  numLives = 10; 
  constructor(first, last){
    this.first = first;
    this.last = last;
  }
  get score() { // 아래의의 getScore()메서드와 똑같이 동작한다. 하지만 이 get 구문이 더욱 깔끔하다
    return this.#score;
  }
  getScore(){ 
    return this.#scroe;
  }
  updateScore(newScore){
    this.#score = newScore;
  }
  loseLife(){
    this.numLives -= 1;
  }
}
const player1 = new Player("blue","steele");
console.log(player1.score) // 메서드가 아닌 프로퍼티처럼 보여주기 때문

#score를 통해 프라이빗으로 만들었지만 getter를 통해 score점수를 부르는 메서드를 만들고 이를 호출할 때 프로퍼티처럼 호출하니player1.getScore()가 아닌 player1.score이와 같이 프로퍼티에 접근하는 듯한 코드가 되었다
만약 이경우 player1.score = -2394293이렇게 한다 해도 변경되지 않는다 getter만 정의했기 때문이다.

Setter

프로퍼티를 설정하는 객체 접근자로 정의할 때 비슷한 구문을 사용하면 된다.
메서드를 작성한 것 처럼 동작한다. 하지만 getter와 마찬가지로 프로퍼티처럼 취급한다!

class Player{
  #score = 0;
  numLives = 10; 
  constructor(first, last){
    this.first = first;
    this.last = last;
  }
  get score() {
    return this.#score;
  }
  set score(newScore) { // 이름은 동일하게 해도 된다
    if(newScore < 0 ) {
    	throw new Error("Score must be positive")
    }
    this.#score = newScore;
  }
  updateScore(newScore){
    this.#score = newScore;
  }
  loseLife(){
    this.numLives -= 1;
  }
}
const player1 = new Player("blue","steele");
console.log(player1.score)  // 0
player1.score = 30
console.log(player1.score)  // 30

이 모습은 일반적인 정규 프로퍼티(원시형)와 다를바 없어 보인다. player1.score통해 값을 불러올 수 있고 player.score = 2042로 값을 변경할 수도 있다.
하지만 값을 도와주는 부수적인 로직이 class안을 살펴보면 존재한다.
코드를 래핑해서 score가 변경될 때 올바른 값이 들어왔는지 로직에서 확인한다.

정적(static) 필드와 메서드

정적은 클래스 필드 같은 프로퍼티의 앞부분에 static을 넣거나 메서드 앞에 넣음으로 JS에 해당 프로퍼티나 메서드가 클래스 자체에 존재하며 개별 인스턴스에는 없다고 알린다.

class Player{
  static description = "Player In Our Game";
  #score = 0;
  numLives = 10; 
  constructor(first, last){
    this.first = first;
    this.last = last;
  }
  get score() {
    return this.#score;
  }
  set score(newScore) { // 이름은 동일하게 해도 된다
    if(newScore < 0 ) {
    	throw new Error("Score must be positive")
    }
    this.#score = newScore;
  }
  updateScore(newScore){
    this.#score = newScore;
  }
  loseLife(){
    this.numLives -= 1;
  }
}
const player1 = new Player("blue","steele");

static description = "Player In Our Game";이 부분에서 만약 description앞의 static이 없다면 모듬 Player에 존재할 것이다.
각 인스턴스화 된 Player마다 누구는 그대로 문자열을 쓰고 누구는 내용을 변경하여 완전히 다른 것을 쓸 것이다

하지만 정적으로(static을 추가하여)만들면 프로퍼티는 개별 인스턴스가 아닌 Player 클래스에만 존재한다.
따라서 인스턴스에는 관계없는 기능에 사용할 수 있는 방식이라고 할 수 있다.
그대로 예시를 본다면

class Player{
  #score = 0;
  numLives = 10; 
  constructor(first, last){
    this.first = first;
    this.last = last;
  }
  static randomPlayer() { // 예시로 하드코딩된 값이고 실제로 랜덤 플레이어를 배출하면 된다
    new Player("Andy", "Samberg")
  }
  get score() {
    return this.#score;
  }
  set score(newScore) { // 이름은 동일하게 해도 된다
    if(newScore < 0 ) {
    	throw new Error("Score must be positive")
    }
    this.#score = newScore;
  }
  updateScore(newScore){
    this.#score = newScore;
  }
  loseLife(){
    this.numLives -= 1;
  }
}
const player1 = new Player("blue","steele");
player1.randomPlayer() // Error player1 안에 randomPlayer라는 메서드 자체가 존재하지 않는다

const Player2 = Player.randomPlayer() // 새로운 랜덤 플레이어를 생성함!

이처럼 특정 인스턴스와 관련 없으면서 클래스 자체와 연관된 기능에 static을 사용할 수 있다.

클래스 확장

JS에서는 다른 부모 클래스로부터 상송되는 클래스를 가질 수 있고 또 다른 클래스와 함께 기능을 공유할 수 있다.
위의 Player 클래스가 있다는 가정하에 추가적인 class를 만들어 보자

class AdminPlayer extends Player { //extends를 통해 Player의 프로퍼티들을 받았다
	isAdmin = true
}
const adminPlayer = new AdminPlayer() // Player프로퍼티 + isAdmin 프로퍼티를 추가적으로 가지고 있는 Player가 만들어졌다

이처럼 Player 의 프로퍼티들을 extends를 통해 확장하여 가져올 수 있다.

super()

super는 클래스 상속이나 클래스 확장 시 사용되는데 구체적으론 자식 클래스에 생성자를 추가할 때 사용된다.
현재

class AdminPlayer extends Player { 
	isAdmin = true
}
const adminPlayer = new AdminPlayer()

아까 작성한 코드를 보면 AdminPlayer에는 생성자가 없는 상태이다
다만 JS는 부모 클래스에 생성자 함수가 있으면 자동으로 그 함수를 호출하긴 한다. 따라서 이름의 firstlast를 인자로 넘겨줄 경우 해당 프로퍼티들에 값이 생긴다.(적지 않을경우 프로퍼티는 있지만 값이 undefined로 되어있다.)

하지만 무엇인가를 추가해야 할 경우가 많다고 생각하면 생성자를 추가하거나 자식클래스에 기능을 추가하는 것이다.

class AdminPlayer extends Player { 
  constructor(powers){
    this.powers = powers; 
  }
  isAdmin = true
}
const adminPlayer = new AdminPlayer(["delete","restore world"])

앞서 Player에서 작성했던 것과 똑같이 작성해 보았다. JS는 어떻게 반응할까?
둘 중 하나만 실행되고 JS가 처음 마주치는 AdminPlayerconstructor(powers) 이 부분만 실행될 것까?
일단 실행시켜본다면

에러가 발생한다! 엑세스 하거나 파생 생성자에서 반환하기 전에 반드시 파생 클래스의 슈퍼 생성자를 호출하라는 에러가 발생한다.
(Uncaught ReferenceError: Must call super constructor in derived class befor...)

이는 자식 클래스에 생성자가 있지만 부모 클래스 생성자를 먼저 호출하지 않았기 때문이다.
이때 super()를 사용한다
super는 슈퍼 클래스에 있는 constructor() 함수를 참조한다. 부모 클래스 , 파생클래스 , 기본 클래스도 모두 같은 말이다.

class AdminPlayer extends Player { 
  constructor(first,last,powers){
    super(first, last) // 여기서 슈퍼 클래스는 Player이다 
    this.powers = powers; 
  }
  isAdmin = true
}
const adminPlayer = new AdminPlayer("adimn","mCadmin",["delete","restore world"])

이 경우 정상적으로 Player의 생성자에서 만들어지는 firstlast 프로퍼티를 가지고 추가적으로 AdminPlayer의 클래스의 생성자에서 만들어지는 powers프로퍼티를 모두 가질 수 있다.

profile
https://mo-i-programmers.tistory.com/

0개의 댓글