자바스크립트/모던 자바스크립트

자바스크립트 클래스 2 - 클래스 바디

Jake Seo 2023. 2. 1. 23:38

클래스 바디의 필드와 메서드

  • 클래스 바디란 {} 내부의 요소들을 말한다.
  • 클래스 바디엔 대표적으로 상태를 저장하는 필드와 상태를 이용해 동작을 수행하는 메서드가 있다.

클래스의 바디에 올 수 있는 요소의 경우의 수

  • 종류: Getter, Setter, Method, Field
  • 위치: Static, Instance
  • 접근: Public, Private

클래스의 바디에 오는 요소는 위 3가지를 모두 가져야 한다. 총 16가지 조합이 가능하다.

클래스에만 있는 특별한 것들

클래스 필드 추가하기

class ExampleClass {
  constructor() {
    this.field = 1;
  }
}

const c = new ExampleClass();
console.log(c.field); // 1
  • 예전엔 생성자에서 this.fieldName = fieldValue 와 같은 문법으로 필드를 추가해야 했다.
class ExampleClass {
  field = 1;
}

const c = new ExampleClass();
console.log(c.field); // 1
  • 이제는 위와 같은 문법으로도 추가 가능하다.
  • 기본 할당은 자연적으로 구성, 쓰기, 열거를 지원한다.

클래스 private 필드

class ClassWithPrivateField {
  #privateField;

  constructor() {
    this.#privateField = 42;
  }

  getPrivateField() {
    return this.#privateField;
  }
}

const c = new ClassWithPrivateField();
console.log(c.getPrivateField()); // 42
console.log(c.#privateField); // Uncaught SyntaxError: Private field '#privateField' must be declared in an enclosing class
  • private field 는 클래스 외부에서 접근이 불가능한 필드이다.
  • 클래스 내부에서만 접근이 가능하다.
class ClassWithPrivateField {
  #privateField;

  constructor() {
    this.#privateField = 42;
    this.#randomField = 666; // Uncaught SyntaxError: Private field '#randomField' must be declared in an enclosing class
  }
}
  • private field 는 일반 필드와 다르게 반드시 위에서 먼저 선언 후에 사용해야 한다.

클래스 private 메서드

class ClassWithPrivateMethod {
  #privateMethod() {
    return "hello world";
  }

  getPrivateMessage() {
    return this.#privateMethod();
  }
}

const instance = new ClassWithPrivateMethod();
console.log(instance.getPrivateMessage()); // "hello worl​d"
  • 직접 접근할 수는 없지만 클래스 내부에서 접근 가능한 메서드이다.
class ClassWithPrivateAccessor {
  #message;

  get #decoratedMessage() {
    return `✨${this.#message}✨`;
  }
  set #decoratedMessage(msg) {
    this.#message = msg;
  }

  constructor() {
    this.#decoratedMessage = "hello world";
    console.log(this.#decoratedMessage);
  }
}

new ClassWithPrivateAccessor(); //`✨hello world✨`
  • private method 역시 접근자로도 사용될 수 있다.

클래스 public static 필드

class ClassWithStaticField {
  static staticField = "static field";
}

console.log(ClassWithStaticField.staticField); // "static field"​
  • public static 필드는 인스턴스로 접근하는 것이 아니라 클래스 생성자로 접근할 수 있는 필드를 말한다.
  • 모든 클래스가 같은 값을 갖는 필드 등을 캐싱해놓으면 유리하다.
class ClassWithStaticField {
  static baseStaticField = "base field";
}

class SubClassWithStaticField extends ClassWithStaticField {
  static subStaticField = "sub class field";
}

console.log(SubClassWithStaticField.subStaticField); // "sub class field"

console.log(SubClassWithStaticField.baseStaticField); // "base field"
  • public static 필드도 상속의 대상이다.
  • 다른 언어에서는 정적 필드에 대한 상속을 지원하지 않으므로 눈여겨볼만하다.

클래스 public static 메서드

class ClassWithStaticMethod {
  static baseStaticMethod() {
    return "base static method output";
  }
}

console.log(ClassWithStaticMethod.baseStaticMethod()); // base static method output
  • public static 필드 뿐만 아니라 당연히 메서드도 지원한다.
class ClassWithStaticField {
  static baseStaticField = "base static field";
  static anotherBaseStaticField = this.baseStaticField;

  static baseStaticMethod() {
    return "base static method output";
  }
}

class SubClassWithStaticField extends ClassWithStaticField {
  static subStaticField = super.baseStaticMethod();
}

console.log(ClassWithStaticField.anotherBaseStaticField); // "base static field"
console.log(SubClassWithStaticField.subStaticField); // "base static method output"
  • public static 메서드 역시 상속의 대상이다.
  • 상속 후에 super 키워드를 통해 static method 를 불러올 수도 있다.
class ClassWithStaticMethod {
  static staticProperty = "someValue";
  static staticMethod() {
    return "static method has been called.";
  }
  static {
    console.log("Class static initialization block called");
  }
}

// Class static initialization block called
console.log(ClassWithStaticMethod.staticProperty); // "someValue"
console.log(ClassWithStaticMethod.staticMethod()); // "static method has been called."
  • static 블록은 클래스가 평가되며 자동으로 실행된다.

클래스 메서드 추가

class Color {
  constructor(r = 0, g = 0, b = 0) {
    this.r = r;
    this.g = g;
    this.b = b;
  }

  toString() {
    return this.rgb;
  }
}

위는 toString() 을 추가하는 예시이다. 이전 ES5 코드에서는 아래와 같이 작성해야 했다.

Color.prototype.toString = function toString() {
  return this.rgb;
};
  • 클래스 메서드 구문이 더욱 선언적이다.
  • 새로운 구문을 이용하면 자연적으로 메서드는 enumerable 하지 않게 추가된다.
    • 이전엔 Object.defineProperty() 를 이용해야 했다.
  • 새로운 문법을 이용하면 메서드의 prototype 속성에 아무것도 존재하지 않는다.
  • 이전엔 .prototype.toString = function toString() { ... } 을 이용했으므로 자동으로 Object 를 상속하여 prototypeFunction.prototype 이 들어있었다.

클래스 정의 방법

// 일반 정의
class Color {}

// 익명 클래스 정의
let Color = class {};

// 클래스 정의를 변수로 참조하기
let C = class Color {};
  • 총 3가지의 정의 방법이 있다.
  • 클래스 선언은 함수 선언처럼 호이스팅 되지 않는다.
  • 임시 데드존과 같이 호이스팅 된다.
  • let, const 와 마찬가지로 전역에서 선언해도 전역 객체의 속성이 아니다.

클래스 생성자의 특징

  • new 키워드를 사용해야만 생성자 호출이 가능하다.
    • 생성자 함수를 직접 호출하려하면, 오류가 발생한다.
    • 사실 Reflect 를 이용하면 호출이 가능하긴 하다.

ES5 에서 함수를 클래스처럼 이용하는 방식은 사실상 new 키워드 없이도 함수호출이 되어 혼란스러운 버그를 초래하기도 했다. this instanceof Color 와 같은 구문을 통해 예외처리를 해주어야만 했다.

Color(); // TypeError: Class constructor Color cannot be invoked without 'new'

클래스 내부 코드의 특징

  • 항상 엄격 모드("strict mode")이다.
  • 클래스 내부 속성을 정의할 때 세미콜론이나 컴마를 쓰지 않는다.
  • 내부 메서드를 선언적이고 간결하게 정의할 수 있다.
    • ES5는 Color.prototype.toString = function toString() {...} 와 같은 방식으로 정의해왔다.

이전보다 메모리 효율이 좋다.

// ES2015 `class` 키워드를 이용한 클래스와 메서드 정의
class Color {
  print() {
    console.log("color");
  }
}

var c = new Color();

console.log(typeof c.print.prototype); // undefined

// ES5 `function` 키워드를 이용한 클래스와 메서드 정의
function ColorFunction() {}
const Color2 = ColorFunction;
Color2.prototype.print = function () {
  console.log("color");
};
var c2 = new Color2();

console.log(typeof c2.print.prototype); // object
  • class 키워드를 이용한 클래스 메서드에는 prototype 이 안붙어있다.
  • function 키워드를 이용한 클래스 메서드에는 prototype 이 붙어있다.

정적 메서드 코드 살펴보기

class Color {
  constructor(r = 0, g = 0, b = 0) {
    this.r = r;
    this.g = g;
    this.b = b;
  }

  get rgb() {
    return `rgb(${this.r}, ${this.g}, ${this.b})`;
  }

  set rgb(value) {
    // TODO ...
  }

  toString() {
    return this.rgb;
  }

  static fromCSS(css) {
    const match = /^#?([0-9a-f]{3}|[0-9a-f]{6});?$/i.exec(css);

    if (!match) {
      throw new Error("invalid CSS code: " + css);
    }

    let vals = match[1];
    if (vals.length === 3) {
      vals = vals[0] + vals[0] + vals[1] + vals[1] + vals[2] + vals[2];
    }

    return new this(
      parseInt(vals.substring(0, 2), 16),
      parseInt(vals.substring(2, 4), 16),
      parseInt(vals.substring(4, 6), 16)
    );
  }
}
  • static 키워드가 붙은 메서드는 개별 인스턴스 메서드가 아닌 Color 클래스 자체의 메서드가 된다.
Color.fromCSS = function fromCSS(css) {};
  • ES5 식으로는 위와 같이 표현된다. 프로토타입 속성이 아님에 유의하자.

접근자 속성 코드 살펴보기

class Color {
  constructor(r = 0, g = 0, b = 0) {
    this.r = r;
    this.g = g;
    this.b = b;
  }

  get rgb() {
    return `rgb(${this.r}, ${this.g}, ${this.b})`;
  }

  set rgb(value) {
    let s = String(value);
    let match = /^rgb\((\d{1, 3}),(\d{1, 3}), (\d{1,3})\)$/i.exec(
      s.replace(/\s/g, "")
    );

    if (!match) {
      throw new Error(`Invalid rgb color string '${s}'`);
    }
  }

  toString() {
    return this.rgb;
  }

  static fromCSS(css) {
    const match = /^#?([0-9a-f]{3}|[0-9a-f]{6});?$/i.exec(css);

    if (!match) {
      throw new Error("invalid CSS code: " + css);
    }

    let vals = match[1];
    if (vals.length === 3) {
      vals = vals[0] + vals[0] + vals[1] + vals[1] + vals[2] + vals[2];
    }

    return new this(
      parseInt(vals.substring(0, 2), 16),
      parseInt(vals.substring(2, 4), 16),
      parseInt(vals.substring(4, 6), 16)
    );
  }
}

let c = new Color();
r.rgb = "rgb(30, 144, 255)";

ES5 에서 접근자 코드를 작성했던 방법

ES5 에서는 아래와 같이 작성해야 했다.

Object.defineProperty(Color.prototype, "rgb", {
  get: function () {
    return `${this.r}, ${this.g}, ${this.b}`;
  },
  set: function (value) {
    // ...
  },
  configurable: true,
});

static 접근자

원한다면 정적인 접근자 속성 정의도 가능하다.

class StaticAccessorExample {
  static get cappedClassName() {
    return this.name.toUpperCase();
  }
}

계산된 메서드 이름

런타임에 결정되는 이름으로 메서드를 만들고 싶을 때 유용하다.

let name = "foo" + Math.floor(Math.random() * 1000);
class SomeClass {
  [name]() {
    // ...
  }
}
  • 대괄호 안에 어떤 표현이든 넣을 수 있다.
  • 클래스 정의가 평가되는 시점에 식이 평가된다.
  • 결과가 문자열이나 심볼이 아닌 경우, 문자열로 반환된다.
  • 결과가 메서드 이름으로 사용된다.

정적 메서드

class Guide {
  static [6 * 7]() {
    // Guide["42"]() -> logging "GOOD"
    console.log("GOOD!");
  }
}

ES5

var name = "foo" + Math.floor(Math.random() * 1000);
SomeClass.prototype[name] = function () {
  // ...
};
반응형