클래스 속성을 밖에서 함부로 읽거나 고치지 못하게 막는 방법은 크게 두 가지입니다. 타입스크립트의 private·protected는 타입 검사 때만 접근을 막고 실행 전에는 흔적 없이 지워지는 표기이고, 자바스크립트의 # 비공개 필드는 타입을 지운 실행 때에도 밖에서 접근할 수 없도록 막는 런타임 문법입니다.
이전 레슨에서 만든 StampCard의 도장 개수(stamps)를 밖에서 card.stamps = 999처럼 직접 조작하지 못하게 숨기면서, public·private·protected와 # 필드가 타입 검사 단계와 Node 실행 단계에서 각각 어떻게 다르게 동작하는지 비교해 봅니다.
1public · private · protected 세 가지 접근 범위
타입스크립트는 속성이나 메서드 앞에 접근 범위를 적는 세 가지 키워드를 제공합니다.
public(기본값): 아무것도 안 적으면public입니다. 클래스 안팎 어디서나 읽고 쓸 수 있습니다.private: 선언한 그 클래스 본문 안에서만 쓸 수 있습니다. 자식 클래스나 클래스 밖에서는 막힙니다(TS2341).protected: 선언한 클래스 안과extends로 물려받은 자식 클래스 안에서만 쓸 수 있습니다. 클래스 밖에서는 막힙니다(TS2445).
먼저 private으로 stamps를 감추고 add와 count 메서드로만 다루는 StampCard를 작성해 봅니다.
class StampCard {
public owner: string;
private stamps: number = 0;
constructor(owner: string) {
this.owner = owner;
}
add(n: number): number {
this.stamps += n;
return this.stamps;
}
count(): number {
return this.stamps;
}
}
클래스 안의 add와 count 메서드는 this.stamps를 자유롭게 읽고 고칩니다.
const card =
new StampCard("민지");
console.log(card.add(2));
console.log(card.count());
2
2반면 protected는 extends로 상속받은 자식 클래스까지는 접근을 열어 주고, 인스턴스 바깥에서 직접 읽는 것만 막습니다.
class BaseCard {
protected stamps: number = 0;
}
class VipCard extends BaseCard {
bonus(): number {
return this.stamps + 5;
}
}
const vip = new VipCard();
console.log(vip.stamps);
main.ts(12,17): error TS2445: Property 'stamps' is protected and only accessible within class 'BaseCard' and its subclasses.2private과 #의 결정적 차이: 타입 지우기 뒤
그렇다면 private stamps로 선언한 속성을 클래스 밖에서 card.stamps로 읽으면 타입 검사와 실행 때 각각 어떻게 될까요?
먼저 private stamps를 클래스 밖에서 읽는 코드를 검사하고 실행해 봅니다.
class StampCard {
public owner: string;
private stamps: number = 0;
constructor(owner: string) {
this.owner = owner;
}
}
const card =
new StampCard("민지");
console.log(card.stamps);
main.ts(12,18): error TS2341: Property 'stamps' is private and only accessible within class 'StampCard'.0타입 검사기는 TS2341 오류를 내지만, Node가 타입을 지우고 실행할 때는 private 키워드가 공백으로 사라져 평범한 stamps = 0 속성이 됩니다. 그래서 실행 결과로 0이 그대로 찍힙니다.
이번에는 자바스크립트 비공개 필드 #stamps를 쓰고 클래스 밖에서 card.#stamps를 읽어 봅니다.
class StampCard {
owner: string;
#stamps: number = 0;
constructor(owner: string) {
this.owner = owner;
}
}
const card =
new StampCard("민지");
console.log(card.#stamps);
main.ts(12,18): error TS18013: Property '#stamps' is not accessible outside class 'StampCard' because it has a private identifier.main.ts:12console.log(card.#stamps); ^ SyntaxError: Private field '#stamps' must be declared in an enclosing class#stamps는 타입을 지운 뒤에도 #stamps 그대로 남아, 타입 검사 때는 TS18013으로 막히고 실행 때에도 자바스크립트 엔진이 SyntaxError로 접근을 원천 차단합니다.
| 구분 | private stamps | #stamps |
|---|---|---|
| 누구의 문법인가 | 타입스크립트 전용 표기 | 자바스크립트 표준 문법 |
| 타입 지우기 뒤 | private이 지워져 stamps가 됨 | #stamps 그대로 남음 |
| 밖에서 읽을 때(검사) | TS2341 오류 | TS18013 오류 |
| 밖에서 읽을 때(실행) | 값(0)이 그대로 읽힘 | SyntaxError로 막힘 |
Object.keys(card) | 'stamps'가 목록에 나옴 | 목록에 나오지 않음 |
예제
실행 결과를 먼저 적어 본 뒤 펼쳐 보세요. 맞혀 보기는 점수에 들지 않습니다.
잔액을 #balance 비공개 필드로 숨기고 deposit 메서드로 입금하는 Account 클래스입니다.
class Account {
readonly owner: string;
#balance: number = 0;
constructor(owner: string) {
this.owner = owner;
}
deposit(n: number): number {
this.#balance += n;
return this.#balance;
}
}
const acc = new Account("민지");
console.log(acc.deposit(5000));
console.log(acc.deposit(3000));
무엇이 차례로 찍힐까요?
실행 결과 보기
5000
8000#balance는 클래스 안의 deposit 메서드에서는 this.#balance로 읽고 쓸 수 있지만, 클래스 밖에서는 타입 검사(TS18013)와 실행(SyntaxError) 양쪽에서 모두 안전하게 보호됩니다.
자주 틀리는 곳
틀린 생각: private을 붙이면 실행 때나 Object.keys에서도 속성이 숨겨진다.
private은 오직 타입 검사기 눈에만 보이는 표기입니다. 실행 시점에는 일반 속성과 똑같아서 Object.keys나 JSON.stringify를 부르면 private 속성이 밖으로 고스란히 드러납니다.
class Account {
public id: string;
private pin: number = 1234;
#secret: number = 9999;
constructor(id: string) {
this.id = id;
}
}
const acc = new Account("a1");
console.log(Object.keys(acc));
[ 'id', 'pin' ]맞는 규칙: private pin은 'pin'으로 드러나지만 #secret은 전혀 나오지 않습니다(타입이 실행 때 남는다는 생각). 실행 중에도 밖으로 새어 나가면 안 되는 값은 반드시 # 필드로 선언합니다.
틀린 생각: 부모의 private 속성은 extends로 상속받은 자식 클래스에서 읽을 수 있다.
부모가 private이나 #으로 감춘 속성은 자식 클래스에서도 읽을 수 없습니다.
class BaseCard {
private stamps: number = 0;
}
class VipCard extends BaseCard {
read(): number {
return this.stamps;
}
}
main.ts(7,17): error TS2341: Property 'stamps' is private and only accessible within class 'BaseCard'.맞는 규칙: 자식 클래스에서 읽어야 하는 속성은 private 대신 protected로 선언합니다.
확인 문제
1. private stamps: number = 0;으로 선언한 속성은 타입을 지우고 Node로 실행하면 어떻게 되나요?
힌트
2절의 표에서 private stamps가 타입 지우기 뒤에 어떻게 바뀌는지 살펴보세요.
정답
private과 : number가 모두 지워져 일반 속성 stamps = 0;이 됩니다. 따라서 타입 검사 때는 TS2341로 막히지만, 실행 시점에는 밖에서 읽어도 0이 그대로 나오고 Object.keys에도 드러납니다(타입이 실행 때 남는다는 생각).
2. 실행 중에도 밖에서 읽거나 Object.keys로 꺼낼 수 없게 완전히 숨기려면 어떻게 선언해야 하나요?
힌트
자바스크립트 표준 비공개 필드 기호를 떠올려 보세요.
정답
자바스크립트 비공개 필드인 #stamps: number = 0;처럼 이름 앞에 #을 붙여 선언합니다. # 필드는 타입을 지운 뒤에도 그대로 남아 실행 때 밖에서 접근하면 SyntaxError로 막힙니다.
3. extends로 물려받은 자식 클래스 안에서는 읽을 수 있게 하되 클래스 밖에서는 막으려면 어떤 접근 제어자를 쓰나요?
힌트
1절에서 배운 세 가지 키워드(public · private · protected) 가운데 하나입니다.
정답
protected를 씁니다. protected 속성은 선언한 클래스와 자식 클래스 본문 안에서는 this.속성으로 읽을 수 있고, 인스턴스 바깥에서 읽으면 TS2445 오류로 막힙니다.
컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요.
2절의 private stamps 코드와 #stamps 코드를 각각 main.ts에 넣어 npx tsc와 node main.ts로 돌려 결과를 비교해 보세요. 다음 레슨 interface를 지키는 클래스에서는 클래스 선언 자리에서 모양을 점검하는 implements를 배웁니다.
검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.