4강. 프로토타입(Prototype)과 객체 모델
by
gg582 · 2026-10-19 10:46:12 · 45 views · 7 min read
Table of contents
- 4강. 프로토타입(Prototype)과 객체 모델
- 들어가기 전에
- 객체는 어떻게 생성되는가
- 프로토타입(Prototype)이란 무엇인가
- [[Prototype]]과 prototype 프로퍼티
- 프로토타입 체인(Prototype Chain)
- class 문법의 정체
- 상속(Inheritance)의 내부 동작
- this가 결정되는 방식
- this 바인딩의 4가지 규칙
- 1. 기본 바인딩 (일반 함수 호출)
- 2. 암시적 바인딩 (메서드 호출)
- 3. 명시적 바인딩 (call, apply, bind)
- 4. new 바인딩 (생성자 호출)
- 예외: 화살표 함수(Arrow Function)의 this
- 실행 흐름 전체 정리
- 이번 강의 요약
- 다음 강의
4강. 프로토타입(Prototype)과 객체 모델
들어가기 전에
JavaScript를 배우다 보면 다음과 같은 코드를 자주 보게 됩니다.
const user = {
name: "Alice"
};
console.log(user.toString());
우리는 user 객체에 name만 정의했습니다.
toString이라는 메서드는 만든 적이 없습니다.
그런데 에러가 나지 않고 다음과 같이 출력됩니다.
[object Object]
이번에는 배열을 보겠습니다.
const arr = [1, 2, 3];
arr.push(4);
우리는 배열에 숫자만 넣었을 뿐, push 함수를 정의하지 않았습니다.
도대체 toString과 push는 어디서 온 것일까요?
그리고 다른 언어에서 흔히 쓰는 class 문법은 JavaScript에서 어떻게 동작할까요?
이번 강의에서는 JavaScript 객체 모델의 핵심인 프로토타입(Prototype)과 this 바인딩을 살펴봅니다.
객체는 어떻게 생성되는가
JavaScript에서 객체를 만드는 가장 기본적인 방법은 두 가지입니다.
첫 번째는 객체 리터럴입니다.
const user = {
name: "Alice",
greet() {
console.log("Hello, " + this.name);
}
};
두 번째는 생성자 함수(Constructor Function)입니다.
비슷한 구조의 객체를 여러 개 만들어야 할 때 사용합니다.
function User(name) {
this.name = name;
this.greet = function() {
console.log("Hello, " + this.name);
};
}
const user1 = new User("Alice");
const user2 = new User("Bob");
하지만 이 방식에는 큰 문제가 있습니다.
console.log(user1.greet === user2.greet);
출력은
false
입니다.
객체를 1,000개 만들면 동일한 기능을 하는 greet 함수도 메모리에 1,000개 생성됩니다.
메모리 낭비입니다.
이 문제를 해결하기 위해 등장하는 개념이 프로토타입입니다.
프로토타입(Prototype)이란 무엇인가
JavaScript는 클래스 기반 언어가 아니라 프로토타입 기반 언어입니다.
전통적인 클래스 기반 언어(Java, C++ 등)는 설계도(Class)를 바탕으로 객체를 찍어냅니다(복사).
반면 JavaScript는 이미 존재하는 다른 객체를 연결(참조)하여 기능을 공유합니다.
이때 공유되는 원본 객체를 프로토타입(Prototype)이라고 부릅니다.
아까의 코드를 프로토타입을 이용해 수정해 보겠습니다.
function User(name) {
this.name = name;
}
User.prototype.greet = function() {
console.log("Hello, " + this.name);
};
const user1 = new User("Alice");
const user2 = new User("Bob");
이제 다시 비교해 보면
console.log(user1.greet === user2.greet);
출력은
true
입니다.
메모리 구조는 다음과 같습니다.
┌───────────────────┐ ┌───────────────────┐
│ user1 │ │ user2 │
│ name: "Alice" │ │ name: "Bob" │
└─────────┬─────────┘ └─────────┬─────────┘
│ │
└─────────────┬─────────────┘
↓
┌───────────────────────────┐
│ User.prototype │
│ greet: function() {...} │
└───────────────────────────┘
user1과 user2는 greet 함수를 직접 가지고 있지 않습니다.
대신 User.prototype에 있는 하나의 greet 함수를 공유합니다.
[[Prototype]]과 prototype 프로퍼티
여기서 많은 입문자가 헷갈려 하는 두 가지 개념을 구분해야 합니다.
[[Prototype]](또는__proto__)
- 모든 객체가 가지는 내부 슬롯입니다.
- 자신의 부모 역할을 하는 프로토타입 객체를 가리키는 실제 연결 고리입니다.
prototype프로퍼티
- 오직 함수(생성자)만 가지는 프로퍼티입니다.
new키워드로 객체를 생성할 때, 생성될 객체의[[Prototype]]으로 설정할 대상을 가리킵니다.
const user1 = new User("Alice");
이 코드가 실행되면 다음 연결이 만들어집니다.
user1.[[Prototype]] ───► User.prototype
코드로 확인해 보면
console.log(Object.getPrototypeOf(user1) === User.prototype);
출력은
true
입니다.
프로토타입 체인(Prototype Chain)
그렇다면 user1.greet()를 호출할 때 엔진 내부에서는 무슨 일이 일어날까요?
실행 순서는 다음과 같습니다.
user1.greet() 호출
↓
1. user1 객체 내부에 greet가 있는가? ──► 없음
↓
2. [[Prototype]]을 따라 이동 (User.prototype)
↓
3. User.prototype 내부에 greet가 있는가? ──► 있음
↓
4. 실행
만약 User.prototype에도 없다면 어떻게 될까요?
처음에 보았던 user1.toString()의 탐색 과정을 보겠습니다.
user1
↓ (없음)
User.prototype
↓ (없음)
Object.prototype ──► toString 발견 및 실행
↓ (만약 여기도 없다면)
null (탐색 종료, undefined 반환)
이처럼 객체들이 [[Prototype]]으로 연쇄적으로 연결된 구조를 프로토타입 체인(Prototype Chain)이라고 합니다.
모든 객체의 프로토타입 체인 최상단에는 Object.prototype이 있으며, 그 위는 null입니다.
class 문법의 정체
ES6부터 JavaScript에도 class 키워드가 도입되었습니다.
class User {
constructor(name) {
this.name = name;
}
greet() {
console.log("Hello, " + this.name);
}
}
const user = new User("Alice");
그렇다면 JavaScript가 클래스 기반 언어로 바뀐 것일까요?
아닙니다.
User의 타입을 확인해 보겠습니다.
console.log(typeof User);
출력은
function
입니다.
class 문법은 완전히 새로운 객체 모델이 아니라, 기존 프로토타입 기반 코드를 보기 쉽게 작성하도록 돕는 문법적 설탕(Syntactic Sugar)입니다.
내부적으로는 앞서 작성했던 생성자 함수 및 프로토타입 연결과 똑같이 동작합니다.
class User { ... }
↓ (내부 동작)
function User() { ... }
User.prototype.greet = ...
상속(Inheritance)의 내부 동작
extends를 사용한 상속도 마찬가지입니다.
class Animal {
eat() {
console.log("Eating");
}
}
class Dog extends Animal {
bark() {
console.log("Woof");
}
}
const dog = new Dog();
dog.eat();
이 코드는 클래스의 기능을 복사해 오는 것이 아닙니다.
프로토타입 체인을 한 단계 더 연결하는 것입니다.
dog
↓ [[Prototype]]
Dog.prototype (bark)
↓ [[Prototype]]
Animal.prototype (eat)
↓ [[Prototype]]
Object.prototype (toString)
↓ [[Prototype]]
null
dog.eat()를 실행하면 엔진은 dog → Dog.prototype → Animal.prototype 순서로 올라가며 eat 메서드를 찾아 실행합니다.
즉, JavaScript의 상속은 복사(Copy)가 아니라 위임(Delegation)입니다.
this가 결정되는 방식
객체와 프로토타입을 다룰 때 빠질 수 없는 것이 this입니다.
많은 개발자가 this를 "메서드가 정의된 객체"라고 오해합니다.
다음 코드를 보겠습니다.
const user = {
name: "Alice",
greet() {
console.log(this.name);
}
};
user.greet();
const fn = user.greet;
fn();
출력은
Alice
undefined
입니다.
같은 함수인데 왜 결과가 다를까요?
JavaScript에서 일반 함수의 this는 함수가 선언될 때가 아니라, 호출될 때(런타임) 결정되기 때문입니다.
누가 그 함수를 호출했는지가 핵심입니다.
this 바인딩의 4가지 규칙
this는 호출 방식에 따라 다음 4가지로 결정됩니다.
1. 기본 바인딩 (일반 함수 호출)
function foo() {
console.log(this);
}
foo();
누구도 점(.)을 찍어 호출하지 않았습니다.
이 경우 this는 전역 객체(window 또는 global)가 됩니다. 단, strict mode에서는 undefined가 됩니다.
2. 암시적 바인딩 (메서드 호출)
const user = {
name: "Alice",
greet() {
console.log(this.name);
}
};
user.greet();
점(.) 앞의 객체가 this가 됩니다.
여기서는 user가 점 앞에 있으므로 this는 user입니다.
아까 const fn = user.greet; fn();에서 undefined가 나온 이유는, 호출 시점에 점(.) 없이 일반 함수로 호출되어 기본 바인딩이 적용되었기 때문입니다.
3. 명시적 바인딩 (call, apply, bind)
개발자가 직접 this를 지정하는 방식입니다.
function greet() {
console.log(this.name);
}
const user = { name: "Alice" };
greet.call(user);
출력은
Alice
입니다. call, apply, bind를 사용하면 강제로 this를 특정 객체에 고정할 수 있습니다.
4. new 바인딩 (생성자 호출)
function User(name) {
this.name = name;
}
const user = new User("Alice");
new 키워드로 함수를 호출하면 내부적으로 다음 과정이 일어납니다.
1. 빈 객체 {} 생성
↓
2. 빈 객체의 [[Prototype]]을 User.prototype에 연결
↓
3. 생성된 빈 객체를 this로 바인딩
↓
4. 함수 내부 코드 실행 (this.name = "Alice")
↓
5. 생성된 객체 반환
따라서 이때의 this는 새로 생성되는 인스턴스 객체 자신이 됩니다.
예외: 화살표 함수(Arrow Function)의 this
화살표 함수는 위 4가지 규칙을 따르지 않습니다.
const user = {
name: "Alice",
waitAndGreet() {
setTimeout(() => {
console.log(this.name);
}, 1000);
}
};
user.waitAndGreet();
출력은
Alice
입니다.
만약 setTimeout에 일반 함수를 넣었다면 this는 전역 객체가 되어 undefined가 출력되었을 것입니다.
화살표 함수는 자체적인 this를 아예 가지지 않습니다.
대신 자신이 선언된 시점의 상위 스코프(Lexical Scope)의 this를 그대로 참조합니다.
여기서 화살표 함수의 상위 스코프는 waitAndGreet 메서드이고, 이 메서드의 this는 user이므로 Alice가 정상적으로 출력됩니다.
실행 흐름 전체 정리
다음 코드가 실행될 때 프로토타입과 this가 어떻게 함께 동작하는지 정리해 보겠습니다.
class User {
constructor(name) {
this.name = name;
}
greet() {
console.log(this.name);
}
}
const user = new User("Alice");
user.greet();
전체 동작 과정은 다음과 같습니다.
1. new User("Alice") 실행
→ 새 객체(user) 생성, user.name = "Alice" 저장
→ user.[[Prototype]]이 User.prototype을 가리킴
↓
2. user.greet() 호출
↓
3. 프로토타입 체인 탐색
→ user에 greet 없음
→ User.prototype에서 greet 발견
↓
4. this 결정 (암시적 바인딩)
→ 호출 주체인 점(.) 앞의 객체가 user이므로 this = user
↓
5. console.log(this.name) 실행
→ user.name인 "Alice" 출력
메서드는 User.prototype에 있지만, 호출을 user.greet()로 했기 때문에 내부의 this는 User.prototype이 아니라 user를 가리키게 됩니다.
이번 강의 요약
JavaScript는 클래스가 아닌 프로토타입 기반 객체 모델을 사용합니다.
모든 객체는 내부 슬롯 [[Prototype]]을 통해 부모 역할을 하는 객체와 연결되며, 이를 따라 프로퍼티와 메서드를 탐색하는 구조를 프로토타입 체인이라고 합니다.
class와 extends 문법은 새로운 상속 모델이 아니라 프로토타입 체인을 다루기 쉽게 만든 문법적 설탕입니다.
일반 함수의 this는 함수가 정의된 위치가 아니라 어떻게 호출되었는가에 따라 동적으로 결정됩니다.
반면 화살표 함수는 고유한 this를 갖지 않으며, 선언될 당시의 상위 스코프 this를 그대로 사용합니다.
다음 강의
다음 강의에서는 JavaScript의 스코프와 클로저를 살펴봅니다.
- 실행 컨텍스트(Execution Context)
- 렉시컬 스코프(Lexical Scope)
- 호이스팅(Hoisting)과 TDZ(Temporal Dead Zone)
- 클로저(Closure)의 원리와 메모리 구조
- 클로저의 실전 활용과 메모리 누수 주의점
이 강의를 통해 변수가 메모리에 어떻게 저장되고 탐색되는지, 그리고 함수가 선언된 환경을 기억하는 원리를 이해하게 됩니다.