TypeScript - Classes

lbr·2022년 8월 26일
0

What are Classes

  • javascript object 를 만드는 blueprint (청사진, 설계도)
  • 클래스 이전에 object 를 만드는 기본적인 방법은 function
  • JavaScript 에도 class 는 es6 부터 사용 가능
    - 단, 접근제어자는 부족합니다.
  • OOP 을 위한 초석
  • TypeScript 에서는 클래스도 사용자가 만드는 타입의 하나

javascript에서 지원하는 클래스 보다도 더 강력한 기능들을 가지고 있습니다.

Quick Start

실습을 위한 설정

mkdir class
cd class
npm init -y
npm install typescript -D
npx tsc --init : typescript 설정파일생성

ts-> js 변환 : npx tsc
node로 실행 : node example.js

실습

class Person {}

const p1 = new Person();

console.log(p1);

출력결과 : Person {}

ts 변환 타겟이 ES5이하라면 js로 변환시 Person은 function으로 변환이 되고,
es2015(ES6)이상이면 js가 클래스를 지원하기 때문에 그대로 class로 변환됩니다.

class Person {
  name;

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

const p1 = new Person("Mark");

console.log(p1);

출력결과 : Person { name: 'Mark' }

정리

  • class 키워드를 이용하여 클래스를 만들 수 있다.
  • class 이름은 보통 대문자를 이용한다.
  • new 를 이용하여 class 를 통해 object 를 만들 수 있다.
  • constructor 를 이용하여 object 를 생성하면서 값을 전달할 수 있다.
  • this 를 이용해서 만들어진 object 를 가리킬 수 있다.
  • JS 로 컴파일되면 es5 의 경우 function 으로 변경된다.

constructor & initailize

클래스의 생성자와 초기화에 대해서 자세히 알아보겠습니다.

실습

class Person {
  // strict: true상태에서
  // 초기화된 값이 없으면 에러가 발생합니다.
  // strict: false에서는 에러가 발생하지 않습니다.
  name: string = "Mark";
  age!: number;
}

const p1 = new Person();

console.log(p1);
p1.age = 39;
console.log(p1.age);

인스턴스를 생성 후 프로그래머가 어디선가 직접 값을 할당해 주겠다는 의미로 !를 사용하여 명시적으로 표현하면 초기값을 할당하지 않아도 에러가 나지 않습니다.
그대신 개발자 스스로가 주의해야 합니다.

script: false 상태에서 초기값을 주지 않을 경우

age는 number 속성이어야 하지만 초기값을 주지 않아서 undefined 가 출력됩니다.
이런 경우를 실수하지 않기 위해서 tsconfig.json 에서 strictPropertyInitialization: true 옵션을 제공합니다.

초기값 할당에는 2가지 방법이 있습니다.

class Person {
  name: string = "Mark";
  age: number;

  constructor(age: number) {
    this.age = age;
  }
}

const p1 = new Person(39);
console.log(p1);
console.log(p1.age);
  1. 변수 선언과 동시에 초기화.
  2. constructor 를 사용하여 초기화.

오버로딩없이 constructor 구현하기

javascript에서는 overloading 기능을 사용할 수 없지만,
typescript에서는 overloading 기능을 사용할 수 있습니다.

하지만 지금은 constructor 를 오버로딩 없이 구현해보겠습니다.

class Person {
  name: string = "Mark";
  age: number;

  constructor(age?: number) {
    if (age === undefined) {
      this.age = 20;
    } else {
      this.age = age;
    }
  }
}

const p1 = new Person(39);
const p2 = new Person();

constructor에는 async를 붙일 수 없습니다.

async를 사용하여 값을 초기화하고 싶다면 따로 class안에 init() 함수를 만들어 async 처리하고, 인스턴스 생성 후 await p1.init();하여 값을 초기화 합니다. 프로퍼티에는 뒤늦게 처리한다라는 의미로 !를 사용하여 표현합니다.

정리

  • 생성자 함수가 없으면, 디폴트 생성자가 불린다.
  • 프로그래머가 만든 생성자가 하나라도 있으면, 디폴트 생성자는 사라진다.
  • strict 모드에서는 프로퍼티를 선언하는 곳 또는 생성자에서 값을 할당해야 한다.
  • 프로퍼티를 선언하는 곳 또는 생성자에서 값을 할당하지 않는 경우에는 ! 를 붙여서 위험을 표현한다.
  • 클래스의 프로퍼티가 정의되어 있지만, 값을 대입하지 않으면 undefined 이다.
  • 생성자에는 async 를 설정할 수 없다.

접근 제어자 (Access Modifiers)

class 안의 프로퍼티 생성자 메소드에 접근 제어자를 붙여서
외부에서 접근할 수 있는지 또는 상속간에 접근할 수 있는지 아니면 내부에서만 접근할 수 있는지를 설정해 줄 수 있습니다.

typescript는 기본적으로 외부에서 접근이 가능합니다.

외부 접근 제어자 : public
설정을 하지 않으면 기본값으로 public입니다. 직접 명시적으로 public이라고 작성할 수 있습니다.

실습

접근제어자 public

class Person {
  public name: string = "Mark";
  public age!: number;

  public constructor(age?: number) {
    if (age === undefined) {
      this.age = 20;
    } else {
      this.age = age;
    }
  }

  public async init() {}
}

const p1 = new Person(39);
console.log(p1);

접근제어자가 public 이기 때문에 외부에서 모두 접근이 가능합니다.

constructor 도 접근제어자가 public 이기 때문에 new Person() 을 호출할 수 있는 것 입니다.

접근제어자 private

constructor를 private로 바꿔보기

constructor 의 접근제어자를 private 로 변경하면 생성자를 외부에서 호출할 수 없기 때문에 new 를 호출할 수 없게 됩니다.
이것은 매우 특수한 케이스 입니다.
이를 이용해서 싱글톤 패턴 등을 구현할 수 있습니다. 이 부분은 뒤에서 다루겠습니다.

프로퍼티를 private로 바꿔보기

class Person {
  public name: string = "Mark";
  private age!: number;

  public constructor(age?: number) {
    if (age === undefined) {
      this.age = 20;
    } else {
      this.age = age;
    }
  }

}

const p1 = new Person(39);
console.log(p1);

위처럼 private로 바꾼 프로퍼티는 자동완성에서도 보이지 않습니다.

예전에는 javascript에서는 private를 제공하지 않았기 때문에 개발자들끼리 컨번션으로 private 라는 것을 표현하기 위해 변수명과 함수명 앞에 _ 를 붙였습니다. 예 : _age _init()
그래서 지금에와서 private 을 직접 사용해도 예전의 습관적으로 지금도 _ 를 앞에 붙여주는 경우가 많습니다. 예 : private _age

접근제어자 protected

외부에서는 접근이 불가능하지만 상속관계에서는 접근이 가능한 접근제어자입니다.
이 부분은 클래스의 상속에 대해서 배운 후에 살펴보겠습니다.

정리

  • 접근 제어자에는 public, private, protected 가 있다.
  • 설정하지 않으면 public 이다.
  • 클래스 내부의 모든 곳에 (생성자, 프로퍼티, 메서드) 설정 가능하다.
  • private 으로 설정하면 클래스 외부에서 접근할 수 없다.
  • 자바스크립트에서 private 지원하지 않아 오랜동안 프로퍼티나 메서드 이름 앞에 _ 를 붙여서 표현했다.

initialization in constructor parameters

생성자의 parameter를 받아서 클래스의 property로 초기화하는 간단하게 코딩하는 방법을 알아보겠습니다.

변경 전

class Person {
  public name: string;
  private age: number;
  
  public constructor(name: string, age: number) {
	this.name = name;
    this.age = age;
  }
}

const p1 = new Person("Mark", 39);

변경 후

class Person {
  public constructor(public name: string, private age: number) {

  }
}

const p1 = new Person("Mark", 39);

위와 아래의 코드는 동일합니다.

Getters & Setters

class안의 프로퍼티의 값을 가져오는 것을 get,
class안의 프로퍼티의 값을 설정하는 것을 set 이라고 합니다.

get을 하는 함수를 getter,
set을 하는 함수를 setter 라고 합니다.

class Person {
  public constructor(private _name: string, private age: number) {}

  get name() {
    console.log("get");
    return this._name + ' Lee';
  }

  set name(n: string) {
    console.log("set");
    this._name = n;
  }
}

const p1 = new Person("Mark", 39);
console.log(p1.name); // get 을 하는 함수 getter
p1.name = "typescript"; // set을 하는 함수 setter
console.log(p1.name);

프로퍼티를 private 로 설정하고, 값의 설정과 값을 가져오는 것은 settergetter 로 접근합니다.

//  get name() {
//    console.log("get");
//    return this._name + ' Lee';
//  }

위처럼 getter 함수를 사용하지 않으면 코드를 js로 변환한 후 실행하면 p1.name은 undefined 로 나옵니다. private로 설정하면 외부에서 접근할 수 없기에 없는 값처럼 나옵니다.

반대로 setter 함수를 사용하지 않으면 값을 변경해 줄 수 없습니다.
이 경우 값을 setter 하려면 에러를 발생시킵니다.
이 데이터는 값을 읽기만 가능합니다 라는 상황을 연출할 수 있습니다.

readonly properties

class의 프로퍼티에 readonly 라는 키워드를 붙여서 set은 할 수 없고 get만 할 수 있는 형태에 대해서 배워보겠습니다.

class Person {
  public readonly name: string = 'Mark';

  
  public constructor(private _name: string, private age: number) {}
}

const p1 = new Person("Mark", 39);
console.log(p1.name); 
p1.name = "typescript"; // 에러

readonly 를 사용하면 오로지 값을 읽기만 가능하고, 값을 변경하려고 하면 에러를 발생시킵니다.

이번에는 private에 readonly를 붙여보겠습니다.

class Person {
  public readonly name: string = "Mark";
  private readonly country: string;

  public constructor(private _name: string, private age: number) {
    this.country = "korea";
  }

  hello() {
    this.country = "USA"; // 에러
  }
}

readonly 가 붙은 private 은 첫 초기화 후에 다른 메소드안에서는 값을 바꿀 수 없게 만들어 줄 수 있습니다.

정리

readonly 키워드를 사용하는 경우에는 public이든 private 이든 초기화되는 영역에서만 값을 세팅할 수 있고 다른 곳에서는 다른 값으로 바꿀 수 없습니다.

Index Signatures in class

class 안에서 Index Signatures 를 선언하고 사용하는 방법에 대해서 알아보겠습니다.

class는 object를 만들어내는 blueprint 같은 역할을 합니다.
만약에 object가

이런 모습이라면 어떤식으로 class를 정의해야 할지 고민이 들겁니다.

같은 타입의 object임에도 서로 다른 프로퍼티의 갯수와 이름을 가지고 있는 인스턴스를 만들기 위해 class를 어떻게 정의하는지 보겠습니다.

// 만들려는 인스턴스 예시
// {mark: 'male', jade: 'male'}
// {chloe: 'female', alex: 'male', anna: 'female'}

// class Students {
//   mark: string = 'male';
// }
// 이렇게 프로퍼티 이름을 직접 기재하면 동적으로 처리를 할 수가 없습니다.
// 그래서 프로퍼티의 이름이 동적이라면 이런식으로 처리를 하면 안됩니다.

// 이런 경우에 쓸 수 있는 방식이 인덱스 시그니처입니다.
class Students {
  [index: string]: string;
}

const a = new Students();
a.mark = "male";
a.jade = "male";

console.log(a);
// 출력 결과 : Students { mark: 'male', jade: 'male' }

// 다른 예시로 한번 더
const b = new Students();
b.chloe = "remale";
b.alex = "male";
b.anna = "female";

console.log(b);
// 출력 결과 : Students { chloe: 'remale', alex: 'male', anna: 'female' }

사실 위의 상황에서는 아래처럼 class를 정의하는 것이 좀 더 맞을겁니다.

class Students {
  [index: string]: "male" | "female";
}

이제 2가지만 짚고 넘어가겠습니다.

  1. class에서 프로퍼티를 선언하면 프로퍼티를 항상 초기화해야 됐습니다.
    하지만 지금 인덱스 시그니처 같은 경우에는 초기값 자체를 할당할 수 없는 상황입니다. 일종의 옵션널 프로퍼티처럼 프로퍼티가 있을 수도 있고, 없을 수도 있기 때문입니다.
    그래서 초기값을 할당할 수도 없겠지만.. 할당할 필요없습니다.

  2. 만약 아래처럼 필수로 와야 하는 프로퍼티가 있다면

class Students {
  [index: string]: "male" | "female"; // 의미 : 어떤 이름인지는 모르지만 이 클래스의 프로퍼티에는 항상 "male"이나 "female"이 와야 합니다.
  
  mark = "hello"; // 에러1 : 값이 "male" 이나 "female"이 와야 합니다.
  mark = "male"; // 에러2 : mark가 "male"이라는 타입이 아니기 때문에 에러.
  mark: "male" = "male"; // 에러 없음.
}

Students 라는 클래스는 mark 라는 프로퍼티가 "male" 이라는 값을 가지고 있으면서, 옵셔널하게 다른 프로퍼티들도 추가할 수 있는 class가 됩니다.

에러2 출력문

정리

프로퍼티가 고정된 형태가 아니라 동적으로 프로퍼티가 들어오는 경우에 고려해 볼만한 기능입니다.

Static Properties & Methods

propertymethod 앞에 static 이라는 키워드를 붙여서
static property , static method 로 쓰는 방법을 함께 알아보겠습니다.

static 메소드

// 기존 방식
class Person {
  public hello() {
    console.log('안녕하세요');
  }
}

const p1 = new Person();
p1.hello();
// static 메소드
class Person {
  public static hello() {
    console.log('안녕하세요');
  }
}
// const p1 = new Person();
// p1.hello(); // static 키워드가 붙으면 더 이상 이렇게 호출할 수 없습니다.

// static 메소드는 아래처럼 인스턴스를 생성하지 않고도 바로 클래스명으로 접근이 가능합니다.
Person.hello();

이번에는 static property를 만들어보겠습니다.

static 프로퍼티

// static 프로퍼티
class Person {
  public static CITY = "Seoul";

  public static hello() {
    console.log('안녕하세요');
  }
}

Person.CITY;

해당 클래스로부터 만들어진 오브젝트에서 공통적으로 사용하고 싶은 데이터가 있다면 static으로 선언하여 어디서나 접근할 수 있게 합니다.

private static

class Person {
  private static CITY = "Seoul";

  public static hello() {
    console.log("안녕하세요", Person.CITY);
  }
}

// Person.CITY; // 에러 : 접근 못함.

private로 선언하면 밖에서는 사용할 수 없겠지만, 해당 클래스 안에서는 사용할 수 있습니다.

공유를 한다는 것이 정확히 어떤 의미인지 코드로 확인해 보겠습니다.

서로 다른 인스턴스에서 static을 활용한 데이터 공유

class Person {
  public static CITY = "Seoul";

  public hello(instanceName: string) {
    console.log(`${instanceName}: 안녕하세요`, Person.CITY);
  }

  public change() {
    Person.CITY = "LA";
  }
}

const p1 = new Person();
p1.hello("p1");

const p2 = new Person();
p2.hello("p2");

p1.change();
p2.hello("p2");

출력결과

p1에 의해서 static 프로퍼티가 바뀌면 다른 인스턴스 객체인 p2에서도 바뀐 프로퍼티값으로 확인됩니다.

Singletons

Singletons 용어에는 single 이라는 단어가 들어가 있습니다.

어플리케이션이 실행되는 중간에 클래스로부터 단 하나의 오브젝트만 생성을 해서 사용하는 패턴을 Singletons 패턴이라고 합니다.

class로부터 인스턴스를 만들어 낼 때에는 항상 new 라는 키워드를 사용해왔습니다.

class ClassName {
  private static instance: ClassName | null = null;

  // 이 함수를 이용해서 객체를 꺼내올 수 있습니다.
  public static getInstance(): ClassName {
    // ClassName 으로부터 만든 object가 있으면 그것을 return
    // ClassName 으로부터 만든 object가 없으면 만들어서 return
    if (ClassName.instance === null) {
      ClassName.instance = new ClassName();
    }
    return ClassName.instance;
  }

  private constructor() {}
}

// const a = new ClassName();
// const b = new ClassName();
// new 라는 키워드를 통해서 인스턴스를 생성하는 행위를 막아야합니다.

// 이제는 new를 우리가 직접하는 것이 아니라
// 중간 매개체를 이용해서 그 매개체한테서 생성된 인스턴스를 넘겨 받는 방식으로
// 단일 객체 패턴을 만들어 낼 수 있습니다.

// 매개체역할을 할 함수를 만듭니다.
const a = ClassName.getInstance(); // 최초이기 때문에 만들어서 리턴
const b = ClassName.getInstance(); // a가 만들어 놓은 것이 있기 때문에 그것을 리턴
// a와 b는 단일 오브젝트 하나를 공유하고 있습니다.

console.log(a === b); // 출력 : true

상속(Inheritance)

상속은 클래스가 다른 클래스를 상속받아서(기능을 그대로 물려받아서) 자신만의 기능을 추가하거나 덮어씌워서 사용할 수 있게 해줍니다.

class Parent {
  constructor(protected _name: string, private _age: number) {}

  public print(): void {
    console.log(`이름은 ${this._name} 이고, 나이는 ${this._age} 입니다.`);
  }
}
// protected: 외부에서는 접근할 수 없지만, 상속관계에서는 접근할 수 있습니다.

const p = new Parent('Mark', 39);
// p._age // 접근불가
// p._name // 접근불가

p.print(); // 실행결과 : 이름은 Mark 이고, 나이는 39 입니다.

class Child extends Parent {
  public gender = "male"; // 자식클래스에서 새로 추가
}

// const c = new Child(); // 에러내용 : 2개의 인수가 필요한데 0개를 가져왔습니다.
const c = new Child("Son", 5);
// c._name // 접근불가
// c._age // 접근불가
c.print();

자식클래스에서 새로운 프로퍼티를 추가할 수 있습니다.
자식을 생성하면 부모 constructor가 불리기 때문에 생성자에서 부모 constructor 를 초기화해주기 위한 인수를 넘겨주어야합니다.

class Child extends Parent {
  // 기존에 있던 프로퍼티를 오버라이드
  public _name = 'mark Jr.';

  public gender = "male"; // 자식클래스에서 새로 추가
}

const c = new Child("Son", 5);

c.print();

c._name // 접근가능 // 할당한 값과 접근제어자까지 오버라이드가 가능합니다.

프로퍼티도 오버라이드가 가능합니다. 값 할당과 접근제어자까지 자식클래스에서 변경할 수 있습니다.

class Parent {
  constructor(protected _name: string, private _age: number) {}

  public print(): void {
    console.log(`이름은 ${this._name} 이고, 나이는 ${this._age} 입니다.`);
  }
}
// protected: 외부에서는 접근할 수 없지만, 상속관계에서는 접근할 수 있습니다.

const p = new Parent("Mark", 39);
// p._age // 접근불가
// p._name // 접근불가

p.print(); // 실행결과 : 이름은 Mark 이고, 나이는 39 입니다.

class Child extends Parent {
  public gender = "male"; // 자식클래스에서 새로 추가

  constructor(age: number) {
    // 자식의 생성자에서는 super()를 무조건 먼저 호출해주어야 합니다.
    // 생성자를 먼저 호출해주어야 프로퍼티의 값을 초기화해줄수 있습니다.
    // 제일 먼저 초기화부터 하지 않으면, 값의 호출을 요하는 로직에서 초기화되지않은 값에 접근할수도 있기 때문입니다.
    super("Mark Jr", age); // 부모의 생성자를 호출
  }
}

// const c = new Child("Son", 5); // 에러내용 : 1개의 인수가 필요한데 2개를 가져왔습니다.

// 자식클래스에서 부모클래스의 생성자를 오버라이드해보겠습니다.
const c = new Child(5);

c.print();

// protected와 private 키워드를 적절히 사용하여 영역이 오염되지 않게 하는 것이 좋습니다.

자식클래스에서 부모클래스의 생성자를 오버라이드해보겠습니다.
자식의 생성자에서는 super()를 무조건 먼저 호출해주어야 합니다. 하지않으면 에러가 발생합니다.
생성자를 먼저 호출해주어야 프로퍼티의 값을 초기화해줄 수 있습니다. 제일 먼저 초기화부터 하지 않으면, 값의 호출을 요하는 로직에서 초기화되지않은 값에 접근할수도 있기 때문입니다.

protected와 private 키워드를 적절히 사용하여 영역이 오염되지 않게 하는 것이 좋습니다.

abstract Classes

abstract 라는 키워드를 이용하면 완전하지않은 클래스를 표현할수 있고, 완전하지않은 클래스는 new 를 이용해서 객체로 만들어낼 수 없습니다.
이 완전하지 않은 객체를 상속을 이용해서 완전하게 만든 다음에 사용할 수 있도록 안내할 수 있습니다.

// 클래스 안에서 abstract를 사용한다면, 클래스 키워드 앞에 abstract를 붙여주어야합니다. 
abstract class AbstractPerson {
  protected _name: string = 'Mark';

  abstract setName(name: string): void; // abstract는 구현하지 않습니다.
}

// new AbstractPerson(); // 에러메세지 : Cannot create an instance of an abstract class.

// 상속을 이용해서 완전하게 만든 다음에 사용할 수 있습니다.
class Person extends AbstractPerson {
  setName(name: string): void {
    this._name = name;
  }
}

const p = new Person(); // 추상클래스인 부모클래스를 상속받아 구현한 자식클래스를 통하여 인스턴스 생성이 가능합니다.
p.setName('Mark');

정리

  • abstract class 는 new를 이용하여 인스턴스를 만들 수 없습니다.
  • abstract class 를 상속받으면 abstract가 붙은 함수를 구현해야 합니다.

참고

추상클래스를 상속받은 자식클래스에서 자식클래스의 이름을 블록 지정하고 나오는 전등아이콘을 클릭하면 메뉴가 생깁니다.
추상 클래스 구현을 클릭하면 추상클래스 구현에 필요한 폼을 자동으로 생성해 줍니다.

class Person extends AbstractPerson {
  setName(name: string): void {
    throw new Error("Method not implemented.");
  }
}

정리

클래스를 작성하면 어플리케이션을 구조적으로 작성할 수 있도록 도움을 줍니다.

0개의 댓글