🔧 1. Prototype의 기초 개념
1-1. 모든 객체는 prototype을 가진다
// 기본 객체도 prototype을 가짐
const obj = { name: "김철수" };
console.log(obj.__proto__ === Object.prototype); // true
// 배열도 prototype을 가짐
const arr = [1, 2, 3];
console.log(arr.__proto__ === Array.prototype); // true
// 함수도 prototype을 가짐
function hello() {}
console.log(hello.__proto__ === Function.prototype); // true
/*
1. Prototype 객체는 하나만 존재
모든 인스턴스가 같은 prototype 객체를 참조
메소드들도 하나만 존재
2. 하지만 실행 시 this가 다름
3. 그래서 같은 메소드로 다른 결과
메소드는 공유하지만
각 인스턴스의 데이터(this.name, this.count 등)는 독립적
*/
1-2. Prototype Chain의 원리
const person = { name: "김철수" };
// person 객체에서 toString()을 찾는 과정:
// 1. person 객체 자체에서 찾기 → 없음
// 2. person.__proto__ (Object.prototype)에서 찾기 → 있음!
person.toString(); // "[object Object]"
// 이것이 prototype chain
// person → Object.prototype → null
1-3. 생성자 함수와 prototype
// 생성자 함수
function Person(name) {
this.name = name;
}
// prototype에 메소드 추가
Person.prototype.greet = function() {
return `Hello, I'm ${this.name}`;
};
Person.prototype.age = 0; // 기본값 설정
// 인스턴스 생성
const kim = new Person("김철수");
const lee = new Person("이영희");
// 메소드 사용 (prototype에서 상속받음)
console.log(kim.greet()); // "Hello, I'm 김철수"
console.log(lee.greet()); // "Hello, I'm 이영희"
// prototype은 모든 인스턴스가 공유
console.log(kim.__proto__ === Person.prototype); // true
console.log(lee.__proto__ === Person.prototype); // true
console.log(kim.__proto__ === lee.__proto__); // true
1-4. new 키워드의 마법
function Person(name) {
this.name = name;
this.energy = 100;
}
// new Person("김철수")가 하는 일:
// 1. 빈 객체 생성: {}
// 2. 그 객체의 __proto__를 Person.prototype으로 설정
// 3. Person 함수를 그 객체 컨텍스트로 실행 (this = 새 객체)
// 4. 완성된 객체 반환
const kim = new Person("김철수");
// kim = { name: "김철수", energy: 100, __proto__: Person.prototype }
🎯 2. Class 문법 (ES6)
2-1. Class의 기본 구조
class Person {
// constructor: 인스턴스 생성 시 호출
constructor(name, age) {
this.name = name;
this.age = age;
this.energy = 100;
}
// 메소드들 (자동으로 prototype에 추가됨)
greet() {
return `Hello, I'm ${this.name}`;
}
sleep() {
this.energy = 100;
return `${this.name} is sleeping`;
}
// 정적 메소드 (클래스 자체에 속함)
static getSpecies() {
return "Homo sapiens";
}
}
// 사용법
const kim = new Person("김철수", 25);
console.log(kim.greet()); // "Hello, I'm 김철수"
console.log(Person.getSpecies()); // "Homo sapiens"
2-2. Class vs Function 생성자 비교
// Function 방식 (ES5)
function PersonFunc(name) {
this.name = name;
}
PersonFunc.prototype.greet = function() {
return `Hello, I'm ${this.name}`;
};
// Class 방식 (ES6) - 내부적으로는 같음
class PersonClass {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, I'm ${this.name}`;
}
}
// 결과는 동일
const func = new PersonFunc("김철수");
const clss = new PersonClass("이영희");
console.log(func.__proto__ === PersonFunc.prototype); // true
console.log(clss.__proto__ === PersonClass.prototype); // true
2-3. Class의 특별한 기능들
class Person {
constructor(name) {
this.name = name;
this._age = 0; // private 관례 (실제로는 public)
}
// Getter
get age() {
return this._age;
}
// Setter
set age(value) {
if (value < 0) {
throw new Error("나이는 음수가 될 수 없습니다");
}
this._age = value;
}
// 정적 메소드
static compare(person1, person2) {
return person1.age - person2.age;
}
}
const kim = new Person("김철수");
kim.age = 25; // setter 호출
console.log(kim.age); // getter 호출, 25
// 정적 메소드 사용
const lee = new Person("이영희");
lee.age = 30;
console.log(Person.compare(kim, lee)); // -5
/*
정적 메소드:
클래스 자체에 속함 (인스턴스에 속하지 않음)
클래스명.메소드명() 으로 호출
this는 클래스 자체를 가리킴
인스턴스 데이터에 접근 불가
유틸리티, 팩토리, 검증 함수 등에 적합
인스턴스 메소드:
각 인스턴스에 속함
인스턴스.메소드명() 으로 호출
this는 해당 인스턴스를 가리킴
인스턴스 데이터에 접근 가능
쉽게 기억하기: "인스턴스 없이도 쓸 수 있는 기능 = 정적 메소드"
*/
🔗 3. Prototype 방식 상속
3-1. 단계별 상속 구현
// 1단계: 부모 생성자
function Animal(name, habitat) {
this.name = name;
this.habitat = habitat;
this.energy = 100;
}
Animal.prototype.eat = function(food) {
this.energy += 20;
return `${this.name}가 ${food}를 먹었습니다`;
};
Animal.prototype.sleep = function() {
this.energy = 100;
return `${this.name}가 잠을 잡니다`;
};
// 2단계: 자식 생성자
function Dog(name, habitat, breed) {
// 부모 생성자 호출 (중요!)
Animal.call(this, name, habitat);
this.breed = breed;
this.loyalty = 100;
}
// 3단계: prototype chain 연결 (핵심!)
Dog.prototype = Object.create(Animal.prototype);
// 4단계: constructor 복구 (중요!)
Dog.prototype.constructor = Dog;
// 5단계: 자식만의 메소드 추가
Dog.prototype.bark = function() {
this.energy -= 10;
return `${this.name}가 멍멍!`;
};
Dog.prototype.wagTail = function() {
this.loyalty += 5;
return `${this.name}가 꼬리를 흔듭니다`;
};
// 6단계: 부모 메소드 오버라이드
Dog.prototype.eat = function(food) {
// 부모 메소드 호출
const result = Animal.prototype.eat.call(this, food);
// 자식만의 추가 로직
if (food === "뼈다귀") {
this.energy += 10;
return result + " (뼈다귀 보너스!)";
}
return result;
};
3-2. 복잡한 상속 구조
// 3세대 상속: Animal → Dog → GermanShepherd
function GermanShepherd(name, habitat) {
Dog.call(this, name, habitat, "German Shepherd");
this.trainedSkills = [];
}
GermanShepherd.prototype = Object.create(Dog.prototype);
GermanShepherd.prototype.constructor = GermanShepherd;
GermanShepherd.prototype.guard = function() {
this.energy -= 15;
return `${this.name}가 경비를 섭니다`;
};
GermanShepherd.prototype.learn = function(skill) {
this.trainedSkills.push(skill);
return `${this.name}가 ${skill}을 배웠습니다`;
};
// 사용법
const rex = new GermanShepherd("렉스", "집");
console.log(rex.eat("뼈다귀")); // Animal + Dog 로직
console.log(rex.bark()); // Dog 로직
console.log(rex.guard()); // GermanShepherd 로직
console.log(rex instanceof GermanShepherd); // true
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
🎯 4. Class 방식 상속
4-1. extends와 super 사용법
// 부모 클래스
class Animal {
constructor(name, habitat) {
this.name = name;
this.habitat = habitat;
this.energy = 100;
}
eat(food) {
this.energy += 20;
return `${this.name}가 ${food}를 먹었습니다`;
}
sleep() {
this.energy = 100;
return `${this.name}가 잠을 잡니다`;
}
static getKingdom() {
return "Animalia";
}
}
// 자식 클래스
class Dog extends Animal {
constructor(name, habitat, breed) {
super(name, habitat); // 부모 constructor 호출
this.breed = breed;
this.loyalty = 100;
}
bark() {
this.energy -= 10;
return `${this.name}가 멍멍!`;
}
wagTail() {
this.loyalty += 5;
return `${this.name}가 꼬리를 흔듭니다`;
}
// 부모 메소드 오버라이드
eat(food) {
const result = super.eat(food); // 부모 메소드 호출
if (food === "뼈다귀") {
this.energy += 10;
return result + " (뼈다귀 보너스!)";
}
return result;
}
// 정적 메소드도 상속 가능
static getSpecies() {
return "Canis lupus";
}
}
4-2. 깊은 상속 구조
class GermanShepherd extends Dog {
constructor(name, habitat) {
super(name, habitat, "German Shepherd");
this.trainedSkills = [];
}
guard() {
this.energy -= 15;
return `${this.name}가 경비를 섭니다`;
}
learn(skill) {
this.trainedSkills.push(skill);
return `${this.name}가 ${skill}을 배웠습니다`;
}
// 조부모 메소드까지 접근
eat(food) {
if (food === "전용사료") {
// 할아버지(Animal) 메소드 직접 호출은 불가능
// 하지만 부모(Dog) 메소드를 통해 간접 접근
const result = super.eat(food);
this.trainedSkills.forEach(skill => {
this.energy += 5; // 훈련된 스킬당 보너스
});
return result + ` (${this.trainedSkills.length}개 스킬 보너스!)`;
}
return super.eat(food);
}
}
🔍 5. 두 방식의 상세 비교
5-1. 문법적 차이점
// ========== Prototype 방식 ==========
function Animal(name) {
this.name = name;
}
function Dog(name, breed) {
Animal.call(this, name); // 부모 생성자 호출
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype); // 상속 설정
Dog.prototype.constructor = Dog; // constructor 복구
Dog.prototype.bark = function() { // 메소드 추가
return `${this.name} barks`;
};
Dog.prototype.speak = function() { // 오버라이드
return Animal.prototype.speak.call(this) + " woof!";
};
// ========== Class 방식 ==========
class Animal {
constructor(name) {
this.name = name;
}
}
class Dog extends Animal { // extends로 상속
constructor(name, breed) {
super(name); // super로 부모 호출
this.breed = breed;
}
bark() { // 메소드 추가
return `${this.name} barks`;
}
speak() { // 오버라이드
return super.speak() + " woof!";
}
}
5-2. 실행 시점과 동작
// 두 방식 모두 동일한 prototype chain 생성
const prototypeDog = new PrototypeDog("바둑이", "진돗개");
const classDog = new ClassDog("멍멍이", "골든리트리버");
// 내부 구조 동일
console.log(prototypeDog.__proto__ === PrototypeDog.prototype); // true
console.log(classDog.__proto__ === ClassDog.prototype); // true
// instanceof 결과도 동일
console.log(prototypeDog instanceof PrototypeDog); // true
console.log(prototypeDog instanceof Animal); // true
console.log(classDog instanceof ClassDog); // true
console.log(classDog instanceof Animal); // true
5-3. 장단점 비교
Prototype 방식:
- ✅ 더 명시적, 내부 동작 이해하기 좋음
- ✅ 메모리 사용량 조금 더 효율적
- ✅ 동적으로 prototype 수정 가능
- ❌ 문법이 복잡하고 실수하기 쉬움
- ❌ constructor 복구 등 보일러플레이트 많음
Class 방식:
- ✅ 직관적이고 다른 언어와 유사
- ✅ 실수할 여지가 적음
- ✅ super 키워드로 부모 접근 쉬움
- ✅ 정적 메소드, getter/setter 등 추가 기능
- ❌ 내부 동작이 숨겨져 있음
- ❌ hoisting이 되지 않음
🎯 6. 실무에서의 사용법과 권장사항
6-1. 현대적 사용 패턴
// 현대적 Class 사용법
class ApiClient {
constructor(baseUrl, apiKey) {
this.baseUrl = baseUrl;
this.apiKey = apiKey;
}
async get(endpoint) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
headers: { 'Authorization': `Bearer ${this.apiKey}` }
});
return response.json();
}
}
class UserApiClient extends ApiClient {
constructor(baseUrl, apiKey) {
super(baseUrl, apiKey);
}
async getUser(id) {
return this.get(`/users/${id}`);
}
async updateUser(id, data) {
const response = await fetch(`${this.baseUrl}/users/${id}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
return response.json();
}
}
6-2. 권장사항과 베스트 프랙티스
✅ 추천:
- 새로운 프로젝트에서는 Class 문법 사용
- 상속보다는 Composition 패턴 고려
- private 필드 사용 (최신 브라우저)
class BankAccount {
#balance = 0; // private 필드
constructor(owner) {
this.owner = owner;
}
deposit(amount) {
this.#balance += amount;
return this.#balance;
}
getBalance() {
return this.#balance;
}
}
❌ 주의사항:
- 너무 깊은 상속 구조는 피하기
- Prototype 직접 수정은 신중하게
- Class와 Function 생성자 혼용 피하기
🔚 마무리
JavaScript의 상속은 Prototype 기반이지만, Class 문법으로 더 쉽게 사용할 수 있다.
핵심 이해:
- 모든 것은 Prototype Chain – Class도 내부적으로는 Prototype 사용
- 상속 = Prototype Chain 연결 – 부모의 속성과 메소드에 접근 가능
- 오버라이드 = 자식에서 재정의 – super로 부모 기능 활용 가능
- new 키워드가 모든 마법을 만듦 – 객체 생성과 prototype 연결
실무에서는 Class 문법을 사용하되, Prototype의 원리를 이해하고 있으면 JavaScript를 더 깊이 있게 활용할 수 있다.
Leave a Reply