JavaScript의 객체 리터럴은 멋지다


저자 Dmitri Pavlutin 원문 https://rainsoft.io/why-object-literals-in-javascript-are-cool/

JavaScript 에서 ECMAScript 2015 이전의 객체 리터럴(객체 초기화자)는 꽤 기본적이었다. 2가지 종류의 속성을 정의하는것이 가능했다 :

  • 한쌍의 프로퍼티명과 값 { name1: value1 }
  • 계산된 속성값을 위한 접근자들 { get name() {...} }과 설정자들 { set name() {...} }

안타깝지만 객체리터럴의 실현성은 다음 예제로 설명 가능하다.

var myObject = {  
  myString: 'value 1',
  get myNumber() {
    return this._myNumber;
  },
  set myNumber(value) {
    this._myNumber = Number(value);
  }
};
myObject.myString; // => 'value 1'  
myObject.myNumber = '15';  
myObject.myNumber; // => 15  

자바스크립트는 프로토타입 기반의 언어이며, 모든 것이 다 객체이다. 이런 언어에서는 객체 생성, 프로토타입 접근, 프로토타입 설정을 편하게 할 수 있다.

객체를 정의하는 것과 프로토타입을 설정 하는 일은 똑같은 작업이다. 그래서 프로토타입도 한 문장의 객체 리터럴 안에서 설정할 수 있어야 한다고 생각한다.

공교롭게도 객체 리터럴은 그런 면에서 간단하고 깔끔한 해결책이 될 수 없다. 그래서 Object.create()와 리터럴을 조합하여 프로토타입을 설정할 수 있다.

var myProto = {  
  propertyExists: function(name) {
    return name in this;    
  }
};
var myNumbers = Object.create(myProto);  
myNumbers['array'] = [1, 6, 7];  
myNumbers.propertyExists('array');      // => true  
myNumbers.propertyExists('collection'); // => false  

이 해결책은 불편하기 그지없다. 자바스크립트는 프로토타입 기반의 언어인데, 왜 프로토타입을 이용해서 객체를 만들 때 이런 귀찮은 일들을 해야 하는 걸까?

그래도 언어가 변화하고 있어서 비슷한 자바스크립트의 어려움들이 하나씩 해결되고 있다.

이 글에서는 위에 언급된 문제들을 ES2015가 해결했는지, 객체 리터럴을 개선하기 위해 어떤 것들이 추가되었는지 보여줄 것이다.

  • 프로토타입을 객체 생성할때 설정하기
  • 축약 메소드 선언
  • 상위 호출하기
  • 계산된 속성이름

그리고 앞으로는 어떻게 변화할 것인지, 새로운 제안은 어떤 것들이 있는지 볼 것이다.

1. 프로토타입을 객체 생성할때 설정하기

이미 알고 있듯이 존재하는 객체의 프로토타입에 접근하기 위한 옵션으로 게터 속성 __proto__가 있다.

var myObject = {  
  name: 'Hello World!'
};
myObject.__proto__;                         // => {}  
myObject.__proto__.isPrototypeOf(myObject); // => true  

myObject.__proto__는 객체 myObject의 프로토타입을 반환한다.

좋은 점은 ES2015에서 __proto__를 리터럴로 생성하는 객체 내부에서 속성 이름으로 사용할 수 있다 는 것이다. { __proto__: protoObject }

__proto__를 객체 초기화 단계에서 사용한다면, 처음에 보여주었던 방법보다는 조금 나은 해결책이 될 것이다.

var myProto = {  
  propertyExists: function(name) {
    return name in this;    
  }
};
var myNumbers = {  
  __proto__: myProto,
  array: [1, 6, 7]
};
myNumbers.propertyExists('array');      // => true  
myNumbers.propertyExists('collection'); // => false  

myNumbers객체는 프로토타입 객체인 myProto를 속성이름 __proto__에 초기화 하여 생성했다. 이 객체는 한 문장으로 바꿀 수 있는데, 바로 Object.create()이다.

보시다시피 __proto__를 이용하는것은 간단하다.

잠깐 다른 이야기를 하자면, 나는 단순하고 유연한 해결책이 많은 노력과 설계를 필요로 하는데에 의문점이 든다. 만약 해결책이 간단하다면 설계하기 쉽다고 생각이 되고, 설계하기 쉬운 것이 간단한 해결책이라고 생각되기 때문이다:

  • 단순하고 간단하게 만들기는 어렵다
  • 복잡하고 이해하기 어렵게 만드는 것은 쉽다

만약 어떤 것이 너무 복잡하게 보이거나 사용하기 어렵다면, 아마 충분한 고민을 통하지 않았을 것이다.

단순함에 대한 여러분의 의견은 어떤가? (자유롭게 댓글로 적어보세요)

2.1 특별한 경우의 __proto__ 사용

__proto__를 쓰는 것은 간단하지만, 조심해서 사용해야 하는 몇 가지 경우가 있다. __proto__는 객체 리터럴에서 한 번만 사용 되어야 한다. 만약 두 번 이상 사용된다면 자바스크립트는 에러를 반환할것이다.

var object = {  
  __proto__: {
    toString: function() {
      return '[object Numbers]'
    }
  },
  numbers: [1, 5, 89],
  __proto__: {
    toString: function() {
      return '[object ArrayOfNumbers]'
    }
  }
};

예제의 객체 리터럴에서 __proto__는 두번 사용되었다. 이런 경우 SyntaxError: Duplicate __proto__ fields are not allowed in object literals가 반환될 것이다.

자바스크립트는 __proto__에 객체 혹은 null할당만 허용한다. 원시형 변수나 undefined를 할당하려는 시도는 그대로 무시된다.

다음의 예제를 보자:

var objUndefined = {  
  __proto__: undefined
};
Object.getPrototypeOf(objUndefined); // => {}  
var objNumber = {  
  __proto__: 15
};
Object.getPrototypeOf(objNumber);    // => {}

위의 객체 리터럴에서는 15와 undefined__proto__에 값으로 할당하려고 했다. 객체 혹은 null만 프로토타입이 될 수 있고 objectUndefined, objectNumber는 여전히 각자의 기본 프로토타입을 가지고 있다(기본 자바스크립트 객체 {}).

2. 축약 메소드 정의 (Shorthand method definition)

짧은 문법으로 객체 내부에서 함수를 선언하는 것이 가능하다. 마치 function 키워드와 : 를 빼고 말이다. 이를 축약 메소드 정의 라고 한다.

한번 메소드를 축약 정의해보자:

var collection = {  
  items: [],
  add(item) {
    this.items.push(item);
  },
  get(index) {
    return this.items[index];
  }
};
collection.add(15);  
collection.add(3);  
collection.get(0); // => 15  

add()get() 메소드는 collection 객체 내부에 축약형으로 정의되었다.

이 방법은 디버깅에 유용하다는게 장점이다. 또한 collection.add.name를 실행하면 'add'를 반환한다.

3. 상위 호출 하기

재미있는 개선점 중 하나는 super 키워드의 사용해서 프로토타입 체인을 통해 속성을 상속받을 수 있다는 점이다. 한번 예제를 통해 확인해보자:

var calc = {  
  sumArray (items) {
    return items.reduce(function(a, b) {
      return a + b;
    });
  }
};
var numbers = {  
  __proto__: calc,
  numbers: [4, 6, 7],
  sumElements() {
    return super.sumArray(this.numbers);
  }
};
numbers.sumElements(); // => 17  

calcnumbers객체의 프로토타입이다. 객체 numbers의 메소드 sumElements에서는 super키워드를 통해 상위 메소드에 접근 가능하다.

결국 super는 프로토타입 체인 오브젝트로 상속받은 속성에 접근하는 지름길(shortcut)이다. 이전의 예제에서는 프로토타입에서 직접 calc.sumArray()를 호출해서 사용했다. 하지만 프로토타입 체인의 객체에 접근하기 때문에 super키워드를 사용하는 것이 더 좋다. 그리고 상속받은 속성이라는 뜻이 명확히 전달되기 때문에 더 그렇다.

3.1 super의 사용의 제한점

super는 오직 객체 리터럴의 축약 메소드 정의 내부에서만 사용할 수 있다.

만약 일반적인 메소드 정의에서 사용하려고 하면 자바스크립트는 에러를 반환한다.

var calc = {  
  sumArray (items) {
    return items.reduce(function(a, b) {
      return a + b;
    });
  }
};
var numbers = {  
  __proto__: calc,
  numbers: [4, 6, 7],
  sumElements: function() {
    return super.sumArray(this.numbers);
  }
};
// Throws SyntaxError: 'super' keyword unexpected here
numbers.sumElements();  

메소드 sumElements는 속성으로 정의되었다. 하지만 super는 축약 메소드 내부에서만 사용될 수 있고, 위와 같은 상황에서는 SyntaxError: 'super' keyword unexpected here 에러가 발생한다.

이런 제한은 객체 리터럴로 선언된 객체들에는 영향을 미치지 않는다. 메소드 축약 정의를 사용하는 대부분은 더 짧은 문법을 선호하기 때문이다.

4. 계산된 속성 이름

ES2015 이전의 객체 리터럴 초기화에서 속성 이름은 대부분 고정된 문자열이었다. 속성 이름을 계산된 이름으로 사용하려면, 속성 접근자를 사용해야 했다.

function prefix(prefStr, name) {  
   return prefStr + '_' + name;
}
var object = {};  
object[prefix('number', 'pi')] = 3.14;  
object[prefix('bool', 'false')] = false;  
object; // => { number_pi: 3.14, bool_false: false }  

확실히 이 방법으로 속성을 정의하는 것은 기분이 썩 좋지 않다.

계산된 속성명은 문제를 우아하게 해결할 수 있다. 만약 속성명이 평가될 때 대괄호로 속성명 표현식을 감싼다면 표현식의 평가된 후 속성명이 된다.{[expression]: value}

위의 예제를 개선해보자:

function prefix(prefStr, name) {  
   return prefStr + '_' + name;
}
var object = {  
  [prefix('number', 'pi')]: 3.14,
  [prefix('bool', 'false')]: false
};
object; // => { number_pi: 3.14, bool_false: false }  

[prefix('number', 'pi')] 는 속성명을 prefix('number', 'pi')의 수행결과로 저장한다. 결과는 'number_pi'가 된다. 마찬가지로 [prefix('bool', 'false')]의 결과 는 'bool_false'가 된다.

4.1 Symbol을 속성명으로 사용하기

Symbol또한 속성명으로 계산되어 사용될 수 있다. 위와 마찬가지로 대괄호로 감싸기만 하면 된다: { [Symbol('name')]: 'Prop value' }.

예를 들어 특수한 속성인 Symbol.iterator로 객체 자신의 속성명들을 순회하는 경우를 생각해보자.

var object = {  
   number1: 14,
   number2: 15,
   string1: 'hello',
   string2: 'world',
   [Symbol.iterator]: function *() {
     var own = Object.getOwnPropertyNames(this),
       prop;
     while(prop = own.pop()) {
       yield prop;
     }
   }
}
[...object]; // => ['number1', 'number2', 'string1', 'string2']

[Symbol.iterator]: function *() { }는 속성을 정의하는데 객체 자신의 속성들을 순회한다. 펼침 연산자 [...object]는 자신의 속성들을 리스트로 반환하는데 쓰인다.

5. 앞으로는? rest와 spread속성

객체 리터럴에서의 Rest와 Spread 속성은 현재 제안 단계중 초안 상태(stage 2)이다. 아마도 새로운 자바스크립트 버전에 포함될 가능성이 있는 단계이다.

두 속성은 이미 ES2015의 배열에서는 사용 가능하다.

Rest 속성은 한 객체에서 속성들을 가져와 destructuring 할당하는 것을 가능하게 해준다. 다음 예제를 보면 object가 destructuing되어도 프로퍼티들이 남아있는 것을 알 수 있다.

var object = {  
  propA: 1,
  propB: 2,
  propC: 3
};
let {propA, ...restObject} = object;  
propA;      // => 1  
restObject; // => { propB: 2, propC: 3 }  

Spread 속성은 객체리터럴 내부로 한 객체의 속성들을 복사하는 것을 가능하게 해준다. 다음 예제를 보면 object객체 리터럴에 source객체의 속성들이 복사된 것을 알 수 있다.

6. 결론

자바스크립트는 큰 도약을 하고 있다.

상대적으로 작은 부분인 객체 리터럴도 ES2015에서 개선되도록 고려되었다. 그리고 한 무더기의 새로운 기능들이 제안 되고 있다.

여러분은 객체 프로토타입을 초기화 단계에서 __proto__속성명으로 설정할 수 있다. 이것은 Object.create()보다 훨씬 쉬운 방법이다.

메소드 선언은 더 짧아지고, function 키워드를 직접 작성할 필요도 없다. 그리고 그 내부에서 super키워드를 사용해서 프로토타입 체인을 통해 상속받은 속성에 쉽게 접근할 수도 있다.

만약 속성명이 실행 시점에 계산되어야 한다면 대괄호를 이용해서 표현식을 감싸서 [표현식] 이런 형태로 객체를 초기화할 수 있다.

그러므로, 지금의 객체 리터럴은 멋지다!


박정환, FE Development Lab2016.07.25Back to list