tw.heo Github

[object Object]의 정체가 뭘까? 바닥까지 파헤쳐보기

[Object object]가 출력되는 이유를 프로토타입과 함께 알아본다.

Sep 16, 2025

#Javascript

한창 JavaScript 복습을 하던 중 [object Object] 를 만났다. 한창 JavaScript 복습중이었기 때문에 구석구석 살펴보고자 정체를 알아보기로 했다.

이번 글에서는 [object Object] 가 무엇인지 알아보고, 그 아래에 숨겨진 프로토타입, 프로토타입 체인의 동작 원리를 살펴본다.

[object Object]

const obj = { x: 1 };
console.log(obj); // { 'x' : 1 }
console.log(obj + ""); // [object Object]

obj를 그대로 출력하면 객체를 그대로 보여준다. 하지만 문자열 ''를 더한 경우 [object Object]가 출력된다. 이는 객체가 문자열로 암묵적 타입 변환되었기 때문이다.

이때 객체가 문자열로 변환될 때 내부적으로 어떤 일이 일어날까?

JavaScript 엔진은 내부적으로 toString 메서드를 호출한다. 이 메서드는 객체를 [object 타입] 형식으로 반환하기 때문에 [object Object]라는 결과가 나온 것이다.

하지만 객체 리터럴로 선언했을 뿐인데 toString 메서드를 호출한다. 직접 정의하지도 않았는데 어디서 추가된 것일까?

toString 은 어디서 왔을까?

여러분이 Java와 같은 객체지향 언어를 공부했다면 toString 메서드가 꽤 익숙할 것이다.

class Obj {}
Obj obj = new Obj();
System.out.println(obj.toString());

Java에서는 toString 을 하위 클래스가 직접 구현하지 않아도 사용할 수 있다. 모든 클래스가 상위 클래스인 Object 로부터 이 메서드를 상속받기 때문이다.

JavaScript에서도 비슷하다. 앞서 본 toString 메서드는 Object 라는 최상위 클래스에서 온 것이다.

다만, JavaScript는 클래스가 아닌 프로토타입 기반의 상속을 구현한다.

prototype

프로토타입은 상위 객체가 하위 객체에게 공유할 프로퍼티를 정의하는 객체이다. 예를 들어 상위 객체가 프로토타입 객체에 메서드를 정의하면, 하위 객체도 해당 메서드를 사용할 수 있다.

프로토타입 객체는 [[Prototype]] 내부 슬롯으로 표현된다.

프로토타입 체인이 상위 객체와 하위 객체를 연결한다.

프로토타입 체인의 끝은 Object.prototype이다. 하위 객체에서 이를 접근하기 위해 __proto__ 접근자 프로퍼티를 사용할 수 있다.

ES6 이후에는 __proto__대신 Object.getPrototypeOf를 사용하는 것을 권장한다.

실제로 JavaScript의 빌트인 객체들은 모두 Object의 인스턴스이다. 프로토타입을 통해 이와 연결되어 있다. 이것이 의미하는 바는, 곧 빌트인 객체들 또한 Object.prototype의 메서드를 사용할 수 있다는 것이다.

Function.prototype.__proto__ === Object.prototype; // true
Array.prototype.__proto__ === Object.prototype; // true
RegExp.prototype.__proto__ === Object.prototype; // true
Promise.prototype.__proto__ === Object.prototype; // true
...

[[Prototype]] 이 참조하는 프로토타입 객체는, 해당 객체가 생성된 방식에 의해 결정된다.

  • 객체 리터럴의 경우, 프로토타입은 Object.prototype 이다.
  • 생성자함수로 생성된 인스턴스의 프로토타입은 생성자함수.prototype 이다.
  • Object.create() 메서드로 생성된 경우, 인자로 전달된 객체를 프로토타입으로 설정한다.

따라서 {}(객체 리터럴) 의 경우, 프로토타입은 Object.prototype이다.

{}.__proto__ === Object.prototype; //true

prototype chain

프로로타입 객체는 프로토타입 체인으로 연결된다.

호출 시 프로토타입 체인으로 연결되는 과정은 아래와 같다.

  • 현재 메서드를 호출한 객체의 스코프 내에 해당 메서드가 있는지 확인한다.
  • 만약 없다면, 프로토타입 체인을 따라 [[Prototype]] 로 이동한다. 그리고 해당 스코프 내에 메서드가 있는지 확인한다.
  • 이 과정을 찾을 때까지 반복한다.
  • 프로토타입 체인의 종점인 Object.prototype 에 메서드가 정의되어 있다. 이것을 실행한다.

오버라이딩이 가능한 것도 이러한 원리다.

const obj = {
toString() {
return "my obj";
},
};
console.log(obj + ""); // "my obj"

Object.prototype.toString()

지금까지 내용을 간단하게 정리하면,

  • [object Object] 는 객체가 문자열로 평가된 결과다.
  • 객체가 문자열로 평가될 때, toString 이 실행된다.
  • 객체 리터럴은 자기 자신에게서 이를 찾는다.
  • 객체 리터럴 자체에는 없기 때문에, 프로토타입 체인을 따라 Object.prototype 의 toString 을 찾는다.

결국, 실행되는 것은 Object.prototype.toString() 이다.

모든 것은 Object 에서 시작된다.

프로토타입 체인이 동작하는 방식을 좀 더 자세히 알아보자.

JavaScript가 지원하는 타입은 7개의 원시 타입과 object 타입까지 더해 총 8개다.

  • Boolean
  • Number
  • String
  • Symbol
  • BigInt
  • Undefined
  • Null

null, undefined를 제외한 나머지 타입들은 래퍼객체를 갖고 있다. 중요한 것은 해당 래퍼객체들이 모두 Object 의 인스턴스라는 것이다. 이는 곧, 원시 값들이 일시적으로 래퍼객체로 변환될 때, Object의 프로퍼티에 접근할 수 있다는 것을 의미한다.

Boolean instanceof Object; //true
Number instanceof Object; //true
String instanceof Object; //true
Symbol instanceof Object; //true
BigInt instanceof Object; //true
undefined instanceof Object; //false
null instanceof Object; //false

사실 우리는 원시값이 래퍼객체로 변환되어 메서드를 찾는 경험을 알게 모르게 많이 봐왔다. 아래 예시를 보자.

"hello".hasOwnProperty("toString"); // false

먼저, 결과로 false 가 나온다는 것은 함수가 존재하고 실행되었다는 것을 의미한다.

"hello" 문자열 자체는 원시타입이다. 하지만 함수를 호출할 시 내부적으로 String 래퍼 객체로 감싸진다.

또한 String.prototype 에서 hasOwnProperty 메서드를 찾으려 시도한다. 하지만 이 메서드는 String.prototype 에 없으므로, 엔진은 프로토타입 체인을 따라 Object.prototype 으로 이동하여 메서드를 찾아 실행한다.

그렇다면 공유할 수 없는 프로퍼티라면?

공유할 수 없는 프로퍼티인 경우가 있을 것이다. 예를 들어 “객체를 동결하는 Object.freeze” 메서드가 있다.

이 메서드는 말 그대로 “객체”의 프로퍼티를 변경불가하도록 만든다. 이걸 123 같은 숫자 원시타입에 사용할 수 있을까?

답은 “안된다”이다. 이처럼 객체 자체에만 적용할 수 있는 메서드는 프로토타입으로 공유하지 않고, static method 로 정의된다.

Array, Date, String 같은 다른 빌트인 객체들도 “해당 타입(생성자)에서만 실행할 수 있는 함수”를 정적 메서드로 정의한다.

이것이 두 호출 방식의 차이이다.

  • 프로토타입 메서드: “hello”.toUpperCase()처럼 원시값(의 래퍼 객체)이 프로토타입 체인을 통해 호출
  • 정적 메서드: Object.freeze()처럼 생성자(Object.~)가 직접 호출

참고