이번 포스팅에서는 자바스크립트 객체에 대해서 공부한 내용을 정리해 보려고 한다.
객체 타입
객체는 선언적(Declarative, 또는 Literal) 형식과 생성자 형식, 두 가지로 정의한다.
그리고 객체는 언어 타입(Language Types)이라 불리는 7개의 주요 타입 중 하나를 가지고 있다.
- null
- undefined
- boolean
- number
- string
- object
- symbol(ES6에서 추가)
참고로 여기에서 단순 원시타입(Simple Primitives) 5개(string, number, boolean, null, undefined)는 객체가 아니다. 그리고 복합 원시타입(Complex Primitives)이라는 독특한 객체 하위 타입도 존재한다. 예를 들면 function 타입이 여기에 속한다. 자바스크립트 함수는 기본적으로 (호출 가능한 특성이 고정된 객체이므로 '일급(First Class)'이며 여타의 일반 객체와 똑같이 취급된다.
여기서 일급 객체란 무엇일까? 일급 객체란 다른 함수에 인자로 전달할 수 있고 다른 함수로부터 함수를 반환받을 수 없으며, 함수 자체를 변수에 할당하거나 자료구조에 저장할 수 있는 특성을 가진 객체를 의미한다. 다른 객체에 적용 가능한 연산을 모두 적용할 수 있는 경우, 예컨데 함수에 매개변수로 넘기기, 수정하기, 변수에 대입하기 등과 같은 연산을 지원할 때, 그러한 객체를 일급 객체라고 부른다.
내장 객체로 불리는 객체 하위 타입도 있다. 내장 객체는 진짜 타입처럼 보이는 데다 자바의 String 클래스처럼 타 언어와 유사한 겉모습 때문에 클래스처럼 느껴질 수도 있다. 그러나 이들은 단지 자바스크립트의 내장 함수일 뿐, 각각 생성자(Constructor)로 사용되어 주어진 하위 타입의 새 객체를 생성한다. 목록은 아래와 같다
- String
- Number
- Boolean
- Object
- Function
- Array
- Date
- RegExp
- Error
다행인(?) 점은 자바스크립트 엔진은 상황에 맞게 문자열 원시 값을 String 객체로 자동 강제변환(Coerce)하므로 명시적으로 객체를 생성할 일은 거의 없다. 여러 자바스크립트 커뮤니티에서도 되도록 생성자 형식은 지양하고 리터럴 형식을 사용하도록 적극 권장한다.
계산된 프로퍼티명
객체는 특정한 위치에 저장된 모든 타입의 값, 즉 프로퍼티로 내용(contents)이 채워진다. 엔진이 값을 저장하는 방식은 구현 의존적(implementation-dependent)인데, 이는 객체 컨테이너(Object Container)에 담지 않는 것이 일반적이다. 객체 컨테이너에는 실제로 프로퍼티 값이 있는 곳을 가리키는 포인터(레퍼런스) 역할을 담당하는 프로퍼티 명이 담겨 있다. 객체 프로퍼티 명은 언제나 문자열이므로 객체와 배열 사이에 숫자를 써서 혼동되는 코드를 만드는 방법 등은 지양해야 한다.
ES6부터 계산된 프로퍼티명(Computed Property Name)이라는 기능이 추가되었다. 객체 리터럴 선언 구문의 키 이름 부분에 해당 표현식을 넣고 [ ]로 감싸면 된다.
계산된 프로퍼티명은 ES6 심볼에서 가장 많이 사용한다. 심볼은 새로운 원시 데이터 타입으로 불분명하고 예측 불가능한 값(기술적으로는 문자열 값)을 가진다. 심볼의 실제 값(이론적으로는 자바스크립트 엔진마다 제각각일 수 있다)을 직접 다룰 일은 거의 없으므로 Symbol.Something 같은 심볼명을 사용하면 된다.
프로퍼티 vs 메서드
많은 개발자들은 접근하려는 객체 프로퍼티 값이 함수면 어떤 식으로든 구별을 하려고 한다. 다른 언어에서 객체(클래스)에 부속된 함수를 메서드라고 부르고 자바스크립트 함수 역시 객체의 부속물이라 생각하기에 '프로퍼티 접근'에 대비되는 용어로 '메서드 접근'이라는 말을 종종 사용한다. 하지만 엄밀히 말해 함수는 결코 객체에 속하는 것이 아니며, 객체 레퍼런스로 접근한 함수를 그냥 메서드라고 칭하는 것은 그 의미를 지나치게 확대해서 해석하는 것이다.
객체에 존재하는 프로퍼티에 접근할 때 마다 반환 값 타입에 상관없이 항상 프로퍼티 접근을 하고 이런 식으로 함수를 가져왔다고 해서 저절로 함수가 메서드가 되는 건 아니다. 프로퍼티 접근 결과 반환된 함수 역시 마찬가지다.
someFoo나 myObject.someFoo 모두 같은 함수를 가리키는 개별 레퍼런스일 뿐, 뭔가 특별한 다른 객체가 '소유한' 함수라는 의미는 아니다. foo() 안에 this 레퍼런스가 정의되어 있다면 myObject.someFoo에서 발생할 암시적 바인딩이 두 레퍼런스의 유일한 차이점이다. 물론 그렇다고 두 레퍼런스를 메서드라 부르는 건 온당치 않다. 결론은 자바스크립트에서 '함수'와 '메서드'는 서로 바꿔 사용할 수 있다.
함수 표현식을 객체 리터럴의 한 부분으로 선언해도 이 함수가 저절로 객체에 달라붙는 건 아니며 해당 함수 객체를 참조하는 레퍼런스가 하나 더 생기는 것 뿐이다.
객체 복사
이번에는 객체 복사(copy)에 대해서 알아보자. 이 역시 많은 개발자들이 혼동하는 개념 중 하나이다.
myObject의 사본은 정확히 어떻게 표현해야 할까? 먼저 얕은 복사(Shallow Copy), 깊은 복사(Deep Copy) 중 선택해야 한다. 얕은 복사 후 생성된 새 객체의 a 프로퍼티는 원래 값 2가 그대로 복사되지만 b,c,d 프로퍼티는 원 객체(Original Object)의 레퍼런스와 같은 대상을 가리키는 또 다른 레퍼런스다. 깊은 복사를 하면 myObject는 물론이고 anotherObject와 anotherArray까지 모조리 복사한다.
하지만 여기서 문제는 anotherArray가 anotherObject와 myObject를 가리키는 레퍼런스를 갖고 있으므로 원래 레퍼런스가 보존되는게 아니라 이들까지 함께 복사된다. 결국, 환형 참조(Circular Reference) 형태가 되어 무한 복사의 구렁텅이에 빠지고 만다. 이 문제는 오랫동안 고민했지만 아직까지 뾰족한 답이 없다.
JSON 안전한(JSON-Safe) 객체, 즉 [JSON 문자열 <-> 객체 직렬화 및 역직렬화]를 해도 구조와 값이 같은 객체는 쉽게 복사할 수 있으므로 하나의 대안이 될 수는 있다. 물론 100% JSON 안전한 객체여야 한다.
var newObj = JSON.parse( JSON.stringify( someObj ) );
한편, 얕은 복사는 이해하기 쉽고 별다른 이슈가 없기 때문에 ES6부터는 Object.assign() 메서드를 제공한다. 이 메서드의 첫째 인자는 타겟 객체(Target Object)이고 둘째 인자 이후는 하나 또는 둘 이상의 소스 객체(Source Object)로, 소스 객체의 모든 열거 가능한 것(Enumerable)과 보유 키(Owned Keys)를 순회하면서 타겟 객체로 복사한다. 그래서 다음과 같이 타겟 객체를 간편하게 만들 수 있다.
프로퍼티 서술자
ES5 부터 모든 프로퍼티는 프로퍼티 서술자(Property Descriptor)로 표현된다.
평범한 객체의 프로퍼티 서술자는 다음과 같이 writable, configurable, enumerable 세 가지가 있다.
- writable: 프로퍼티 값의 쓰기 가능 여부는 writable로 조정한다.
- configurable: 프로퍼티가 설정가능하면 defineProperty()로 프로퍼티 서술자를 변경할 수 있다.
- enumerable: 반복문처럼 루프 객체 프로퍼티를 열거하는 구문에서 해당 프로퍼티의 표출 여부를 나타낸다.
정리하기
- 자바스크립트 객체는 리터럴 형식(ex. var a = { })과 생성자 형식(var a = new Array( )) 두 가지 형태를 가진다. 대부분 리터럴 형식을 쓰는 편이 좋지만 생성시 옵션을 더 주기 위해 생성자 형식을 쓰는 경우도 더러 있다.
- 많은 사람들이 "자바스크립트는 모든 것이 다 객체다"라고 말하지만 사실과 다르다. 객체는 6개(또는 7개)의 원시 타입 중 하나이고 함수를 비롯한 하위 타입이 있다. 예를 들면 [object Array]라는 레이블로 표시되는 배열 객체라는 독특한 하위 타입도 가능하다.
- 객체는 키/값의 쌍을 모아 놓은 저장소고 값은 프로퍼티를 통해 접근할 수 있다. 프로퍼티에 접근하면 엔진 내부에서는 실제로 기본 [[Get]](값을 세팅할 때는 [[Put]]) 연산을 호출하는데, 객체 자체에 포함된 프로퍼티 뿐만 아니라 필요하면 [[Prototype]] 연쇄를 순회하며 찾아본다.
- 프로퍼티는 프로퍼티 서술자를 통해 제어 가능한 writable, configurable 등의 특정한 속성을 지닌다. 그리고 객체는 Object.preventExtensions(), Object.seal(), Object.freeze() 등을 이용하여 자신에게 여러 단계의 불변성을 제공할 수 있다.
- 프로퍼티가 반드시 값을 가져야 하는 것은 아니며, 게터/세터로 '접근자 프로퍼티' 형태를 취할 수도 있다. 예를 들어, 열거 가능성을 조정하여 for ... in 루프 순회 시 노출 여부를 마음대로 바꿀 수 있다.
- ES6부터는 for ... of 구문에서 한 번에 하나씩 다음 데이터값으로 이동하는 next() 메서드를 가진 내장/커스텀 @@iterator 객체를 통해 자료구조에서 여러 값을 순회할 수 있다.
참고문헌
- <YOU DON'T KNOW JS (this와 객체 프로토타입, 비동기와 성능)> 카일 심슨 저
'Prog. Langs & Tools > JavaScript' 카테고리의 다른 글
JS #6. 자바스크립트 작동 위임(behavior delegation) (0) | 2020.05.06 |
---|---|
JS #5. 자바스크립트 프로토타입(Prototype) (0) | 2020.04.27 |
JS #4. 자바스크립트 클래스(Class) (0) | 2020.04.22 |
JS #2. this에 대한 모든 것(ES5, ES6) (0) | 2020.04.08 |
JS #1. ES6에서 import/export 올바르게 이해하고 사용하기 (2) | 2020.01.28 |