sbyeol3/articles

[번역] 자바스크립트 객체 깊게 이해하기

Opened this issue · 0 comments

원문 : Diving Deeper in JavaScripts Objects

자바스크립트 객체는 짧고 간결한 문법이 드러내는 것보다 더 많은 것들을 포함하고 있습니다. 자바스크립트에서 객체를 생성하고 사용하는 것은 쉽고, 어렵지 않고, 유연하기 때문에 많은 개발자들이 객체에 더 많은 것이 있다는 것을 알지 못합니다.

우리는 자바스크립트 객체의 가려진 점들을 살펴보고 복잡한 점을 이해해볼 겁니다. 이 아티클을 읽고 나면 여러분은 다음과 같은 질문들에 답할 수 있습니다.

  • 어떻게 프로퍼티를 지울 수 없게 할 수 있는가?
  • 프로퍼티 접근자는 무엇이고 그들의 특징은 무엇인가?
  • 어떻게 프로퍼티를 변경 불가능하게 하거나 숨길 수 있는가?
  • 왜 어떤 프로퍼티들은 for-in문이나 Object.keys에 보이지 않는가?
  • 변경으로부터 객체를 어떻게 "보호"할 수 있는가?
  • 다음과 같은 코드를 어떻게 이해할 수 있는가?
obj.id = 5;
console.log(obj.id)
// => '101' ( 5 in binary )

자바스크립트 객체에 대한 기초를 먼저 보고 싶다면 제가 쓴 다른 아티클을 참고하세요.

팁 : 자바스크립트 컴포넌트를 만들고 재사용하려면 Bit을 사용하세요. 이는 빠르게 빌드하고 Lego처럼 컴포넌트를 사용할 수 있습니다.

프로퍼티의 타입

데이터 프로퍼티

이런 식으로 무수히 많은 객체를 생성할 수 있습니다.

const obj = {
  name: 'Arfat',
  id: 5
}

obj.name  // => 'Arfat'

obj 객체에서 nameid 프로퍼티는 데이터 프로퍼티입니다. 대부분의 자바스크립트 코드를 이루는 평범한 형태의 프로퍼티입니다. 그렇다면 다른 프로퍼티 타입은 무엇일까요?

접근자 프로퍼티 (Accessor Properties)

C#이나 파이썬 같은 다른 언어로 치면 gettersetter 로 이해할 수 있는데요. 접근자 프로퍼티는 getset 함수, 두 가지 함수의 조합입니다.

전통적인 key: value 문법을 사용하는 대신에 다음의 문법을 사용할 수 있습니다.

const accessorObj = {
  get name() {
    return 'Arfat';
  }
};

accessorObj.name; // => 'Arfat'

const dataObj = {
  name: 'Arfat',
};

dataObj.name; // => 'Arfat'

accessorObjdataObj를 보며 비교해보세요. 두 객체는 동일한 동작을 합니다. get 키워드에 함수를 정의하였습니다. 접근자 프로퍼티를 읽을 때는 함수를 호출하기 위해 소괄호를 사용할 필요가 없습니다. 즉, accessorObj.name(); 으로 쓰면 틀린 것입니다.

accessorObj.name을 읽을 때 name 함수는 실행되어 name key에 해당하는 값을 반환합니다.

key에 해당하는 값을 가져올 때 사용하여 get 함수는 getter로 불립니다. 만약 여러분이 accessorObj.name = 'New Person';로 값을 업데이트하면 실제로 갱신이 일어나지 않습니다. name 키에 해당하는 setter 함수가 없기 때문입니다. setter 함수는 getter 프로퍼티의 값들을 설정하는 데 사용됩니다.

const accessorObj = {
  _name: 'Arfat',
  get name() {
    return this._name;
  },
  set name(value) {
    this._name = value;
  }
};

setter 함수는 파라미터로 할당된 값을 받습니다. 프로퍼티에 값을 저장하거나 전역변수를 저장할 수 있습니다. 이 경우에서는 전통적인 "private" 프로퍼티인 _name을 만들고 해당 값에 name 값을 저장합니다.

getter 함수에서는 해당 값을 반환하기 전에 프로퍼티를 수정하거나 오버라이딩 할 수 있습니다. 다음 예제가 이를 설명해줍니다. 또한 위 질문들에 대한 답을 하나 해줍니다.

const obj = {
  get name() {
    return this._name.toUpperCase();
  },
  set name(value) {
    this._name = value;
  },
  get id() {
    return this._id.toString(2); // Returns binary of a number
  },
  set id(value) {
    this._id = value;
  }
}

obj.name = 'Arfat';
obj.name;
// => 'ARFAT'

obj.id = 5;
obj.id;
// => '101

평범한 데이터 프로퍼티가 있는데도 왜 접근자 프로퍼티를 사용해야 할까요? 프로퍼티 접근을 기록하거나 프로퍼티가 가진 값들에 대한 기록을 유지해야 하는 경우들이 꽤 존재합니다. 접근자 프로퍼티는 객체 프로퍼티의 사용을 쉽게 할 수 있는 함수의 힘을 제공합니다. 접근자 사용에 대해 더 알아보시려면 여기를 읽어보세요.

어떻게 자바스크립트는 프로퍼티가 접근자 프로퍼티인지 데이터 프로퍼티인지 알 수 있을까요? 알아봅시다.

객체 프로퍼티 디스크립터

언뜻 보기에는 객체의 키와 값 간에 1:1 매핑이 있는 것처럼 보일 수 있지만 전혀 사실이 아닙니다.

프로퍼티 어트리뷰트 (Property Attributes)

객체의 모든 키는 키에 관련된 값의 특성을 정의하는 프로퍼티 어트리뷰트의 집합을 포함합니다. key-value 쌍을 설명하는 _메타 데이터_로 생각할 수 있는 것이죠. 짧게 말하면, 어트리뷰트는 객체 프로퍼티의 상태를 정의하고 설명하기 위해 사용됩니다.

프로퍼티 어트리뷰트 집합은 프로퍼티 디스크립터로 불립니다.

디스크립터에는 6가지의 프로퍼티 어트리뷰트가 있습니다.

  • [[Value]]
  • [[Get]]
  • [[Set]]
  • [[Writable]]
  • [[Enumerable]]
  • [[Configurable]]

왜 어트리뷰트 이름이 [[]]로 감싸져 있을까요? 두 개의 대괄호는 ECMA 스펙에 따라 **내부 프로퍼티(internal properties)**를 의미합니다. 자바스크립트 프로그래머가 코드 내에서 직접적으로 건드릴 수 없는 프로퍼티인 것이죠. 내부 프로퍼티를 조작하기 위해서는 언어가 제공하는 메소드가 필요합니다.

예제를 봅시다.

위 이미지에서 객체는 x, y 2개의 키를 가집니다. 각 프로퍼티에 받는 어트리뷰트 목록을 볼 수 있죠.

자바스크립트에서 어떻게 동일한 정보를 얻을 수 있을까요? Object.getOwnPropertyDescriptor 함수를 사용해서 그 정보를 얻을 수 있습니다. 객체와 프로퍼티 이름을 받아서 필요한 어트리뷰트를 갖는 객체를 반환합니다. 여기 코드 샘플이 있습니다.

const object = {
  x: 5,
  y: 6
};

Object.getOwnPropertyDescriptor(object, 'x');
/* 
{ 
  value: 5, 
  writable: true, 
  enumerable: true, 
  configurable: true 
}
*/

어트리뷰트를 살펴보고 어떻게 도움이 되는지 알아봅시다. 그리고 다시 이 이미지로 오겠습니다.

[[Value]]

프로퍼티에 접근했을 때 반환되는 value를 저장합니다. 위 예에서 object.x로 값을 가져오려 할 때 사실은 [[Value]] 어트리뷰트에서 가져오는 것이죠. 데이터 프로퍼티에서 .으로 접근하거나 []으로 접근하는 것도 이런 식으로 작동합니다.

[[Get]]

getter 프로퍼티를 생성할 때 선언하는 함수에 대한 참조를 저장합니다. 프로퍼티에 대해 접근할 때마다 프로퍼티 값을 가져오기 위해 빈 인수 리스트와 함께 호출됩니다.

[[Set]]

setter 프로퍼티를 생성할 때 선언한 함수에 대한 참조를 저장합니다. 프로퍼티에 대해 접근할 때마다 할당된 값을 포함한 인수 목록을 유일한 인수로 함께 호출됩니다.

  set x(val) {
    console.log(val) // => 23
  }
}

obj.x = 23;

위 예에서 할당문의 오른쪽의 값은 setter 함수에 var 인자로 전달됩니다. 증거로 이 코드를 볼 수 있습니다.

[[Writable]]

이 값은 boolean 타입입니다. 값을 덮어쓸 수 있는지 아닌지에 대한 값이죠. false인 경우에는 프로퍼티의 값을 변경할 수 없습니다.

[[Enumerable]]

이 값도 boolean 타입입니다. 이 어트리뷰트는 for-in 루프에서 보이는지 아닌지를 알려줍니다. 만약 true라면 for-in 루프문에서 이 프로퍼티를 반복할 수 있습니다.

[[Configurable]]

이 값 역시 boolean입니다.

이 값이 false라면

  • 프로퍼티의 삭제가 불가능합니다.
  • 또한 데이터 프로퍼티접근자 프로퍼티로 또는 그 반대로 바꾸는 것도 불가능합니다. 즉 두 프로퍼티 타입의 변환이 불가능합니다.
  • 어트리뷰트 값을 바꾸는 것도 불가능합니다. enumerable configurable get set 모두 고정된 상태입니다.

이 프로퍼티의 영향은 프로퍼티 타입에 따라 다릅니다. 위에 설명한 특징과 별도로 또 다음과 같은 특징이 있습니다.

  • 프로퍼티가 데이터 프로퍼티라면, writabletrue에서 false로만 설정할 수 있습니다.
  • writable이 false가 되기 전에, [[Value]] 어트리뷰트의 변경이 가능합니다. 그러나 한번 writable이 false가 되면 configurable 또한 false가 됩니다. 프로퍼티가 쓸 수 없고 삭제할 수 없고 변경 불가능하게 되는 겁니다.

6가지 어트리뷰트 프로퍼티가 모든 프로퍼티 타입에 존재하는 것은 아닙니다.

  • 데이터 프로퍼티의 경우 value writable enumerable configurable 가 있습니다.
  • 접근자 프로퍼티의 경우 valuewritable 대신에 get set이 있습니다.

디스크립터 사용하기

디스크립터에 대해 배웠는데 이제 우리의 객체의 디스크립터를 어떻게 설정하고 변경할까요? 자바스크립트에는 이 디스크립터를 사용할 수 있는 여러 함수들이 있습니다. 살펴봅시다.

Obejct.getOwnPropertyDescriptor

위에서 봤던 함수입니다. 객체와 프로퍼티 이름을 받아 디스크립터를 포함한 객체나 undefined를 반환합니다.

Object.defineProperty

Object의 static 메소드로 주어진 객체에 대해 새로운 프로퍼티를 정의하거나 수정합니다. 객체, 프로퍼티 이름, 디스크립터 세 가지 인수를 받고 변경된 객체를 반환합니다. 예제를 봅시다.

const obj = {};

Object.defineProperty(obj, 'id', {
  value: 42
});

console.log(obj);
// => { }

console.log(obj.id);
// => 42

Object.defineProperty(obj, 'name', {
  value: 'Arfat',
  writable: false,
  enumerable: true,
  configurable: true
});

console.log(obj.name);
// => 'Arfat'

obj.name = 'Arfat Salman'

console.log(obj.name);
// => 'Arfat' 
// (instead of 'Arfat Salman')

Object.defineProperty(obj, 'lastName', {
  value: 'Salman',
  enumerable: false,
});

console.log(Object.keys(obj));
// => [ 'name' ]

delete obj.id;

console.log(obj.id);
// => 42

Object.defineProperties(obj, {
  property1: {
    value: 42,
    writable: true
  },
  property2: {}
});

console.log(obj.property1)
// => 42

길어보이지만 사실은 간단합니다. 하나씩 살펴볼까요.

  • 세 번째 줄에서, obj와 프로퍼티 이름 id, [[Value]] 필드를 의미하는 value 키에 42 값을 가지는 디스크립터 세 가지 인수를 defineProperty로 전달합니다.
  • enumberable이나 configurable과 같은 프로퍼티 어트리뷰트를 수정하지 못한다면 기본 값으로 false로 설정되어 있다는 것을 기억하세요. 이 경우에는 writable, enumerable, configurable 모두 false로 설정되어 있는 id를 가지고 있습니다.
  • 7번째 줄에서 obj를 출력하지만 id 프로퍼티가 열거 불가능한 속성이므로 출력되지 않습니다. 하지만 10번째 줄에서 보이듯이 프로퍼티는 존재합니다.
  • 13번째 줄에서 전체 디스크립터 집합을 정의합니다. writablefalse로 설정합니다.
  • 20번째 줄과 25번째 줄에서 name 프로퍼티를 출력합니다. 23번째 줄에서 값을 수정했지만 non-writability하기 때문에 수정 효과가 없습니다. 그래서 기존의 값이 두 번 보이는 거죠.
  • 37번 째 줄에서 id를 제거하려 하지만 configurablefalse로 되어 있기 때문에 39번째 줄에서 보이는 것처럼 제거되지 않았습니다.
  • 42번째 줄에서 defineProperty 의 배치 버전인 Object.defineProperties를 사용합니다.

Object.defineProperty는 한번에 하나의 프로퍼티만 설정합니다. 한 번에 여러 프로퍼티를 설정하려면 Object.defineProperties를 사용할 수 있습니다.

객체의 보호

아무도 객체를 변경하지 못하게 하고 싶을 때가 있습니다. 자바스크립트의 유연성 덕분에 건드리려고 하지 않았지만 실수로 객체에 프로퍼티를 할당하기가 쉽습니다. 자바스크립트에서 객체를 보호하는 주요 3가지 방법이 있습니다. 3가지 방법에 대해 알아봅시다.

Object.preventExtensions

Object.preventExtensions 메소드는 객체에 프로퍼티를 더하여 새로운 프로퍼티를 추가하는 것을 방지합니다. 객체를 받아 해당 객체가 확장되지 못하게 합니다.

그러나 프로퍼티를 삭제하는 것은 가능하니 주의하세요.

const obj = {
  id: 42
};

Object.preventExtensions(obj);

obj.name = 'Arfat';
console.log(obj); // => { id: 42 } 

Object.isExtensible을 사용하여 객체가 확장 불가능한지 확인할 수 있습니다. true를 반환한다면 해당 객체에 프로퍼티를 추가할 수 있는 것이죠.

Object.seal

seal 메소드는 객체를 봉인합니다. 봉인한다는 것은 -

  • Object.preventExtensions와 같이 새로운 프로퍼티를 추가하는 것을 막습니다.
  • 존재하는 모든 프로퍼티를 non-configurable하게 만듭니다.
  • 프로퍼티의 값들이 writable한 경우라면 여전히 변경 가능합니다.
  • 즉, 프로퍼티의 추가 및 삭제를 막는 것입니다.
const obj = {
  id: 42
};
Object.seal(obj);
delete obj.id  // (does not work)
obj.name = 'Arfat'; // (does not work)
console.log(obj); // => { id: 42 }
Object.isExtensible(obj); // => false
Object.isSealed(obj); //=> true

객체가 봉인되었는지 확인하려면 Object.isSealed를 사용하세요.

Object.freeze

freeze는 자바스크립트에서 객체가 가질 수 있는 가장 큰 보호막입니다.

  • Object.freeze를 사용하여 객체를 봉인합니다.
  • 존재하는 모든 프로퍼티의 _변경_을 막습니다.
  • 객체가 봉인되었기 때문에 디스크립터가 변경되는 것도 막습니다.
const obj = {
  id: 42
};

Object.freeze(obj);
delete obj.id  // (does not work)
obj.name = 'Arfat'; // (does not work)
console.log(obj); // => { id: 42 }
Object.isExtensible(obj); // => false
Object.isSealed(obj); //=> true
Object.isFrozen(obj); // => true

Object.isFrozen을 사용하여 객체가 얼었는지 확인할 수 있습니다.

중요한 점은 이 메소드들은 객체의 직접적인 프로퍼티만 다룰 수 있다는 것입니다. 중첩된 객체에 영향을 줄 수는 없습니다.

테이블 표로 정리된 것을 보십시오.

결론

자바스크립트에서 객체는 굉장히 흔하게 사용되기 때문에 객체의 진짜 힘을 아는 것이 중요합니다. 이 아티클이 효과적으로 잘 전달되었기를 바랍니다. 또한 아티클 초반에 있던 질문들에 대해 이제 답을 하실 수 있다면 좋겠습니다. 읽어주셔서 감사합니다.