37. TypeScript (2)

JSLEE·2024년 6월 18일

사용자 정의 타입

저번 포스트에서는 enum을 활용하여 성별(gender)에 대한 property를 만들었다.

사실, enum 외에도 TS에서는 다양한 방법으로 사용자 정의 타입을 생성할 수 있다.

사용자 정의 타입 생성 방법
1. type alias :
type alias는 type이라는 키워드를 사용해, 내가 정의한 식별자가 어떤 데이터 타입인지 새롭게 정의할 수 있다.

type MyName = string ;
let abc : MyName ;
abc = "abc"

위 코드에서, MyName 이라는 데이터타입은 곧 string과 같다.
유사하게, 객체형태도 데이터타입으로 지정할 수 있다.

type Student = {
  name : string ,
  age? : number 
} ;
let student1 : Student = {name : "lee" , age : 18} ;

위 코드에서, Student라는 데이터타입을 만들고, 해당 타입에는 name이라는 프로퍼티가 필수적으로 들어가야하고, age는 optional로 들어가도록 지정되었다.
따라서, Student 타입을 사용하는 변수를 생성한다면, name이라는 속성이 반드시 포함되어야 한다.

그리고, type 키워드는 &를 통해 interface를 연결하여 사용할 수 있다. 다음의 코드를 보자.

interface Cat {
  meow() : void ;
}

interface Dog {
  bark() : void ;
}

type Animal = Cat & Dog ; 

class MyAnimal implements Animal {
  meow() {
    console.log("meow") ; 
  }  
  bark() {
    console.log("bark") 
  }
}  

let animal1 : MyAnimal = new MyAnimal() ;
animal1.bark() ; // bark
animal1.meow() ; // meow

Cat과 Dog 인터페이스를 type 키워드를 통해 하나의 Animal이라고 별칭을 정했고, 이를 구현한 MyAnimal 클래스를 만들어서 사용했다.

type 키워드는 & 를 이용해 다음과 같은 방법으로 확장이 가능하다.

type Person = {
  name: string;
  age: number;
};

type Employee = Person & {
  companyId: string;
};

let employee: Employee = {
  name: "Lee",
  age: 30,
  companyId: "ABC123"
};

console.log(employee)

2. interface :
인터페이스는 저번 포스트에서와 다루었듯이, interface 키워드를 사용하여 구현한다.

interface Student {
  name: string;
  age?: number;
}

인터페이스는 단독적으로 사용할 수 없다. 컴파일을 수행해보면 .js파일에는 아무런 코드도 작성되어 있지 않음을 확인할 수 있다.
따라서, implements를 이용하여 클래스에 구현하여 사용한다.

class MyStudent implements Student {
  name: string;
  age?: number;

  constructor({ name, age }: { name: string; age?: number }) {
    this.name = name;
    this.age = age !== undefined ? age : 0; 
  }
}

또한, extends 키워드를 사용해서, 인터페이스를 다른 인터페이스를 확장해서 사용할 수 있다.

interface ExStudent extends Student{
  grade : number ; 
}

3. Literal Type :
리터럴 타입은 변수, 매개변수가 특정한 값만 가질 수 있도록 제한하는 타입이다. 다음의 코드를 보자.

type Gender = 'male' | 'female' 

Gender 라는 타입은, 'male' 이라는 문자열과, 'female'이라는 문자열을 제외한 다른 값들은 일절 허용하지 않는다.
따라서, let a : Gender = "none" 과 같이 입력한다면, 에러가 발생한다.

'male' 과 같은 문자열뿐만 아니라, 숫자, Boolean 등에도 모두 사용할 수 있다.


Union

Union이란, 우리가 리터럴 타입에서 봤던 | 기호를 이용하여 타입을 제한하는 것이다.
다른 점이 있다면, 리터럴 타입은 값을 제한했던 반면에 Union 타입은 데이터타입을 제한한다.
다음의 코드를 보자.

type num = string | number ;

let myNum : num ;
myNum = "123" ;
myNum = 123 ;

내가 num이라는 데이터타입을 (string, number) 의 두가지타입으로 제한했다.
따라서 num 타입을 사용한 변수에는 string과 number 타입만 사용가능하게 되는것이다.

타입 가드

유니온타입은 여러 자료형을 사용하는 만큼, 그에 맞는 코드 검증도 요구된다. 다음의 함수를 생각해보자.

function printId(id: string | number) {
  console.log(id.toUpperCase());
}

id가 string 타입으로 들어오면 문제가 없겠지만,
number 타입으로 들어온다면, toUpperCase() 메소드에서 오류가 발생할 것이다.
따라서, 특정 데이터 타입에 맞는 처리를 따로 해줘야한다.
이를 타입 가드라고 부른다.
다음의 코드는 타입 가드를 사용한 코드이다.

function printId(id: string | number) {
    if (typeof id === "string") {
        console.log("Your ID (string) is: " + id.toUpperCase());
    } else {
        console.log("Your ID (number) is: " + id);
    }
}

위 코드에서는 매개변수로 number 자료형이 들어와도, 그에 맞는 동작을 수행할 수 있다.
타입가드에는 typeof를 이용한다는 것을 기억하자.


Array, Tuple

Array

JS에서 배열을 생성할 땐, 다음과 같이 작성했었다.

let num1 = [1,2,3,4,5] ;

let num2 = new Array([1,2,3,4,5]);

TS에서 배열을 생성하는 방법은 다음과 같다.

let num1 : number[] = [1,2,3,4,5]

let strAndNum : (string | number)[] = [1 , 'apple' , 'banana', 'orange']

배열에서, 여러 자료형의 사용을 허가하려면 위처럼 파이프라인(|) 기호를 사용하여 여러개의 자료형을 명시할 수 있다.

Tuple

Tuple은 일반 배열과 다르게 타입의 순서가 정해져있어서, 할당하는 값의 순서가 중요하다.
Tuple은 JS에서 없는 데이터형으로, 다음과 같은 형태를 가진다.

let myTuple : [number, string, boolean ] = [1, 'hello', true] ;

클래스 / 객체 생성하기

직원에 대한 정보를 나타내는 변수들이 필요하다고 해보자.
클래스를 사용하지 않으면 다음과 같이 작성해야한다.

// 클래스를 사용하지 않았을 때.
let employeeName: string;
let age: number;
let employeeJob: string;

function printEmployee(name: string, age: number, job: string): void {
  console.log(`${name}의 나이는 ${age}이고, 직업은 ${job} 입니다.`);
}

printEmployee("lee" , 18, "developer") ;

위 코드에서, 한명의 employee를 생성한다면 직접 입력해도 문제되지 않겠지만, 만약 100명의 employee가 있다고 생각해보자.
우리는 그 employee의 name, age, job을 일일히 입력할 순 없다.

그럼, 클래스를 사용해서 위의 name, age, job 프로퍼티를 하나의 틀로써 만들어보자.

// 클래스를 사용할 때.

class Employee {
  name: string;
  age: number;
  job: string;

  printEmployee(): void {
    console.log(
      `${this.name}의 나이는 ${this.age}이고, 직업은 ${this.job} 입니다.`
    );
  }
}

let employee1 = new Employee();
employee1.name = "lee";
employee1.age = 18;
employee1.job = "developer";

employee1.printEmployee();

위처럼 코드를 작성하면, 우리는 Employee라는 틀을 가지고, 손쉽게 employee1이라는 인스턴스를 만들 수 있다.
위 코드에서 불편한 점이 있는데, name, age, job을 프로퍼티를 이용해서 직접입력했다는 점이다.
이를 해결할 방법은 생성자를 이용하는 것이다.

생성자 , public 접근제한자

생성자는 constructor라는 함수를 이용한다.
다음의 코드를 보자.

class Employee {
  name: string;
  age: number;
  job: string;

  printEmployee(): void {
    console.log(
      `${this.name}의 나이는 ${this.age}이고, 직업은 ${this.job} 입니다.`
    );
  }

  constructor(name : string, age : number , job : string) {
    this.name = name ;
    this.age = age ;
    this.job = job ;

  }
}

let employee1 = new Employee("lee" , 18 , "developer");

employee1.printEmployee();

이 때, 주의해야할 점은, constructor의 매개변수에도 타입에 대해 명시를 해줘야한다는 점이다.
만약 constructor(name, age, job) 같이 작성했다면, 클래스 프로퍼티에서 name :string 과 같이 명시해 주었음에도 name에는 any 타입의 값이 들어갈 수 있다.
즉, name에 숫자값이 들어갈 수도 있게 된다는 말이다.

클래스의 프로퍼티에도 데이터타입을 명시해줘야하고, 생성자의 매개변수에도 데이터타입을 명시해줘야 하는 번거로움이 있는데, 이건 public이나 private 접근제한자로 해결할 수 있다. 두 키워드의 차이는, 외부에서 접근할 수 있는지의 여부이다.
다음의 코드를 보자.

class Employee {
  constructor(public name : string , public age? : number , public job? : string) {}

  printEmployee(): void {
    console.log(
      `${this.name}의 나이는 ${this.age}이고, 직업은 ${this.job} 입니다.`
    );
  }

}

위 코드에서는 class에서 프로퍼티에 대한 명시가 없음에도, name, age, job 이라는 프로퍼티를 가지게 된다. 또한, 자동으로 매개변수를 통해 초기화까지 수행해준다. 외부에서 직접적인 접근을 막고싶다면 public 대신 private를 사용하면 된다.

이제 함수에서 선택적 매개변수의 주의사항에 대해 알아보자.
함수에서 선택적 매개변수를 사용할 때에는, 해당 속성의 값의 순서는 맨 뒤로 이동해야한다.
코드를 통해 살펴보자.

// constructor의 age에 optional parameter 적용
  constructor(name : string, age? : number , job : string) {
    this.name = name ;
    this.age = age ;
    this.job = job ;

  } 
  // 에러 발생. age는 job보다 뒤에 있어야함.

위 코드에서, age를 선택적 매개변수로 사용하고 싶다면 오류가 발생한다.
이 경우에는 job까지 선택적 매개변수로 바꾸거나, age를 매개변수의 맨 뒷자리로 이동시켜야 한다.

  constructor(name : string, job : string, age? : number ) {
    this.name = name ;
    this.age = age ;
    this.job = job ;
  } 

이 이유는 간단하다.
constructor(name : string, age? : number , job : string)라는 상황에서, new Employee("lee", "developer") 라고 입력을 하면, age 위치에는 "developer" 값이 들어가게되고, job에는 아무런 값도 안들어가는 것처럼 컴퓨터는 인식을 할 것이기 때문이다.

따라서, 선택적 매개변수는, 매개변수의 마지막에 위치시켜야한다.


접근제한자 (public, private, protected)

객체지향의 캡슐화 철학에서는, 데이터 값을 직접 수정하는 것은 안된다.
다음의 코드를 보자.

let employee1 = new Employee("lee", "developer" , 18) ;

employee1.name = "kim" // 이게 가능하면 안된다.

따라서, 직접적인 접근을 제한하고 간접적으로 접근하도록 해야한다. 이 때 private 접근제한자를 사용한다.

class Employee {
  private _name: string;
  private _age: number;
  private _job: string;
}

let employee1 = new Employee("lee", "developer" , 18) ;
employee1.name = "kim" // 에러 발생

한가지 짚고 넘어가자면, private로 지정된 프로퍼티에는 언더바(_)를 사용하는 규칙이 있다.

protected라는 접근 제한자도 있는데, protected는 부모 클래스와 서브 클래스의 인스턴스에서 접근 가능하다.(즉, 상속된 클래스에서까지 접근 가능.)

private를 사용하여 이렇게 외부에서의 접근을 막게할 수 있다.
이렇게 외부의 접근을 막으면, 데이터의 수정이 필요할 때에는 어떻게 해야할까?
이 때 사용하는 것이 getter와 setter 이다.

getter / setter

getter와 setter는 TS뿐만 아니라, 다른 언어에서도 프로퍼티에 대한 접근을 제한하는 캡슐화 방식을 사용할 때, 프로퍼티에 접근하기 위해 만들어준다.

TS에서는 getter와 setter 메소드에 대해 제공해준다.
getter는 get 키워드를 사용하고, setter는 set 키워드를 사용한다.
다음의 코드를 보자.

  get name(){
    // 추가 동작 가는ㅇ
    return this._name ; 
  }

  set name(name : string){
    // 추가 동작 가능
    this._name = name ;
  }

이렇게 get과 set 메소드를 정의해주면, employee1.name = 'kim'과 같이 사용할 수 있다.
이렇게 getter, setter 메소드로 간접 접근을 하게되면, 유효성 검사와 같이 추가적인 코드를 구현할 수 있다는 장점이 있다. 예를 들어서, name이 "undefined" 라는 string으로 들어온다면, throw new Error("잘못된 문자열") 이라는 오류를 발생하게 할 수 있는 것이다.

profile
공부한 내용들을 정리하기 위해 사용하는 블로그입니다.

0개의 댓글