[모던JS: Core] 클래스 (1)

KG·2021년 5월 22일
0

모던JS

목록 보기
14/47
post-thumbnail

Intro

본 포스팅은 여기에 올라온 게시글을 바탕으로 작성되었습니다.
파트와 카테고리 동일한 순서로 모든 내용을 소개하는 것이 아닌, 몰랐거나 새로운 내용 위주로 다시 정리하여 개인공부 목적으로 작성합니다.
중간중간 개인 판단 하에 필요하다고 생각될 시, 기존 내용에 추가로 보충되는 내용이 있을 수 있습니다.

클래스와 기본 문법

앞서서 우리는 생성자 함수(new Function)와 prototype을 통해 객체를 생성하는 방법과 상속에 대한 개념을 다뤘다. 이처럼 자바스크립트는 프로토타입 기반의 언어라는 점을 살펴보았다. 그러나 자바스크립트의 중심이 되는 자료형은 객체(형)이다. 즉 우리는 자바스크립트로 객체 지향 방식을 구현할 수 있고, 이를 위해 프로토타입을 활용했다.

그러나 보통 객체 지향형 언어의 대표격으로 뽑히는 JAVAC++과 같은 언어에서는 그 중심에 Class라는 개념이 자리잡고 있다. 자바스크립트는 한동안 프로토타입만을 사용하여 객체 지향을 구현했지만, ES6(ES2015)에 드디어 class라는 키워드가 지원되기 시작했다. 따라서 자바스크립트에서도 클래스 개념을 이용하여 객체 생성과 상속을 구현할 수 있게 되었다.

다만 자바스크립트는 여전히 프로토타입 기반의 언어라는 점에 주의하자. 클래스는 Syntax Sugar로 지원되는 일종의 문법적인 양념일 뿐, 자바스크립트의 패러다임이 클래스로 전환된 것이 아니다. 상속 관점에서 자바스크립트의 유일한 생성자는 객체뿐이고, 각각의 객체는 숨김 프로퍼티인 [[Prototype]]을 통해 다른 객체를 가리킨다. 이는 class를 통해 객체를 생성하더라도 바뀌지 않는 매커니즘이다.

class는 객체를 생성하기 위해 추가된 일종의 템플릿이며, 그 내부는 프로토타입을 이용해서 만들어졌다. 따라서 자바스크립트에서는 class 역시 약간 특별한 함수로 취급한다. class는 객체 생성과 상속에 있어서 해당 방식이 더 익숙한 개발자들에겐 더 편리하게 자바스크립트로 이를 구현할 수 있는 문법적인 환경을 제공해준다.

1) 기본 문법

class는 ES6에서 뒤늦게 추가되었고, 다른 언어에서의 쓰임을 최대한 모방하여 자바스크립트의 기존 프로토타입의 쓰임에 연결했기 때문에 그 문법 역시 다른 언어에서와 상당히 유사하다.

class MyClass {
  constructor() {
    // 생성자 정의
  }
  
  method1() { ... }
  method2() { ... }
  method3() { ... }
  ...
}
  
const myClass = new MyClass();

위와 같이 클래스를 선언하고 객체를 생성할 수 있다. new 키워드와 함께 클래스를 호출해야 객체의 기본 상태를 설정해주는 생성자 메서드 constructor가 자동으로 호출되어 객체를 초기화 하고, 내부에서 정의한 메서드가 들어있는 객체가 생성된다. 생성자 함수 패턴과 비교했을 때와 크게 다른 점이 없다. 다음 코드를 보며 어떤 차이가 있는지 먼저 간단하게 음미해보자.

// class 사용
class User {
  constructor(name) {
    this.name = name;
  }
  sayHi() {
    console.log(this.name);
  }
}

let user = new User('KG');
user.sayHi();	// KG

// 생성자 함수 사용
function User (name) {
  this.name = name;
}

User.prototype.sayHi = function () {
  console.log(this.name);
}

let user = new User('KG');
user.sayHi();	// KG

클래스 선언은 letconst처럼 블록 스코프에 선언되며, 호이스팅이 일어나지 않는다.

2) 클래스란

앞서 언급한 바와 같이 클래스는 자바스크립트에서 새롭게 창안한 개체(entity)가 아니다. 클래스는 적어도 자바스크립트에서는 기존 프로토타입을 이용하는 함수의 한 종류에 해당한다.

// 클래스가 함수의 한 종류라는 증거
class User {
  constructor(name) {
    this.name = name;
  }
  sayHi() {
    console.log(this.name);
  }
}

console.log(typeof User);	// function

클래스 문법의 내부적인 로직은 다음의 순서와 같다.

  1. 위 코드에서 User라는 이름을 가진 함수를 생성
  2. 함수 본문은 생성자 메서드인 constructor에서 가져온다. 만약 constructor가 정의되지 않았다면 본문이 비워진 함수가 만들어진다.
  3. 클래스 내에서 정의한 메서드(sayHi)는 모두 User.prototype에 저장

따라서 클래스로 생성한 객체에서 메서드를 호출하는 행위는, 이전 챕터에서 자세히 설명한 동작 방식을 거쳐 메서드를 프로토타입에서 가져오게 된다. 위에서 클래스와 프로토타입으로 동일한 객체 생성 로직을 비교한 코드를 다시 살펴보면 이를 알 수 있다.

생성자 함수 패턴의 경우에는 자동으로 constructor 프로퍼티를 자신의 prototype에 가지며 생성이 되고, 해당 prototype에 메서드를 외부에서 다시 저장하고 있다. 그러나 클래스의 경우에는 내부에서 constructor를 일종의 메서드 형태로 정의하고, 그 외 내부 메서드들은 모두 자동으로 자신의 prototype에 저장된다.

따라서 클래스는 다음과 같은 성질을 가지는 것을 알 수 있다.

  • typeof Class : function
  • Class === Class.prototype.constructor : 생성자 메서드와 동일
  • Class.prototype.someMethod : 내부에서 정의한 메서드는 prototype에 자동으로 저장
  • Object.getOwnPropertyNames(Class.prototype) : constructorsomeMethod...

3) Syntax Sugar

처음에 살펴보았듯이 기존 프로토타입 체인의 방식을 class를 이용해서 동일하게 구현할 수 있고, 그에 대한 역도 성립한다. 따라서 class는 일종의 Syntax Sugar 그 이상도 이하도 아니라고 보는 일부 의견도 있다. 그러나 두 가지 방법에는 중요한 차이가 몇 가지 있다.

  1. class로 만든 함수에는 특수 내부 프로퍼티인 [[FunctionKind]]: "classCounstructor가 이름표 처럼 할당된다.
  • 자바스크립트는 다양한 방법을 사용해 함수에 [[FunctionKind]]: "classCounstructor 프로퍼티가 있는지 확인한다. 이런 검증 과정이 내부적으로 돌아가기 때문에 classnew와 함께 호출하지 않는다면 에러가 발생한다.
  • 그러나 생성자 함수에서는 new와 함께 호출하지 않더라도 에러는 발생하지 않는다. 그러나 객체가 제대로 생성되지 않아 문법 오류가 발생할 수 있다.
  • 또한 대부분의 자바스크립트 엔진은 클래스 생성자를 문자열로 표현할 때 class라는 문자열로 표현한다.

  1. 클래스의 메서드는 열거할 수 없다.
  • 클래스의 prototype 프로퍼티에 추가된 메서드 전체의 enumerable 플래그는 false이다.
  • 따라서 for...in으로 객체를 순회할 때 메서드에 접근할 수 없는데, 보통 객체 메서드는 순회 대상에서 제외하고자 하는 경우가 많기에 이는 유용한 특징이다.
  1. 클래스는 항상 엄격모드(use strict)로 실행된다.
  • 클래스 생성자 안 코드 전체에는 자동으로 엄격 모드가 적용된다.

이 외에도 class를 사용하면 기존 방식과 달리 상속을 하는 방법 또는 기타 관련된 다양한 기능이 조금 더 딸려온다. 이러한 관점에서 class를 조금 더 특별한 Syntax Sugar 이상으로 취급하는 의견도 존재한다.

그러나 분명한건 class는 완전히 새로운 개념으로 창안된 문법이 아니라, 기존 자바스크립트의 prototype을 이용하여 구현되었다는 점이다. 또 비교적 최근에 추가된 문법이기 때문에 추가적으로 제공되는 편의 기능이 더 구현되어 있다. 따라서 중요한 것은 내가 class를 사용하는 것이 개발 생산성에 더 도움이 될 지 판단하고, 어떤 방식으로 접근할 지 결정하는 것이다. classprototype 간의 추가적인 차이를 조금 더 디테일하게 보고 싶다면 여기를 참고하자.

4) 클래스 표현식

앞서 클래스 역시 일종의 함수라고 했다. 또 이전 챕터에서 함수를 다룰 때, 함수 선언식과 함수 표현식의 방식이 있다는 것을 살펴보았다. 즉 클래스 역시 클래스 표현식으로 만들 수 있다.

let User = class {
  sayHi() {
    console.log('hello');
  }
};

let user = new User();
user.sayHi();	// hello

또한 함수 챕터에서 기명 함수 표현식을 다룬 적이 있는데, 클래스 역시 이와 비슷하게 기명 클래스 표현식으로 응용할 수 있다. 클래스 표현식에 이름을 붙이면, 이 이름은 오직 클래스 내부에서만 사용 가능하다. 다만 기명 클래스 표현식은 명세서에는 없는 용어인데, 기명 함수 표현식과 유사하게 동작한다.

let User = class MyClass {
  sayHi() {
    console.log(MyClass);
  }
};

new User().sayHi();	// MyClass의 정의 출력

console.log(MyClass);	// ReferenceError

또한 아래와 같이 필요에 따라 클래스를 동적으로 생성하는 것 역시 가능하다.

function makeClass(phrase) {
  return class {
    sayHi() {
      console.log(phrase);
    }
  }
}

let User = makeClass('Hello');
new User().sayHi();	// Hello

5) getter와 setter

객체 리터럴 방식으로 객체 생성시에는 접근자 프로퍼티인 getter/setter를 통해 특정 값에 접근 또는 설정이 가능했다. class 역시 이와 유사하게 getter/setter를 지원한다.

class User {
  constructor (name) {
    // this.name은 내부 메서드 setter 활성화와 동일하다
    // 따라서 객체 생성 시 setter를 통해 생성하는 것과 같다
    this.name = name;
    
    // this.username = name;
    // 이 경우 생성되는 객체에는 username 프로퍼티에
    // 생성자에 인수로 들어온 name이 할당된다.
  }
  
  get name() {
    return this._name;
  }
  
  set name(value) {
    this._name = value
  }
}

let user = new User("John");
alert(user.name); // John

6) 클래스 필드

클래스 필드는 최근에 도입된 기능이기 때문에, 구식 브라우저에서는 폴리필이 필요할 수 있다. 이는 2021년 5월 기점으로 아직 stage-3에 등재되어 있으며 정식으로 채택되지 않았다. 다만 크롬 최신 버전 등 일부 브라우저에서는 해당 기능을 사용할 수 있다. 만약 내 브라우저가 클래스 필드를 지원하는지 알아볼려면 이 링크를 확인해보자.

클래스 필드 문법은 자바 또는 리액트 프레임워크를 다뤄보았다면 익숙한 개념일 수 있다. 간단하게 말하면, 클래스의 속성을 constructor 생성자 안에 넣지 않고, 그저 class 내부에 선언하듯 적어주는 것을 의미한다. 즉 어떤 종류의 프로퍼티도 클래스에 쉽게 추가할 수 있게 되었다. 다른 언어에서는 너무 당연한 문법일 수 있지만 자바스크립트에서는 아직 정식으로 구현이 되어 있지 않다.

클래스 필드로 클래스에 프로퍼티를 추가하는 경우 생성자(constructor)를 통해 생성하는 것과 달리 가장 큰 차이점은, Class.prototype이 아닌 개별 객체에만 클래스 필드가 설정된다는 점이다. 또한 클래스 필드엔 복잡한 표현식이나 함수 호출 결과 역시 모두 할당이 가능하다. 클래스 필드 문법을 이용하면 클래스 블록 안에서 할당 연산자(=)를 이용해 인스턴스 속성을 지정할 수 있다.

class User {
  name = "John";	// consturctor 없이 클래스 필드로 선언

  sayHi() {
    alert(`Hello, ${this.name}!`);
  }
}

new User().sayHi(); // Hello, John!

alert(user.name); // John, 개별 객체에만 할당됨
alert(User.prototype.name); // undefined

클래스 블록 내에서는 일반 자바스크립트 문법과 다른 규칙이 적용된다. 즉 클래스 필드를 이용하여 프로퍼티 값을 할당할 때 let 또는 const 키워드를 사용하지 않는 것에 주의하자.

7) 클래스 필드 바인딩 메서드

자바스크립트에서 함수는 항상 동적인 this를 갖는다. 따라서 객체 메서드의 경우, 다른 컨텍스트에서 호출하게 되면 this는 원래 객체를 참조하지 않는 것을 앞서 살펴보았다. 이는 class라고 다른 규칙이 적용되지 않는다. 따라서 class 역시 this에 대한 정보를 소실할 수 있다.

class Button {
  constructor(value) {
    this.value = value;
  }

  click() {
    alert(this.value);
  }
}

let button = new Button("hello");

setTimeout(button.click, 1000); // undefined

setTimeout은 인수로 전달하는 함수의 this를 전역 객체 window로 넘기기 때문에 위와 같이 undefined가 출력되었다. 이를 해결하기 위한 방법을 앞서 함수 챕터에서 살펴보았는데 크게 두 가지 방법이 있다.

  1. setTimeout(() => button.click(), 1000) 같이 화살표 함수를 래퍼로 하여 전달
  2. 생성자 안 에서 메서드를 객체에 바인딩하기

다만 class 에서는 클래스 필드를 이용하여 또 다른 바인딩을 시도할 수 있다. 앞서 클래스 필드는 할당 연산자(=)를 이용해서 프로퍼티를 지정할 수 있고, 이때 지정할 수 있는 값은 원시값뿐이 아닌 함수나 복잡한 계산식 모두 가능하다.

class Button {
  constructor(value) {
    this.value = value;
  }
  click = () => {
    alert(this.value);
  }
}

let button = new Button("hello");

setTimeout(button.click, 1000); // hello

위처럼 자체 메서드를 화살표 함수로 선언하게 되면, 각 Button 객체마다 독립적인 함수를 만들게 되고, 함수의 this를 해당 객체에 바인딩 시켜줄 수 있다. 왜냐하면 화살표 함수는 this 정보를 갖고 있지 않아 외부의 this를 들고오기 때문이다. 이때 외부의 this는 당연히 인스턴스 객체 자신을 가리키게 된다. 따라서 개발자는 button.click을 아무 곳에 전달하더라도 의도한 this 값을 활용할 수 있다. 이러한 클래스 필드 기능은 특히 브라우저 환경에서 메서드를 이벤트 리스너로 설정할 때 특히 유용하다.

다만 클래스 필드로 선언되는 모든 프로퍼티는 Class.prototype에 저장되지 않고, 개별 객체에 저장된다는 점을 유의하자. 즉 각각의 인스턴스 객체에 저장되기 때문에 클래스 필드를 통해 정의한 메소드는 인스턴스 생성시 마다 매번 새로 생성되기 때문에 메모리를 더 차지하게 될 수 있다.

References

  1. https://ko.javascript.info/classes
  2. https://developer.mozilla.org/ko/docs/Web/JavaScript/Inheritance_and_the_prototype_chain
  3. https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Classes
  4. https://www.toptal.com/javascript/es6-class-chaos-keeps-js-developer-up
  5. https://www.zerocho.com/category/ECMAScript/post/58ef998e177375001892f897
profile
개발잘하고싶다

0개의 댓글