[JavaScript] class(클래스)에 대해 알아보자

비얌·2022년 11월 12일
1
post-thumbnail

개요

자바스크립트의 클래스에 대해 공부해보기로 했다. 공부를 결심한 계기는 다음과 같다.

우테코 프리코스에서 2주 차 미션을 받았는데, App.js에 다음과 같이 적혀있었다.

class App {
  play() {}
}

module.exports = App;

class가 뭔지, class를 어떻게 쓰는지 몰랐던 나는 검색 그리고 검색을 통해 'class를 실행시키는 것'에 성공했다. 하지만 이게 뭔지, 어떻게 동작하는지는 잘 몰랐고.. 그게 코드리뷰에서 드러났다.

class에서 메서드를 쓸 때 this를 사용하지 않은 경우 static으로 선언해주는 것을 지향한다고 한다. this는 어디선가 들어봤고 static은 들어본 적도 없다...! 그래서 이번 포스팅을 통해 알아보기로 했다.


민재 님이 첨부해주신 URL에 가보면 Airbnb 자바스크립트 컨벤션 9.7 절에서 다음과 같이 설명하고 있다. 클래스 메소드는 this를 사용하거나 static을 사용해야 한다. this는 왜 쓴 것인지 조금 이해가 가는데, static은 정말 생전 처음 보는 것이다. 그래서 학습할 때 static를 유의해서 보기로 했다.



자바스크립트 클래스

학습은 모던 자바스크립트 튜토리얼에서 했다.

민재 님이 첨부해주신 Airbnb 자바스크립트 컨벤션에 나오는 '9.3 정적 메서드(아마 static)'가 모던 자바스크립트 튜토리얼에 나오는 걸 보고 이 자료로 학습하면 될 것 같다고 생각했다.


(1) 클래스와 기본 문법

  • 자바스크립트에서 클래스는 함수의 한 종류이다.

기본 문법

class MyClass {
  // 여러 메서드를 정의할 수 있음
  constructor() { ... }
  method1() { ... }
  method2() { ... }
  method3() { ... }
  ...
}

사용 예시

  • 맨 아래의 두 줄을 통해 class를 사용할 수 있다.
  • 클래스명에는 파스칼케이스(PascalCase)를 사용해야 한다.
class User {

  constructor(name) {
    this.name = name;
  }

  sayHi() {
    alert(this.name);
  }

}

// 사용법:
let user = new User("John");
user.sayHi();

✨ (2) constructor 사용법 (추가)

constructor는 new를 선언할 때 생긴다. 다시 이 코드를 보자.

constructor에 name이 있으므로 new User() 안에 꼭 name을 써줘야 한다.

class User {

  constructor(name) {
    this.name = name;
  }

  sayHi() {
    alert(this.name);
  }

}

// 사용법:
let user = new User("John");
user.sayHi();

하지만 인수 없이 new User()라고만 쓰는데, constructor를 쓸 수 있는 경우도 있다.

아래의 코드와 같이 constructor에서 name을 선언하되 지정해주지 않고, 아래에서 this.name = 'John'이라고 대입해주면 된다.

class User {

  constructor(name) {
    this.name;
  }
	
  sayHi() {
    this.name = 'John';
    alert(this.name);
  }

}

// 사용법:
let user = new User();
user.sayHi();

그런데 그러면 아래처럼 constructor 없이 변수를 선언해주는 것과 뭐가 다른 건지 모르겠다.

class User {
  #name;
	
  sayHi() {
    this.#name = 'John';
    console.log(this.#name);
  }

}

// 사용법:
let user = new User();
user.sayHi();

그리고, 변수와 constructor 둘 다 선언해주는 아래의 코드와 뭐가 다른지도 잘 모르겠다.

class User {
  #name;
	
  constructor(name) {
    this.#name = name;
  }

  sayHi() {
    console.log(this.#name);
  }

}

// 사용법:
let user = new User('John');
user.sayHi();

👉 알았음!!

일단 #name이 없어도 코드가 돌아가는 데는 문제가 없지만(constructor에서 this.#name = name이라고 정해주기 때문)

  1. 가독성을 높인다
  2. private 변수로만 쓰라고 임의적으로 말한다

는 의도가 있다고 한다!


(3) this

this가 뭔지 잘 몰라서 찾아봤다.

  • 메서드 내부에서 this 키워드를 사용하면 객체에 접근할 수 있다.
  • 이때 '점 앞’의 this는 객체를 나타낸다(정확히는 메서드를 호출할 때 사용된 객체)
  • (예)
let user = {
  name: "John",
  age: 30,

  sayHi() {
    // 'this'는 '현재 객체'를 나타냅니다.
    alert(this.name);
  }

};

user.sayHi(); // John
  • this를 사용하지 않고 외부 변수를 참조해 객체에 접근하는 것도 가능하다.
  • (예)
let user = {
  name: "John",
  age: 30,

  sayHi() {
    alert(user.name); // 'this' 대신 'user'를 이용함
  }

};
  • 🔥 그런데 이렇게 외부 변수를 사용해 객체를 참조하면 예상치 못한 에러가 발생할 수 있으므로 this를 사용하자.
let user = {
  name: "John",
  age: 30,

  sayHi1() {
    console.log(this.name);
  },
  
  sayHi2() {
    console.log(user.name);
  },
};

let admin = user;
user = null; // user를 null로 덮어씀

admin.sayHi1(); // 'John'

admin.sayHi2(); // 'error'
  • sayHi1에서 오류가 발생하지 않는 이유는 name이 this로 인해 무조건 user 객체를 참고하기 때문이다.
  • sayHi2에서 오류가 발생하는 이유는 name이 user.으로 인해 엉뚱한 객체인 null을 참고하기 때문이다.

클래스 표현식

  • 함수처럼 클래스도 다른 표현식 내부에서 정의, 전달, 반환, 할당할 수 있다.
  • 이렇게 클래스를 변수에 할당하는 건 처음보았다!
let User = class {
  sayHi() {
    alert("안녕하세요.");
  }
};

(4) 클래스 상속

  • 클래스 상속을 사용하면 클래스를 다른 클래스로 확장할 수 있다.
  • 기존에 존재하던 기능을 토대로 새로운 기능을 만들 수 있다.

먼저 클래스 Animal을 만들어보자.

class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  run(speed) {
    this.speed = speed;
    alert(`${this.name} 은/는 속도 ${this.speed}로 달립니다.`);
  }
  stop() {
    this.speed = 0;
    alert(`${this.name} 이/가 멈췄습니다.`);
  }
}

let animal = new Animal("동물");

또다른 클래스 Rabbit을 만들어보자. 동물 관련 메서드가 담긴 Animal을 확장해서 만들어야 한다.

class Rabbit extends Animal {
  hide() {
    alert(`${this.name} 이/가 숨었습니다!`);
  }
}

let rabbit = new Rabbit("흰 토끼");

rabbit.run(5); // 흰 토끼 은/는 속도 5로 달립니다.
rabbit.hide(); // 흰 토끼 이/가 숨었습니다!
  • 클래스 Rabbit을 사용해 만든 객체는 rabbit.hide() 같은 Rabbit에 정의된 메서드에도 접근할 수 있고, rabbit.run() 같은 Animal에 정의된 메서드에도 접근할 수 있다.

(5) 정적 메서드와 정적 프로퍼티

드디어 궁금했던 static에 대해 설명하고 있는 부분이다.

정적 메서드

  • "prototype"이 아닌 클래스 함수 자체에 메서드를 설정할 수 있다.

  • 이런 메서드를 정적(static) 메서드라고 부른다.

  • 정적 메서드는 아래와 같이 클래스 안에서 static 키워드를 붙여 만들 수 있다.

  • (예 1)

class User {
  static staticMethod() {
    alert(this === User);
  }
}

User.staticMethod(); // true
  • (예 2)
class User { }

User.staticMethod = function() {
  alert(this === User);
};

User.staticMethod(); // true
  • 정적 메서드는 메서드를 프로퍼티 형태로 직접 할당하는 것과 동일한 일을 한다.
  • 정적 메서드는 어떤 특정한 객체가 아닌 클래스에 속한 함수를 구현하고자 할 때 주로 사용된다.
  • User.staticMethod()가 호출될 때 this의 값은 클래스 생성자인 User 자체가 된다(점 앞 객체).

정적 프로퍼티

  • 정적 프로퍼티는 일반 클래스 프로퍼티와 유사하게 생겼는데 앞에 static이 붙는다는 점만 다르다.
class Article {
  static publisher = "Ilya Kantor";
}

alert( Article.publisher ); // Ilya Kantor
  • 아래의 자바스크립트 컨벤션을 다시 한번 상기해보려고 캡쳐를 가져왔다. class 안에서 메서드를 쓰려면 this를 사용하거나 static을 사용해야 한다.

💥 그런데!!! 이렇게 찾아봐도 대체 static을 왜 쓰는지 이해가 잘 안 가서(+ 클래스를 왜 쓰는지도) 시니하 님께 질문을 했다.

시니하 님은 직접 코드를 짜서 보여주셨는데!! 바로 Counter 예시였다.

아래와 같은 main.js를 실행시키면 class로 만든 Counter.wrapped가 5개 실행된다.

// main.js
import { Counter } from "./Counter";

const app = document.getElementById('app');

if (app) {
  for (let i = 0; i < 5; i++) {
    const counter = Counter.wrapped(app);
    counter.render();
  }
}

실행 화면은 다음과 같다. 개별적인 counter가 잘 동작한다. 이와 같이 어떠한 기능을 한번만 쓰는게 아니라 여러번 사용할 때 class가 유용한 것 같다.

그럼 이 Counter.wrapped가 뭔지 알아보자. wrapped는 Counter.js에 정의되어있다. Counter.js의 내용은 아래와 같다.

// Counter.js
export class Counter {
  #parent;
  #previousEl = null;
  #count;

  constructor(parent, initialCount = 0) {
    this.#parent = parent;
    this.#count = initialCount;
  }

  static wrapped(parent, initialCount = 0) {
    const wrapper = document.createElement("div");
    wrapper.className = "wrapper";
    parent.append(wrapper);
    return new Counter(wrapper, initialCount);
  }

  count() {
    return this.#count;
  }

  action(command) {
    switch (command.type) {
      case "increment": {
        this.#count++;
        this.render();
        break;
      }
    }
  }

  render() {
    if (this.#previousEl) {
      this.#parent.removeChild(this.#previousEl);
    }

    const el = document.createElement("div");
    el.className = "counter";
    el.append(`Count: ${this.#count}`);

    const button = document.createElement("button");
    button.append("+");
    button.addEventListener("click", () => {
      this.action({ type: "increment" });
    });

    el.append(button);
    this.#parent.append(el);
    this.#previousEl = el;
  }
}

wrapped는 counter 기능을 하는 div를 감싸는 div이다.

따라서 counter 기능과는 연관이 없다. 이렇듯 class 안의 메서드에 특별하게 영향을 미치는 것이 아니라 전체적으로 영향을 미칠 때 static을 사용한다.

하지만, 그것은 이 wrapped를 class 안에 선언할 때 사용하는 것이라서 class 안에서 static으로 wrapped를 선언하는 대신 class 밖에서 일반 함수로 정의하는 방법도 있다.

아래의 코드는 위에 쓴 Counter.js의 일부분을 그대로 가져온 것이다. 이처럼 wrapped를 class 밖에서 일반 함수로 선언하여도 정상적으로 쓸 수 있다. 그래서 static을 사용해야 하는 경우는 흔하지 않다고 한다.

export const wrapped = (parent, initialCount = 0) => {
  const wrapper = document.createElement("div");
  wrapper.className = "wrapper";
  parent.append(wrapper);
  return new Counter(wrapper, initialCount);
};

export class Counter {
  ...
}

(6) private, protected 프로퍼티와 메서드

  • 객체 지향 프로그래밍에서 가장 중요한 원리 중 하나는 '내부 인터페이스와 외부 인터페이스를 구분 짓는 것’이다.

  • 내부 인터페이스와 외부 인터페이스를 구분하는 방법을 ‘반드시’ 알고 있어야 한다.

  • 커피머신에 내부/외부 인터페이스를 비유한 글

    커피 머신은 꽤 믿음직한 기계입니다. 수년 간 사용할 수 있고, 중간에 고장이 나도 수리를 받으면 됩니다.
    
    외형은 단순하지만 커피 머신을 신뢰할 수 있는 이유는 모든 세부 요소들이 기계 내부에 잘 정리되어 숨겨져 있기 때문입니다.
    
    커피 머신에서 보호 커버를 제거하면 사용법이 훨씬 복잡해지고 위험한 상황이 생길 수 있습니다. 어디를 눌러야 할지 모르고 감전이 될 수도 있기 때문입니다.
    
    앞으로 학습하겠지만, 프로그래밍에서 객체는 커피 머신과 같습니다.
    
    프로그래밍에서는 보호 커버를 사용하는 대신 특별한 문법과 컨벤션을 사용해 안쪽 세부 사항을 숨긴다는 점이 다릅니다.

내부 인터페이스와 외부 인터페이스

  • 내부 인터페이스(internal interface) – 동일한 클래스 내의 다른 메서드에선 접근할 수 있지만, 클래스 밖에선 접근할 수 없는 프로퍼티와 메서드
    외부 인터페이스(external interface) – 클래스 밖에서도 접근 가능한 프로퍼티와 메서드

  • (예) 커피 머신은 보호 커버에 둘러싸여 있기 때문에 보호 커버를 벗기지 않고는 커피머신 외부에서 내부로 접근할 수 없다. 밖에선 세부 요소를 알 수 없고, 접근도 불가능하다. 내부 인터페이스의 기능은 외부 인터페이스를 통해야만 사용할 수 있다.

  • 이런 특징 때문에 외부 인터페이스만 알아도 객체를 가지고 무언가를 할 수 있다. 객체 안이 어떻게 동작하는지 알지 못해도 괜찮다는 점은 큰 장점으로 작용한다.

  • public: 어디서든지 접근할 수 있으며 외부 인터페이스를 구성한다.
    private: 클래스 내부에서만 접근할 수 있으며 내부 인터페이스를 구성할 때 쓰인다.

private 프로퍼티

이건 이번 우테코 프리코스 3주 차 미션 파일에 등장하는 개념이라서 주의깊게 보자.

  • private 프로퍼티와 메서드는 #으로 시작한다.
  • #이 붙으면 클래스 안에서만 접근할 수 있다.
  • private 필드는 this[name]로 사용할 수 없다.(보안 강화를 위해)
  • (예)
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


🐹 회고

우테코 프리코스 2주 차 미션을 수행하며 처음 접한 class가 무엇인지 정확하게 파악하지 못하고 문제를 풀었다. 그래서 3주 차인 지금이라도 class에 대한 개념을 잡고 싶어서 포스팅을 하게되었다.(특히 class 안에서 메서드를 쓸 때 this나 static을 어떻게 써야하는지가 궁금했다)

사실 인터넷에서 공부한 내용만으로 공부하여 포스팅을 한번 했었는데, static을 왜 쓰는지 이해가 안가서 시니하 님께 질문하게 되었다. 시니하 님이 직접 코드를 짜서 설명해주신 뒤에야 궁금증이 시원하게 해결된 것 같다!!! 그래서 새롭게 알게 된 내용을 포스팅에 추가했다. 감사합니다 감사합니다..🥺🥺

그리고 코드 리뷰에서 스터디 팀원이 지적해주지 않았다면 절대 찾아서 공부하지 못했을 것 같다. 무엇을 모르는지도 모르는 상태였으니까...! 이부분을 콕 집어서 리뷰해준 팀원에게 감사의 말씀을 전하고 싶다😎

profile
🐹강화하고 싶은 기억을 기록하고 공유하자🐹

2개의 댓글

comment-user-thumbnail
2022년 11월 12일

1개의 답글