Index
① 클래스의 정의
② 메서드
③ 프로퍼티
1. 클래스의 정의
“클래스는 기본적으로 함수이며, 값처럼 사용할 수 있는 일급객체이다. “
JavaScript의 클래스는 기본적으로 프로토타입(prototype) 기바 객체지향 프로그래밍의 문법적 설탕(syntatic sugar)이다.
즉, 클래스는 기존 프로토타입 기반의 객체지향 문법을 더 직관적이고 사용하기 쉽게 만든 표현이다. JavaScript는 내부적으로 여전히 프로토타입 체인을 사용하여 상속과 메서드 공유를 구현한다.
🧐 Q. 문법적 설탕(Syntatic Sugar)이란?
문법적 설탕이란, 기존에 가능하던 기능을 더 간단하고 보기 좋은 방식으로 표현하기 위한 문법을 뜻합니다.
즉, 새로운 기능을 추가하는 것이 아니라, 기존 기능을 사용하기 편리하고 가독성을 높이는 방식으로 재구성한 것이다.
(1) 프로토타입 방식으로 객체 정의
// (1-1) 생성자 함수
function Person(name, age) {
this.name = name;
this.age = age;
}
// (1-2) 프로토타입 메서드
Person.prototype.sayHello = function() {
console.log(`Hi, I'm ${this.name} and I'm ${this.age} years old.`);
};
// (1-3) 정적 메서드
Person.sayHello = function() {
console.log('Hello')
}
const person1 = new Person("Alice", 25);
person1.sayHello(); // Hi, I'm Alice and I'm 25 years old.
(2) 클래스 방식으로 객체 정의
class Person {
// (2-1) 생성자
constructor(name, age) {
this.name = name;
this.age = age;
}
// (2-2) 프로토타입 메서드
sayHello() {
console.log(`Hi, I'm ${this.name} and I'm ${this.age} years old.`);
}
// (2-3) 정적 메서드
static sayHello() {
console.log('Hello!');
}
}
const person1 = new Person("Alice", 25);
person1.sayHello(); // Hi, I'm Alice and I'm 25 years old.
차이점
- 프로토타입 방식: 더 많은 코드와 복잡한 구조
- 클래스 방식: 문법적으로 직관적이고, 객체지향 언어(C++, Java)와 비슷한 표현
하지만, 내부적으로 class
로 작성된 코드는 여전히 프로토타입 제인을 사용해 동작한다.
2. 메서드
2.1 프로토타입 메서드
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`Hi! My name is ${this.name}`);
}
}
const me = new Person("Kim");
me.sayHi(); // Hi! My name is Kim
🧐 Q. me.constructor
가 Person 클래스인데 왜 Person.prototype에 constructor 프로퍼티가 있을까요?
Answer)
constructor
는 JavaScript 클래스의 특별한 메서드입니다.
constructor
는 클래스가 정의될 때, 클래스의 프로토타입(Person.prototype
)에 정의되지 않습니다.- 실제로
constructor
는 클래스 자체의 함수 정의 내부에 존재합니다. - 클래스의 인스턴스를 생성할 때, 이
constructor
가 호출됩니다.
- 실제로
Person.prototype
에 연결된 이유는 메서드 상속 때문입니다.- 클래스 내부에서 정의된 모든 메서드(
sayHi
같은)는 클래스의 프로토타입 객체(Person.prototype
)에 저장됩니다. constructor
도 클래스의 인스턴스를 생성하는 역할을 수행하기 때문에 프로토타입 객체와 연결됩니다.
- 클래스 내부에서 정의된 모든 메서드(
2.2 정적 메서드
class Person {
constructor(name) {
this.name = name;
}
static sayHi() {
console.log("Hi!");
}
}
Person.sayHi(); // Hi!

Static methods, prototype methods, and prototype chain
Ungmo Lee. (2020). Modern Javascript DeepDive. wikibooks. p.431.
3. 프로퍼티
3.1 인스턴스 프로퍼티
class Person {
constructur(name) {
this.name = name;
}
}
const me = new Person("Kim");
console.log(me); // Person {name: "Lee"}
3.2 접근자 프로퍼티
접근자 프로퍼티는 클래스에서도 사용할 수 있다.
Point!
(1) Getter는 속성에 접근할 때 동작하며, 해당 속성을 읽는 것처럼 보이지만 실제로는 함수가 호출됩니다.
(2) Setter는 속성에 값을 할당할 때 동작하며, 해당 속성에 값을 저장하는 것처럼 보이지만 실제로는 함수가 호출됩니다.
(3) 클래스의 메서드는 기본적으로 프로토타입 메서드가 된다. 따라서 클래스의 접근자 프로퍼티 또한 인스턴스 프로퍼티가 아닌 프로토타입의 프로퍼티가 된다.
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// Getter: fullName에 접근할 때 호출됨
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
// Setter: fullName에 값을 할당할 때 호출됨
set fullName(name) {
[this.firstName, this.lastName] = name.split(" ");
}
}
const me = new Person("Jinho", "Tak");
// Getter 호출
console.log(me.fullName); // "Jinho Tak"
// Setter 호출
me.fullName = "John Doe";
console.log(me.firstName); // "John"
console.log(me.lastName); // "Doe"
3.3 클래스 필드 정의제안 & private 필드 정의 & statics 필드 정의
class MyClass {
// 정적 프로퍼티
static staticProperty = 'I am a static property';
// 인스턴스 프로퍼티
// 😀 클래스 필드 문법을 사용하면 constructor 없이도 동일한 결과를 얻을 수 있습니다.
instanceProperty = 'I am an instance property';
// 비공개(Private) 프로퍼티
#privateProperty = 'I am private';
// 비공개 프로퍼티에 접근하는 메서드
getPrivateProperty() {
return this.#privateProperty;
}
}
const instance = new MyClass();
console.log(MyClass.staticProperty); // "I am a static property"
console.log(instance.instanceProperty); // "I am an instance property"
console.log(instance.#privateProperty); 1)
console.log(instance.getPrivateProperty()); 2)
1) 클래스 외부에서 비공개 필드에 직접 접근하려는 시도
console.log(instance.#privateProperty);
// SyntaxError: Private field '#privateProperty' must be declared in an enclosing class
2) 클래스 내부에서 정의된 메서드로 #privateProperty에 접근
console.log(instance.getPrivateProperty());
// "I am private"
4. 상속에 의한 클래스 확장
4.1 클래스 상속
JavaScript에서 클래스는 extends
키워드를 사용하여 기존 클래스를 상속하고 확장할 수 있습니다. 이 문법은 자식 클래스가 부모 클래스의 속성과 메서드를 재사용하거나 확장할 수 있도록 지원한다.
4.2 extends 키워드
베이스 클래스(Base Class)
- 기본 클래스로, 다른 클래스에 의해 상속되는 클래스를 의미한다.
- 일반적으로 계층 구조의 최상위에 있는 클래스를 지칭한다.
- 다른 말로 슈퍼클래스(Superclass)라고도 사용된다.
슈퍼클래스(Superclass)
- 부모 클래스로, 다른 클래스(서브클래스)에 의해 상속되는 클래스다.
- 자식 클래스는 슈퍼클래스의 속성과 메서드를 상속받는다.
- 베이스 클래스와 비슷한 의미로 사용되지만, 슈퍼클래스는 계층 구조의 중간에서도 사용될 수 있다.
서브클래스(Subclass)
- 자식 클래스로, 다른 클래스(슈퍼클래스)를 상속받아 기능을 확장하거나 재정의하는 클래스다.
- 서브클래스는 슈퍼클래스의 속성과 메서드를 상속받으며, 필요하면 새로운 속성과 메서드를 추가할 수 있다.
class BaseClass {
greet() {
console.log("Hello from BaseClass");
}
}
class SuperClass extends BaseClass {
greet() {
console.log("Hello from SuperClass");
}
}
class SubClass extends SuperClass {
greet() {
console.log("Hello from SubClass");
}
}
const subInstance = new SubClass();
subInstance.greet(); // "Hello from SubClass"
4.3 동적상속
-
부모 클래스(슈퍼클래스)를 런타임에 동적으로 지정할 수 있습니다.
-
extends
키워드에는 정적 클래스뿐만 아니라 동적으로 반환된 클래스 또는 생성자 함수도 사용할 수 있습니다. -
동적 상속은 상속 계층 구조를 유연하게 설계하고, 상황에 따라 다른 부모 클래스를 사용할 수 있도록 합니다.
function getBaseClass() {
return Math.random() > 0.5
? class A {
greet() {
console.log("Hello from A");
}
}
: class B {
greet() {
console.log("Hello from B");
}
};
}
class Child extends getBaseClass() {
greetChild() {
console.log("Hello from Child");
}
}
const instance = new Child();
instance.greet(); // "Hello from A" 또는 "Hello from B"
instance.greetChild(); // "Hello from Child"
4.4 서브클래스의 constructor
서브클래스에서 construtor를 생략하면 클래스에 다음과 같은 constructor
가 암묵적으로 정의된다. args는 new 연산자와 함께 클래스를 호출할 때 전달한 인수의 리스트다.
class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent {}
const child = new Child("Alice");
console.log(child.name); // "Alice"
암묵적으로 정의된 constructor
는 다음과 같다.
class Child extends Parent {
constructor(...args) {
super(...args);
}
}
4.5 super 키워드
4.5.1 super
호출
/*
1) 서브클래스에서 `constructor` 를 생략하지 않는 경우 서브클래스의 `constructor`에서는 반드시 `super`를 호출해야 한다.
*/
class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent {
constructor(name, age) {
// this.age = age; // super 호출 전에 this를 참조하면 에러 발생
super(name); // 부모 클래스의 생성자 호출,
this.age = age; // 이후에 this를 참조 가능
}
}
const child = new Child("Alice", 25);
console.log(child.name); // "Alice"
console.log(child.age); // 25
/*
2) 서브클래스의 `constructor`에서 `super`를 호출하기 전에는 `this`를 참조할 수 없다.
*/
class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent {
constructor(name, age) {
// this.age = age; // super 호출 전에 this를 참조하면 에러 발생
super(name); // 부모 클래스의 생성자 호출
this.age = age; // 이후에 this를 참조 가능, 아닐경우 ReferenceError
}
}
const child = new Child("Alice", 25);
console.log(child.name); // "Alice"
console.log(child.age); // 25
/*
3) `super`는 반드시 서브클래스의 `constructor`에서만 호출한다. 서브클래스가 아닌 클래스의 `constructor`나 함수에서 `super`를 호출하면 에러가 발생한다.
*/
class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent {
constructor(name) {
super(name); // 서브클래스의 constructor에서만 호출 가능
}
}
const child = new Child("Alice");
console.log(child.name); // "Alice"
4.5.2 super
참조
JavaScript에서 super
참조는 [[HomeObject]]
를 가지는 함수에서만 사용할 수 있다.
class Parent {
greet() {
console.log("Hello from Parent");
}
}
class Child extends Parent {
greet() {
super.greet(); // Parent의 greet 메서드 호출
console.log("Hello from Child");
}
}
const child = new Child();
child.greet();
// 출력:
// "Hello from Parent"
// "Hello from Child"
/*
Child 클래스의 greet 메서드는 super.greet()를 호출하여 부모 클래스 Parent의 greet 메서드를 실행합니다.
이 동작이 가능한 이유는 greet 메서드가 클래스의 메서드로 정의되어 [[HomeObject]]를 가지고 있기 때문입니다.
*/
/*
2) 객체 리터럴의 축약 메서드에서 super 참조
*/
const parent = {
greet() {
console.log("Hello from Parent");
},
};
const child = {
greet() {
super.greet(); // parent 객체의 greet 메서드 호출
console.log("Hello from Child");
},
};
// child의 프로토타입을 parent로 설정
Object.setPrototypeOf(child, parent);
child.greet();
// 출력:
// "Hello from Parent"
// "Hello from Child"
/*
child.greet는 축약 메서드로 정의되었으므로 [[HomeObject]]를 가집니다.
이를 통해 super.greet()를 호출할 때 parent.greet 메서드를 참조할 수 있습니다.
*/
/*
3) 일반 함수에서 super 참조
*/
class Parent {
greet() {
console.log("Hello from Parent");
}
}
class Child extends Parent {
greetFunction() {
const func = function () {
super.greet(); // 에러 발생
};
func();
}
}
const child = new Child();
child.greetFunction();
// SyntaxError: 'super' keyword unexpected here
/*
일반 함수(위의 func)는 [[HomeObject]]를 가지지 않으므로 super를 참조할 수 없습니다.
*/
/*
4) 화살표 함수에서의 super
*/
class Parent {
greet() {
console.log("Hello from Parent");
}
}
class Child extends Parent {
greet() {
const arrowFunc = () => {
super.greet(); // 부모의 greet 메서드 호출
};
arrowFunc();
}
}
const child = new Child();
child.greet();
// 출력:
// "Hello from Parent"
/*
화살표 함수는 this와 super를 상위 스코프에서 상속받으므로, super.greet()를 사용할 수 있습니다.
*/
super
를 사용할 수 있는 함수
- 클래스 메서드: 클래스 내부에서 정의된 메서드는
[[HomeObject]]
를 가지므로super
를 사용할 수 있습니다. - 객체 리터럴의 축약 메서드: 객체의 축약 메서드는
[[HomeObject]]
를 가지므로super
를 참조할 수 있습니다. - 화살표 함수: 자체적으로
[[HomeObject]]
를 가지지 않지만, 상위 메서드에서super
를 상속받아 사용할 수 있습니다.
super
를 사용할 수 없는 함수
- 일반 함수:
function
키워드로 정의된 함수는[[HomeObject]]
를 가지지 않으므로super
를 참조할 수 없습니다.
4.5.3 수퍼클래스의 정적메서드도 사용가능
class Parent {
static staticMethod() {
console.log("Hello from Parent");
}
}
class Child extends Parent {}
// 서브클래스에서 슈퍼클래스의 정적 메서드 호출
Child.staticMethod(); // "Hello from Parent"
4.6 상속 클래스의 인스턴스 생성과정
추후 업데이트 예정
4.7 표준 빌트인 생성자 함수 확장
class MyArray extends Array {
uniq() {
return this.filter((v, i, self) => self.indexOf(v) === i);
}
average() {
return this.reduce((pre, cur) => pre + cur, 0) / this.length;
}
}
const myArray = new MyArray(1, 1, 2, 3);
console.log(myArray);
console.log(myArray.uniq());
console.log(myArray.average());
🧐 Q. MyArray 클래스에서 메서드(uniq
, average
)를 구현했을 때, Array
의 기본 동작 때문에 새롭게 반환되는 배열이 MyArray
가 아니라 Array
의 인스턴스로 반환될 수 있다. 이경우 메서드 체이닝이 불가능한데 어떻게 해야 할까?
🧐 Q. 먼저 이런일이 왜 발생되는 걸까?
Array
의 메서드(filter
, map
, slice
등)는 새로운 배열을 반환할 때, 기본적으로 Array
생성자를 사용하여 반환한다.
즉, 상속받은 MyArray
클래스에서 호출하더라도 새롭게 생성된 배열은 MyArray
가 아닌 Array
의 인스턴스다.
class MyArray extends Array {
uniq() {
return this.filter((v, i, self) => self.indexOf(v) === i); // 새로운 배열 반환
}
}
const myArray = new MyArray(1, 1, 2, 3);
const uniqArray = myArray.uniq(); // filter 호출
console.log(uniqArray instanceof MyArray); // false
console.log(uniqArray instanceof Array); // true
Answer)
Symbol.species
를 사용한 해결 방법
Array
와 같은 클래스는 Symbol.species
라는 특수한 속성을 가지고 있습니다.
Symbol.species
는 filter
, map
같은 메서드가 새롭게 배열을 반환할 때 사용할 생성자(constructor)를 지정합니다.
기본 동작:
Array
의 기본Symbol.species
는Array
생성자를 반환합니다.- 즉,
filter
같은 메서드는 항상 기본 배열(Array)을 반환합니다.
Symbol.species
를 커스터마이징:
MyArray
클래스에서 Symbol.species
를 MyArray
생성자를 반환하도록 설정하면,
filter
, map
같은 메서드가 새 배열을 반환할 때 MyArray
인스턴스를 반환하도록 동작을 변경할 수 있습니다.
class MyArray extends Array {
static get [Symbol.species]() {
return MyArray; // 반환 타입을 MyArray로 설정
}
uniq() {
return this.filter((v, i, self) => self.indexOf(v) === i); // MyArray 인스턴스를 반환
}
}
const myArray = new MyArray(1, 1, 2, 3);
const uniqArray = myArray.uniq(); // MyArray 인스턴스를 반환
console.log(uniqArray instanceof MyArray); // true
console.log(uniqArray instanceof Array); // true
const result = myArray
.uniq()
.map((x) => x * 2)
.average(); // 메서드 체이닝도 가능!
console.log(result); // (2 + 4 + 6) / 3 = 4
Symbol.species
를 사용하여 반환 타입을 지정함으로써, MyArray
메서드가 항상 MyArray
인스턴스를 반환하도록 설정함으로써 문제를 해결한다!
🧐 Q. Symbol.species
를 static
getter로 정의하는 이유는 무엇일까?
Answer)
Symbol.species`가 클래스 전체(즉, 클래스의 모든 인스턴스)에서 일관되게 동작해야 하기 때문이다.!