본문 바로가기

블록체인_9기/💙 TYPESCRIPT

54강_230831_TypeScript(type Array & Tuple, Interface?, 선택적 프로퍼티 (?:), implements, Interface class type, 전략 패턴(Strategy Pattern), readonly, 로그인 작업 전략패턴)

728x90

 

 

 

 


Type Array & Tuple

 

👩‍💻 Array

배열 타입은 두 가지 방법으로 정의할 수 있다.

첫 번째 방법은 ' 배열 요소들을 나타내는 타입 뒤에 [ ] 를 사용 '하는 방법이고, 

두 번째 방법은 ' Array<> 배열 타입 '을 사용하는 방법이다.

let fruits: string[] = ['Apple', 'Banana', 'Mango'];
// or
let fruits: Array<string> = ['Apple', 'Banana', 'Mango'];
// 오직 숫자 아이템만 허용
let nums:number[] = [100, 101, 102];

// 오직 문자 아이템만 허용
let strs:string[] = ['apple', 'banana', 'melon'];

// 오직 불리언 아이템만 허용
let boos:boolean[] = [true, false, true];

// 모든 데이터 타입을 아이템으로 허용 (any 타입)
let someArr: any[] = [0, 1, {}, [], 'str', false];

// 특정 데이터 타입만 아이템으로 허용 (union 타입)
let selects:(number | string)[] = [102, 'apple'];
// 나머지 매개변수(스프레드 연산자) 를 이용한 배열 반환 함수
function getArr(...args: number[]): number[] {
   return args;
}
getArr(1, 2, 3, 4, 5); // [1, 2, 3, 4, 5]

 

 

👩‍💻 Tuple

튜플은 배열의 서브타입이다. 각 배열의 자리에 타입을 지정하는 타입으로, 배열의 크기와 탑이 고정된 타입이다.

const tuple: [string, number, object] = ["hello", 123, {}];
let x: [string, number]; // 튜플 타입으로 선언

x = ["hello", 10]; // 성공
x = [10, "hello"]; // 오류 (원소 타입이 안맞음)
x = ["hello", 10, 99]; // 오류 (원소 갯수가 안맞음)
/* 타입 뿐만 아니라 값 자체를 고정 */
let tuple: [1, number];
tuple = [1, 2];
tuple = [1, 3];

tuple = [2, 3]; // Error - TS2322: Type '2' is not assignable to type '1'.



let tuple: [string, number];
tuple = ['a', 1];
tuple = ['b', 2];

tuple.push(3);
console.log(tuple); // ['b', 2, 3];

tuple.push(true); // Error - 그렇다고 해서 튜플에 정의되지 않는 타입을 넣을수는 없다.
// push() 함수를 사용하여 요소를 추가하는 경우 처음 할당된 값의 타입과 정확히 일치해야 한다.

 

 

 


Interface

 

🧐 Interface란?

  • 두 개의 시스템 사이에 상호 간에 정의한 약속 혹은 규칙을 포괄하여 의미한다.
  • 프로그래밍에서 클래스 또는 함수의 '틀'을 정의하는 것처럼, 타입의 '틀'로서 사용할 수 있는 것이 인터페이스인 것이다.
  • 여러 함수가 특정한 시그니처를 동일하게 가져야 할 경우 또는 여러 클래스가 동일한 명세를 정의해야 하는 경우 인터페이스를 통해 정의가 가능하다.
    • 인터페이스에 선언된 프로퍼티 또는 메소드의 구현을 강제하여 각 클래스와 함수간의 일관성을 유지할 수 있다.

 

인터페이스는 다음을 정의할 수 있다.

  • 객체의 스펙(속성과 속성의 타입)
  • 함수의 파라미터
  • 함수의 스펙(파라미터, 반환 타입 등)
  • 배열과 객체를 접근하는 방식
  • 클래스
// 인터페이스명은 대문자로 짓는다
interface Human {
  name: string; // name 키는 문자열 타입
  age: number; // age 키는 넘버 타입
  boo(): void; // boo 함수는 void 타입
}

// 인터페이스 자체를 타입으로 줘서 객체 생성
const person: Human = {
  name: "da",
  age: 5,
  boo: () => console.log("this is boo"),
};

// 매개변수에서 인터페이스를 타입으로 받는다.
function booboo(a: Human): void {
  console.log(`${a.name} is ${a.age} years old`);
};

booboo(person); // da is 5 years old
person.boo(); // this is boo

 

💡 인터페이스 명
보통 인터페이스 명을 명명할 때 이름 앞에 항상 대문자 I 를 붙이는 암묵적인 약속이 있다. 
해당하는 단어가 인터페이스임을 알 수 있도록 붙이는 것이다.

 

 

🧐 선택적 프로퍼티 (?:)

 

인터페이스를 사용할 때 인터페이스로 정의한 속성을 꼭 모두 사용할 필요가 없는 경우가 있을 수 있다. 이럴 경우에 필수로 사용해야하는 속성이 아닌 속성을 선택적으로 설정할 수 있는데 선택적 프로퍼티(Optional Properties)로 설정할 수 있다.

콜론(:)앞에 물음표(?)를 넣으면 사용할 수 있다. 

 

interface IBlock {
  id: number;
  title: string;
  content: string;
  date: number;
  like: number;
  // 객체의 구조에서 없어도 되는 구조일 경우, 해당 방법으로 정의한다.
  hit?: number;
}

const Block: IBlock = {
  id: 0,
  title: "",
  content: "",
  date: 0,
  like: 0,
};
// hit를 정의하지 않아도 에러가 발생하지 않는다.
interface User {
   name: string;
   age: number;
   gender?: string;
}

let user: User = {
   name: 'jeff',
   age: 30,
};

user.age = 10;
user.gender = "male"; // 선택적 프로퍼티에 의해서 나중에 속성값을 넣어줄수도 있다.

user.lalala = ''; // 그렇다고 해서 아예 정의되지도 않는 속성을 마음대로 집어넣을 수는 없다.

 

 

🧐 implements

interface IProduct {
  name: string;
  price?: number;
}

// implements: class에 구조가 만족하는지 여부를 체크한다.
// 상속 개념과는 다르고, 클래스에서 위에 선언한 IProduct 구조와 일치하는지만 체크한다. 
class product implements IProduct {
  name: string;
  price: number;
  constructor(name: string, price: number) {
    this.name = name;
    this.price = price;
  }
}

 

 

 


Interface class type

클래스 인터페이스 예시를 살펴보고자 한다. 예시는 상품과 가격, 할인률에 따른 코드이다.

 

상품 정보를 담는 클래스를 생성한다. 

Product 클래스에서는 name, price, discountAmount에 대한 값을 넣어야 하고, 해당 값들은 순서대로 string, number, number 타입을 가진다. 세 속성들은 ' private ' 키워드로 직접 접근을 막아두었다. 이 키워드는 '접근제한자' 라고 외부에서 특정 메서드나 프로퍼티에 접근하는 범위를 제한할 수 있다. 자세한 내용은 추후에 알아보도록 하자. 이번에 사용한 private는 직접 참조를 할 수 없도록 설정하는 키워드이다.

직접 참조를 할 수 없기 때문에 get 메서드를 사용해서 값을 호출하는 방법으로 사용이 가능하다.

마찬가지로, 값을 수정하기 위해서 set 메서드를 사용하면 값을 수정할 수 있다.

class Product {
  // private: 접근 불가 키워드. 직접 참조가 안되는 값이다.
  private name: string;
  private price: number;
  private discountAmount: number;
  // constructor: 생성자 메서드. 객체를 생성하고 초기화할 때 사용한다.
  constructor(name: string, price: number) {
    this.name = name;
    this.price = price;
    this.discountAmount = 0;
  }

  // get 메서드: private 속성에 접근이 가능하다.
  // getName: name 속성 값을 반환한다.
  getName(): string {
    return this.name;
  }

  // getPrice: price 속성값에 할인금액이 제외된, 즉 할인된 상품의 금액을 반환한다. 
  getPrice(): number {
    return this.price - this.discountAmount;
  }

  // getProduct: name속성과 getPrice로 할인된 상품의 가격을 반환한다.
  getProduct() {
    return { name: this.name, price: this.getPrice() };
  }


  // 할인가 조정하는 함수
  // set 메서드: 값을 전달하여 조정할 수 있다.
  // setDiscountAmount: 매개변수로 전달받은 값으로 discountAmount 속성의 값을 업데이트한다.
  setDiscountAmount(amount: number): void {
    this.discountAmount = amount;
  }
}

const product = new Product("블록", 1000);

product.setDiscountAmount(200);
console.log(product.getProduct());
 npm init -y
 npm i -D typescript
 npm i -D ts-node
 npx ts-node ./product.ts
 
 # setDiscountAmount 입력 전 결과
 { name: '블록', price: 1000 }
 
 # setDiscountAmount 입력 후 결과
 { name: '블록', price: 800 }

 

 

 


전략 패턴(Strategy Pattern)

  • 전략패턴은 실행(런타임)중에 알고리즘 전략을 선택하여 객체 동작을 실시간으로 바뀌도록 할 수 있게 하는 행위 디자인 패턴이다.
    • 여기서 '전략'이란 일종의 알고리즘이 될 수 도 있으며, 기능이나 동작이 될 수도 있는 특정한 목표를 수행하기 위한 행동 계획을 말한다.

즉, 어떤 일을 수행하는 알고리즘이 여러가지 일때, 동작들을 미리 전략으로 정의함으로써 손쉽게 전략을 교체할 수 있는, 알고리즘 변형이 빈번하게 필요한 경우에 적합한 패턴이다.

위에서 클래스 인터페이스의 예시로 들었던 상품정보를 담는 클래스의 할인률을 여러가지 방법으로 정의하여 사용하는 코드를 구현하고자 한다.

// 할인 인터페이스
interface Discount {
  // getDisCountPrice 메서드로 선언은 되고, 구현내용은 정의되지 않은 상태. 할인된 가격을 반환한다. 
  getDisCountPrice(price: number): number;
}

// 가격만 수정하는 할인 (고정값 할인)
class FlatDiscount implements Discount {
  private amount: number;
  constructor(amount: number) {
    this.amount = amount;
  }
  getDisCountPrice(price: number): number {
    return price - this.amount;
  }
}

// 할인으로 가격 수정(퍼센트 할인)
class PercentDiscount implements Discount {
  private amount: number;
  constructor(amount: number) {
    this.amount = amount;
  }
  getDisCountPrice(price: number): number {
    return price * (1 - this.amount / 100);
  }
}

// 고정값으로도 할인하고 퍼센트로도 할인하는 함수
class FlatPercentDiscount implements Discount {
  private flatAmount: number;
  private percent: number;
  constructor(flatAmount: number, percent: number) {
    this.flatAmount = flatAmount;
    this.percent = percent;
  }
  getDisCountPrice(price: number): number {
    const FlatDiscountAmount = price - this.flatAmount;
    return FlatDiscountAmount * (1 - this.percent / 100);
  }
}

// 할인의 기능에 대한 유지보수가 좋아진다.
// 다른 방식의 할인을 추가하고 싶다면, 클래스 하나만 더 추가하면 된다.

// 상품 클래스. 
class Products {
  private name: string;
  private price: number;
  constructor(name: string, price: number) {
    this.name = name;
    this.price = price;
  }

  getName(): string {
    return this.name;
  }

  getPrice(): number {
    return this.price;
  }
}

// 할인율을 적용하는 클래스
class ProductsDiscount {
	// product는 상품을 나타내는 Products 객체를 참조
  private product: Products;
  	// discount는 할인된 상품의 가격을 나타내는 Discount 객체를 참조
  private discount: Discount;
  constructor(product: Products, discount: Discount) {
    // 상품의 name과 price값을 가지고 있는 객체를 참조
    this.product = product;
    // 상품의 할인 방식을 가지고 있는 클래스 참조
    this.discount = discount;
  }
  
  getPrice(): void {
    console.log(this.discount.getDisCountPrice(this.product.getPrice()));
  }
}


const _product = new Products("mac", 1000000);
const _product2 = new Products("window", 20000);

const productDiscount = new PercentDiscount(10);
const productDiscount2 = new FlatDiscount(1000);
const productDiscount3 = new FlatPercentDiscount(1000, 10);


const productWithDiscount = new ProductsDiscount(_product, productDiscount);
productWithDiscount.getPrice();
// 결과: 900000

const productWithDiscount2 = new ProductsDiscount(_product2, productDiscount3);
productWithDiscount2.getPrice();
// 결과: 17100

 해당 방식으로 코드를 작성했다. 이 코드의 연산을 자세히 살펴보고자 한다.

예시로 위에 작성된

const productWithDiscount = new ProductsDiscount(_product, productDiscount);
productWithDiscount.getPrice();
// 결과: 900000

// 매개변수로 전달된 객체
const _product = new Products("mac", 1000000);
const productDiscount = new PercentDiscount(10);

해당 코드의 결과 값이 어떻게 90000이 나왔는지 살펴보면, productWithDiscount 객체로 ProductsDiscount를 실행한다. 실행할 때, _product와 productDiscount 객체를 매개변수로 전달한다. 그렇다면 ProductsDiscount 를 확인해보자.

class ProductsDiscount {
  private product: Products;
  private discount: Discount;
  constructor(product: Products, discount: Discount) {
    this.product = product;		
    // this.product == {this.name = "mac", this.price = 1000000}
    this.discount = discount;	
    // this.discount == PercentDiscount 클래스
  }
  getPrice(): void {
    console.log(this.discount.getDisCountPrice(this.product.getPrice()));
  }
}

다음과 같이 매개변수로 전달된 것을 확인할 수 있다.  productWithDiscount 객체 선언 아래를 확인하면 productWithDiscount.getPrice(); 코드를 확인할 수 있다. productWithDiscount 객체의 getPrice를 실행시키는 값을 반환하면 되는 것이다. getPrice를 확인해보면, 

  getPrice(): void {
    console.log(this.discount.getDisCountPrice(this.product.getPrice()));
  }

this.discount.getDisCountPrice 에서 discount는 PercentDiscount 클래스를 의미하므로 PercentDiscount 클래스의 getDisCountPrice를 실행하면 되는 것이다. 매개변수로는 product의 getPrice로, ' 1000000 '의 값을 전달한다. 해당 코드를 살펴보자.

class PercentDiscount implements Discount {
  private amount: number;
  constructor(amount: number) {
    this.amount = amount;
  }
  getDisCountPrice(price: number): number {
    return price * (1 - this.amount / 100);
  }
}

위의 ' _product ' 객체와 ' productDiscount ' 객체에서 매개변수로 받아온 값을 대입해서 살펴보면 productDiscount에서 매개변수로 ' 10 '을 보냈다. getDisCountPrice를 진행하면 amount의 값은 10으로 ' price *  ( 1 - 10 / 100 ) '으로 진행된다. 여기서 price는 productWithDiscount에서 받아왔으므로 식은 ' 1000000 * ( 1 - 10 / 100 ) '으로 1000000의 0.9의 값인 900000이 결과값이 되는 것이다.

 

 

 


읽기 전용 프로퍼티 readonly

 

  • 읽기 전용 속성(readonly property)은 단어 그대로, 인터페이스로 객체를 처음 생성할 때만 값을 할당하고 그 이후에는 변경할 수 없는 속성을 의미한다.
  • 다음과 같이 readonly 속성을 앞에 붙이면 간단하게 적용 된다.
  • 인터페이스로 객체를 처음 선언하여 값을 대입할때는 문제가 없다. 그러나 그 후에 따로 프로퍼티에 접근해서 수정하려고 하면 오류가 나게 된다.
interface User {
   name: string;
   age: number;
   gender?: string;
   readonly birthYear: number; // 읽기 전용 속성
}

let user: User = {
   name: 'jeff',
   age: 30,
   birthYear: 2010, // 최초에 값을 초기화 할때만 할당이 가능
};

user.birthYear = 1999; // Error - 이후에는 수정이 불가능

 

Readonly 유틸리티(Utility) 활용

Readonly 유틸리티(Utility)를 활용하면 프로퍼티마다 readonly를 적어주지 않아도 구현이 가능하다. 아래는 그에 대한 예시이다.

// readonly 무식하게 찍기
interface IUser {
  readonly name: string,
  readonly age: number
}

let user: IUser = {
  name: 'Neo',
  age: 36
};

user.age = 85; // Error
user.name = 'Evan'; // Error

// ----------------------------------------------------------------------------

// Readonly 유틸리티(Utility) 활용
interface IUser {
  name: string,
  age: number
}

let user: Readonly<IUser> = { // Array 처럼 따로 Readonly 라는 자료형이 있다고 생각하면 된다
  name: 'Neo',
  age: 36
};

user.age = 85; // Error
user.name = 'Evan'; // Error

 

읽기 전용 배열 ReadonlyArray<T>

ReadonlyArray<T> 유틸리티 타입을 사용하면 읽기 전용 배열을 생성할 수 있다. 선언하는 시점에만 값을 정의할 수 있고, 후에 배열의 내용을 변경할 수 없다.

let arr: ReadonlyArray<number> = [1,2,3]; // 읽기 전용 배열

arr.splice(0,1); // error
arr.push(4); // error
arr[0] = 100; // error

 

 

 


로그인 작업 전략패턴

 

Authent.ts

// 로그인 가입 관련된 작업
// 카카오, 네이버, 구글

import { Strategy } from "./auth";

// 인증에 필요한 이메일, 패스워드를 담는 인터페이스
export interface AuthProps {
  email: string;
  password: string;
}

// 검증 결과에 대한 값과 메세지를 담는 인터페이스
interface AuthenticatonResponse {
  success: boolean;
  message?: string;
}

// 검증에 대한 인터페이스
interface Authenticator {
  // 검증에 대한 요청 처리
  // authenticate 메서드: AuthProps 타입의 credentials를 매개변수로 받는다.
  // Promise를 반환한다. 반환되는 Promise는 AuthenticatonResponse 타입을 갖는다.
  authenticate(credentials: AuthProps): Promise<AuthenticatonResponse>;
}

// 이메일 로그인 로직 클래스
export class EmailAuthenticator implements Authenticator {
  // 비동기함수. AuthProps 타입의 credentials를 매개변수로 받아 인증을 수행.
  async authenticate(credentials: AuthProps): Promise<AuthenticatonResponse> {
    // 로직은 없다. 요청과 응답 코드가 들어갈 부분
    console.log("email login");
    // 인증이 성공했을 경우 success가 true를 반환한다.
    return { success: true };
  }
}

// 카카오 로그인 로직 클래그
export class KakaoAuthenticator implements Authenticator {
  async authenticate(credentials: AuthProps): Promise<AuthenticatonResponse> {
    // 카카오 로그인 로직 들어갈 부분
    console.log("kakao login");
    return { success: true };
  }
}

// 로그인에 대한 서비스를 처리할 클래스 구조
export interface LoginService {
  // 로그인 로직에 대한 함수 구조
  // login 메서드: 두 개의 매개변수와 Promise를 반환한다.
  	// type: string 타입으로 로그인의 유형을 나타내는 문자열이다. "email" or "kakao"
    // credentials: AuthProps 타입으로, 인증에 필요한 이메일과 패스워드를 담고있는 객체이다.
  login(type: string, credentials: AuthProps): Promise<AuthenticatonResponse>;
}

// 로그인 클래스에 로그인 서비스 구조를 상속 받고
export class Login implements LoginService {
  // strategy 타입을 매개변수로 받는다.
  // 
  constructor(private readonly strategy: Strategy) {}
  async login(
    type: "email" | "kakao",
    credentials: AuthProps
  ): Promise<AuthenticatonResponse> {
    // result : 로그인 로직이 들어있는 객체
    // 여기에서 어떤 로그인 로직으로 처리할 지 type 구분해서
    const result = await this.strategy[type].authenticate(credentials);
    return result;
  }
}

 

 

auth.ts

import {
  EmailAuthenticator,
  KakaoAuthenticator,
  AuthProps,
  Login,
  LoginService,
} from "./Authent";

interface IEmailSender {
  sendEmail(email: string): void;
}

class EmailSender implements IEmailSender {
  sendEmail(email: string): void {}
}

export interface Strategy {
  email: EmailAuthenticator;
  kakao: KakaoAuthenticator;
}

class Auth {
    // private 키워드가 붙어서 생성자에 넘겨받은 값이 객체의 키로 추가된다. 
  constructor(
    private readonly authProps: AuthProps,
    private readonly emailSender: EmailSender,
    private readonly loginService: LoginService
  ) {}
  // 로그인 로직 구조
  public async login() {
    console.log(this);
    await this.loginService.login("kakao", this.authProps);
  }
  // 이메일 인증 처리 구조
  public register(): void {
    this.emailSender.sendEmail(this.authProps.email);
  }
}

// 유저의 email과 password 임시 객체
const authProps: AuthProps = { email: "wee@gmail.com", password: "1234" };
const _emailSender = new EmailSender()

// email 로그인 로직 클래스 동적할당
const _email = new EmailAuthenticator()
// kakao 로그인 로직 클래스 동적할당
const _kakao = new  KakaoAuthenticator()

// 로그인 서비스 로직을 가지고 있는 객체
const _strategy : Strategy = {
    email : _email,
    kakao : _kakao
}

const _loginService = new Login(_strategy)
const auth = new Auth(authProps, _emailSender, _loginService)
auth.login()
// npx ts-node ./auth/auth.ts 

// 결과
// Auth {
//     authProps: { email: 'wee@gmail.com', password: '1234' },
//     emailSender: EmailSender {},
//     loginService: Login {
//       strategy: { email: EmailAuthenticator {}, kakao: KakaoAuthenticator {} }
//     }
//   }
//   kakao login

 

 

 

 

 

 

 

 

728x90