-
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' 카테고리의 다른 글
IntersectionObserver를 활용한 element 감지 및 응용 (무한스크롤, 중간광고 예제 포함) (0) 2023.04.08 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