클래스 기본 구조와 필드 선언
가장 기본적인 클래스는 아무런 멤버도 없는 빈 클래스입니다.
class EmptyClass {}
그러나 실용적인 클래스는 데이터를 저장하기 위해 필드를 선언합니다. 필드는 기본적으로 public이며, 명시적 타입 지정과 초기값 할당이 가능합니다.
class Coordinate {
x: number;
y: number;
// 필드 초기값으로 타입 추론
z = 0;
}
const point = new Coordinate();
point.x = 10;
point.y = 20;
console.log(`좌표: (${point.x}, ${point.y}, ${point.z})`);
// 출력: 좌표: (10, 20, 0)
엄격한 속성 초기화와 확실한 할당
TypeScript의 --strictPropertyInitialization 옵션은 클래스 필드가 생성자 내에서 반드시 초기화되어야 함을 요구합니다. 만약 생성자에서 초기화하지 않으면 오류가 발생합니다.
class Uninitialized {
message: string; // 오류: 생성자에서 초기화되지 않음
}
// 해결 방법 1: 생성자에서 초기화
class Initialized {
message: string;
constructor(msg: string) {
this.message = msg;
}
}
// 해결 방법 2: 확실한 할당 단언자 (!) 사용, 나중에 초기화 보장
class DeferredInit {
message!: string;
}
const deferred = new DeferredInit();
deferred.message = "나중에 초기화";
console.log(deferred.message);
읽기 전용 필드 (readonly)
읽기 전용 필드는 생성자에서만 수정할 수 있으며, 클래스 외부에서는 재할당할 수 없습니다.
class ImmutablePoint {
readonly x: number;
readonly y: number = 0; // 초기값 지정 가능
constructor(x: number) {
this.x = x;
}
}
const p = new ImmutablePoint(5);
console.log(`ImmutablePoint: (${p.x}, ${p.y})`);
// p.x = 10; // 오류: 읽기 전용 필드는 재할당 불가
생성자: 초기화와 상속
생성자는 인스턴스 초기화를 담당하며, 기본값과 상속 시 `super` 호출을 지원합니다.
class Shape {
color: string;
constructor(color = "black") {
this.color = color;
}
}
class Square extends Shape {
size: number;
constructor(size: number, color?: string) {
super(color);
this.size = size;
}
}
const sq = new Square(4, "red");
console.log(`사각형: ${sq.color}, 크기: ${sq.size}`); // 출력: 사각형: red, 크기: 4
메서드 정의와 오버라이딩
클래스 메서드는 클래스의 동작을 정의합니다. 메서드는 기본적으로 public이며, 파생 클래스에서 오버라이딩하여 재정의할 수 있습니다.
class Vehicle {
move(): string {
return "이동 중";
}
}
class Car extends Vehicle {
move(): string {
return "자동차가 달립니다";
}
}
const car = new Car();
console.log(car.move()); // 출력: 자동차가 달립니다
접근자 (Getters/Setters)
접근자는 클래스 필드에 접근할 때 추가 로직을 수행할 수 있도록 도와줍니다. 게터(getter)는 값을 읽을 때, 세터(setter)는 값을 쓸 때 호출됩니다.
class Rectangle {
private _width: number = 0;
private _height: number = 0;
get area(): number {
return this._width * this._height;
}
set dimensions({ width, height }: { width: number; height: number }) {
this._width = width;
this._height = height;
}
}
const rect = new Rectangle();
rect.dimensions = { width: 5, height: 10 };
console.log("면적:", rect.area); // 출력: 면적: 50
인덱스 시그니처
클래스에서 동적으로 키를 다뤄야 할 경우, 인덱스 시그니처를 사용할 수 있습니다.
class Dictionary {
[key: string]: string | number;
addEntry(key: string, value: string | number): void {
this[key] = value;
}
}
const dict = new Dictionary();
dict.addEntry("age", 30);
dict.addEntry("name", "Alice");
console.log(dict["name"]); // 출력: Alice
접근 제어자: public, protected, private
멤버의 접근성을 제어하여 클래스 내부와 외부의 경계를 설정할 수 있습니다.
public
기본 접근자로, 클래스 외부에서도 자유롭게 접근할 수 있습니다.
class PublicExample {
message: string = "Public";
}
const pe = new PublicExample();
console.log(pe.message); // 접근 가능
protected
해당 클래스와 파생 클래스 내에서만 접근할 수 있습니다.
class ProtectedExample {
protected secret: string = "Not for public";
}
class DerivedExample extends ProtectedExample {
reveal(): void {
console.log(this.secret);
}
}
const de = new DerivedExample();
de.reveal(); // 출력: Not for public
// de.secret; // 오류: 외부에서는 접근 불가
private
해당 클래스 내에서만 접근할 수 있으며, 파생 클래스에서도 접근할 수 없습니다.
class PrivateExample {
private hidden: number = 42;
getHidden(): number {
return this.hidden;
}
}
const pe2 = new PrivateExample();
console.log(pe2.getHidden()); // 출력: 42
// console.log(pe2.hidden); // 오류: private 멤버 접근 불가
this와 컨텍스트 보장
클래스 내 메서드는 this를 통해 해당 클래스 인스턴스에 접근할 수 있습니다. TypeScript는 함수의 첫 번째 매개변수로 this 타입을 지정하거나 화살표 함수를 사용하여, 메서드가 올바른 컨텍스트에서 호출되도록 강제할 수 있습니다.
class Counter {
count = 0;
increment(this: Counter): void {
this.count++;
}
}
const counter = new Counter();
counter.increment(); // 정상 호출
const inc = counter.increment;
// inc(); // 오류: 'this' 컨텍스트가 맞지 않음
화살표 함수
class SafeCounter {
count = 0;
// 화살표 함수를 사용하여 각 인스턴스마다 독립된 함수를 생성
increment = (): void => {
this.count++;
};
}
const sc = new SafeCounter();
const incSafe = sc.increment;
incSafe(); // 안전하게 호출됨
console.log(sc.count); // 출력: 1
this 매개변수
class Clock {
time = 0;
advance(this: Clock): void {
this.time++;
}
}
const clock = new Clock();
const advance = clock.advance;
// advance(); // 오류: this 컨텍스트 누락
clock.advance();
console.log(clock.time); // 출력: 1
매개변수 속성
생성자 매개변수에 접근 제어자를 붙여 필드를 자동 생성합니다.
class UserProfile {
// 생성자 파라미터 앞에 붙은 접근 제어자가 곧 클래스 필드가 됩니다.
constructor(public readonly username: string, private age: number) {}
display(): void {
console.log(`User: ${this.username}, Age: ${this.age}`);
}
}
const profile = new UserProfile("minji", 29);
profile.display(); // 출력: User: minji, Age: 29
// profile.age; // 오류: age는 private 멤버
클래스 표현식
클래스 표현식은 클래스 선언과 매우 유사하지만, 이름이 없이 표현할 수 있다는 점이 특징입니다. 표현식에 의해 생성된 클래스는 상수에 할당할 수 있으며, 그 상수를 통해 인스턴스를 생성할 수 있습니다.
const DataHolder = class<T> {
data: T;
constructor(value: T) {
this.data = value;
}
getData(): T {
return this.data;
}
};
const holder = new DataHolder<number>(42);
console.log(holder.getData()); // 출력: 42
생성자 시그니처와 InstanceType
생성자 시그니처는 `new`로 호출 가능한 타입을 정의하며, `InstanceType`으로 인스턴스 타입을 추출합니다.
class Circle {
radius: number;
timestamp: number;
constructor(radius: number) {
this.radius = radius;
this.timestamp = Date.now();
}
}
type CircleInstance = InstanceType<typeof Circle>;
function resize(circle: CircleInstance, factor: number): void {
circle.radius *= factor;
}
const circ = new Circle(5);
resize(circ, 2);
console.log(`반지름: ${circ.radius}`); // 출력: 반지름: 10
추상 클래스
추상 클래스는 직접 인스턴스화할 수 없으며, 하위 클래스에서 추상 멤버를 구현해야 합니다.
abstract class Device {
abstract powerOn(): string;
status(): string {
return "장치 준비됨";
}
}
class Lamp extends Device {
powerOn(): string {
return "램프가 켜졌습니다";
}
}
const lamp = new Lamp();
console.log(lamp.status()); // 출력: 장치 준비됨
console.log(lamp.powerOn()); // 출력: 램프가 켜졌습니다
추상 생성자 시그니처
추상 클래스를 상속받은 구체적인 클래스만 생성할 수 있도록 제한합니다.
function activate(ctor: new () => Device): string {
const device = new ctor();
return device.powerOn();
}
console.log(activate(Lamp)); // 출력: 램프가 켜졌습니다
// activate(Device); // 오류: 추상 클래스는 생성 불가
클래스 간 관계: 구조적 타이핑
클래스의 명시적 상속 관계 없이도 구조적으로 동일하면 호환됩니다.
동일 구조 호환
class PointA {
x: number = 0;
y: number = 0;
}
class PointB {
x: number = 0;
y: number = 0;
}
const pt: PointA = new PointB();
console.log(`Point: (${pt.x}, ${pt.y})`); // 출력: Point: (0, 0)
하위 구조 호환
class BaseEntity {
id: string;
name: string;
}
class DetailedEntity {
id: string;
name: string;
details: string;
}
const entity: BaseEntity = new DetailedEntity();
entity.id = "e1";
entity.name = "항목";
console.log(`${entity.id}: ${entity.name}`); // 출력: e1: 항목
빈 클래스 호환
class VoidClass {}
function handle(obj: VoidClass): void {
console.log("처리됨:", obj);
}
handle(new VoidClass()); // 출력: 처리됨: {}
handle({ key: "value" }); // 출력: 처리됨: { key: "value" }'TypeScript' 카테고리의 다른 글
| [TypeScript] 유틸리티 타입: 복잡한 타입을 간단하게 제공 (0) | 2025.02.28 |
|---|---|
| [TypeScript] 템플릿 리터럴 타입: 문자열 동적으로 다루기 (0) | 2025.02.27 |
| [TypeScript] Mapped Types로 타입 재사용하기 (1) | 2025.02.27 |
| [TypeScript] 조건부 타입: 입력과 출력의 유연함 (0) | 2025.02.27 |
| [TypeScript] 인덱스 액세스 타입(Indexed Access Types): 객체 속성 정밀 추출 (0) | 2025.02.27 |