
Intro
지난 글에서는, 멀티 패러다임 언어로써의 자바스크립트가 추구하는 방식에 대해 개괄적으로 알아봤다.
흔히 말하는 C++과 자바 같은 객체 지향 프로그래밍 언어(OOP)는 클래스(Class) 와 인스턴스(Instance) 개념을 사용해 객체를 생성하고 상속하며, public, private, protected 등의 접근 제한자를 통해 캡슐화를 지원한다.
이러한 클래스 기반 언어와 달리 자바스크립트는 프로토타입 기반의 객체 지향 프로그래밍 언어이다. 비록 ES6에서 class가 도입되었지만, 이는 프로토타입 기반 OOP 패턴을 편리하게 사용할 수 있게 해주는 문법적 설탕(Syntactic Sugar) 이다.
이번글에서는 자바스크립트의 프로토타입(Prototype), 클래스(Class) 개념을 통해서 자바스크립트가 어떤 방식으로 객체지향 프로그래밍을 구현하는지 보다 자세하게 알아보자.
1. 객체 지향 프로그래밍 (Object-Oriented Programming)
객체 지향 프로그래밍은 명령형 프로그래밍에서 파생된 패러다임이다. 데이터와 그 데이터를 처리하는 동작을 하나의 객체로 묶어 관리한다.
예를 들어, 사람은 이름, 생년월일, 주소, 성별, 나이, 신장, 체중, 학력, 성격, 직업 등 다양한 속성(Attribute/property) 을 갖는다.
이 때, 이름이 정상영이고, 성별은 남성이며, 나이는 30세인 사람과 같이 속성을 구체적으로 표현하면 특정한 사람을 다른 사람과 구별하여 인식할 수 있다. 이러한 객체를 자바스크립트로 표현하면 다음과 같다.
const jeongsangyoung = { // 데이터 name: '정상영', gender: 'male', birth: 1996, // 데이터를 처리하는 동작 // 한국 나이 구하기 getAge() { return new Date().getFullYear() - this.birth + 1; }, };
이처럼 객체 지향 프로그래밍은 객체의 상태를 나타내는 데이터와 상태 데이터를 조작할 수 있는 동작을 하나의 논리 단위로 묶어 생각한다. 이때 객체의 상태 데이터를 프로퍼티(Property), 동작을 메서드(Method) 라 부른다.
각 객체는 자신의 고유한 기능을 수행하면서 다른 객체와 관계성을 가질 수 있다.
2. 상속과 프로토타입

각 객체는 고유의 기능을 갖는 독립적인 요소로 볼 수 있지만, 자신의 고유한 기능을 수행하면서 다른 객체와 관계성을 가질 수 있다.
상속(Inheritance) 은 객체 지향 프로그래밍의 핵심 개념으로, 어떤 객체의 프로퍼티 또는 메서드를 다른 객체가 그대로 사용할 수 있는 것을 말한다. 객체간의 상속을 통해 기존의 코드를 적극적으로 재사용하여, 불필요한 중복을 제거할 수 있다. 그리고 자바스크립트는 프로토타입을 기반으로 상속을 구현한다.
2.1 프로토타입(Prototype)
프로토타입이란 어떤 객체의 부모(상위) 역할을 하는 객체로서, 다른 객체에 프로퍼티와 메서드를 공유하는 역할을 한다. 프로토타입을 상속받은 자식(하위) 객체는 부모 객체의 프로퍼티와 메서드를 자신의 프로퍼티처럼 사용할 수 있다.
function Person(name) { this.name = name; } Person.prototype.sayHello = function () { console.log(`안녕하세요, ${this.name}입니다.`); }; const person1 = new Person('정상영'); person1.sayHello(); // "안녕하세요, 정상영입니다."
위의 코드처럼 Person.prototype에 메서드를 추가하면, 해당 생성자로 만든 모든 객체가 같은 메서드를 공유하게 된다. 이는 메모리 효율성 측면에서 큰 장점이다.
자바스크립트의 모든 객체는 [[Prototype]]이라는 내부 슬롯을 가지며, 내부 슬롯의 값은 부모 객체의 참조 즉, 프로토타입의 참조다. [[Prototype]]은 프로퍼티가 아니라 내부 슬롯이기 때문에 직접 접근할 수는 없지만, 자바스크립트의 모든 객체가 가지고 있는 __proto__ 접근자 프로퍼티를 통해 간접적으로 프로토타입에 접근할 수 있다.
2.2 프로토타입 체인(Prototype Chain)
만약 어떤 객체에서 특정 프로퍼티나 메서드를 호출하였으나 해당 객체에 존재하지 않는다면, 자바스크립트는 [[Prototype]]이 참조하고 있는 부모 객체를 찾아본다. 만약 부모 객체에도 해당 프로퍼티 혹은 메서드가 존재하지 않으면, 부모의 부모 순으로 탐색을 진행하다가 최상위 객체인 Object가 가지고 있는 Object.prototype에 도달한다. 이러한 연결관계를 프로토타입 체인이라 부른다.
const parent = { name: 'parent', sayHello() { console.log(`Hello, I'm ${this.name}`); }, }; const child = { name: 'child', }; // child의 프로토타입을 parent로 설정: Object.setPrototypeOf(child, parent); child.sayHello(); // child에는 sayHello 메서드가 없지만, // 프로토타입 체인을 따라 parent 객체에서 찾아 호출 // 출력: Hello, I'm child
위의 코드에서 child 객체에는 sayHello 메서드가 정의되어 있지 않지만, Object.setPrototypeOf(child, parent)로 child의 부모 객체(프로토타입)를 parent로 설정했기 때문에, 프로토타입 체인을 통해 parent 객체의 sayHello 메서드를 찾아 사용할 수 있다. 만약 프로토타입 체인 상에서 프로퍼티나 메서드를 끝내 찾지 못한다면 undefined를 반환한다. 당연히 프로토타입 체인이 길어질수록 검색 시간은 길어진다.
2.3 Object와 Object.prototype
내가 헷갈렸던 점은, 자바스크립트의 내장 생성자 함수인 Object 자체(즉, Object라는 함수 객체)와 그 생성자로부터 만들어지는 객체가 상속받는 Object.prototype이 서로 다르다는 것이었다.
Object는 함수이므로 Object.__proto__는 Function.prototype을 참조한다.
console.log(typeof Object); // "function" console.log(Object.__proto__ === Function.prototype); // true
모든 객체가 궁극적으로 상속받는 프로토타입 객체는 바로 Object.prototype이며, 여기서는 더 이상 상속할 프로토타입이 없기 때문에 Object.prototype.__proto__가 null이 된다.
console.log(Object.prototype.__proto__); // null
즉, 프로토타입 체인의 진짜 끝은 Object.prototype.__proto__이다.
2.4 __proto__와 prototype 프로퍼티
객체가 가지고 있는 Prototype에 접근하기 위해선 두 가지 방법이 있다.
__proto__접근자 프로퍼티: 인스턴스가 참조하는 프로토타입 객체에 접근하는 getter/setterprototype프로퍼티: 생성자 함수(또는 클래스) 의 인스턴스가 상속받게 될 속성과 메서드를 담고 있는 객체
정리하면 다음과 같다.
- 인스턴스 ->
__proto__-> 부모 객체의 프로토타입 - 생성자 함수(또는 클래스) ->
prototype-> 인스턴스가 상속받을 프로퍼티와 메서드가 보관된 객체
// 생성자 함수 function Person(name) { this.name = name; } const jeongsangyoung = new Person('정상영'); // Person.prototype과 jeongsangyoung.__proto__는 결국 동일한 프로토타입을 가리킨다. console.log(Person.prototype === jeongsangyoung.__proto__); // true
2.5 constructor
constructor 프로퍼티는 객체의 프로토타입 안에 기본적으로 존재하며, 해당 객체를 생성한 생성자 함수(또는 클래스)를 가리키는 프로퍼티이다.
function Person(name) { this.name = name; } console.log(Person.prototype.constructor === Person); // true
예를들어, Person.prototype 내부에는 constructor 프로퍼티가 있으며, 그 값은 Person 함수 자신을 가리킨다. 이 덕분에 어떤 인스턴스가 만들어졌을 때, 그 인스턴스의 __proto__가 참조하는 constructor를 통해 어떤 생성자(혹은 클래스)로부터 만들어졌는지 확인할 수 있다.
하지만, 자바스크립트는 프로토타입 객체를 임의로 재할당하는 것이 가능하기 때문에 절대적이지는 않다.
3. class 키워드의 도입과 문법적 설탕

글의 초반부에서도 설명했지만 자바스크립트는 프로토타입 기반의 객체 지향 프로그래밍 언어이다. 따라서 ES6에서 도입된 class 키워드는, 프로토타입 패턴을 보다 편하게 사용할 수 있도록 제공한 문법적 설탕이다. 내부 동작은 기존의 방식과 동일하게 프로토타입 체인을 통해 상속을 구현하며, 개발자가 클래스 기반 언어에서 사용하던 익숙한 문법을 사용할 수 있게 해줄 뿐이다.
3.1 클래스 정의
클래스를 정의하는 방법은 두 가지가 있다. 클래스 선언문과 클래스 표현식이다.
// 클래스 선언문 class Person { // 클래스 필드 (퍼블릭) name = '기본 이름'; // 정적(클래스) 필드 static species = '호모 사피엔스'; // private 필드 (# 사용) - 최신 환경이나 바벨 등에서만 동작 #privateField = '비공개 필드'; constructor(name) { // 실제론 this.name을 setter로 연결할 수도 있음 this.name = name; } // 프로토타입 메서드 sayHello() { console.log(`안녕하세요, ${this.name}입니다.`); } // getter/setter 예시 // 외부에서는 personInstance.name 을 통해 접근할 때 // 아래 getter/setter가 작동 (내부적으로 _name 에 연결) get name() { return this._name; } set name(value) { // 이 예시에서는 단순히 이름이 비어있으면 기본값을 넣어주는 식 if (!value) { this._name = '이름 없음'; } else { this._name = value; } } // 정적 메서드 static createAnonymous() { return new Person('Anonymous'); } // private 필드 접근 메서드 showPrivateField() { console.log(this.#privateField); // 클래스 내부에서만 접근 가능 } } // 클래스 표현식 const Animal = class { constructor(type) { this.type = type; } speak() { console.log(`${this.type}가(이) 소리를 냅니다.`); } }; // 인스턴스 생성 const dog = new Animal('강아지'); dog.speak(); // "강아지가(이) 소리를 냅니다."
3.2 클래스의 주요 특징
-
호이스팅
클래스는 함수로 평가된다. 따라서 클래스 선언문도 함수 선언문처럼 호이스팅이 발생하지만, 클래스 선언문은
let,const등과 같은 방식으로 호이스팅 과정에서 초기화되지 않는다는 점이 다르다. 즉, TDZ(Temporal Dead Zone) 가 존재하기 때문에, 클래스 선언문 이전에 클래스를 참조하면ReferenceError에러가 발생한다. -
엄격 모드(strict mode)
클래스는 기본적으로 엄격 모드를 사용한다.
'use strict'를 명시적으로 선언하지 않아도, 클래스 내에서 암묵적 전역 변수가 허용되지 않는 등, 보다 엄격한 문법 규칙이 적용된다. -
메서드 정의
클래스 내부에서 메서드는 축약 메서드 문법을 사용하여 정의한다. 함수 키워드를 사용하지 않으며, 쉼표도 붙이지 않는다.
3.3 상속과 extends, super
ES6의 클래스 문법을 통해서 상속을 손쉽게 구현할 수 있다. class에서는 extends와 super 키워드를 사용한다.
class Animal { constructor(name) { this.name = name; } move() { console.log(`${this.name}가 움직입니다.`); } sound() { console.log(`${this.name}가 소리를 냅니다.`); } } class Dog extends Animal { constructor(name, breed) { // super()로 부모 클래스(Animal)의 constructor 호출 super(name); this.breed = breed; } // 오버라이드(override): 부모의 메서드를 재정의할 수 있음 move() { console.log(`${this.name} (${this.breed})가 네 발로 달립니다.`); } bark() { console.log(`${this.name}가 멍멍 짖습니다.`); } // 부모의 sound() 메서드도 호출이 필요하다면 super를 이용 sound() { console.log(`[Dog version]`); super.sound(); // 부모(Animal)의 sound() 호출 } } const dog = new Dog('멍멍이', '시바견'); dog.move(); // "멍멍이 (시바견)가 네 발로 달립니다." dog.bark(); // "멍멍이가 멍멍 짖습니다." dog.sound(); // "[Dog version]" / "멍멍이가 소리를 냅니다."
extends: 상속할 부모 클래스를 지정하는 키워드super: 부모 클래스의 생성자(constructor) 를 호출하거나, 부모의 메서드와 프로퍼티에 접근
3.4 클래스와 프로토타입
클래스 문법을 사용해도 결국 내부적으로는 프로토타입을 통해 상속이 이뤄진다. 클래스 문법으로 생성한 객체의 내부 [[Prototype]]은 여전히 해당 클래스의 prototype 객체를 참조한다. 결과적으로 class는 클래스 기반 객체 지향 프로그래밍 언어처럼 코드를 작성할 수 있게 해주지만, 자바스크립트가 프로토타입을 기반으로 동작한다는 사실은 바뀌지 않는다.
class Person { constructor(name) { this.name = name; } sayHello() { console.log(`안녕하세요, ${this.name}입니다.`); } } const jeongsangyoung = new Person('상영'); // 사실상 아래와 동일하게 동작하는 코드다 function PersonNotClass(name) { this.name = name; } PersonNotClass.prototype.sayHello = function () { console.log(`안녕하세요, ${this.name}입니다.`); }; const jeongsangyoung2 = new PersonNotClass('상영');
마무리
자바스크립트의 객체 지향 프로그래밍은 프로토타입을 기반으로 하며, 전통적인 클래스 기반 언어와 달리 런타임에 객체의 프로퍼티나 메서드를 동적으로 추가/삭제/변경할 수 있는 큰 유연성을 지닌다. 또한 ES6에서 도입된 클래스 문법은 이를 보다 직관적으로 사용할 수 있게 해준다. 각각의 방식은 다음과 같은 특징이 있다.
프로토타입 기반 접근
- 장점: 동적인 객체 확장이 용이하며, 메모리 효율적
- 단점: 상속 구조가 복잡해질 수 있고, 가독성이 떨어질 수 있음
클래스 문법
- 장점: 직관적인 코드 작성, 더 나은 가독성, 현대적인 기능(private 필드 등) 지원
- 단점: 프로토타입 동작 방식을 이해하지 못하면 디버깅이 어려울 수 있음
실제 개발에서는 상황에 따라 적절한 방식을 선택하되, 실무에서는 대체로 클래스 문법을 사용하는 것이 권장된다. 다만, 자바스크립트가 동작하는 핵심 개념이 프로토타입에 있다는 점, 그리고 Object.__proto__ === Function.prototype 그리고 Object.prototype.__proto__ === null과 같은 프로토타입 구조를 이해하면 더 깊은 수준에서 언어의 작동 원리를 파악할 수 있을 것이다.