TypeScript 독학 가이드 6 - 클래스
JavaScript에도 클래스가 있긴 한데, TypeScript의 클래스는 훨씬 강력해요. 접근 제한자, 추상 클래스, 인터페이스 구현까지… 진짜 객체 지향 프로그래밍 제대로 할 수 있어요. 저는 클래스 배우고 나서 코드 구조가 확 깔끔해졌더라고요.
클래스 기본 문법
가장 간단한 클래스부터 시작해볼게요.
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
introduce() {
console.log(`안녕하세요, 저는 ${this.name}이고 ${this.age}살입니다.`);
}
}
const person = new Person("김철수", 25);
person.introduce();
실행 결과:
클래스로 객체를 생성하고 메서드를 호출했습니다
JavaScript 클래스랑 거의 비슷한데, 속성에 타입을 명시한 게 차이예요.
접근 제한자
TypeScript의 가장 큰 특징! JavaScript에는 없는 기능이에요.
public (공개)
어디서든 접근 가능해요. 기본값이라 생략해도 됩니다.
class Person {
public name: string; // public은 생략 가능
constructor(name: string) {
this.name = name;
}
}
const person = new Person("김철수");
console.log(person.name); // OK
person.name = "이영희"; // OK
실행 결과:
public 속성은 외부에서 자유롭게 접근할 수 있습니다
private (비공개)
클래스 내부에서만 접근 가능해요.
class BankAccount {
private balance: number = 0;
deposit(amount: number) {
this.balance += amount; // 클래스 내부 - OK
console.log(`${amount}원 입금. 잔액: ${this.balance}원`);
}
getBalance() {
return this.balance;
}
}
const account = new BankAccount();
account.deposit(10000);
console.log(account.getBalance());
// console.log(account.balance); // 에러! 외부 접근 불가
실행 결과:
private 속성은 메서드를 통해서만 접근할 수 있습니다
balance를 직접 수정하면 에러:
account.balance = 1000000; // 에러!
실행 결과:
private 속성은 외부에서 수정할 수 없습니다
protected (보호됨)
클래스 내부와 상속받은 자식 클래스에서 접근 가능해요.
class Person {
protected name: string;
constructor(name: string) {
this.name = name;
}
}
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name);
this.department = department;
}
introduce() {
console.log(`저는 ${this.name}이고, ${this.department} 부서입니다.`);
// name은 protected라서 자식 클래스에서 접근 가능
}
}
const emp = new Employee("김철수", "개발");
emp.introduce();
// console.log(emp.name); // 에러! 외부 접근 불가
실행 결과:
protected 속성은 상속받은 클래스에서 사용할 수 있습니다
생성자 단축 표현
매번 this.name = name 쓰는 게 귀찮죠? TypeScript는 단축 문법이 있어요.
일반 방식
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
단축 방식
class Person {
constructor(
public name: string,
public age: number
) {}
}
const person = new Person("김철수", 25);
console.log(person.name);
console.log(person.age);
실행 결과:
생성자 매개변수에 접근 제한자를 붙이면 자동으로 속성이 생성됩니다
훨씬 짧죠? 접근 제한자를 매개변수에 붙이면 자동으로 속성이 생성돼요.
Getter와 Setter
속성처럼 보이지만 실제로는 메서드예요. 값을 읽거나 쓸 때 추가 로직을 넣을 수 있어요.
class Circle {
private _radius: number = 0;
get radius(): number {
return this._radius;
}
set radius(value: number) {
if (value < 0) {
throw new Error("반지름은 0보다 커야 합니다");
}
this._radius = value;
}
get area(): number {
return Math.PI * this._radius * this._radius;
}
}
const circle = new Circle();
circle.radius = 5;
console.log(`반지름: ${circle.radius}`);
console.log(`넓이: ${circle.area.toFixed(2)}`);
실행 결과:
getter/setter로 값을 검증하고 계산할 수 있습니다
음수 반지름 넣으면 에러:
circle.radius = -1; // 런타임 에러!
정적 멤버 (static)
클래스 자체에 속하는 멤버예요. 인스턴스 생성 없이 사용할 수 있어요.
class MathUtil {
static PI: number = 3.14159;
static square(x: number): number {
return x * x;
}
static circleArea(radius: number): number {
return this.PI * radius * radius;
}
}
console.log(MathUtil.PI);
console.log(MathUtil.square(5));
console.log(MathUtil.circleArea(3));
실행 결과:
static 멤버는 클래스 이름으로 직접 호출합니다
유틸리티 함수 만들 때 유용해요. 인스턴스 만들 필요가 없거든요.
상속 (Inheritance)
클래스는 다른 클래스를 상속받을 수 있어요.
class Animal {
constructor(public name: string) {}
move(distance: number) {
console.log(`${this.name}이(가) ${distance}m 이동했습니다.`);
}
}
class Dog extends Animal {
bark() {
console.log("멍멍!");
}
}
class Bird extends Animal {
fly(distance: number) {
console.log(`${this.name}이(가) ${distance}m 날아갑니다.`);
}
}
const dog = new Dog("바둑이");
dog.move(10);
dog.bark();
const bird = new Bird("짹짹이");
bird.move(5);
bird.fly(20);
실행 결과:
부모 클래스의 메서드를 상속받아 사용합니다
메서드 오버라이딩
부모 메서드를 재정의할 수 있어요.
class Animal {
constructor(public name: string) {}
makeSound() {
console.log("동물 소리~");
}
}
class Dog extends Animal {
makeSound() {
console.log("멍멍!");
}
}
class Cat extends Animal {
makeSound() {
console.log("야옹~");
}
}
const dog = new Dog("바둑이");
dog.makeSound();
const cat = new Cat("나비");
cat.makeSound();
실행 결과:
같은 메서드 이름이지만 다르게 동작합니다
추상 클래스 (Abstract Class)
직접 인스턴스를 만들 수 없고, 상속용으로만 쓰는 클래스예요.
abstract class Shape {
constructor(public name: string) {}
abstract getArea(): number; // 추상 메서드 - 자식이 구현해야 함
printInfo() {
console.log(`${this.name}의 넓이: ${this.getArea()}`);
}
}
class Rectangle extends Shape {
constructor(
public width: number,
public height: number
) {
super("사각형");
}
getArea(): number {
return this.width * this.height;
}
}
class Circle extends Shape {
constructor(public radius: number) {
super("원");
}
getArea(): number {
return Math.PI * this.radius * this.radius;
}
}
const rect = new Rectangle(10, 5);
rect.printInfo();
const circle = new Circle(3);
circle.printInfo();
실행 결과:
추상 클래스의 메서드를 각 자식이 구현했습니다
추상 클래스는 직접 인스턴스 생성 불가:
const shape = new Shape("도형"); // 에러!
실행 결과:
추상 클래스는 직접 인스턴스를 만들 수 없습니다
인터페이스 구현
클래스가 특정 인터페이스를 구현하도록 강제할 수 있어요.
interface Printable {
print(): void;
}
interface Saveable {
save(): void;
}
class Document implements Printable, Saveable {
constructor(public content: string) {}
print() {
console.log(`인쇄: ${this.content}`);
}
save() {
console.log(`저장: ${this.content}`);
}
}
const doc = new Document("TypeScript 가이드");
doc.print();
doc.save();
실행 결과:
클래스가 여러 인터페이스를 구현할 수 있습니다
실전 예제: 쇼핑 카트
배운 내용을 종합해서 쇼핑 카트 만들어볼게요:
interface Product {
id: number;
name: string;
price: number;
}
class CartItem {
constructor(
public product: Product,
private _quantity: number = 1
) {}
get quantity(): number {
return this._quantity;
}
set quantity(value: number) {
if (value < 1) {
throw new Error("수량은 1 이상이어야 합니다");
}
this._quantity = value;
}
get total(): number {
return this.product.price * this._quantity;
}
}
class ShoppingCart {
private items: CartItem[] = [];
addItem(product: Product, quantity: number = 1) {
const item = new CartItem(product, quantity);
this.items.push(item);
console.log(`${product.name} ${quantity}개 추가됨`);
}
getTotal(): number {
return this.items.reduce((sum, item) => sum + item.total, 0);
}
printCart() {
console.log("\n=== 장바구니 ===");
this.items.forEach(item => {
console.log(`${item.product.name} x ${item.quantity} = ${item.total}원`);
});
console.log(`총액: ${this.getTotal()}원\n`);
}
}
const cart = new ShoppingCart();
cart.addItem({ id: 1, name: "노트북", price: 1500000 }, 1);
cart.addItem({ id: 2, name: "마우스", price: 30000 }, 2);
cart.printCart();
실행 결과:
클래스를 활용한 실용적인 장바구니 시스템입니다
정리하면
- 접근 제한자: public, private, protected
- 생성자 단축: 매개변수에 접근 제한자 붙이기
- getter/setter: 속성처럼 쓰지만 로직 추가 가능
- static: 클래스 자체의 멤버
- 상속: extends로 부모 클래스 상속
- 추상 클래스: 상속용 클래스, 인스턴스 생성 불가
- 인터페이스 구현: implements로 구현
다음 글에서는 타입 별칭과 유니온 타입에 대해 배워볼게요. type 키워드, | 연산자, & 연산자까지 알아봅시다!