자바스크립트에도 class는 있다. 하지만 자바스크립트의 클래스는 안이 다 뚫려 있어서, 아무 데서나 속성을 꺼내 읽고 덮어쓸 수 있다. "이 값은 밖에서 못 건드리게 하고 싶다"는 흔한 요구를 언어 차원에서 막을 방법이 오래 없었다. 타입스크립트는 여기에 접근제어(access control, 속성을 누가 읽고 쓸 수 있는지 제한하는 것)를 붙였다. public, private, protected, readonly 같은 표시를 필드 앞에 달면, 규칙을 어긴 코드를 컴파일 단계에서 잡아낸다. 여기 나오는 건 전부 tsc(타입스크립트 컴파일러)로 돌려 확인한 것이고, 타입 에러도 실제로 나온 메시지 그대로다.


클래스 필드엔 타입을 붙인다. 자바스크립트와 큰 틀은 같은데, 필드를 미리 선언하고 타입을 적는 점이 다르다.


class Account {
owner: string;
balance: number;
constructor(owner: string, initial: number) {
this.owner = owner;
this.balance = initial;
}
deposit(amount: number) {
this.balance += amount;
}
}


필드 owner, balance를 위에 선언해두면 this.balance가 무슨 타입인지 편집기가 안다. 이 상태만으론 밖에서 account.balance = -9999 같은 짓을 막지 못한다. 여기에 접근 표시를 달아야 한다.


public, private, protected로 접근을 나눈다. private는 그 클래스 안에서만, protected는 그 클래스와 자식 클래스에서만 접근을 허용한다. 아무것도 안 붙이면 public이라 어디서든 열린다.


class Account {
private balance = 0;
}
const a = new Account();
console.log(a.balance);
// error TS2341: Property 'balance' is private and only accessible within class 'Account'.


잔액을 밖에서 직접 읽거나 못 쓰게 잠갔다. 돈을 넣고 빼는 건 deposit 같은 메서드를 거치게 강제하는 것이다. protected는 조금 다르다.


class Base { protected id = 1; }
class Child extends Base {
show() { return this.id; } // OK, 자식 안에선 접근됨
}
const c = new Child();
console.log(c.id);
// error TS2445: Property 'id' is protected and only accessible within class 'Base' and its subclasses.


자식 클래스 Child 내부에선 this.id가 되지만, 인스턴스를 통해 밖에서 c.id로 꺼내려 하면 막힌다. 상속 계층 안에서만 공유하고 싶은 값에 쓴다.


readonly는 한 번 정하면 못 바꾸게 한다. 생성자에서 초기값을 넣는 것까진 허용하되, 그 뒤로는 대입을 막는다. 바뀌면 안 되는 식별자 같은 값에 쓴다.


class Account {
readonly owner: string;
constructor(owner: string) { this.owner = owner; }
}
const a = new Account("hong");
a.owner = "kim";
// error TS2540: Cannot assign to 'owner' because it is a read-only property.


생성자 안에서 this.owner = owner는 되는데, 밖에서 바꾸려 하면 거절이다. readonly는 접근 여부와는 별개라 public readonly처럼 접근 표시와 같이 붙일 수 있다. 읽는 건 누구나, 쓰는 건 아무도 못 하게가 된다.


파라미터 프로퍼티로 생성자를 줄인다. 위에서 owner를 선언하고, 생성자 매개변수로 또 받고, this.owner = owner로 옮기는 세 번 반복이 지겹다. 생성자 매개변수 앞에 접근 표시를 달면 이 세 단계를 한 번에 처리한다.


class Point {
constructor(
public x: number,
public y: number,
private label: string,
) {}
}
const p = new Point(1, 2, "origin");
console.log(p.x, p.y); // 1 2


매개변수 앞에 public이나 private를 붙이면 타입스크립트가 같은 이름의 필드를 자동으로 만들고 대입까지 해준다. 위 클래스는 몸통이 텅 비었는데도 x, y, label 필드가 다 생긴다. 필드가 많은 클래스일수록 생성자가 확 짧아진다.


getter와 setter로 접근을 감싼다. 겉으론 그냥 속성처럼 읽고 쓰지만, 실제론 함수가 가로채 검사나 계산을 끼워 넣게 한다.


class Temp {
private _c = 0;
get celsius() { return this._c; }
set celsius(v: number) {
if (v < -273) throw new Error("too cold");
this._c = v;
}
}
const t = new Temp();
t.celsius = 25; // setter 호출
console.log(t.celsius); // 25, getter 호출


t.celsius = 25는 대입처럼 보이지만 set 함수를 부른다. 덕분에 말도 안 되는 온도가 들어오면 그 자리에서 막는다. 실제 값 _cprivate로 숨기고, 바깥엔 검사를 통과한 통로만 열어두는 흔한 방식이다.


static은 인스턴스가 아니라 클래스에 붙는다. 인스턴스마다 하나씩 갖는 게 아니라, 클래스 전체가 공유하는 값이나 함수다.


class Counter {
static count = 0;
constructor() { Counter.count++; }
}
new Counter(); new Counter();
console.log(Counter.count); // 2


countCounter.count로 접근하지 인스턴스로는 못 꺼낸다. 인스턴스가 몇 개 만들어졌는지 세는 것처럼, 개별 물건이 아니라 종류 전체에 딸린 정보에 쓴다.


private 키워드와 #은 다르다. 여기가 진짜 함정이다. 타입스크립트의 private는 컴파일 때만 검사하고, 자바스크립트로 바뀌고 나면 사라진다. 즉 런타임엔 그냥 열린 속성이라 우회로 접근이 된다. 자바스크립트가 자체로 들여온 #(샵) 필드는 런타임까지 진짜로 막는다.


class Safe {
#secret = 42;
reveal() { return this.#secret; }
}
const s = new Safe();
console.log(s.#secret);
// error TS18013: Property '#secret' is not accessible outside class 'Safe' because it has a private identifier.


타입 검사 수준의 캡슐화면 private로 충분하고, 남이 우회로도 절대 못 건드리게 진짜로 잠그려면 #을 쓴다. 둘을 섞어 쓰다 헷갈리기 쉬우니, 팀 안에서 한쪽으로 정해두는 게 낫다.


implements로 인터페이스 계약을 강제한다. 인터페이스는 "이런 메서드와 속성을 갖춰야 한다"는 약속이다. 클래스에 implements를 붙이면 그 약속을 지켰는지 컴파일러가 검사한다.


interface Animal { speak(): string; }
class Dog implements Animal {
speak() { return "woof"; }
}


speak를 빼먹거나 반환 타입을 틀리게 짜면 그 자리에서 에러가 난다. 여러 클래스가 같은 모양을 갖추도록 강제할 때 쓴다. 접근제어와 implements를 같이 쓰면, 밖에서 보이는 겉면은 인터페이스로 고정하고 속사정은 private로 감추는 튼튼한 클래스가 나온다. 자바스크립트에서 "알아서 잘 쓰자"에 기대던 캡슐화가, 타입스크립트에선 어기면 빨간 줄이 뜨는 규칙으로 바뀐다.