게으른개발너D

Classes - class, extends, super, this 본문

개발/JavaScript

Classes - class, extends, super, this

lazyhysong 2023. 4. 27. 13:48

✨ What are Classes ✨

 

classes는 화려한 object이다.

class는 보통 많은 사람들이 라이브러리나 리액트 같은 것을 만들 때 classes를 export한 다음에 classes를 이용한다.

 

우리가 엄청 많은 코드를 가지고 있고, 이걸 구조화하길 원할 때, class를 이용하면 매우 유용하다.

왜냐하면 class를 재사용할 수 있기 때문이다.

 

class는 기본적으로 blueprint(청사진)이며 화려한 object이다.

 


User라는 class를 만들어 보자

class는 안에 constructor라는 걸 가지고 있는데, constructor는 class를 말그대로 construct(구성)한다는 constructor이다.

class User {
  constructor() {
    this.username = "Hwayeon";
  }
}

console.log(User.username);

//undefined

blueprint로 class를 만들어 줘야한다.

 

우리는 이 class를 가지고 생성을 해야한다.

construct 하는 것이다.

 

방법은?

const cuteUser = new User();

 

이렇게 선언해 주는 것이다.

 

cuteUser는 이 class의 instance이다.

instance는 살아있는 class를 의미한다.

 

위에 선언한 class는 죽어있는 class이며 그저 blueprint이다.

const cuteUser = new User();

console.log(cuteUser.username);

// Hwayeon

class에다가 함수도 만들 수 있다.

class User {
  constructor() {
    this.username = "Hwayeon";
  }
  sayHello() {
    console.log("Hello, I'm Hwayeon");
  }
}

const cuteUser = new User();

console.log(cuteUser.username);
// Hwayeon

setTimeout(cuteUser.sayHello, 4000);
// Hello, I'm Hwayeon (4초 후)

하지만 이런건 object로도 할 수 있다. 우움?? 뭔차이람???

아래서 알아보쟈!!!


class의 constructor를 이렇게 정의해 보자

class User {
  constructor(name) {
    this.username = name;
  }
  sayHello() {
    console.log("Hello, I'm Hwayeon");
  }
}

const cuteUser = new User("Hwayeon");

console.log(cuteUser.username);
// Hwayeon

이런 건 object로 할 수 없다.

즉, class는 object 공장이다!

class User {
  constructor(name) {
    this.username = name;
  }
  sayHello() {
    console.log(`Hello my name is ${this.username}!`);
  }
}

const cuteUser = new User("Hwayeon");

cuteUser.sayHello();
// Hello my name is Hwayeon!

 

 

✨ Extending Classes ✨

 

class User {
  constructor(name) {
    this.username = name;
  }
  sayHello() {
    console.log(`Hello my name is ${this.username}!`);
  }
}

const cuteUser = new User("Hwayeon");

cuteUser.sayHello();
// Hello my name is Hwayeon!

this는 기본적으로 class 안에서 볼 수 있고, class 그 자체를 가리킨다.

즉, 'this'는 class 전체를 가리킨다고 볼 수 있다.

그래서 우리가 언제든 뭔가를 추가하고 싶거나 class로부터 어떤 것을 불러오고 싶을 때, this를 사용하면 된다.

 

하지만ㅠㅠ 우리가 function과 function 안의 this 그리고 어떤 일들에 대해 이야기할때는 이해하기 어려울 것이다.

콘솔창에 console.log(this)를 입력하면, window가 나온다ㅋㅋㅋㅋ

그리고 sayHello method 안에 console.log(this)를 입력해면 user class를 출력해준다.

이게 아주 다를 뿐만 아니라, 우리가 어떻게 class와 function을 정의하느냐에 따라 달려있다.

 


class User {
  constructor(name, lastName, email, password) {
    this.username = name;
    this.lastName = lastName;
    this.email = email;
    this.password = password;
  }
  sayHello() {
    console.log(`Hello my name is ${this.username}!`);
  }
}

const cuteUser = new User("Hwayeon", "Song", "lazy@com", "lazy");

위처럼 constructor에 properties를 만들었다.

properties는 우리가 원하는대로 만들 수 있다!

파라미터 값을 사용하지 않고 this.something = "I love you!" 라고 property를 선언해줄 수도 있다.

 


현재 우리는 클래스를 가지고 있고, 브라우저에서 새로고침을 하면 아무 반응도 없는 상태이다.

method를 하나 만들자.

 

class User {
  constructor(name, lastName, email, password) {
    this.username = name;
    this.lastName = lastName;
    this.email = email;
    this.password = password;
  }
  sayHello() {
    console.log(`Hello my name is ${this.username}!`);
  }
  getProfile() {
    console.log(`${this.username} ${this.email} ${this.password}`);
  }
}

const cuteUser = new User("Hwayeon", "Song", "lazy@com", "lazy");

cuteUser.getProfile();

그리고 cuteUser.getProfile();를 써서 properties를 불러올 수 있다

 

또한 properties를 바꿀 수도 있다.

password를 console.log로 찍어보면 이렇게 나온다.

console.log(cuteUser.password);
// lazy

updatePassword라는 다른 function을 만들어보자.

 

class User {
  constructor(name, lastName, email, password) {
    this.username = name;
    this.lastName = lastName;
    this.email = email;
    this.password = password;
  }
  sayHello() {
    console.log(`Hello my name is ${this.username}!`);
  }
  getProfile() {
    console.log(`${this.username} ${this.email} ${this.password}`);
  }
  updatePassword(newPassword, currentPassword) {
    if(currentPassword === this.password) {
      this.password = newPassword;
    } else {
      console.log("Can't change password");
    }
  }
}

const cuteUser = new User("Hwayeon", "Song", "lazy@com", "lazy");

console.log(cuteUser.password); // lazy
cuteUser.updatePassword("hello", "lazy");
console.log(cuteUser.password); // hello

 

이렇게 password를 바꿀 수도있다.

 

한 개의 unit, 한 개의 큰 object로 모든 함수를 클래스 안에 담을 수 있다는 점에서 아주아주 멋지다!

data를 불러 올 수도, 바꿀 수도 있다.

 


이제 User 클래스를 extends 할 거다.

예를들어, user 클래스를 가지고 이쏙, user class의 어떤 부분을 약간 수정하고 싶다면, admin과 admin part person을 위한 클래스를 만들어야한다.

admin person은 sayHello를 부를 수 있고, profile을 가지고 있고 password를 업데이트 할 수 있다.

또한 더 많은 것들을 admin person에 추가할 수 있다.

예를들어 admin만 할 수 있는 웹사이트 전체를 삭제하는 것 같은거??

 

class Admin extends User {
  deleteWebsite() {
    console.log("Deleting the whole website...");
  }
}

 

이제 인스턴스를 만들어보자

const cuteAdmin = new Admin();

cuteAdmin.deleteWebsite();

deleteWebsite를 불러올 것이라서 constructor를 만들지 않았다.

 

만약 이 인스턴스로 email을 불러오면 어떻게 될까?

const cuteAdmin = new Admin();

console.log(cuteAdmin.email); //undefined

undefined가 뜬다.

constructor 안에 아무것도 작성하지 않았기 때문이다!

constructor를 작성해 보자!

 

const cuteAdmin = new Admin("Hwayeon", "Song", "lazy@com", "lazy");

console.log(cuteAdmin.email); //lazy@com

우리가 user class인 new admin을 생성했고, hwayeon, song.. 등등을 admin으로 보내고 있고, admin에 있는 것들이 user 속으로 forwarding하고 있다.

 

이제 문제점을 보자

우리는 더 많은 것들을 추가하고 싶을 수도 있다.

admin은 user이지만 user이상이기 때문이다.

예를들어, super admin의 true/false를 추가해보자.

class Admin extends User {
  constructor(superAdmin) {
    this.superAdmin = superAdmin;
  }
  
  deleteWebsite() {
    console.log("Deleting the whole website...");
  }
}

이렇게 작성하고 refresh를 하면 ..??? 작동을 하지 않는다.

왜냐하면 constructor를 정의해줘서, 기존의 constructor를 잃어버렸기 때문이다.

그래서 admin은 name부터 password가 뭔지 모르고 있다.

 

다음에 수정해 보자ㅋㅋㅋㅋ

 

 

 

✨ Super ✨

 

class User {
  constructor(name, lastName, email, password) {
    this.username = name;
    this.lastName = lastName;
    this.email = email;
    this.password = password;
  }

  getProfile() {
    console.log(`${this.username} ${this.email} ${this.password}`);
  }
  updatePassword(newPassword, currentPassword) {
    if(currentPassword === this.password) {
      this.password = newPassword;
    } else {
      console.log("Can't change password");
    }
  }
}

const sexyUser = new User("Hwayeon", "Song", "lasyhysong@com", "1234");

class Admin extends User {
  constructor(superAdmin) {
    this.superAdmin = superAdmin;
  }
  deleteWebsite() {
    console.log("Deleting the whole website...");
  }
}

const superAdmin = new Admin("Hwayeon", "Song", "lasyhysong@com", "1234", true);

User class를 extends 한 Admin class를 생성했다.

Admin에서 User data와 새로운 data를 함께 사용하고 싶다.

하지만 Admin class에서 constructor를 생성하는 순간 User에서 extends한 constructor는 모두 읽어내지 못했다.

 


일단 User class를 리팩토링 해보자.

User가 constructor에서 값을 많이 가지기 떄문에 object로 나타내보자.

class User {
  constructor(options) {
    this.username = options.username;
    this.lastName = options.lastName;
    this.email = options.email;
    this.password = options.password;
  }
}

constructor를 위와 같이 변경해 줬다.

또는 좀더 명확하게 볼 수 있게  아래처럼 변경해 줄 수도 있다.

 

그리고 instance도 object로 변경해 주었다.

class User {
  constructor({
    username,
    lastName,
    email,
    password
  }) {
    this.username = username;
    this.lastName = lastName;
    this.email = email;
    this.password = password;
  }

  getProfile() {
    console.log(`${this.username} ${this.email} ${this.password}`);
  }
  updatePassword(newPassword, currentPassword) {
    if(currentPassword === this.password) {
      this.password = newPassword;
    } else {
      console.log("Can't change password");
    }
  }
}

const cuteUser = new User({
  username: "Hwayeon",
  lastName: "Song",
  email: "lazyhysong@com",
  password: "1234",
});

위에서 발생한 Admin에서 constructor를 생성했을 때의 문제는 Admin class 안에서 특별한 함수를 호출하면 된다.

 

그 함수는 classes 안에서만 유효하고 super라고 불린다.

class Admin extends User {
  constructor(superAdmin) {
    super();
    this.superAdmin = superAdmin;
  }
}

super는 base class의 constructor를 호출하게 된다.

 

Admin에서 constructor 사용은 다음과 같다.

위에서 User의 param 값을 object로 변경해 주었는데, 이렇게 하면 User의 option 들과 Admin의 option들을 구분해줄 수 있다.

그리고 User의 option들을 super에 입력해준다.

class Admin extends User {
  constructor(userOptions, adminOptions) {
    super(userOptions);
    this.superAdmin = adminOptions.superAdmin;
  }
}

또는 이렇게 작성해줄 수도 있다.

class Admin extends User {
  constructor({ name, lastName, email, password, superAdmin, isActive }) {
    super({ name, lastName, email, password });
    this.superAdmin = superAdmin;
    this.isActive = isActive;
  }
};

const superAdmin = new Admin({
  username: "Hwayeon",
  lastName: "Song", 
  email: "lasyhysong@com", 
  password: "1234",
  superAdmin: true,
  isActive: true,
});

이렇게 하면 super에 의해 extends한 class의 constructor를 사용할 수 있다.


예제를 만들어 보자

  <span id="count">0</span>
  <button id="plus">+</button>
  <button id="minus">-</button>
class Counter {
  constructor({ initialNumber = 0, counterId, plusId, minusId }) {
    this.count = initialNumber;
    this.counter = document.getElementById(counterId);
    this.plusBtn = document.getElementById(plusId);
    this.minusBtn = document.getElementById(minusId);
  }
}

new Counter({
  counterId: "count",
  plusId: "plus",
  minusId: "minus",
});

class의 constructor는 위와같이 작성했고 instance는 아래와 같이 생성해 주었다.

 

그리고 나서 이벤트리스터를 달아줄 것이다.

class Counter {
  constructor({ initialNumber = 0, counterId, plusId, minusId }) {
    this.count = initialNumber;
    this.counter = document.getElementById(counterId);
    this.plusBtn = document.getElementById(plusId);
    this.minusBtn = document.getElementById(minusId);
    this.addEventListeners();
  }
  addEventListeners() {
    this.plusBtn.addEventListener("click", this.increase);
    this.minusBtn.addEventListener("click", this.decease);
  };
  increase() {
    this.count = this.count + 1;
    this.repaintCounter()
  }
  decrease() {
    this.count = this.count - 1;
    this.repaintCounter()
  }
  repaintCounter() {
    this.counter.innerText = this.count;
  }
}

plus와 minus 버튼에 각각 increase, decrease 이벤트를 달아주었다.

해당 메소드들은 각각 count에 1을 더해주거나 빼주고 repaintCounter 메소드를 호출하여 counter span 에 표시하도록 작성했다.

 

 

하지만 console 창에서 에러를 던지는데!!

 

바로 repaintCounter는 함수가 아니라는 에러를 던진다.

 

 

✨ WTF is this ✨

 

class Counter {
  constructor({ initialNumber = 0, counterId, plusId, minusId }) {
    this.count = initialNumber;
    this.counter = document.getElementById(counterId);
    this.plusBtn = document.getElementById(plusId);
    this.minusBtn = document.getElementById(minusId);
    this.addEventListeners();
  }
  addEventListeners() {
    this.plusBtn.addEventListener("click", this.increase);
    this.minusBtn.addEventListener("click", this.decrease);
  };
  increase() {
    this.count = this.count + 1;
    this.repaintCounter();
  }
  decrease() {
    this.count = this.count - 1;
    this.repaintCounter();
  }
  repaintCounter() {
    this.counter.innerText = this.count;
  }
}

new Counter({
  counterId: "count",
  plusId: "plus",
  minusId: "minus",
});

this.repaintCounter는 함수가 아니다! 라는 에러에 직면했다.

일단 console.log로 this를 찍어보자.

addEventListeners와 increase 메소드에 추가해 주었다.

class Counter {
  constructor({ initialNumber = 0, counterId, plusId, minusId }) {
    this.count = initialNumber;
    this.counter = document.getElementById(counterId);
    this.plusBtn = document.getElementById(plusId);
    this.minusBtn = document.getElementById(minusId);
    this.addEventListeners();
  }
  addEventListeners() {
    console.log(this);
    this.plusBtn.addEventListener("click", this.increase);
    this.minusBtn.addEventListener("click", this.decrease);
  };
  increase() {
    console.log(this);
    // this.count = this.count + 1;
    // this.repaintCounter();
  }
  decrease() {
    this.count = this.count - 1;
    this.repaintCounter();
  }
  repaintCounter() {
    this.counter.innerText = this.count;
  }
}

plus 버튼을 클릭하면 console에 이렇게 찍힌다.

addEventListeners 메소드에서는 this가 Couner class를 가리키지만, increase 메소드에서는 plus 버튼을 가리키고 있다.

 

뭐임? 

 

아무튼 this는 기본적인 자바스크립트의 행동이다.

우리가 오직 경험을 통해서 배워야한다.

 

우리가 event listener를 만들면 그 event의 function이 this를 event를 호출한 element 안으로 copy할 것이다.

event의 handler는 this를 event target에 가리키게 한다.

이걸 고치는 방법은 있다.

 

this가 항상 class를 가리키게 하려면? 

함수를 arrow function으로 바꾸면 된다!

method를 arrow function으로 변경해 보자!

class Counter {
  constructor({ initialNumber = 0, counterId, plusId, minusId }) {
    this.count = initialNumber;
    this.counter = document.getElementById(counterId);
    this.plusBtn = document.getElementById(plusId);
    this.minusBtn = document.getElementById(minusId);
    this.addEventListeners();
  }
  addEventListeners = () => {
    console.log(this);
    this.plusBtn.addEventListener("click", this.increase);
    this.minusBtn.addEventListener("click", this.decrease);
  };
  increase = () => {
    this.count = this.count + 1;
    this.repaintCounter();
  }
  decrease = () => {
    this.count = this.count - 1;
    this.repaintCounter();
  }
  repaintCounter = () => {
    this.counter.innerText = this.count;
  }
}

이제 this는 해당 class를 가리키게 되고, this.repaintCounter는 class 안의 함수를 가리키게 된다.

 

이렇게 숫자를 변경시키는 counter를 완성하였다!

 

이걸 class를 사용하지 않고 그냥 document.getElementById를 사용하여 HTML element를 불러와 event listener를 달고 함수를 구현할 수도 있다.

 

하지만 class를 이용하면 같지는 않지만 기능은 같은 다양한 counter를 구현할 수 있다.

 

한가지 버그가 있는데, 해당 class는 Dom을 불러올 때 HTML을 변경시키지 않는다.

그래서 initialNumber를 0이 아닌 다른 숫자로 초기 세팅해 주어도 해당 숫자가 나오지 않는다.

 

코드 하나만 추가하자

this.counter.innerText = initialNumber;

class Counter {
  constructor({ initialNumber = 0, counterId, plusId, minusId }) {
    this.count = initialNumber;
    this.counter = document.getElementById(counterId);
    this.counter.innerText = initialNumber;
    this.plusBtn = document.getElementById(plusId);
    this.minusBtn = document.getElementById(minusId);
    this.addEventListeners();
  }
  addEventListeners = () => {
    this.plusBtn.addEventListener("click", this.increase);
    this.minusBtn.addEventListener("click", this.decrease);
  };
  increase = () => {
    this.count = this.count + 1;
    this.repaintCounter();
  }
  decrease = () => {
    this.count = this.count - 1;
    this.repaintCounter();
  }
  repaintCounter = () => {
    this.counter.innerText = this.count;
  } 
}

new Counter({
  counterId: "count",
  plusId: "plus",
  minusId: "minus",
});

new Counter({
  counterId: "count2",
  plusId: "plus2",
  minusId: "minus2",
  initialNumber: 80,
});

해당 코드를 constructor에 추가하면 DOM을 불러올 때 초기값을 initialNumber로 세팅할 수 있다.

 

 

Comments