-
JavaScript 클래스 문법은 어떻게 쓰는 것일까?Frontend/JavaScript 2023. 2. 22. 15:45반응형
프로토타입의 설명은 생략하였습니다.
클래스
- 자바스크립트는 프로토타입 기반의 객체지향 언어입니다
- 클래스 문법은 ES6(2015)에 와서야 추가되었습니다.
- 클래스 문법이 등장 하기 전에는 어떤 방식으로 객체지향 프로그래밍을 할 수 있었을까요?
클래스를 사용한 객체 생성
- 먼저 널리 알려진 클래스 기반 프로그래밍 언어이자 객체지향 프로그래밍 언어의 대표주자로 불리는 Java와 비교를 하면서 예시를 들겠습니다.
- JavaScript는 알고 Java를 모르시는 분이 다음과 같은 문법을 보게된다면 뭔가 어디서 본듯 한 느낌을 받을 것입니다.
Java의 클래스 사용법
public class Car { String name; int position; public Car(String name) { this.name = name; this.position = 0; } public void move() { position += 1; } }
public class Main { public static void main(String[] args) { Car car = new Car("JavaCar"); System.out.println(car.name + " " + car.position); // JavaCar 0 car.move(); System.out.println(car.name + " " + car.position); // JavaCar 1 } }
JavaScript의 클래스 사용법
class ClassCar { constructor(name) { this.name = name; this.position = 0; } move() { this.position += 1; } } const classCar = new ClassCar('classCar'); console.log(classCar); // ClassCar { name: 'classCar', position: 0 } classCar.move(); console.log(classCar); // ClassCar { name: 'classCar', position: 1 }
- 보시다시피 자바스크립트에서 사용한 클래스 문법은 언뜻 보기에 자바와 유사한 점이 많습니다.
- Java 문법만 보던 사람이 위 예제를 보더라도 바로 이해하고 금방 사용할 수 있을 정도로 문법의 구성이 유사합니다.
- 그렇다면 ES6 이전의 자바스크립트는 도대체 어떻게 객체를 생성했던 것일까요?
- 자바스크립트에는 여러 객체 생성 방법이 있지만, 이 중 생성자 함수를 사용한 객체 생성에 대해서 알아보겠습니다.
생성자 함수를 사용한 객체 생성
const FunctionCar = (function () { function FunctionCar(name) { this.name = name; this.position = 0; } FunctionCar.prototype.move = function () { this.position += 1; }; return FunctionCar; }()); const funCar = new FunctionCar('funCar'); console.log(funCar); // FunctionCar { name: 'funCar', position: 0 } funCar.move(); console.log(funCar); // FunctionCar { name: 'funCar', position: 1 }
- 위의 생성자 함수와 프로토타입을 통해서 클래스 없이도 상속을 구현할 수 있다는 특징이 있습니다.
클래스 vs 생성자 함수
- 앞서 언급했든 클래스는 ES6에 와서야 추가된 문법으로, 기존 자바스크립트의 프로토타입을 기반으로 만들어진 기능입니다.
- 클래스는 문법적 설탕이라는 표현을 자주 쓰는데, 프로토타입을 조금 더 쉽게 사용할 수 있도록 문법이 추가된 기능이기 때문입니다.
- 클래스는 new 연산자 없이 호출하면 에러가 발생합니다.
- 생성자 함수는 new 연산자 없이 호출하면 일반 함수로 호출됩니다.
- 클래스는 상속을 지원하는 extends와 super 키워드가 존재하지만, 생성자 함수는 이들을 지원하지 않습니다.
- 클래스는 항상 use strict 모드로 동작합니다.
클래스 생성
class Car { constructor(name) { // 생성자 this.name = name; this.position = 0; } move() { // 메소드 this.position += 1; } honk() { // 메소드 console.log(`${this.name} : ${'빵'.repeat(this.position)}`); } } const car = new Car('가브리엘');
- 앞서 예시 자료를 보여줬지만, 작성된 클래스는 new 키워드로 인스턴스화 할 수 있습니다. 인스턴스는 클래스로 생성된 오브젝트(객체)를 의미합니다.
- 생성된 오브젝트(객체)는 변수에 할당하여 접근할 수 있습니다.
- new로 객체를 생성하면 constructor가 자동으로 실행되고, 생성자에서 넘겨준 이름과 위치의 초기 값을 멤버 변수에 할당하게 됩니다.
- 클래스 필드는 constructor 메서드 내부에서만 정의할 수 있었으나, 최근에는 클래스 몸체에 직접 정의하는 것이 가능합니다.
- 객체가 생성된 이후에는 객체에 포함되어있는 객체 메서드를 호출할 수 있습니다.
- 위 예시에서는 move()와 honk()를 메서드로 정했습니다.
- 클래스를 new 키워드로 생성하면 Car라는 이름을 가진 함수를 만들게 되고, 함수에는 생성자 메서드인 constructor에서 가져옵니다.
- 나머지 메서드들은 Car.prototype에 저장하게 됩니다.
- 객체를 만들고, 객체의 메서드를 호출하면 prototype 프로퍼티를 통해서 가져오게 됩니다.
클래스를 생성하면 일어나는 일
// 클래스는 함수입니다. console.log(typeof Car); // function // 명확하게 말해서 클래스는 생성자 메서드와 동일합니다. console.log(Car === Car.prototype.constructor); // true // 클래스 내부에서 정의한 메서드는 Car.prototype에 저장됩니다. (일반적인 메서드는 프로퍼티에 저장됩니다.) console.log(Car.prototype.move); // [Function: move] // 현재 프로토타입의 메서드를 조회할 수 있습니다. console.log(Object.getOwnPropertyNames(Car.prototype)); // [ 'constructor', 'move', 'honk' ]
- 클래스를 뜯어보면 위와 같은 형태의 구성임을 알 수 있습니다.
메서드
- 클래스 내부의 메서드는 0개 이상이어야 합니다.
- 클래스 내부에서 정의할 수 있는 메서드는 생성자(constructor), 프로토타입 메서드, 정적 메서드 3가지 입니다.
- constructor는 최대 1개 올 수 있습니다. 2개 이상 오는 경우 에러가 발생합니다.
- constructor는 인스턴스를 생성하고 초기화하는 데 쓰는 특수한 메서드 입니다.
- constructor는 자기 자신을 참조하고 있고, 암묵적으로 this를 반환하므로 별도의 return을 하지 않아야 합니다.
- 클래스의 프로토타입 메서드는 move(){}처럼 프로토타입 프로퍼티를 직접 조작하지 않아도 기본적으로 프로토타입 메서드가 됩니다.
- 클래스가 생성한 인스턴스는 생성자 함수와 같이 프로토타입 체인에 포함이 됩니다.
- 생성자 함수에서의 프로토타입 메서드는 앞선 생성자 함수의 예제의 FunctionCar.prototype.move = function () {}처럼 프로토타입에 직접 명시적으로 프로토타입에 메서드를 추가하였습니다.
- 클래스 몸체에서 정의한 메서드는 인스턴스의 프로토타입에 존재하는 프로토타입 메서드가 됩니다.
- 인스턴스는 프로토타입 메서드를 상속받아서 사용할 수 있습니다.
- 클래스에 의해 생성된 인스턴스는 기존의 객체 생성 방식과 동일하게 적용되므로 생성자 함수와 마찬가지로 프로토타입 기반의 객체 생성 메커니즘을 가집니다.
- 정적 메서드는 인스턴스를 생성하지 않고 호출이 가능한 메서드를 의미합니다.
- 메서드 앞에 static을 입력하면 정적 메서드가 됩니다.
- 정적 메소드는 프로토타입이 아닌 클래스 함수 자체에 설정합니다.
생성자 메서드
class Car { static staticMethod() { console.log(this === Car); } } Car.staticMethod(); // true
- 예제처럼 인스턴스 생성 없이 호출이 가능합니다.
class Car { constructor(name, date) { this.name = name; this.date = date; } static createCar() { // this는 Car입니다. return new this('가브리엘', new Date()); } } const car = Car.createCar(); console.log(car); // Car { name: '가브리엘', date: 2023-02-21T18:03:43.162Z }
- 클래스를 new 키워드 (생성자) 없이 정적처리 하기 위해 사용됩니다.
getter/setter
- 클래스는 접근자 프로퍼티로 getter와 setter를 지원합니다.
- get 키워드와 set 키워드로 사용할 수 있습니다.
class Car { constructor(name) { // 생성자 this.name = name; this.position = 0; } get name() { return this._name; } set name(newName) { this._name = newName; } } const car = new Car('가브리엘'); console.log(car); // Car { _name: '가브리엘', position: 0 } console.log(car.name); // 가브리엘 car.name = '엘리브가'; console.log(car.name); // 엘리브가
- 예제를 확인해보시면 초기 생성된 Car 객체의 이름은 가브리엘이고, 새로운 이름인 엘리브가로 덮어씌우는 모습을 확인할 수 있습니다.
- 특이한 점으로는 생성 직후에 car 객체를 출력해보면, 이름을 name이 아닌 _name으로 관리하는 것을 알 수 있습니다. 반면에 position은 처음 정해준 이름 그대로를 사용합니다.
- 즉, constructor의 this.name은 사실 set을 의미한다는 것 입니다.
- 그렇다면 왜 하필 this._name을 사용했을까요? 그냥 this.name을 해주면 안될까요?
getter/setter 오류
class Car { constructor(name) { // 생성자 this.name = name; } get name() { return this.name; } set name(newName) { this.name = newName; } } const car = new Car('가브리엘');
lass-getter-setter-error.js:11 this.name = newName; ^ RangeError: Maximum call stack size exceeded at set name [as name] (/Users/gabrielyoon7/Code/wooteco_lv1/javascript-for-tecotalk/class/class-getter-setter-error.js:11:15) at set name [as name] (/Users/gabrielyoon7/Code/wooteco_lv1/javascript-for-tecotalk/class/class-getter-setter-error.js:11:15) at set name [as name] (/Users/gabrielyoon7/Code/wooteco_lv1/javascript-for-tecotalk/class/class-getter-setter-error.js:11:15) at set name [as name] (/Users/gabrielyoon7/Code/wooteco_lv1/javascript-for-tecotalk/class/class-getter-setter-error.js:11:15) at set name [as name] (/Users/gabrielyoon7/Code/wooteco_lv1/javascript-for-tecotalk/class/class-getter-setter-error.js:11:15) at set name [as name] (/Users/gabrielyoon7/Code/wooteco_lv1/javascript-for-tecotalk/class/class-getter-setter-error.js:11:15) at set name [as name] (/Users/gabrielyoon7/Code/wooteco_lv1/javascript-for-tecotalk/class/class-getter-setter-error.js:11:15) at set name [as name] (/Users/gabrielyoon7/Code/wooteco_lv1/javascript-for-tecotalk/class/class-getter-setter-error.js:11:15) at set name [as name] (/Users/gabrielyoon7/Code/wooteco_lv1/javascript-for-tecotalk/class/class-getter-setter-error.js:11:15) at set name [as name] (/Users/gabrielyoon7/Code/wooteco_lv1/javascript-for-tecotalk/class/class-getter-setter-error.js:11:15) Node.js v18.14.0
- constructor의 this.name은 클래스 내부에 있는 set 함수를 조작하게 됩니다.
- 그리고 set 함수에 있는 this.name도 자기 자신인 set 함수를 조작하게 됩니다.
- 이 과정이 무한 반복됩니다.
- 따라서 클래스 안에서 this.name = 어떤값; 의 표현이 set 함수를 건들라는 의미를 가지게 되므로, 저장하는 멤버 변수의 이름을 다른 이름인 this._name으로 관리를 하는 것입니다. (_표현법은 자바스크립트 개발자들 사이에서 등장한 암묵적인 표기법입니다.)
- 어차피 이 객체의 멤버 변수는 getter/setter를 통해 접근하게 될 것이므로 실제 멤버변수의 이름이 무엇이든간에 상관이 없어지게 됩니다.
- 최신 자바스크립트는 멤버변수의 private를 지원하므로 this.#name 따위로 관리하는 것이 조금 더 안전할 것입니다.
priavte를 활용한 getter/setter
class Car { #name = ''; constructor(name) { // 생성자 this.name = name; } get name() { return this.#name; } set name(newName) { this.#name = newName; } } const car = new Car('가브리엘'); console.log(car); // Car {} console.log(car.name); // 가브리엘
- 당연하지만 name 변수가 private하게 관리되므로 car를 조회했을 때에는 아무것도 조회가 되지 않을 것입니다.
계산된 메소드 이름
class Car { ['honk' + 'ManyTimes']() { console.log('빵'.repeat(5)); } } const car = new Car(); car.honkManyTimes(); // 빵빵빵빵빵
- 메소드 이름을 계산하여 정하는 것도 가능합니다.
클래스 상속
- 상속은 보통 클래스를 확장하기 위해 사용되는 기능입니다.
- 자바스크립트의 객체는 기본적으로 프로토타입을 상속받고 있으나, 클래스의 상속은 기존 클래스를 상속받아 새로운 클래스를 확장하여 정의하는 것에 기인합니다.
- extends 키워드는 클래스를 다른 클래스의 자식으로 만들기 위해 사용됩니다.
class Vehicle { constructor(name) { this.name = name; this.position = 0; } move() { this.position += 1; } } class Car extends Vehicle { honk() { console.log(`${this.name} : ${'빵'.repeat(this.position)}`); } } const car = new Car('가브리엘'); console.log(car); // Car { name: '가브리엘', position: 0 } car.move(); car.move(); car.move(); console.log(car); // Car { name: '가브리엘', position: 3 } car.honk(); // 가브리엘 : 빵빵빵 console.log(car instanceof Car); // true console.log(car instanceof Vehicle); // true
- 탈것인 Vehicle을 상속받은 Car 예제입니다.
- Car 객체는 move() 메소드가 없음에도 불구하고 움직일 줄 알고, honk()도 가능합니다.
- 상속을 통해 확장된 클래스는 서브클래스/파생클래스/자식클래스 등으로 불리고
- 상속을 해준 클래스는 수퍼클래스/베이스클래스/부모클래스 등으로 부릅니다.
super 키워드 (생성자)
- super를 호출하면 수퍼클래스의 constructor를 호출합니다.
- 수퍼클래스의 constructor 내부에서 추가한 프로퍼티를 그대로 갖는 인스턴스를 생성한다면 서브클래스의 constructor를 생략할 수 있습니다.
- 예를 들면 Car 객체는 consturctor가 없지만 암묵적으로 정의된 constructor의 super가 활성화되어 부모 객체인 Vehicle객체의 constructor에 전달됩니다.
- super는 반드시 서브클래스의 constructor에서만 호출할 수 있습니다.
class Base { constructor(a, b) { this.a = a; this.b = b; } } class Derived extends Base { constructor(a, b, c) { super(a, b); this.c = c; } } const derived = new Derived(1, 2, 3); console.log(derived); // Derived { a: 1, b: 2, c: 3 }
- new로 호출한 Derived 객체는 우선 Derived 클래스의 constructor에 전달됩니다.
- 이때 전달된 인자는 1, 2, 3인데, super(a, b)에 의해서 Base의 constructor에 a, b가 전달되어 a, b를 저장하게 됩니다.
- 그리고 그 다음 줄인 this.c = c;에서 c를 저장하게됩니다.
super 키워드 (참조)
메서드 내에서 super를 참조하는 경우 수퍼클래스의 메서드를 호출합니다.
class Base { constructor(name) { this.name = name; } sayHi() { return `Hi${this.name}`; } }차 class Derived extends Base { sayHi() { return `${super.sayHi()}`; } } const derived = new Derived('가브리엘'); console.log(derived.sayHi()); // Hi가브리엘
- super 참조로 수퍼클래스의 메서드를 참조하려면 super가 수퍼클래스의 메서드가 바인딩된 객체, 즉, 수퍼클래스의 prototype 프로퍼티에 바인딩 된 프로토타입을 참조할 수 있어야 합니다.
상속 클래스의 인스턴스 생성 과정
- 상속 관계에 있는 두 클래스가 협력을 어떻게 하는지 예제를 통해 살펴보면 다음과 같습니다.
class Animal { constructor(name) { this.name = name; } speak() { console.log(`${this.name}은(는) 소리를 냅니다.`); } } class Dog extends Animal { constructor(name, breed) { super(name); this.breed = breed; } speak() { super.speak(); console.log(`${this.name}은(는) 짖습니다.`); } getBreed() { console.log(`${this.name}의 품종은 ${this.breed} 입니다.`); } } const dog = new Dog('뽀삐', '말티즈'); dog.speak(); // 뽀삐은(는) 소리를 냅니다. 뽀삐은(는) 짖습니다. dog.getBreed(); // 뽀삐의 품종은 말티즈 입니다.
- 강아지와 동물의 관계는 상속 관계입니다.
- 동물은 소리를 낼 수 있습니다.
- 강아지는 짖는 소리도 내고, 구체적인 품종이 있습니다.
- 따라서 Animal의 speak() 을 상속하되, 추가 동작을 수행할 수 있습니다.
- getBreed()는 강아지의 고유 메서드로, 부모에게 없는 메서드를 추가하여 사용합니다.
class Animal { constructor(name) { console.log(1); console.log(this); // Dog {} this.name = name; console.log(2); console.log(this); // Dog { name: '뽀삐' } } speak() { console.log(`${this.name}은(는) 소리를 냅니다.`); } } class Dog extends Animal { constructor(name, breed) { super(name); console.log(3); console.log(this); // Dog { name: '뽀삐' } this.breed = breed; console.log(4); console.log(this); // Dog { name: '뽀삐', breed: '말티즈' } } speak() { super.speak(); console.log(`${this.name}은(는) 짖습니다.`); } getBreed() { console.log(`${this.name}의 품종은 ${this.breed} 입니다.`); } } const dog = new Dog('뽀삐', '말티즈'); dog.speak(); // 뽀삐은(는) 소리를 냅니다. 뽀삐은(는) 짖습니다. dog.getBreed(); // 뽀삐의 품종은 말티즈 입니다.
- 실행 순서를 보면 위와 같습니다.
- Animal의 생성자에서 this를 출력해보면 Animal이 아닌 Dog가 출력되는 것을 확인할 수 있는데, 이는 new 연산자가 사용된 곳이 Dog이기 떄문입니다.
- Dog 의 생성자의 super()는 Animal에서 반환한 인스턴스가 this에 바인딩되기 때문에, Dog에 부모의 프로퍼티가 살아있는 것입니다.
- 서브클래스의 constructor에서 super키워드 이전에 this를 사용하려고 하면 오류가 나는데, super가 호출되지 않으면 인스턴스가 생성되지 않고, this 바인딩도 할 수 없기 때문입니다.
- 클래스의 모든 처리가 끝나면 완성된 인스턴스가 바인딩 된 this를 반환합니다.
반응형'Frontend > JavaScript' 카테고리의 다른 글
CustomEvent를 활용하여 customElements에 객체를 전달하는 방법 (0) 2023.03.27 JavaScript의 customElements에서 Proxy을 활용한 객체 변화 감지 (0) 2023.03.03 JavaScript에서 customElements를 활용하여 독립적인 상태 관리하기 (2) 2023.03.01 .onsubmit과 .addEventListener의 차이 (0) 2023.02.27 [JavaScript] null과 undefined의 차이 (0) 2022.08.11