카테고리 없음

자바스크립트 객체와 프로토타입 — 언제, 왜 쓰는 걸까?

taek2-0310 2026. 5. 25. 11:36

객체는 흔히 우리가 너무 많이 쓰지만 프로토타입은 경험할 기회가 흔치 않아요. 저는 항상 무언가를 배우게 되면 “그래서 언제 쓰는데?”라는 생각이 들기 때문에 이 블로그에도 그런 생각을 담아 작성해보았습니다. 혹시 사용하는 상황이라도 기억해둔다면 “아, 이런 상황에서 프로토타입을 쓰던데?” 하며 다시 돌아와 개념을 다지고 사용할 수 있지 않을까요?

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 필터링 병행)