객체의 구조 타입을 구분하는 방법은 ? feat. 프로토타입체인 이해

쏘쏘임·2021년 10월 2일
1

프로토타입 체인 및 객체의 생성 과정에 대한 이해를 위한 심화 예제다. 실제로 객체의 구조 타입을 구분할 땐 그냥 타입스크립트나 prop-types 같은 것들을 사용하는 것이 좋다.

객체의 구조 타입을 구분하기

(배열 [1,2,3] 과 일반 객체 {key: value} 구분)

시도1 : typeof

typeof 연산자를 통해 자료형을 비교해주자 ! ⇒ 실패

typeof 연산자는 피연산자의 평가 전 자료형을 나타내는 문자열을 반환합니다. (MDN)

실패

자바스크립트의 객체들은 모두 object 자료형이라 구분이 안된다.

자바스크립트의 데이터 타입은 7가지의 원시타입(number, string, boolean, undefined, null, symbol)과 1개의 객체 타입(object)으로 나뉜다. 다시 말해 원시타입이 아닌 나머지 모든 것들은 '객체타입' 이다. 이들은 각각의 데이터 구조에 따라 배열, 함수, Set, Map, Date 등등으로 나뉜다.

따라서 typeof 연산자는 객체의 구조 타입을 구분해줄 수 없다.

typeof [] // "object"
typeof {} // "object"
typeof new Map() // "object"

typeof function(){} //"function"

typeof null // "object"

typeof는 null을 제외한 원시타입을 구분하며, 구조체(객체)는 function을 제외하고는 모두 object로 반환한다. (특이하게도 null값은 빈 구조체라하여 object를 반환한다.)

시도2 : 객체의 프로토타입 값 비교 (→ 한계 있음)

객체의 내부슬롯 [[Prototype]] 의 참조값이 Object 프로토타입과 같은지 비교 ⇒ 같으면 일반 객체 발견?

접근 방법

모든 객체는 자신의 프로토타입이 있다. 리터럴로 생성하건 빌트인 생성자 함수를 사용하건 자바스크립트 엔진이 OrdinaryObjectCreate 추상연산을 통해 적절한 구조타입의 생성자 함수로 객체 인스턴스를 만들어주기 때문이다.

예를 들어 배열과 일반 객체를 생성한 아래 예제의 경우에 대한 프로토타입 체인을 살펴보자.

const arr = []
const arrBuiltIn = new Array()

const normalObject = {}
const objBuiltIn = new Object()

일반객체와 배열의 프로토타입 체인

위의 그림은 배열과 객체를 생성자 함수 호출, 리터럴 방식으로 생성했을 때의 프로토타입 체인이다. 배열 객체는 Array.prototype을, 일반 객체는 Object.prototype을 바로 상속받는 것을 볼 수 있다. 그렇다면 아래처럼 생성된 객체의 프로토타입 값을 비교해보면 되지 않을까?

Object.getPrototypeOf(obj) === Object.prototype

obj.constructor === Object

한계

위의 경우는 한계가 있다. 커스텀 생성자함수로 생성한 일반객체의 경우 해당 생성자 함수의 프로토타입이 직속이되므로, Object.prototype는 직속 프로토타입이 될 수 없다. 아래 그림을 참고하자.

예외 : 사용자정의 생성자함수로 생성한 일반 객체는 Object.prototype을 직속 프로토타입으로 가지지 않는다.

시도2 : Object.prototype.toString()

toString() method returns a string representing the object. - MDN Object.prototype.toString()

객체와 객체 구조를 [object Object] 형태로 반환해주는 오프젝트 프로토토입 메서드 toString()을 사용하자.

// 일반객체인지 확인
function isNormalObject(obj) {
  return Object.prototype.toString.call(obj) === '[object Object]';
}

정상적으로 생성한 모든 객체들의 프로토체인 최종점은 Object 프로토타입이다. 이 프로토타입의 메서드인 toString[object Object], [object Array]와 같은 모양으로 일반객체의 타입을 반환한다.

이 때, toString 메서드는 자식 프로토타입에서 오버라이딩할 수 있으므로 꼭 Object.prototype.toString.call(비교할object) 로 지정하여 호출해야 한다.(물론 call은 apply 등으로 바꿀 수 있다.)

toString() 메서드는 지정된 배열 및 그 요소를 나타내는 문자열을 반환한다. - MDN

위는 정의는 모든 toString 메서드에게 공통적으로 적용된다. 다만 각각의 프로토타입 메서드들에 따라 오버라이딩되어 다르게 적용될 수 있다.

오브젝트의 자식 프로토타입에 의해 오버라이딩된 Object.prototype.toString 예시를 Array.prototype.toStirng과 비교하여 살펴보자.

Object.prototype.toString()

모든 객체에는 객체가 텍스트 값으로 표시되거나 객체가 문자열이 예상되는 방식으로 참조 될 때 자동으로 호출되는 toString() 메서드가 있습니다. 기본적으로 toString() 메서드는 Object에서 비롯된 모든 객체에 상속됩니다. 이 메서드가 사용자 지정 개체에서 재정의되지 않으면 toString()은 "[object type]"을 반환합니다. 여기서 type은 object type입니다. 다음 코드는 이것을 설명합니다. (MDN)

Array.prototype.toString()
Array 객체는 Object의 toString 메서드를 재정의(override)합니다. Array 객체에 대해, toString 메서드는 배열을 합쳐(join) 쉼표로 구분된 각 배열 요소를 포함하는 문자열 하나를 반환합니다. 예를 들어, 다음 코드는 배열을 생성하며 그 배열을 문자열로 변환하기 위해 toString을 사용합니다.
JavaScript는 배열이 텍스트 값으로 표현되거나 배열이 문자열 연결(concatenation)에 참조될 때 자동으로 toString 메서드를 호출합니다. (MDN)

[1,2].toString(); // 1,2

const foo() {};
foo.toString(); // "function foo(){}"

암묵적 타입변환

한계

하지만 여기도 문제는 있다. Object.prototype의 메서드를 이용해도 반환값인 [object Object] 형태의 값을 문자열 형태로 직접 타이핑하여 비교하기 때문이다. 이는 애초 메서드의 용도와 다를 뿐더러 하드코딩의 불안한 느낌을 지울 수 없다. 또한 (그래서는 안되겠지만) Object.prototype.toString 자체를 다른 코드로 덮어버릴 수 있어 안전하지 않다.

결론 : 객체 구조 타입 구분은 라이브러리나 프레임워크를 사용하자.

Array.isArray 와 같은 빌트인 메서드가 없는 경우 객체의 구조 타입을 확인하는 것은 쉽지 않다. Object.prototype.toString.call(obj) 를 통해 최선의 방법에 가까운 것을 도출했지만 엔진이 출력해주는 값은 문자열로 비교해야하는 단점이 있고, 해당 빌트인 프로퍼티 메서드가 수정될 경우 작동하지 않는다는 허점이있다.

타입 비교는 타입스크립트를 사용하자.

키워드

  • typeof
  • Object.getPrototypeOf
  • constructor
  • Object.prototype.toString()

참고 링크

profile
무럭무럭 자라는 주니어 프론트엔드 개발자입니다.

0개의 댓글