TypeScript 입문 6편 - 클래스 만들기

(수정: ) learning by Seven Fingers Studio 18분
TypeScript클래스OOP객체지향웹개발

typescript guide 06 classes

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();

실행 결과:

✓ 컴파일 성공
안녕하세요, 저는 김철수이고 25살입니다.

클래스로 객체를 생성하고 메서드를 호출했습니다

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);  // 에러! 외부 접근 불가

실행 결과:

✓ 컴파일 성공
10000원 입금. 잔액: 10000원
10000

private 속성은 메서드를 통해서만 접근할 수 있습니다

balance를 직접 수정하면 에러:

account.balance = 1000000;  // 에러!

실행 결과:

✗ 타입 에러
Property 'balance' is private and only accessible within class 'BankAccount'

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);

실행 결과:

✓ 컴파일 성공
김철수
25

생성자 매개변수에 접근 제한자를 붙이면 자동으로 속성이 생성됩니다

훨씬 짧죠? 접근 제한자를 매개변수에 붙이면 자동으로 속성이 생성돼요.

typescript guide 06 classes

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)}`);

실행 결과:

✓ 컴파일 성공
반지름: 5
넓이: 78.54

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));

실행 결과:

✓ 컴파일 성공
3.14159
25
28.27431

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);

실행 결과:

✓ 컴파일 성공
바둑이이(가) 10m 이동했습니다.
멍멍!
짹짹이이(가) 5m 이동했습니다.
짹짹이이(가) 20m 날아갑니다.

부모 클래스의 메서드를 상속받아 사용합니다

메서드 오버라이딩

부모 메서드를 재정의할 수 있어요.

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();

실행 결과:

✓ 컴파일 성공
사각형의 넓이: 50
원의 넓이: 28.274333882308138

추상 클래스의 메서드를 각 자식이 구현했습니다

추상 클래스는 직접 인스턴스 생성 불가:

const shape = new Shape("도형");  // 에러!

실행 결과:

✗ 타입 에러
Cannot create an instance of an abstract class

추상 클래스는 직접 인스턴스를 만들 수 없습니다

인터페이스 구현

클래스가 특정 인터페이스를 구현하도록 강제할 수 있어요.

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();

실행 결과:

✓ 컴파일 성공
인쇄: TypeScript 가이드
저장: TypeScript 가이드

클래스가 여러 인터페이스를 구현할 수 있습니다

실전 예제: 쇼핑 카트

배운 내용을 종합해서 쇼핑 카트 만들어볼게요:

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();

실행 결과:

✓ 컴파일 성공
노트북 1개 추가됨
마우스 2개 추가됨
=== 장바구니 ===
노트북 x 1 = 1500000원
마우스 x 2 = 60000원
총액: 1560000원

클래스를 활용한 실용적인 장바구니 시스템입니다

정리하면

  • 접근 제한자: public, private, protected
  • 생성자 단축: 매개변수에 접근 제한자 붙이기
  • getter/setter: 속성처럼 쓰지만 로직 추가 가능
  • static: 클래스 자체의 멤버
  • 상속: extends로 부모 클래스 상속
  • 추상 클래스: 상속용 클래스, 인스턴스 생성 불가
  • 인터페이스 구현: implements로 구현

운영자 실전 노트

실제 프로젝트 진행하며 겪은 문제

  • React 프로젝트에서 class 사용했다가 함수형 컴포넌트 트렌드와 맞지 않아 리팩토링 → 상황에 맞는 패러다임 선택 필요
  • private 필드를 충분히 활용하지 않아 내부 상태가 외부에서 직접 수정되는 버그 발생 → 캡슐화의 중요성 재인식

이 경험을 통해 알게 된 점

  • class는 강력하지만 프로젝트 스타일(함수형 vs OOP)을 고려해 선택해야 한다
  • private 접근 제한자는 단순한 제약이 아닌, 버그 예방 도구다

다음 글에서는 타입 별칭과 유니온 타입을 배운다. type 키워드, | 연산자, & 연산자까지 다룬다.


다음 글 보기

← 이전 글
TypeScript 독학 가이드 5 - 인터페이스
다음 글 →
TypeScript 독학 가이드 7 - 타입 별칭과 유니온 타입
← 블로그 목록으로