객체는 흔히 우리가 너무 많이 쓰지만 프로토타입은 경험할 기회가 흔치 않아요. 저는 항상 무언가를 배우게 되면 “그래서 언제 쓰는데?”라는 생각이 들기 때문에 이 블로그에도 그런 생각을 담아 작성해보았습니다. 혹시 사용하는 상황이라도 기억해둔다면 “아, 이런 상황에서 프로토타입을 쓰던데?” 하며 다시 돌아와 개념을 다지고 사용할 수 있지 않을까요?
1부. 객체 리터럴 — 객체를 만드는 가장 쉬운 방법
객체가 뭔지부터
자바스크립트에서 원시값(숫자, 문자열, 불리언 등)을 제외한 거의 모든 값은 객체입니다. 함수도, 배열도, 정규표현식도 객체예요.
객체는 키(key)와 값(value)으로 이루어진 프로퍼티의 집합입니다. 그 중에서 값이 함수인 프로퍼티는 특별히 메서드라고 부릅니다.
const person = {
name: 'Lee', // 프로퍼티: 상태를 나타냄
age: 20,
sayHello() { // 메서드: 동작을 나타냄
console.log(`Hi, ${this.name}!`);
}
};
핵심은 상태(데이터)와 동작(행동)을 하나의 단위로 묶는다는 겁니다. 관련 있는 것들을 한 곳에 모아 관리하는 거죠.
객체를 만드는 방법은 5가지
자바스크립트는 클래스 없이도 객체를 만들 수 있습니다. 방법이 다섯 가지나 있어요.
| 객체 리터럴 | const obj = { name: 'Lee' } |
| Object 생성자 함수 | const obj = new Object() |
| 생성자 함수 | const obj = new Person('Lee') |
| Object.create | const obj = Object.create(proto) |
| 클래스(ES6) | class Person { } |
그럼 각각 언제 쓸까요?
✅ 객체 리터럴 — 단순하고 일회성인 데이터 묶음
// 딱 한 번만 쓸 설정값, 옵션 객체, 반환값 등
const config = {
host: 'localhost',
port: 3000,
debug: true
};
// API 응답 데이터 임시 가공
const user = {
id: response.userId,
name: response.userName,
};
같은 구조의 객체를 여러 개 만들 필요가 없을 때 가장 편합니다. 코드가 짧고 직관적이에요.
✅ 생성자 함수 / 클래스 — 같은 구조의 객체를 여러 개 만들 때
// 같은 구조의 객체가 여러 개 필요할 때
function Person(name, age) {
this.name = name;
this.age = age;
}
const lee = new Person('Lee', 20);
const kim = new Person('Kim', 25);
lee, kim 처럼 같은 틀(구조)의 인스턴스를 찍어낼 때 씁니다. 객체 리터럴로 하면 같은 코드를 반복 작성해야 하니까요.
✅ Object.create — 프로토타입을 직접 지정할 때
const animal = {
breathe() { console.log('숨 쉰다'); }
};
const dog = Object.create(animal); // animal을 프로토타입으로 지정
dog.bark = function() { console.log('멍!'); };
dog.breathe(); // 숨 쉰다 (animal에서 상속)
dog.bark(); // 멍!
상속 관계를 매우 명시적으로 설정하고 싶을 때, 또는 null 프로토타입 객체(순수한 데이터 저장용 객체)가 필요할 때 씁니다.
// 프로토타입 체인이 없는 순수 객체 (Map 대신 쓸 때)
const dict = Object.create(null);
dict['hello'] = '안녕';
// toString, hasOwnProperty 같은 기본 메서드가 없어서 키 충돌 걱정 없음
솔직히 ✅ Object.create 같은 건 본 적이 없어서 알아봤는데 예전에는 자바스크립트에서 class를 표현할 방법이 없어 사용했었다고 해요. 그런데 자바스크립트에 class 문법이 생기면서 잘 사용되지 않게 되었다고 합니다. 그마저도 요즘은 class 대신 객체 리터럴을 사용하는 경우도 많아서 그냥 이런 게 있구나 정도로만 알아두시면 될 것 같아요.
ES6 축약 문법
프로퍼티 축약
const name = 'Lee';
const age = 20;
// 변수명과 키 이름이 같으면 생략 가능
const person = { name, age };
// { name: 'Lee', age: 20 } 과 동일
함수에서 객체를 반환하거나 상태를 모아서 넘길 때 자주 씁니다.
function getUser(id) {
const name = fetchName(id);
const email = fetchEmail(id);
return { name, email }; // 축약 표현
}
메서드 축약
// 예전 방식
const obj = {
greet: function() { ... }
};
// ES6 방식
const obj = {
greet() { ... }
};
단순히 짧아진 것이 아니라 ES6 메서드 축약으로 정의한 메서드는 내부 동작이 다릅니다(non-constructor, super 사용 가능). 클래스 메서드처럼 동작한다고 이해하면 됩니다.
📢 AI에게 물어본 "내부 동작이 다릅니다(non-constructor, super 사용 가능)."이 뭘 의미할까?
1. non-constructor — "이건 생성자로 못 써"
new로 호출할 수 없다는 뜻이에요.
// 일반 function으로 정의한 메서드
const obj1 = {
greet: function() { return 'hi'; }
};
// ES6 축약으로 정의한 메서드
const obj2 = {
greet() { return 'hi'; }
};
// 일반 function → new로 호출 가능 (constructor)
new obj1.greet(); // ✅ 되긴 됨 (빈 객체 생성됨)
// ES6 축약 → new로 호출 불가 (non-constructor)
new obj2.greet(); // ❌ TypeError: obj2.greet is not a constructor
근데 솔직히 객체 안에 있는 메서드를 new로 호출할 일은 현실에서 없어요. 그래서 이 차이가 실무에서 체감되는 경우는 거의 없고, "아 그런 차이가 있구나" 정도로만 알면 충분해요.
2. super — 이게 실제로 중요한 차이
super는 부모 클래스의 메서드를 호출할 때 쓰는 키워드예요.
class Animal {
speak() {
return '소리를 냅니다';
}
}
class Dog extends Animal {
speak() {
const parent = super.speak(); // 부모의 speak() 호출
return `멍! (${parent})`;
}
}
const d = new Dog();
d.speak(); // "멍! (소리를 냅니다)"
여기까진 자연스럽죠. 문제는 객체 리터럴 안에서 super를 쓰고 싶을 때 발생해요.
const animal = {
speak() {
return '소리를 냅니다';
}
};
// ❌ 일반 function으로 정의하면 super 못 씀
const dog1 = {
speak: function() {
return super.speak(); // SyntaxError!
}
};
// ✅ ES6 축약으로 정의하면 super 사용 가능
const dog2 = {
speak() {
return super.speak(); // 정상 동작
}
};
Object.setPrototypeOf(dog2, animal);
dog2.speak(); // "소리를 냅니다"
super를 쓰려면 ES6 축약 메서드 문법으로 정의해야 한다는 게 핵심이에요.
근데 실무에서 객체 리터럴에 super 쓸 일이 있나?
솔직히 클래스 쓰면 해결되는 경우가 대부분이라 자주 보이진 않아요.
그나마 쓰이는 경우는 Vue 옵션 API나 믹스인 패턴처럼 객체를 조합하는 상황 정도인데, 현대 코드에선 이것도 클래스나 컴포지션 패턴으로 대체되는 추세라서요.
결론적으로 이 두 차이는 "ES6 메서드 축약은 그냥 짧은 게 아니라 내부적으로 다르게 동작한다" 는 걸 알려주는 포인트예요. 실무 체감보다는 동작 원리 이해 쪽에 가깝습니다.
계산된 프로퍼티 이름
const prefix = 'btn';
const ui = {
[`${prefix}-primary`]: '#3b82f6',
[`${prefix}-danger`]: '#ef4444',
};
// { 'btn-primary': '#3b82f6', 'btn-danger': '#ef4444' }
키를 동적으로 결정해야 할 때 유용합니다. 팩토리 패턴이나 설정 객체를 생성할 때 자주 쓰입니다.
2. 프로토타입 — 자바스크립트 상속의 실체
왜 프로토타입이 필요한가?
생성자 함수로 객체를 여러 개 만들다 보면 문제가 생깁니다.
function Circle(radius) {
this.radius = radius;
this.getArea = function() { // ❌ 문제!
return Math.PI * this.radius ** 2;
};
}
const c1 = new Circle(1);
const c2 = new Circle(2);
const c3 = new Circle(3);
c1, c2, c3가 가진 getArea는 내용이 완전히 동일한데 3개가 따로 만들어집니다. 인스턴스가 1000개면 같은 함수가 1000개 생기는 거예요. 메모리 낭비이고 성능에도 악영향을 줍니다.
console.log(c1.getArea === c2.getArea); // false — 다른 함수 객체!
프로토타입으로 해결하기
공통 메서드는 프로토타입에 한 번만 정의하고, 모든 인스턴스가 그걸 공유하면 됩니다.
function Circle(radius) {
this.radius = radius; // 인스턴스마다 다른 값 → 각자 소유
}
// 공통 동작은 프로토타입에 한 번만
Circle.prototype.getArea = function() {
return Math.PI * this.radius ** 2;
};
const c1 = new Circle(1);
const c2 = new Circle(2);
console.log(c1.getArea === c2.getArea); // true — 같은 함수 공유!
원칙: 인스턴스마다 달라지는 값(상태)은 직접 소유하고, 모든 인스턴스가 똑같이 하는 행동(메서드)은 프로토타입에서 공유한다.
프로토타입 체인 — 어떻게 찾아가는가
객체에서 프로퍼티를 찾을 때 자바스크립트는 이렇게 동작합니다.
me.hasOwnProperty('name') 호출 시:
1. me 객체 안에서 hasOwnProperty 찾음 → 없음
2. me.__proto__ (= Person.prototype) 에서 찾음 → 없음
3. Person.prototype.__proto__ (= Object.prototype) 에서 찾음 → 있음! 실행
이것이 프로토타입 체인입니다. 최상위는 항상 Object.prototype이고, 거기서도 없으면 undefined를 반환합니다.
function Person(name) { this.name = name; }
Person.prototype.sayHello = function() {
console.log(`Hi, ${this.name}!`);
};
const me = new Person('Lee');
me.sayHello(); // 'Hi, Lee!' ← Person.prototype에서 상속
me.hasOwnProperty('name'); // true ← Object.prototype에서 상속
me.foo; // undefined ← 체인 끝까지 없으면 undefined
3각 관계: 생성자 함수 ↔ 프로토타입 ↔ 인스턴스
function Person(name) { this.name = name; }
const me = new Person('Lee');
// 세 가지가 서로 연결되어 있음
Person.prototype.constructor === Person; // true
me.__proto__ === Person.prototype; // true
me.constructor === Person; // true (프로토타입 통해 상속)
이 3각 관계를 알아야 프로토타입 교체나 instanceof 동작을 이해할 수 있습니다.
정적 메서드 vs 프로토타입 메서드 — 언제 뭘 쓸까?
function MathUtils() {}
// 프로토타입 메서드 — 인스턴스가 있어야 호출 가능
MathUtils.prototype.double = function(n) {
return n * 2;
};
// 정적 메서드 — 인스턴스 없이 바로 호출 가능
MathUtils.square = function(n) {
return n * n;
};
const util = new MathUtils();
util.double(5); // 10 ✅
MathUtils.square(5); // 25 ✅
util.square(5); // ❌ TypeError
판단 기준: 메서드 내부에서 this(인스턴스)를 참조하는가?
- this 참조 필요 → 프로토타입 메서드
- this 참조 불필요 → 정적 메서드로 만드는 게 더 명확
실제 예시로 보면:
// Object.create — 정적 메서드 (인스턴스 없이 씀)
const obj = Object.create(null);
// obj.hasOwnProperty — 프로토타입 메서드 (인스턴스가 씀)
obj.hasOwnProperty('key');
프로퍼티 섀도잉 — 오버라이딩의 실체
Person.prototype.sayHello = function() {
console.log(`Hi, ${this.name}`); // 프로토타입 메서드
};
const me = new Person('Lee');
// 인스턴스에 같은 이름의 메서드 추가
me.sayHello = function() {
console.log(`Hey! ${this.name}`); // 인스턴스 메서드
};
me.sayHello(); // "Hey! Lee" — 인스턴스 메서드가 우선
인스턴스 메서드가 프로토타입 메서드를 가립니다(섀도잉). 프로토타입 메서드가 덮어써지는 게 아니라 체인에서 먼저 발견되는 인스턴스 메서드가 실행되는 겁니다.
인스턴스 메서드를 삭제하면 다시 프로토타입 메서드가 보입니다.
delete me.sayHello;
me.sayHello(); // "Hi, Lee" — 이제 프로토타입 메서드 호출
instanceof와 프로퍼티 확인
instanceof — 프로토타입 체인 확인
me instanceof Person // Person.prototype이 me의 체인에 있는가?
me instanceof Object // Object.prototype이 me의 체인에 있는가?
constructor 프로퍼티가 아니라 프로토타입 체인 상에 존재하는지를 확인하는 거라는 점이 중요합니다.
프로퍼티 존재 확인
const person = { name: 'Lee', address: 'Seoul' };
// in 연산자: 프로토타입 체인 전체에서 확인
'name' in person // true
'toString' in person // true (Object.prototype에 있으니까!)
// hasOwnProperty: 자기 자신의 프로퍼티만 확인
person.hasOwnProperty('name') // true
person.hasOwnProperty('toString') // false
상속받은 것까지 포함해서 볼 때는 in, 자기 것만 볼 때는 hasOwnProperty.
프로퍼티 열거 — for...in의 함정
const person = {
name: 'Lee',
address: 'Seoul',
__proto__: { age: 20 } // 상속받은 프로퍼티
};
// for...in: 상속받은 것까지 열거
for (const key in person) {
console.log(key); // name, address, age 모두 출력!
}
// 자기 것만 열거하려면 hasOwnProperty 필터링 필요
for (const key in person) {
if (!person.hasOwnProperty(key)) continue;
console.log(key); // name, address만 출력
}
이런 번거로움 때문에 보통 아래를 더 권장합니다.
Object.keys(person) // ['name', 'address'] — 자신의 열거 가능한 키만
Object.values(person) // ['Lee', 'Seoul']
Object.entries(person) // [['name', 'Lee'], ['address', 'Seoul']]
배열 순회에는 for...in 대신 for, forEach, for...of를 씁니다. 배열도 객체라서 for...in으로 돌리면 숫자 인덱스 외에 추가 프로퍼티도 딸려 나올 수 있거든요.
3부. 정리 — 언제 무엇을 쓸 것인가
객체 생성 방법 선택 기준
단순 데이터 묶음, 한 번만 씀
→ 객체 리터럴 { }
같은 구조의 객체를 여러 개 만들어야 함
→ 생성자 함수 / 클래스
프로토타입을 직접 지정하고 싶음
→ Object.create()
순수한 데이터 저장소 (메서드 없는 맵처럼)
→ Object.create(null)
메서드 정의 위치 선택 기준
인스턴스마다 다르게 동작해야 함 (this 참조 필요)
→ 프로토타입 메서드
인스턴스와 무관한 유틸성 기능 (this 참조 불필요)
→ 정적 메서드
프로퍼티 열거/확인 선택 기준
프로토타입 체인 포함해서 확인
→ in 연산자
자기 자신 것만 확인
→ hasOwnProperty
자기 자신 것만 열거
→ Object.keys / Object.values / Object.entries
상속 포함 열거 (드물게)
→ for...in (hasOwnProperty 필터링 병행)