writed by johan
자바스크립트의 내부 슬롯(Internal Slot)과 내부 메서드(Internal Method)는 ECMAScript 명세에서 사용되는 개념으로, 엔진이 어떻게 동작하는지를 설명하는 데 사용된다. 이들은 자바스크립트 엔진의 내부 동작 방식을 추상화하여 표현한 것이며, 일반적으로 개발자가 직접 접근하거나 조작할 수 없다.
단, 일부 내부 슬롯과 내부 메서드에 한하여 간접적으로 접근할 수 있는 수단을 제공하기는 한다.
예를 들어, 모든 객체는 [[Prototype]]이라는 내부 슬롯을 갖는다. 이 경우, __proto__
를 통해 간접적으로 내부 슬롯 [[Prototype]]
에 접근할 수 있다.
const o = {};
o.[[Prototype]] // Uncaught SyntaxError: Unexpected token '['
o.__proto__ // Object.prototype
내부 슬롯은 객체의 '상태'를 나타내는 속성이다. 예를 들어, 배열 객체는 [[Prototype]]
, [[Extensible]]
와 같은 기본적인 내부 슬롯을 가지고 있으며 추가로 배열의 길이([[ArrayLength]]
)나 배열 요소([[DefineOwnProperty]]
) 등을 관리하는 내부 슬롯을 가진다.
내부 메서드는 객체가 어떻게 동작해야 하는지 정의하는 메서드이다. 예를 들어, 모든 일반 객체는 속성에 접근하거나 설정할 때 호출되는 [[Get]]
, [[Set]]
등의 기본적인 내부 메서드를 가지고 있다.
아래 코드에서 obj
라는 객체가 있다고 할 때,
const obj = {a: 1};
우리가 코드 상에서 보기에 이 obj라는 객체에는 'a'라는 프로퍼티 하나만 있는 것처럼 보인다. 그러나 실제로 이 obj객체에 대해 ECMAScript 명세 상에서 보면, 여러가지 내장 슬롯과 내장 메서드들이 함께 작동하여 자바스크립트 엔진에서 해당 객체를 처리한다.
예를 들어, 우리가 'a' 프로퍼티 값을 가져오려면 다음과 같이 작성한다
console.log(obj.a);// 출력: 1
하지만 실제로 이 코드가 실행되면 자바스크립트 엔진은 obj객체의 [[Get]]
이라고 불리우는 내장 메서드를 호출하게 된다. 즉, 우리가 보기엔 단순한 코드처럼 보여도 실제로 ECMAScript 명세 상에서 본다면 많은 복잡한 과정들이 진행되고 있다.
이처럼 내부 슬롯과 내부 메서드는 자바스크립트 엔진의 동작을 이해하는 데 필요한 개념이지만, 일반적인 개발 작업에서는 직접적으로 사용하거나 접근하는 경우는 거의 없다.
자바스크립트 엔진은 프로퍼티를 생성할 때 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의한다.
자바스크립트에서 프로퍼티는 데이터 프로퍼티와 접근자 프로퍼티 두 가지 종류가 있다. 이들 각각은 서로 다른 어트리뷰트를 가진다.
키와 값으로 구성된 일반적인 프로퍼티다.
값([[Value]]
), 쓰기 가능([[Writable]]
), 열거 가능([[Enumerable]]
), 설정 가능([[Configurable]]
)이라는 네 가지 어트리뷰트를 갖는다.
◦ [[Value]]
: 해당 키에 연결된 값이다.
◦ [[Writable]]
: 해당 키의 값을 변경할 수 있는지 여부를 결정한다.
◦ [[Enumerable]]
: for-in 루프나 Object.keys() 같은 메서드에서 해당 키를 열거할 수 있는지 여부를 결정한다.
◦ [[Configurable]]
: 해당 키의 삭제, 속성 변경을 허용할 것인지 결정한다.
자체적으로는 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할때 호출되는 접근자 함수(accessor function)로 구성된 프로퍼티다.
getter([[Get]]
), setter([[Set]]
), 열거 가능([[Enumerable]]
), 설정 가능([Configurable]
)이라는 네 가지 어트리뷰트를 갖는다.
◦ [[Get]]
: 속성에 접근할 때 호출되는 함수이다.
◦ [[Set]]
: 속성에 값을 할당할 때 호출되는 함수이다.
◦ [Enumerable]
, [Configurable]
: 데이터 프로퍼티와 동일한 의미를 갖는다.
위와 같은 프로퍼티 어트리뷰트들은 자바스크립트 엔진이 관리하는 내부 상태 값인 내부 슬롯이다. 따라서 프로퍼티 어트리뷰트에 직접 접근할 수 없지만 Object.getOwnPropertyDescriptor 메서드를 사용하여 간접적으로 확인할 수는 있다.
이때 해당 메서드는 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터(PropertyDescriptor) 객체를 반환한다.
let obj = {
a: 1,
get b() {
return 2;
}
};
let descriptorA = Object.getOwnPropertyDescriptor(obj, 'a');
console.log(descriptorA);
// 출력: { value: 1, writable: true, enumerable: true, configurable: true }
let descriptorB = Object.getOwnPropertyDescriptor(obj, 'b');
console.log(descriptorB);
// 출력: { get: [Function], set: undefined, enumerable:true, configurable:true }
ES8에서는 모든 프로퍼티의 프로퍼티 어트리뷰트를 제공하는 Object.getOwnPropertyDescriptors 메서드가 도입되었다.
복잡한 비즈니스 로직이 필요한 경우나 데이터 유효성 검사가 필요한 경우 등에 사용된다.
setter를 통해 사용자가 잘못된 값을 입력하는 것을 방지하고, 필요한 경우 적절한 에러 메시지를 제공할 수 있다.
class User {
constructor(name) {
this._name = name;
}
get name() {
return this._name;
}
set name(value) {
if (value.length < 4) {
console.log('Name is too short.');
return;
}
this._name = value;
}
}
let user = new User("John");
user.name = ""; // Name is too short.
getter와 setter를 이용하면 객체의 내부 상태를 외부로부터 숨길 수 있다. 이는 캡슐화라는 객체 지향 프로그래밍의 핵심 원칙을 따르는 것이다.
class BankAccount {
constructor(balance) {
this._balance = balance;
}
// Getter: 외부에서 '_balance'에 직접 접근하지 않고, 'balance'라는 인터페이스로 값을 읽을 수 있게 한다.
get balance() {
return this._balance;
}
// Setter: 외부에서 '_balance'에 직접 접근하지 않고, 'balance'라는 인터페이스로 값을 변경할 수 있게 한다.
set balance(amount) {
if (amount < 0) {
console.log('Balance cannot be negative.');
return;
}
this._balance = amount;
}
}
let account = new BankAccount(1000);
console.log(account.balance); // 출력: 1000
account.balance = -500; // 출력: Balance cannot be negative.
console.log(account.balance); // 출력: 1000
account.balance = 2000;
console.log(account.balance); // 출력: 2000
getter나 setter 안에 로깅, 데이터 변환 등 추가적인 코드를 넣어 실행할 수 있다.
let user = {
_firstName: "John",
_lastName: "Doe",
get fullName() {
console.log('Getting full name');
return `${this._firstName} ${this._lastName}`;
},
set fullName(value) {
console.log('Setting full name');
const [firstName, lastName] = value.split(' ');
this._firstName = firstName;
this._lastName = lastName;
}
};
console.log(user.fullName); // 출력: Getting full name, John Doe
user.fullName = "Jane Smith"; // 출력: Setting full name
console.log(user.fullName); // 출력: Getting full name, Jane Smith
위 코드에서 접근자 프로퍼티 fullName을 통해 프로퍼티 값에 접근하면 내부적으로 다음과 같이 동작한다.
Object.getPropertyDescriptor() 메서드를 통해 get
혹은 set
속성이 존재하면 그것은 접근자 프로퍼티이며, 그렇지 않고 value
와 writable
속성이 존재하면 데이터 프로퍼티이다. 만약 어떤 속성도 없다면 해당 이름의 프로퍼티는 객체에 존재하지 않는다.
여기서 말하는 프로퍼티 정의란 새로운 프로퍼티를 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의하거나, 기존 프로퍼티의 프로퍼티 어트리뷰트를 재정의하는 것을 말한다.
이를 통해 객체의 프로퍼티가 어떻게 동작해야 하는지를 명확히 정의할 수 있다.
Object.defineProperty 메서드를 사용하면 프로퍼티 어트리뷰트를 정의할 수 있다.
이 메서드는 주로 라이브러리나 프레임워크를 작성하는 개발자들이 사용하며, 이들은 이 메서드를 통해 API의 공개 인터페이스를 세밀하게 제어하거나, 변경을 추적하는 등의 복잡한 동작을 구현한다.
또는 Vue.js와 같은 일부 프레임워크에서는 Object.defineProperty()
를 내부적으로 사용하여 데이터 바인딩 및 변경 감지 기능을 구현하기도 한다.
하지만 ES6 이후에 클래스와 getter/setter 문법이 도입되면서, 보다 간결하고 직관적인 방식으로 접근자 프로퍼티를 정의할 수 있게 되어 실무에서 Object.defineProperty()
보다는 클래스와 getter/setter 문법을 더 많이 사용하는 경향이 있다.
let obj = {};
Object.defineProperty(obj, 'name', {
value: 'John Doe',
writable: false,
enumerable: true,
configurable: false
});
console.log(obj.name); // 출력: John Doe
obj.name = 'Jane Smith';
console.log(obj.name); // 출력: John Doe, 변경되지 않았다.
Object.preventExtensions()
, Object.seal()
, Object.freeze()
메서드들은 객체의 변경 가능성을 제한하는 데 사용 된다.
Object.preventExtensions(obj)
: 이 메서드는 객체에 새로운 속성을 추가하는 것을 방지한다. 기존 속성의 값은 변경할 수 있고, 기존 속성 자체를 삭제할 수도 있다.
Object.seal(obj)
: 이 메서드는 객체에 새로운 속성을 추가하거나 기존 속성을 삭제하는 것을 방지한다. 또한, 기존 속성의 설정(configurable
, enumerable
, 등)도 변경할 수 없다. 그러나 writable 속성이 true인 경우, 기존 속성의 값을 변경할 수 있다.
Object.freeze(obj)
: 이 메서드는 객체에 대해 어떠한 변경도 허용하지 않는다. 즉, 새로운 속성 추가, 기존 속성 삭제, 기존 속성 값 변경 등이 모두 불가능하다.
Object.freeze()
메서드는 객체의 직접적인 속성만을 동결하기 때문에, 중첩된 객체(즉, 객체의 속성이 또 다른 객체인 경우)에 대해서는 객체를 값으로 갖는 모든 프로퍼티에 대해 재귀적으로 Object.freeze()
를 호출하여 깊은 동결(deep freeze)을 구현해야 한다.