클래스 필드는 기본적으로 공개(public
)이지만, 해시(#
) 접두사를 사용하여 비공개(private
) 클래스 멤버를 생성할 수 있습니다. 이러한 클래스 기능들의 개인 정보 보호는 JavaScript
자체에 의해 강제됩니다.
비공개 멤버는 이 문법이 존재하기 전에는 언어 자체에 내장되어 있지 않았습니다. 프로토타입 상속에서는 WeakMap
객체나 클로저를 사용하여 동작을 흉내 낼 수 있지만, #
문법의 편의성과는 비교할 수 없습니다.
class ClassWithPrivate {
#privateField;
#privateFieldWithInitializer = 42;
#privateMethod() {
// ...
}
static #privateStaticField;
static #privateStaticFieldWithInitializer = 42;
static #privateStaticMethod() {
// ...
}
}
추가적인 문법 제약사항이 있습니다.
static
) 및 인스턴스(instance
) 속성 사이에서 공유됩니다. 유일한 예외는 두 선언이 getter-setter
쌍을 정의할 때입니다.#constructor
일 수 없습니다.대부분의 클래스 기능은 비공개 상황에서의 대응 기능이 있습니다:
이러한 기능들은 비공개 속성(private properties
)이라고 일컫습니다. 그러나 생성자는 JavaScript
에서 비공개로 만들 수 없습니다. 클래스 외부에서 클래스의 인스턴스를 생성하는 것을 방지하기 위해서는 비공개 플래그를 사용해야 합니다.
비공개 속성은 #
이름(해시 이름)으로 선언되며, # 접두사가 있는 식별자입니다. 해시 접두사는 속성 이름의 고유한 부분입니다. 이전의 언더스코어(_
) 접두사 관례와 관련이 있지만, 일반적인 문자열 속성이 아니므로 대괄호 표기법으로 동적으로 액세스할 수는 없습니다.
클래스 외부에서 #
이름을 참조하는 것은 구문 오류입니다. 또한 클래스 본문에서 선언되지 않은 비공개 속성을 참조하거나 선언된 속성을 삭제하려고 하면 구문 오류가 발생합니다.
class ClassWithPrivateField {
#privateField;
constructor() {
this.#privateField = 42;
delete this.#privateField; // 구문 오류
this.#undeclaredField = 444; // 구문 오류
}
}
const instance = new ClassWithPrivateField();
instance.#privateField === 42; // 구문 오류
JavaScript
는 동적인 언어이므로 이러한 컴파일 타임 확인을 수행할 수 있습니다. 이는 해시 식별자 구문을 통해 가능한데, 이로 인해 일반적인 속성과는 구문 수준에서 다른 특성을 가지게 됩니다.
참고:
Chrome 콘솔
에서 실행되는 코드는 클래스 외부에서 비공개 속성에 액세스할 수 있습니다. 이는JavaScript
구문 제한의 개발자 도구에서만 허용됩니다.
객체에서 비공개 속성을 액세스할 때 해당 속성이 없는 경우, 일반적인 속성과는 달리 undefined
를 반환하는 대신 TypeError
가 발생합니다.
class C {
#x;
static getX(obj) {
return obj.#x;
}
}
console.log(C.getX(new C())); // undefined
console.log(C.getX({})); // TypeError: Cannot read private member #x from an object whose class did not declare it
in
연산자를 사용하여 가능성 있는 비공개 필드(또는 비공개 메서드)를 확인할 수 있습니다. 이렇게 하면 비공개 필드나 메서드가 존재하는 경우 true
를 반환하고, 그렇지 않으면 false
를 반환합니다.
비공개 필드가 누락될 가능성이 있는지 (또는 비공개 메서드) 확인하기 위해 in
연산자를 사용할 수 있습니다. 이는 비공개 필드나 메서드가 있는 경우 true
를 반환하고, 그렇지 않으면 false
를 반환합니다.
비공개 이름이 항상 미리 선언되고 삭제할 수 없다는 사실로부터의 직접적인 결과는 다음과 같습니다: 현재 클래스의 비공개 속성 중 하나를 가진 객체를 찾았다면 (try...catch
나 in
확인 중 얻은 경우), 모든 다른 비공개 속성을 가지고 있을 것입니다. 클래스의 비공개 속성을 가진 객체는 일반적으로 해당 클래스에 의해 생성된 것을 의미합니다 (항상 그렇지는 않음에 유의).
비공개 속성은 프로토타입 상속 모델의 일부가 아니며, 현재 클래스의 본문 내에서만 액세스할 수 있으며 하위 클래스에 상속되지 않습니다. 서로 다른 클래스 내에서 동일한 이름의 비공개 속성은 완전히 다른 속성으로 간주되며 서로 연동되지 않습니다. 이들은 각 인스턴스에 부착된 외부 메타데이터로 생각하고 클래스가 관리하는 것으로 관리됩니다.
비공개 속성에는 프로토타입 상속 모델에 포함되지 않으므로 현재 클래스 본문 내에서만 액세스할 수 있으며 하위 클래스에서 상속되지 않습니다. 서로 다른 클래스 내에서 동일한 이름의 비공개 속성은 완전히 다르며 상호 작용하지 않습니다. 이들은 클래스에 의해 생성된 각 인스턴스에 부착된 외부 메타데이터로 간주되고 클래스에 의해 관리됩니다.
비공개 필드와 메서드는 클래스의 내부에서 데이터와 기능을 은닉하는 데 사용됩니다. 이는 클래스를 조직화하고 외부에 노출되는 인터페이스를 제한하는 데 도움이 됩니다. 비공개 필드와 메서드는 클래스의 내부 구현 세부 정보를 숨기고 클래스의 외부 사용자가 잘못된 방식으로 클래스를 변경하거나 접근하는 것을 방지하는 데 도움이 됩니다.
비공개 속성은 클래스의 일부로써 표현되며, 클래스의 인스턴스에 연결된 외부 메타데이터로서의 역할을 합니다. 이를 통해 클래스의 내부 상태와 동작을 관리하고, 클래스를 보다 견고하고 유지보수 가능한 코드로 만들 수 있습니다.
비공개 필드에는 비공개 인스턴스 필드와 비공개 정적 필드가 있습니다. 비공개 필드는 클래스 선언 내부에서만 액세스할 수 있습니다.
공개(public
) 필드와 마찬가지로 비공개 인스턴스 필드는 다음과 같은 특징을 가집니다:
super()
가 호출된 직후에 추가됩니다.class ClassWithPrivateField {
#privateField;
constructor() {
this.#privateField = 42;
}
}
class Subclass extends ClassWithPrivateField {
#subPrivateField;
constructor() {
super();
this.#subPrivateField = 23;
}
}
new Subclass(); // 일부 개발 도구에서는 Subclass {#privateField: 42, #subPrivateField: 23}와 같이 표시됩니다.
참고: ClassWithPrivateField 기본 클래스의 #privateField는 ClassWithPrivateField에만 비공개이며, Subclass에서는 접근할 수 없습니다.
클래스의 생성자는 새로운 this
로 사용할 다른 객체를 반환할 수 있습니다. 파생 클래스에서 private
필드를 해당 반환된 객체에 정의할 수 있습니다. 이는 관련 없는 객체에 private
필드를 "찍어낼"
수 있는 것을 의미합니다.
class Stamper extends class {
// 생성자가 주어진 객체를 반환하는 기본 클래스
constructor(obj) {
return obj;
}
} {
// 이 선언은 기본 클래스 생성자가 반환한 객체에 private 필드를 "찍어냅니다."
#stamp = 42;
static getStamp(obj) {
return obj.#stamp;
}
}
const obj = {};
new Stamper(obj);
// `Stamper`는 `Base`를 호출하고 `obj`를 반환하므로 `obj`가
// 새로운 `this` 값이 됩니다. `Stamper`는 `obj`에 `#stamp`를 정의합니다.
console.log(obj); // 일부 개발 도구에서는 {#stamp: 42}와 같이 표시됩니다.
console.log(Stamper.getStamp(obj)); // 42
console.log(obj instanceof Stamper); // false
경고: 이는 혼란스러울 수 있는 작업입니다. 일반적으로 생성자에서 아무것도 반환하지 않는 것이 권장됩니다. 특히 this와 관련이 없는 것을 반환하는 경우에는 더욱 그렇습니다.
공개(public
) 정적 필드와 마찬가지로 비공개 정적 필드는 다음과 같은 특징을 가집니다:
class ClassWithPrivateStaticField {
static #privateStaticField = 42;
static publicStaticMethod() {
return ClassWithPrivateStaticField.#privateStaticField;
}
}
console.log(ClassWithPrivateStaticField.publicStaticMethod()); // 42
비공개 정적 필드에는 제약이 있습니다: 비공개 정적 필드를 정의한 클래스만이 해당 필드에 액세스할 수 있습니다. 이는 this
를 사용할 때 예상치 못한 동작을 일으킬 수 있습니다. 다음 예제에서는 Subclass.publicStaticMethod()
을 호출할 때 this
가 Subclass
클래스(아닌 ClassWithPrivateStaticField
클래스)를 가리키므로 TypeError
가 발생합니다.
class ClassWithPrivateStaticField {
static #privateStaticField = 42;
static publicStaticMethod() {
return this.#privateStaticField;
}
}
class Subclass extends ClassWithPrivateStaticField {}
Subclass.publicStaticMethod(); // TypeError: Cannot read private member #privateStaticField from an object whose class did not declare it
super
를 사용하여 메서드를 호출할 때도 마찬가지입니다. super
메서드는 super
클래스가 this
로 호출되지 않으므로 동일한 문제가 발생합니다.
class ClassWithPrivateStaticField {
static #privateStaticField = 42;
static publicStaticMethod() {
// super를 통해 호출될 때 `this`는 여전히 Subclass를 가리킵니다.
return this.#privateStaticField;
}
}
class Subclass extends ClassWithPrivateStaticField {
static callSuperMethod() {
return super.publicStaticMethod();
}
}
Subclass.callSuperMethod(); // TypeError: Cannot read private member #privateStaticField from an object whose class did not declare it
메서드를 호출할 때는 항상 this
대신 클래스 이름을 통해 비공개 정적 필드에 접근하는 것이 좋습니다. 이렇게 하면 상속이 메서드를 깨뜨리지 않습니다.
비공개 메서드에는 비공개 인스턴스 메서드와 비공개 정적 메서드가 있습니다. 비공개 메서드는 클래스 선언 내부에서만 액세스할 수 있습니다.
공개(public
) 메서드와는 달리 비공개 인스턴스 메서드는 다음과 같은 특징을 가집니다:
.prototype
속성이 아닌 클래스의 인스턴스에서만 사용할 수 있습니다.class ClassWithPrivateMethod {
#privateMethod() {
return 42;
}
publicMethod() {
return this.#privateMethod();
}
}
const instance = new ClassWithPrivateMethod();
console.log(instance.publicMethod()); // 42
비공개 인스턴스 메서드는 제너레이터(generator
), 비동기(asyhnc
), 비동기 제너레이터(async generator
) 함수일 수 있습니다. 비공개 getter
와 setter
도 가능하며, 공개 getter
와 setter
와 동일한 구문 요구사항을 따릅니다.
class ClassWithPrivateAccessor {
#message;
get #decoratedMessage() {
return `🎬${this.#message}🛑`;
}
set #decoratedMessage(msg) {
this.#message = msg;
}
constructor() {
this.#decoratedMessage = "hello world";
console.log(this.#decoratedMessage);
}
}
new ClassWithPrivateAccessor(); // 🎬hello world🛑
공개 메서드와 달리 비공개 메서드는 클래스의 .prototype
속성에서 액세스할 수 없습니다.
class C {
#method() {}
static getMethod(x) {
return x.#method;
}
}
console.log(C.getMethod(new C())); // [Function: #method]
console.log(C.getMethod(C.prototype)); // TypeError: Receiver must be an instance of class C
공개 정적 메서드와 마찬가지로 비공개 정적 메서드는 다음과 같은 특징을 가집니다:
class ClassWithPrivateStaticMethod {
static #privateStaticMethod() {
return 42;
}
static publicStaticMethod() {
return ClassWithPrivateStaticMethod.#privateStaticMethod();
}
}
console.log(ClassWithPrivateStaticMethod.publicStaticMethod()); // 42
비공개 정적 메서드는 제너레이터(generator
), 비동기(async
), 비동기 제너레이터(async generator
) 함수일 수 있습니다.
비공개 정적 필드에 대해 이전에 언급한 제한이 비공개 정적 메서드에도 적용되며, 마찬가지로 this
사용 시 예상치 못한 동작을 일으킬 수 있습니다. 다음 예제에서는 Subclass.publicStaticMethod()
를 호출할 때 this
가 Subclass
클래스(ClassWithPrivateStaticMethod
클래스가 아닌)를 가리키므로 TypeError
가 발생합니다.
class ClassWithPrivateStaticMethod {
static #privateStaticMethod() {
return 42;
}
static publicStaticMethod() {
return this.#privateStaticMethod();
}
}
class Subclass extends ClassWithPrivateStaticMethod {}
console.log(Subclass.publicStaticMethod()); // TypeError: Cannot read private member #privateStaticMethod from an object whose class did not declare it
다른 많은 언어에서는 생성자를 비공개로 표시하여 클래스 외부에서 인스턴스를 생성하는 것을 방지할 수 있습니다. 즉, 인스턴스를 생성하는 정적 팩토리 메서드(static factory method
)만 사용하거나 인스턴스를 생성할 수 없도록 할 수 있습니다. JavaScript
에는 이를 위한 기본 기능이 없지만, 비공개 정적 플래그(private static flag
)를 사용하여 구현할 수 있습니다.
class PrivateConstructor {
static #isInternalConstructing = false;
constructor() {
if (!PrivateConstructor.#isInternalConstructing) {
throw new TypeError("PrivateConstructor is not constructable");
}
PrivateConstructor.#isInternalConstructing = false;
// 추가적인 초기화 로직
}
static create() {
PrivateConstructor.#isInternalConstructing = true;
const instance = new PrivateConstructor();
return instance;
}
}
new PrivateConstructor(); // TypeError: PrivateConstructor is not constructable
PrivateConstructor.create(); // PrivateConstructor {}
사실 지금 이렇게 번역했지만... 내용을 전부 이해하지는 못했다. 그래도 나중에 다시 번역해서 이해하기 보다는 미리 번역해두면 더 편할 것 같아서 미리 번역해두는 거다. 일단 간단하게 예습 정도 해야할 일이 있어서 이렇게 글을 작성했고 다음에 더 깊게 공부하게 된다면, 해당 자료를 꼭 참고해야겠다.