-
내부 슬릇
- 자바스크립트 엔진의 구현 알고리즘을 설명하기 위해 ECMAscript 사양에서 사용하는 의사 프로퍼티(pseudo property)
-
내부 메서드
- 자바스크립트 엔진의 구현 알고리즘을 설명하기 위해 ECMAscript 사양에서 사용하는 의사 메서드(pseudo method)
-
추가설명
내부 슬롯(Internal Slot)과 내부 메서드(Internal Method)는 JavaScript 언어의 동작을 정의하는 ECMAScript 명세에서 사용되는 특별한 개념입니다. 이 개념들을 이해하기 위해 차근차근 살펴보겠습니다.
먼저 기본적인 맥락을 이해해 봅시다. ECMAScript는 JavaScript의 표준화된 명세로, JavaScript가 어떻게 동작해야 하는지 정의합니다. 이 명세는 JavaScript 엔진(Chrome의 V8, Firefox의 SpiderMonkey 등)이 어떻게 구현되어야 하는지 알려주는 청사진과 같습니다.
내부 슬롯은 JavaScript 객체가 내부적으로 가지고 있는 '비밀 저장소'라고 생각할 수 있습니다.
- 실제 코드에서 직접 접근할 수 없습니다(몇 가지 예외 제외).
- 명세에서는 이중 대괄호
[[...]]로 표기합니다. - JavaScript 엔진이 내부적으로 사용하는 데이터를 저장합니다.
[[Prototype]]: 객체의 프로토타입을 저장하는 내부 슬롯(우리는Object.getPrototypeOf()나__proto__를 통해 간접적으로 접근)[[Value]]: 원시 값을 래핑한 객체(예: String, Number)가 실제 원시값을 저장하는 내부 슬롯
자동차의 엔진 내부 부품들을 생각해보세요. 운전자는 이 부품들을 직접 볼 수 없지만, 자동차가 작동하는 데 필수적입니다. 내부 슬롯은 JavaScript 객체 내부의 '비밀 부품'과 같습니다.
내부 메서드는 JavaScript 객체가 내부적으로 수행하는 '비밀 작업'입니다.
- 역시 코드에서 직접 호출할 수 없습니다.
- 명세에서는
[[...]]형태로 표기합니다. - JavaScript 엔진이 특정 연산을 수행할 때 내부적으로 호출하는 알고리즘입니다.
[[Get]]: 객체의 프로퍼티를 읽을 때 내부적으로 호출되는 메서드[[Set]]: 객체의 프로퍼티에 값을 할당할 때 호출되는 메서드[[Call]]: 함수를 호출할 때 내부적으로 실행되는 메서드
자동차의 기어를 변속할 때, 단순히 기어 레버를 움직이지만 내부적으로는 복잡한 기계적 작업이 수행됩니다. 내부 메서드는 JavaScript에서 우리가 간단한 연산(예:
obj.prop)을 할 때 엔진이 내부적으로 수행하는 복잡한 작업입니다."의사(pseudo)"라는 표현은 이 슬롯과 메서드가 실제 JavaScript 코드 상에서는 존재하지 않지만, 명세에서 언어의 동작을 설명하기 위해 개념적으로 존재한다는 것을 의미합니다.
실제로 JavaScript 코드를 작성할 때는 내부 슬롯이나 내부 메서드를 직접 사용할 수 없습니다. 이들은 명세서를 작성하는 사람들과 JavaScript 엔진을 구현하는 개발자들을 위한 개념적 도구입니다.
let person = { name: "Alice" }; console.log(person.name); // "Alice"
위 코드에서
person.name을 통해 프로퍼티에 접근할 때, JavaScript 엔진은 내부적으로person객체의[[Get]]내부 메서드를 호출합니다. 이 메서드는 대략 다음과 같은 작업을 수행합니다:- 객체 자신이
name프로퍼티를 가지고 있는지 확인 - 있다면 해당 값을 반환
- 없다면 객체의
[[Prototype]]내부 슬롯을 확인하여 프로토타입 체인을 따라 검색
이 모든 과정이
person.name이라는 간단한 표현식 내부에서 자동으로 이루어집니다.내부 슬롯과 내부 메서드는:
- ECMAScript 명세서에서 JavaScript의 동작을 정의하기 위해 사용하는 개념적 장치입니다.
- 실제 JavaScript 코드에서는 직접 접근/호출할 수 없습니다.
- JavaScript 엔진의 내부 구현 세부사항을 추상화한 것입니다.
- 명세서에서는 이중 대괄호
[[...]]로 표기합니다.
JavaScript 개발자로서 내부 슬롯과 내부 메서드를 이해하면 언어가 어떻게 동작하는지 더 깊이 이해할 수 있습니다. 하지만 일상적인 코딩에서는 이들을 직접 다루지 않습니다. 대신 언어가 제공하는 공개 API(메서드, 속성 등)를 통해 간접적으로 사용합니다.
내부 슬릇, 메서드는 이중 대괄호로 감싸져있음 [[ ]]
- 자바스크립트 엔진은 프로퍼티를 생성할 때 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의함
- 프로퍼티의 상태란?
- 프로퍼티의 값 , value
- 값의 갱신여부, writable
- 열거 가능여부, enumerable
- 재정의 가능여부, configurable
- 프로퍼티 어트리뷰트는 내부슬릇으로 정의되어있음
[[Value]][[Writable]][[Enumerable]][[Configurable]]
- 프로퍼티 어트리뷰트는 직접 접근 불가
Object.getOwnPropertyDescriptor로 간접적으로 확인가능
- 프로퍼티 어트리뷰트 정보를 제공하는 객체
const person = {
name: 'Lee'
};
// 프로퍼티 동적 생성
person.age = 20;
// 모든 프로퍼티의 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체들을 반환한다.
console.log(Object.getOwnPropertyDescriptos(person));
{
age: {value: 20, writable: true, enumerable: true, configurable: true}
name: {value: 'Lee', writable: true, enumerable: true, configurable: true}
[[Prototype]]: Object
}Object.getOwnPropertyDescriptos()메서드는 프로퍼티 디스크립터 객체를 반환함
property는 데이터와 접근자 프로퍼티로 구분할 수 있음
- 키와 값으로 구성된 일반적인 프로퍼티
- 엔진이 프로퍼티 생성할 때 아래와 같이 기본값으로 자동 정의
| 특성 이름 | 내부 슬롯 표기 | 설명 | 기본값 |
|---|---|---|---|
| 값(value) | [[Value]] |
프로퍼티의 실제 값 | undefined |
| 쓰기 가능(writable) | [[Writable]] |
값 변경 가능 여부. false일 경우 읽기 전용 |
true |
| 열거 가능(enumerable) | [[Enumerable]] |
반복문(for...in)에서 열거 가능 여부 | true |
| 설정 가능(configurable) | [[Configurable]] |
프로퍼티 삭제, 특성 변경 가능 여부 | true |
-
[[Value]]: 프로퍼티에 접근했을 때 반환되는 값console.log(user.name); // "홍길동" ([[Value]]에 저장된 값 반환)
-
[[Writable]]:false로 설정하면 값 변경 시도가 무시됨// age의 [[Writable]]이 false인 경우 user.age = 50; // 무시됨 console.log(user.age); // 30 (기존 값 유지)
-
[[Enumerable]]:false로 설정하면 반복문에서 건너뜀Object.defineProperty(user, "password", { value: "1234", enumerable: false // 열거 불가능 설정 }); for (let key in user) { console.log(key); // "name", "age"만 출력됨, "password"는 건너뜀 } console.log(Object.keys(user)); // ["name", "age"]
-
[[Configurable]]:false로 설정하면 프로퍼티 삭제 및 특성 재정의 불가// age의 [[Configurable]]이 false인 경우 delete user.age; // 무시됨 // 아래 코드는 오류 발생 - 특성 변경 불가 Object.defineProperty(user, "age", { writable: true });
- 자체적으로 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호풀되는 접근자 함수(accessor function) 으로 구성된 프로퍼티
| 특성 이름 | 내부 슬롯 표기 | 설명 | 기본값 |
|---|---|---|---|
| 획득자(getter) | [[Get]] |
프로퍼티를 읽을 때 호출되는 함수 | undefined |
| 설정자(setter) | [[Set]] |
프로퍼티에 값을 할당할 때 호출되는 함수 | undefined |
| 열거 가능(enumerable) | [[Enumerable]] |
반복문(for...in)에서 열거 가능 여부 | |
| 데이터 프로퍼티와 같음 | false (정의 시) |
||
| 설정 가능(configurable) | [[Configurable]] |
프로퍼티 삭제, 특성 변경 가능 여부 | |
| 데이터 프로퍼티와 같음 | false (정의 시) |
-
[[Get]]: 프로퍼티를 읽을 때 호출되는 함수console.log(user.age); // get 함수가 호출되어 _age 값 반환
-
[[Set]]: 프로퍼티에 값을 할당할 때 호출되는 함수user.age = 40; // set 함수가 호출되어 _age 값을 변경
-
[[Enumerable]]:false로 설정하면 반복문에서 건너뜀Object.defineProperty(user, 'secretInfo', { get() { return "비밀 정보"; }, enumerable: false // 열거 불가능 설정 }); for (let key in user) { console.log(key); // "secretInfo"는 나타나지 않음 }
-
[[Configurable]]:false로 설정하면 프로퍼티 삭제 및 특성 재정의 불가Object.defineProperty(user, 'readOnlyInfo', { get() { return "읽기 전용 정보"; }, configurable: false // 설정 불가능 설정 }); // 아래 코드는 오류 발생 - 특성 변경 불가 Object.defineProperty(user, 'readOnlyInfo', { enumerable: false }); // 아래 코드도 실패 - 삭제 불가 delete user.readOnlyInfo; // false
const person = {
// 데이터 프로퍼티
firstName: "홍",
lastName: "길동",
// fullName은 접근자 함수로 구성된 접근자 프로퍼티임
// 접근자 프로퍼티 (getter)
**get fullName() {**
return `${this.firstName} ${this.lastName}`;
},
// 접근자 프로퍼티 (setter)
**set fullName(name) {**
[this.firstName, this.lastName] = name.split(' ');
}
};
// 데이터 프로퍼티를 통한 프로퍼티 값의 참조
console.log(person.firstName + ' ' + person.lastName);
// getter 호출
console.log(person.fullName); // "홍 길동"
// setter 호출
person.fullName = "김 철수";
console.log(person.firstName); // "김"
console.log(person.lastName); // "철수"접근자 프로퍼티는 데이터 프로퍼티와 달리 [[Value]]와 [[Writable]] 특성이 없습니다. 대신 [[Get]]과 [[Set]] 함수가 있어 프로퍼티 접근과 수정 동작을 제어합니다.
접근자 프로퍼티는 다음과 같은 상황에서 유용하게 사용됩니다:
- 데이터 유효성 검사: setter에서 할당하려는 값의 유효성을 검사할 수 있습니다.
- 계산된 프로퍼티: 다른 프로퍼티를 기반으로 값을 계산할 수 있습니다.
- 내부 구현 은닉: 내부 상태를 직접 노출하지 않고 제어된 방식으로 접근할 수 있게 합니다.
- 부수 효과(side effect) 추가: 프로퍼티 접근이나 수정 시 로깅, 알림 등의 부수 효과를 추가할 수 있습니다.
- 호환성 유지: API를 변경하면서도 이전 버전과의 호환성을 유지할 수 있습니다.
-
추가설명
맞습니다! 객체지향 프로그래밍에서는 접근자 프로퍼티(getter와 setter)를 활용하는 것이 좋은 설계 방식입니다. 이것은 "캡슐화(encapsulation)"라는 중요한 객체지향 원칙과 직접적으로 연결됩니다.
데이터 프로퍼티에 직접 접근하는 대신 접근자 프로퍼티를 사용하면 다음과 같은 이점이 있습니다:
class BankAccount { #balance = 0; // 비공개 필드 get balance() { return this.#balance; } set balance(value) { if (typeof value !== 'number') { throw new Error('잔액은 숫자여야 합니다'); } if (value < 0) { throw new Error('잔액은 0보다 작을 수 없습니다'); } this.#balance = value; } } const account = new BankAccount(); account.balance = 100; // 정상 작동 // account.balance = -50; // 오류: 잔액은 0보다 작을 수 없습니다
setter를 통해 데이터가 항상 유효한 상태를 유지하도록 보장할 수 있습니다.
class Person { #firstName = ''; #lastName = ''; constructor(firstName, lastName) { this.#firstName = firstName; this.#lastName = lastName; } get fullName() { return `${this.#firstName} ${this.#lastName}`; } set fullName(name) { const parts = name.split(' '); this.#firstName = parts[0] || ''; this.#lastName = parts[1] || ''; } }
내부적으로 이름을 어떻게 저장하는지는 외부에서 알 필요가 없습니다. 구현이 변경되어도 인터페이스는 동일하게 유지됩니다.
class Circle { #radius = 0; constructor(radius) { this.radius = radius; // setter 호출 } get radius() { return this.#radius; } set radius(value) { if (value < 0) throw new Error('반지름은 음수가 될 수 없습니다'); this.#radius = value; } get area() { return Math.PI * this.#radius * this.#radius; } get circumference() { return 2 * Math.PI * this.#radius; } } const circle = new Circle(5); console.log(circle.area); // 78.54...
area와 같은 계산된 프로퍼티는 접근할 때마다 최신 결과를 제공합니다.class Observable { #value = null; #observers = []; constructor(value) { this.#value = value; } get value() { return this.#value; } set value(newValue) { const oldValue = this.#value; this.#value = newValue; // 값이 변경되면 옵저버에게 알림 if (oldValue !== newValue) { this.#notifyObservers(oldValue, newValue); } } #notifyObservers(oldValue, newValue) { for (const observer of this.#observers) { observer(oldValue, newValue); } } subscribe(observer) { this.#observers.push(observer); } }
setter를 통해 값이 변경될 때 로깅, 이벤트 발생, UI 업데이트 등의 작업을 수행할 수 있습니다.
class Legacy { getFirstName() { /* ... */ } getLastName() { /* ... */ } } class Modern { get firstName() { /* ... */ } get lastName() { /* ... */ } } // Legacy 클래스를 Modern 인터페이스로 적응시키기 class Adapter { #legacy; constructor(legacy) { this.#legacy = legacy; } get firstName() { return this.#legacy.getFirstName(); } get lastName() { return this.#legacy.getLastName(); } }
접근자 프로퍼티를 통해 기존 코드를 변경하지 않고도 일관된 인터페이스를 제공할 수 있습니다.
접근자 프로퍼티는 다음과 같은 객체지향 원칙을 지원합니다:
- 캡슐화(Encapsulation): 객체의 내부 상태를 외부로부터 숨기고, 오직 허용된 메서드를 통해서만 접근하도록 합니다.
- 정보 은닉(Information Hiding): 구현 세부사항을 숨겨 객체 간의 결합도를 낮춥니다.
- 불변성(Immutability): getter만 제공하고 setter를 제공하지 않음으로써 특정 속성을 불변으로 만들 수 있습니다.
- 계약에 의한 설계(Design by Contract): setter에서 전제조건을 검사함으로써 객체가 항상 유효한 상태를 유지하도록 보장합니다.
실제 프로젝트에서는 모든 프로퍼티에 접근자를 사용하는 것이 항상 최선이 아닐 수 있습니다. 다음 가이드라인을 고려해보세요:
- 중요한 비즈니스 규칙이 있는 프로퍼티에는 접근자를 사용하세요. 예를 들어, 나이, 금액, 날짜 등은 특정 규칙에 따라 검증이 필요할 수 있습니다.
- 내부 구현이 변경될 가능성이 있는 프로퍼티에는 접근자를 사용하세요. 이렇게 하면 나중에 구현을 변경하더라도 외부 인터페이스는 동일하게 유지할 수 있습니다.
- 단순한 데이터 구조에서는 직접 접근이 더 명확하고 간결할 수 있습니다. 예를 들어, 값 객체(Value Object)나 데이터 전송 객체(DTO)와 같은 경우입니다.
- 성능이 중요한 경우에는 접근자 오버헤드를 고려하세요. 접근자 호출은 직접 프로퍼티 접근보다 약간의 오버헤드가 있습니다.
TypeScript나 최신 JavaScript(ES2022+)에서는 비공개 필드(
#)를 사용하여 진정한 캡슐화를 구현할 수 있습니다:class Person { #age = 0; get age() { return this.#age; } set age(value) { if (value >= 0 && value <= 120) { this.#age = value; } else { throw new Error('유효하지 않은 나이입니다'); } } }
이 방식은 객체지향 설계 원칙을 JavaScript에서 제대로 구현할 수 있게 해줍니다.
네, 객체지향적으로 코드를 작성할 때는 데이터 프로퍼티에 직접 접근하기보다 접근자 프로퍼티를 통해 접근하는 것이 좋습니다. 이는 코드의 안정성, 유지보수성, 그리고 내부 상태의 일관성을 유지하는 데 도움이 됩니다. 객체의 내부 상태는 해당 객체가 완전히 제어할 수 있어야 하며, 접근자 프로퍼티는 이러한 제어를 가능하게 해주는 중요한 메커니즘입니다.
객체는 변경 가능한 값이므로 재할당 없이 직접 변경할 수 있다. 이걸 막고자하면 여러 메서드를 사용하면 된다.
| 메서드 | 프로퍼티 추가 | 프로퍼티 삭제 | 프로퍼티 값 변경 | 프로퍼티 특성 변경 | 프로토타입 변경 |
|---|---|---|---|---|---|
Object.preventExtensions(obj) |
❌ 금지 | ⭕ 가능 | ⭕ 가능 | ⭕ 가능 | ❌ 금지 |
Object.seal(obj) |
❌ 금지 | ❌ 금지 | ⭕ 가능 | ❌ 금지 | ❌ 금지 |
Object.freeze(obj) |
❌ 금지 | ❌ 금지 | ❌ 금지 | ❌ 금지 | ❌ 금지 |
Object.preventExtensions- 가장 약한 제한
- 객체에 새로운 프로퍼티를 추가할 수 없게 해줌
Object.seal(obj)- 중간 정도의 제한
- configurable 을 false로 설정하여 추가,삭제, 특성 변경을 제한함
- 프로퍼티의 값은 변경 가능!
Object.freeze(obj)- 가장 강한 제한
- 추가, 삭제, 값 재할당, 특성 변경 다 불가능
- 얘들은 얕은 제한만을 검
- 중첩된 객체에는 영향이 없음
- 중첩된 객체까지 제한하고 싶으면 재귀적으로 해야함
- 중첩된 객체에는 영향이 없음