Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

나를 위한 기록

TypeScript - Class 기초 / 클래스 생성자 /인스턴스 / 상속 /추상클래스 본문

Today I Learned

TypeScript - Class 기초 / 클래스 생성자 /인스턴스 / 상속 /추상클래스

솔솔이소리솔 2023. 12. 13. 20:10

TypeScript를 공부하며 궁금했던 키워드들의 개념과 이해를 위한 예시를 정리해보았다.

 

- Constructor

 - 인스턴스

- Super

- extends

- implements

- 추상클래스

- Generic

 

Constructor

클래스를 만들 때 constructor를 사용하는 이유는 객체가 생성될 때 초기화 작업을 수행하기 위해서다.

constructor는 클래스로부터 객체가 생성될 때 자동으로 호출되는 특별한 메서드다.

class Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

 

여기서 Animal 클래스에는 name이라는 속성이 있다. 이 속성은 동물의 이름을 저장하는 데 사용된다.

 

- name: string; 은 Animal 클래스에 name이라는 문자열 타입의 속성이 있다는 것을 나타낸다. 이건 단순히 속성의 선언일 뿐, 여기에 실제 값은 할당되어 있지 않다.

 

- constructor(name: string) { this.name = name; }는 Animal 클래스의 생성자이다. 이 메서드는 Animal 클래스로부터 객체가 만들어질 때 호출된다. constructor 안에서는 인자로 받은 name 값을 클래스의 name 속성에 할당합니다. 이렇게 하면 각 Animal 객체는 생성될 때 각각 다른 이름을 가질 수 있다.

 

예를 들어, new Animal("호랑이")라고 객체를 생성하면, Animal 클래스의 생성자가 호출되고, "호랑이"라는 이름이 해당 객체의 name 속성에 저장된다. 이렇게 클래스의 인스턴스마다 각각 다른 값을 가질 수 있도록 초기화하는 것이 constructor의 주요 역할이다.

 

간단히 말해, constructor객체가 생성될 때 그 객체의 초기 상태를 설정하는 데 사용되는 메서드라고 이해할 수 있다.

인스턴스

인스턴스는 클래스를 기반으로 만들어진 객체를 의미한다. 클래스는 일종의 청사진이라고 할 수 있는데, 이 청사진을 바탕으로 실제로 작동하는 개별 객체를 생성하는 것이다. 이 생성된 객체를 '인스턴스'다.

 

예를 들어, '자동차'라는 클래스가 있다고 하자. 이 자동차 클래스는 자동차가 가지고 있는 특징들(속성)과 기능(메서드)을 정의하고 있다.

여기서 속성은 색상, 브랜드 같은 것들이고, 메서드는 주행하기, 멈추기 같은 동작을 말한다.

 

이제 이 '자동차' 클래스를 바탕으로 실제 자동차를 만든다고 생각해보자. 예를 들어, '레드 색상의 현대 자동차'라는 구체적인 자동차를 만든다면, 이 구체적인 자동차가 바로 '인스턴스'다. 이 인스턴스는 '자동차' 클래스의 속성과 메서드를 모두 가지고 있으며, 실제로 사용할 수 있다.

 

프로그래밍에서도 이와 비슷하게 작동한다.

클래스를 정의하고, 이 클래스를 바탕으로 실제로 작동하는 객체(인스턴스)를 만들어 사용한다. 각 인스턴스는 동일한 클래스에서 파생되었지만, 각각의 인스턴스는 서로 다른 상태(속성 값)을 가질 수 있다.

 

class 자동차 {
  색상: string;
  브랜드: string;

  constructor(색상: string, 브랜드: string) {
    this.색상 = 색상;
    this.브랜드 = 브랜드;
  }

  주행하기() {
    console.log(this.브랜드 + " 자동차가 주행 중입니다.");
  }
}

let 내자동차 = new 자동차("레드", "현대");
내자동차.주행하기();

 

여기서 내자동차는 자동차 클래스의 인스턴스다. 이 인스턴스는 '레드' 색상과 '현대' 브랜드라는 구체적인 상태를 가지며, 주행하기라는 메서드를 사용할 수 있다.

 

Super

super는 클래스 상속에서 사용되는 특별한 키워드다. 클래스 상속은 한 클래스(자식 클래스)가 다른 클래스(부모 클래스)의 속성과 메서드를 상속받아 사용할 수 있도록 하는 것을 말한다. super 키워드는 이 상황에서 두 가지 주요한 용도로 사용된다.

 

1. 부모 클래스의 생성자 호출:

   - 자식 클래스에서 생성자를 정의할 때, super()를 사용하여 부모 클래스의 생성자를 호출할 수 있다. 이는 자식 클래스에서 부모 클래스의 속성을 초기화하거나, 부모 클래스의 생성자에서 필요한 설정을 수행할 때 필요하다.

   - 부모 클래스의 생성자를 호출하지 않으면, 자식 클래스에서 부모 클래스의 속성을 제대로 사용할 수 없게 된다.

 

2. 부모 클래스의 메서드 호출:

   - 자식 클래스에서 부모 클래스에 정의된 메서드를 오버라이딩(재정의) 했을 때, super 키워드를 사용해서 부모 클래스의 메서드를 호출할 수 있다. 이렇게 하면 부모 클래스의 기능을 확장하거나 수정할 수 있다.

 

class 동물 {
  constructor(이름: string) {
    console.log(이름 + "이(가) 생성되었습니다.");
  }

  소리내기() {
    console.log("동물 소리!");
  }
}


class 강아지 extends 동물 {
  constructor(이름: string) {
    super(이름); // 부모 클래스의 생성자 호출
  }

  소리내기() {
    super.소리내기(); // 부모 클래스의 메서드 호출
    console.log("멍멍!");
  }
}

let 개 = new 강아지("바둑이");
개.소리내기();

 

이 예시에서 강아지 클래스는 동물 클래스를 상속받는다. 강아지 클래스의 생성자에서는 super(이름);을 사용하여 동물 클래스의 생성자를 호출하고, 소리내기 메서드에서는 super.소리내기();를 사용하여 동물 클래스의 소리내기 메서드를 호출한 다음, 추가적인 기능(여기서는 "멍멍!" 출력)을 수행한다.

 

super를 사용함으로써, 상속받은 클래스(자식 클래스)는 부모 클래스의 기능을 활용하고 확장할 수 있다. 이는 객체 지향 프로그래밍에서 중요한 개념 중 하나다.

 

extends 

extends 키워드는 객체 지향 프로그래밍에서 클래스 상속을 나타내기 위해 사용되는 키워드이다. 클래스 상속이란 한 클래스(자식 클래스)가 다른 클래스(부모 클래스)의 속성과 메서드를 물려받아 사용할 수 있게 하는 것을 말한다.

 

extends를 사용하는 주요 이유는 코드의 재사용성을 높이고, 관리를 용이하게 하며, 계층적 구조를 통해 더 명확한 프로그래밍을 가능하게 하는 데 있다.

 

만약 '동물'이라는 기본 클래스를 가지고 있고, 여기에서 '강아지', '고양이' 같은 더 구체적인 클래스를 만들고 싶다고 가정해보자.

'동물' 클래스에는 모든 동물이 공통으로 가지고 있는 속성과 메서드가 정의되어 있을 것이다. 그리고 '강아지'나 '고양이' 클래스에서는 이 '동물' 클래스의 속성과 메서드를 상속받아, 추가적인 속성이나 메서드를 정의할 수 있다.

 

 

class 동물 {
  이름: string;
  
  constructor(이름: string) {
    this.이름 = 이름;
  }

  소리내기() {
    console.log("어떤 소리도 내지 않습니다.");
  }
}

class 강아지 extends 동물 {
  constructor(이름: string) {
    super(이름);
  }

  소리내기() {
    console.log("멍멍!");
  }
}

let 개 = new 강아지("바둑이");
개.소리내기(); // 출력: "멍멍!"

 

 

여기서 강아지 클래스는 동물 클래스를 extends 키워드를 사용하여 상속받는다. 강아지 클래스는 동물의 모든 속성과 메서드를 가지고 있으며, 추가로 소리내기 메서드를 오버라이드(재정의)하여 강아지 특유의 소리를 낼 수 있게 했다.

이렇게 extends를 사용하면, 기존 클래스의 기능을 재사용하고 확장할 수 있어서 코드를 더 깔끔하고 관리하기 쉽게 만들 수 있다.

 

 

implements와 extends

implements: 인터페이스 구현하기

- implements는 클래스가 특정 "규칙" 또는 "약속"을 따르도록 만들 때 사용한다.

- 이 "규칙" 또는 "약속"은 인터페이스(interface)에 정의된다. 인터페이스는 클래스가 어떤 메서드를 가져야 하는지, 그 메서드의 기본적인 형태는 어떤지 정한다. 하지만, 이 메서드들이 실제로 어떻게 동작해야 하는지는 정하지 않는다.

- 클래스가 인터페이스를 implements한다고 하면, 그 클래스는 인터페이스에 정의된 모든 메서드를 실제로 작성해야 한다. 이렇게 하면 다른 사람들이 그 클래스를 사용할 때, 어떤 기능들이 있는지 미리 알 수 있다.

 

extends: 클래스 상속하기

- extends는 한 클래스가 다른 클래스의 기능을 그대로 받아오고, 더 추가하거나 변경할 때 사용한다.

- 상속을 받은 클래스(자식 클래스)는 부모 클래스의 모든 메서드와 속성을 그대로 가지고 있다. 그리고 필요하다면, 자식 클래스에서 부모 클래스의 메서드를 바꿔서(오버라이딩해서) 더 새로운 기능을 추가할 수도 있다.

 

예시)

- 인터페이스 동물이 있고, 소리내기()라는 메서드가 정의되어 있다.

  interface 동물 {
    소리내기(): void;
  }

  

  여기서 강아지 클래스가 동물 인터페이스를 구현(implements)한다면, 강아지 클래스는 반드시 소리내기() 메서드를 작성해야 한다.

 

- 클래스 생물이 있고, 여기에 숨쉬기()라는 메서드가 있다.

  class 생물 {
    숨쉬기(): void {
      console.log("숨을 쉽니다.");
    }
  }

  

  여기서 강아지 클래스가 생물 클래스를 상속(extends)받는다면, 강아지 클래스는 자동으로 숨쉬기() 메서드를 가지게 된다. 그리고 필요하면 강아지 클래스에서 숨쉬기() 메서드를 바꿀 수도 있다.

 

결국 implements는 "이 클래스는 이런 기능을 반드시 가지고 있어야 해"라고 정하는 것이고, extends는 "이 클래스는 이런 기능을 이미 가지고 있어, 그리고 더 추가하거나 바꿀 수 있어"라고 이해할 수 있다.

 

 

추상클래스

추상클래스(abstract class)는 다른 일반 클래스들과는 조금 다른 종류의 클래스다. 이 추상클래스는 직접 객체를 생성할 수 없는 클래스로, 주로 다른 클래스들의 기본 틀로 사용된다. 즉, 추상클래스는 상속을 위해 만들어진 클래스라고 할 수 있다.

 

추상클래스의 중요한 특징은 다음과 같다.

1. 객체 생성 불가:

   - 추상클래스는 직접적으로 인스턴스(객체)를 생성할 수 없다. 즉, new 키워드를 사용해 직접 객체를 만들 수 없다.

 

2. 상속을 위한 기본 틀:

   - 추상클래스는 다른 클래스들이 상속받을 수 있는 기본 틀 역할을 한다. 상속받은 클래스들은 추상클래스에서 정의된 메서드를 구현하거나 오버라이드(재정의)할 수 있다.

 

3. 추상 메서드 포함 가능:

   - 추상클래스는 추상 메서드(abstract method)를 포함할 수 있다. 추상 메서드는 선언만 되어 있고 구체적인 구현은 없는 메서드다.

이 메서드들은 추상클래스를 상속받는 클래스에서 반드시 구현해야 하는 메서드들이다.

 

abstract class 동물 {
  abstract 소리내기(): void; // 추상 메서드

  잠자기(): void {
    console.log("잠을 잡니다.");
  }
}


class 강아지 extends 동물 {
  소리내기(): void {
    console.log("멍멍!");
  }
}

let 개 = new 강아지();
개.소리내기(); // 출력: "멍멍!"
개.잠자기();   // 출력: "잠을 잡니다."

 

여기서 동물은 추상클래스이며, 소리내기는 추상 메서드다. 강아지 클래스는 동물 클래스를 상속받아 소리내기 메서드를 구현한다. 추상클래스인 동물로는 직접 객체를 생성할 수 없지만, 동물을 상속받아 구현된 강아지 클래스로는 객체를 생성할 수 있다.

 

이처럼 추상클래스는 다른 클래스들이 공통적으로 가져야 할 메서드나 속성을 정의하는 데 사용되며, 이를 통해 코드의 재사용성과 관리 효율성을 높일 수 있다.

 

 

추상클래스의  또 다른 예시

추상클래스를 사용하는 좋은 예시 중 하나는 서로 다른 종류의 객체들이 공통된 틀을 공유해야 하는 경우다. 예를 들어, 다양한 종류의 도형을 그리는 그래픽 프로그램을 만든다고 생각해 보자. 이런 경우에 각 도형(원, 사각형, 삼각형 등)은 고유한 방식으로 그려져야 하지만, 모든 도형은 '그리기', '이동하기', '색상 변경하기' 같은 기본적인 동작을 공유한다.

 

이러한 상황에서 추상클래스를 사용하여 각 도형이 따라야 하는 기본적인 틀을 제공할 수 있다. 예를 들어, 도형이라는 추상클래스를 만들고, 이 클래스에서 각 도형이 구현해야 할 추상 메서드(예: 그리기(), 이동하기())를 정의할 수 있다.

 

 

abstract class 도형 {
  abstract 그리기(): void;
  abstract 이동하기(x: number, y: number): void;


  색상변경하기(색상: string): void {
    console.log(색상 + "으로 색상 변경");
  }
}

class 원 extends 도형 {
  그리기(): void {
    console.log("원 그리기");
  }

  이동하기(x: number, y: number): void {
    console.log(원 이동: x=${x}, y=${y});
  }
}

class 사각형 extends 도형 {
  그리기(): void {
    console.log("사각형 그리기");
  }

  이동하기(x: number, y: number): void {
    console.log(사각형 이동: x=${x}, y=${y});
  }
}

let 원1 = new 원();
원1.그리기();
원1.이동하기(10, 20);
원1.색상변경하기("빨강");

let 사각형1 = new 사각형();
사각형1.그리기();
사각형1.이동하기(5, 15);
사각형1.색상변경하기("파랑");

 

이 예시에서 도형 추상클래스는 그리기(), 이동하기() 메서드를 추상 메서드로 정의하고, 색상변경하기() 메서드는 모든 도형에 공통적으로 적용된다. 각 구체적인 도형 클래스(예: 원, 사각형)는 도형 클래스를 상속받아 이 메서드들을 실제로 구현한다.

 

이처럼 추상클래스는 공통된 인터페이스를 제공하면서도, 상속받는 클래스에게 구체적인 구현을 강제하는 방법으로 유용하게 사용된다. 이를 통해 코드의 일관성을 유지하고, 유지보수성을 높이며, 확장성 있는 설계를 할 수 있다.

Generic

제네릭(Generic)은 프로그래밍에서 데이터 타입을 일반화하는 방법이다. 제네릭을 사용하면 하나의 코드를 여러 종류의 타입에 대해 재사용할 수 있다. 이는 코드의 유연성을 높이고 타입 안전성을 강화하는 데 도움을 준다.

 

제네릭의 핵심 개념

1. 타입 매개변수(Type Parameter):

   - 제네릭을 사용할 때는 타입 매개변수(보통 <T>, <U> 등으로 표현)를 사용한다. 이 매개변수는 실제 타입이 결정되기 전에 임시로 사용하는 타입이다.

 

2. 재사용성:

   - 하나의 함수나 클래스를 다양한 타입에 대해 사용할 수 있게 해준다. 예를 들어, 제네릭을 사용한 배열이나 리스트는 어떤 타입의 요소도 저장할 수 있다.

 

3. 타입 안전성:

   - 제네릭을 사용하면 컴파일 시점에 타입을 체크할 수 있다. 이는 런타임에 발생할 수 있는 타입 관련 오류를 줄여준다.

 

4. 유연성:

   - 제네릭은 다양한 타입에 대해 유연하게 작동한다. 따라서 코드를 작성할 때 구체적인 타입에 대해 걱정할 필요가 없다.

 

Generic에서 사용하는 타입 매개변수(예: <T>, <U>)는 임시적인 느낌을 가진다. 이 타입 매개변수는 실제 구체적인 타입(예: number, string)이 결정되기 전까지 사용되는 일종의 '플레이스홀더' 또는 '대체자' 역할을 한다.

 

제네릭을 사용하는 주된 목적은 코드의 범용성과 재사용성을 높이는 것이다. 함수나 클래스를 정의할 때, 구체적인 타입을 지정하지 않고 타입 매개변수를 사용함으로써, 이후에 다양한 타입으로 그 함수나 클래스를 사용할 수 있게 된다.

 

function 항목추가<T>(배열: T[], 항목: T): T[] {
  배열.push(항목);
  return 배열;
}

여기서 <T>는 아직 결정되지 않은 타입을 대표한다. 이 함수는 숫자 배열, 문자열 배열, 객체 배열 등 어떤 종류의 배열에도 사용할 수 있다. 함수를 사용하는 시점에 <number>, <string>, <사용자정의타입> 등 실제 타입으로 <T>를 대체하게 된다.

let 숫자배열 = 항목추가<number>([1, 2, 3], 4); // 여기서 <T>는 number로 대체됨
let 문자열배열 = 항목추가<string>(["안녕", "하세요"], "반갑습니다"); // 여기서 <T>는 string으로 대체됨