๋งž์ถค ์ •์žฅ TypeScript

youngki
  • #TypeScript
  • #Type Guard
  • #static typed language
  • #Type Manipulation
  • #fromm

๋“ค์–ด๊ฐ€๋ฉฐ

์•ˆ๋…•ํ•˜์„ธ์š”. ๋…ธ๋จธ์Šค์—์„œ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์„ ํ•˜๊ณ  ์žˆ๋Š” ์œ ์˜๊ธฐ์ž…๋‹ˆ๋‹ค.

์ธํ„ฐ๋„ท์—์„œ TypeScript์˜ โ€˜Type Guardโ€™์— ๋Œ€ํ•ด ๊ฒ€์ƒ‰์„ ํ•ด๋ณด๋ฉด, ๋งŽ์€ ๊ธ€๋“ค์ด ๋‚˜์˜ค๊ธด ํ•˜์ง€๋งŒ, ๋ง‰์ƒ ์–ด๋–ค ์ƒํ™ฉ์—์„œ ์ด๋ฅผ ์ ์šฉํ• ๋•Œ ์ง„๊ฐ€๋ฅผ ๋ฐœํœ˜ํ•˜๋Š”์ง€์— ๋Œ€ํ•œ ์˜ˆ์‹œ๊ฐ€ ๋ถ€์กฑํ•œ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.
๊ทธ๋ž˜์„œ ์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ TypeScript์˜ Type Guard๋ฅผ ์–ด๋–ป๊ฒŒ ํ™œ์šฉํ–ˆ๋Š”์ง€์— ๋Œ€ํ•ด ์†Œ๊ฐœํ•˜๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค.

๊ฐœ๋ฐœ

ํ”„๋กœ์ ํŠธ ๋ฐฐ๊ฒฝ

์ €ํฌ๋Š” ์–ผ๋งˆ์ „ ๊ณ ๊ฐ์‚ฌ๋ฅผ ์œ„ํ•œ B2B ์„œ๋น„์Šค์ธ ํŒŒํŠธ๋„ˆ์„ผํ„ฐ๋ฅผ ์˜คํ”ˆํ–ˆ์Šต๋‹ˆ๋‹ค.
๊ณ ๊ฐ์‚ฌ๋Š” ํŒŒํŠธ๋„ˆ์„ผํ„ฐ๋ฅผ ํ†ตํ•ด ์•„ํ‹ฐ์ŠคํŠธ์˜ ์ฑ„๋„์— ๊ณต์ง€์‚ฌํ•ญ์„ ์ž‘์„ฑํ•˜๊ณ , Push ์•Œ๋ฆผ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฒก์—”๋“œ ๋กœ์ง ๊ฐœ๋ฐœ ์ „์—, ํ”„๋ก ํŠธ์—”๋“œ์™€ API Spec์„ ์ •์˜ํ•˜๊ณ ,

export class CreateNoticeDto {
  title: string;
  content: string;
  ...
  notification?: {
    targets: NoticeNotificationTargetType[];
    time: number;
    ...
  };
}

DB ํ…Œ์ด๋ธ” ์„ค๊ณ„๋ฅผ ์ง„ํ–‰ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

@Entity('notice')
export class Notice {
  ...

  @Column()
  status: NoticeStatusType;

  @Column()
  title: string;

  @Column()
  content: string;

  @Column()
  targets?: NoticeNotificationTargetType[];

  @Column()
  time?: Date;

  ...
}

ํ”„๋ก ํŠธ์—”๋“œ์™€์˜ interface์—์„œ nested object๋ฅผ ์‚ฌ์šฉํ•œ ์ด์œ ๋Š” notification object ์ „์ฒด๊ฐ€ ํ•„์š”๋กœ ํ•˜๊ฑฐ๋‚˜, ํ•„์š”๋กœ ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
ํ•˜์ง€๋งŒ, DB ํ…Œ์ด๋ธ”์—์„œ๋Š” flatํ•˜๊ฒŒ ๋ฐ์ดํ„ฐ๊ฐ€ ์ •๋ฆฌ๋˜์–ด ๋“ค์–ด๊ฐ€๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ column ๋ณ€์ˆ˜์— ๋Œ€ํ•œ optional์„ ๊ฐ๊ฐ ํ•ด์„ํ•ด์ค˜์•ผ ํ•˜๋Š” ๋ถˆํŽธํ•จ์ด ์ƒ๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
(Type Guard์— ๋Œ€ํ•œ ๋‚ด์šฉ์ด๋ฏ€๋กœ, notification์— ๋Œ€ํ•œ ์ •๊ทœํ™” ๊ฐ™์€ ๊ฒƒ์œผ๋กœ ๋‚ด์šฉ์„ ํ™•์žฅํ•˜์ง€๋Š” ์•Š๊ฒ ์Šต๋‹ˆ๋‹ค.)

์–ด๋–ค ๋ฐ์ดํ„ฐ ๋ฉ์–ด๋ฆฌ(๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์ฒด) ์ „์ฒด๊ฐ€ optionalํ•˜๊ฒŒ ํ–‰๋™์„ ๊ฐ™์ด ํ•  ๋•Œ ๊ฐ ๊ฐœ๋ณ„ ๋ณ€์ˆ˜๋ฅผ flatํ•˜๊ฒŒ ์ €์žฅํ•œ DB ํ…Œ์ด๋ธ”์˜ instance์—์„œ๋Š” ์ด ๋ณ€์ˆ˜๋“ค์„ ๋งŒ์งˆ๋•Œ๋งˆ๋‹ค ์ฒดํฌ(validation)ํ•ด ์ฃผ์–ด์•ผ ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

Validation์˜ ์ข…๋ฅ˜

๋ฒก์—”๋“œ ๋กœ์ง์˜ Guard ์—ญํ• ์€ ๊ด€์‹ฌ์‚ฌ์˜ Scope์— ๋”ฐ๋ผ ๋‹ค์–‘ํ•˜๊ฒŒ ์กด์žฌํ•˜๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. Client๋กœ ๋ถ€ํ„ฐ ์ž…๋ ฅ๋˜๋Š” ๋น„๊ต์  ์ƒํ˜ธ ์˜์กด์ ์ด์ง€ ์•Š์€ ๊ฐœ๋ณ„ ๋ณ€์ˆ˜์— ๋Œ€ํ•œ validation์€ DTO์—์„œ ์ฒ˜๋ฆฌ
  2. ์‚ฌ์šฉ์ž ์ธ์ฆ์€ Framework Guard์—์„œ ์ฒ˜๋ฆฌ
  3. ์„œ๋น„์Šค ๋กœ์ง์—์„œ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰๋˜๋Š” ๋„์ค‘ class-validator ์ˆ˜์ค€ ์ด์ƒ์˜ ๋ณต์žก์„ฑ์„ ํ†ต๊ณผํ•œ ์ดํ›„์— Type Guard ์‚ฌ์šฉ์„ ๊ถŒํ•˜๊ณ  ์‹ถ์–ด ์ด ๊ธ€์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

Derived Type ์ƒ์„ฑ

https://www.typescriptlang.org/docs/handbook/utility-types.html

export type PushableNoticeType = Omit<Notice, 'status' | 'targets' | 'time'> & {
  status: NoticeStatusType.READY;
  targets: NoticeNotificationTargetType[];
  time: Date;
};

Type Guard ์ ์šฉ

https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates

๊ณต์ง€์‚ฌํ•ญ์„ ๋“ฑ๋กํ•  ๋•Œ, Push ์•Œ๋ฆผ์„ ๋ณด๋‚ผ ์ˆ˜๋„ ์žˆ๊ณ , ๋ณด๋‚ด์ง€ ์•Š์„ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
์„ค๋ช…์„ ์œ„ํ•ด nested object์— ํ•ด๋‹นํ•˜๋Š” ๋ณ€์ˆ˜๋ฅผ ๋งŽ์ง€ ์•Š๊ฒŒ ์„ค์ •ํ•˜์˜€์ง€๋งŒ, ๋” ๋งŽ์€ ๋ณ€์ˆ˜๋“ค์ด ์กด์žฌํ•œ๋‹ค๋ฉด, ๋ฒก์—”๋“œ ๋กœ์ง์—์„œ ์ ์  ์ง€์ €๋ถ„ํ•œ ์ฝ”๋“œ๊ฐ€ ๋  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค.
ํ•˜์ง€๋งŒ ์œ„์˜ PushableNoticeType๊ณผ ๊ฐ™์ด, Notice์œผ๋กœ ๋ถ€ํ„ฐ ํŒŒ์ƒ๋˜์–ด ๋นˆ๋ฒˆํ•˜๊ฒŒ ์‚ฌ์šฉ๋  ์ƒˆ๋กœ์šด Type์„ ๋งŒ๋“ค์–ด, Type Guard๋ฅผ ์ ์šฉํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

isPushableNotice(notice: Notice, now: Date): notice is PushableNoticeType {
  const { status, targets, ... } = notice;

  if (!status) {
    // check status
    throw Error('status');
  }

  if (!targets) {
    // check targets
    throw Error('targets');
  }
  
  // check time
  
  return true;
}

(์„ค๋ช…์„ ์œ„ํ•ด, ์‹ค์ œ ๊ตฌํ˜„์—์„œ ์•ฝ๊ฐ„ ์ถ•์•ฝ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.)

isPushableNotice ํ•จ์ˆ˜๊ฐ€ ์œ„์—์„œ ์–ธ๊ธ‰ํ•œ class-validator ์ˆ˜์ค€ ์ด์ƒ์˜ ๋ณต์žก์„ฑ์„ ์ฒดํฌํ•˜๋Š” ์ผ์ข…์˜ validator๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.


Type Guard๊ฐ€ ์ ์šฉ๋œ Service Logic์˜ ํ๋ฆ„์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

async pushNotice(noticeId: number, now: Date) {
  const notice = await this.noticeRepository.getNotice(noticeId);

  // ์—ฌ๊ธฐ๊นŒ์ง€๋Š” notice: Notice
  this.isPushableNotice(notice, now);
  // ์—ฌ๊ธฐ๋ถ€ํ„ฐ๋Š” notice: PushableNoticeType
  
  await this.sendNoticePush(notice, now);
}

isPushableNotice ํ•จ์ˆ˜ ์ „ํ›„๋กœ ์ฝ”๋“œ์—์„œ IDE Tooltip์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋‚˜ํƒ€๋‚ด์ฃผ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

  • isPushableNotice ํ•จ์ˆ˜ ์ด์ „

notice_tooltip.png

  • isPushableNotice ํ•จ์ˆ˜ ์ดํ›„

pushable_notice_tooltip.png

Type Guard๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ƒํ™ฉ์„ ์ƒ์ƒํ•ด ๋ณด๋ฉด,

์–ด๋–ค ์„œ๋ธŒ ํ•จ์ˆ˜ ๋‚ด์—์„œ notice.xxx์— ๋Œ€ํ•œ validation์„ ์ž˜ ํ–ˆ๋‹ค๊ณ  ํ•ด๋„, ๊ทธ ํ•จ์ˆ˜๋ฅผ ๋ฒ—์–ด๋‚œ ๊ณณ์—์„œ๋Š” notice.xxx์— ๋Œ€ํ•œ validation์„ ๋‹ค์‹œ ํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
notice์˜ origin type(Notice)์„ ๊ณ„์† ๋‹ฌ๊ณ  ๋‹ค๋‹ˆ๋ฉด์„œ, ๋ฌธ์ œ ์ƒํ™ฉ๋งˆ๋‹ค IDE์—์„œ ๋นจ๊ฐ„ ๋ฐ‘์ค„์ด ๊ทธ์–ด์ง€๊ฑฐ๋‚˜, ์•„๋‹ˆ๋ฉด ๋ฒก์—”๋“œ ๋กœ์ง ์—ฌ๊ธฐ ์ €๊ธฐ์„œ ๋ฐ˜๋ณต์ ์ธ validation์„ ์—ฌ๋Ÿฌ๋ฒˆ ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด์ œ๊นŒ์ง€ ์„ค๋ช…ํ•œ ๋‚ด์šฉ์„ ๊ทธ๋ฆผ์œผ๋กœ ํ‘œํ˜„ํ•ด ๋ดค์Šต๋‹ˆ๋‹ค.

type_guard.drawio.png

๋งˆ์น˜๋ฉฐ

๊ฐœ์ธ์ ์ธ ๊ฒฌํ•ด๋กœ TypeScript์˜ Type Manipulation์„ ํ†ตํ•œ Type Narrowing์€ ์ •๋ง๋กœ ๋งค๋ ฅ์ ์ž…๋‹ˆ๋‹ค.
Static Type์„ Programming ์‹œ์ ์—์„œ๋Š” Dynamic ํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๋Š” ๋Š๋‚Œ์ด๋ž„๊นŒโ€ฆ

ํšŒ์‚ฌ ๋‚ด๋ถ€ repository์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ utilities๋„ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

export type WithRequiredProperty<ObjectType, Key extends keyof ObjectType> = Omit<ObjectType, Key> & {
    [Property in Key]-?: ObjectType[Property];
};

TypeScript๋ฅผ ์‚ฌ์šฉํ•˜์‹ ๋‹ค๋ฉด, Type Guard, Type Manipulation์œผ๋กœ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์ฒด์˜ ํƒ€์ž…์„ ์ตœ๋Œ€ํ•œ ์ขํ˜€ ๋งž์ถค์ •์žฅ์„ ์ž…ํ˜€ ๋ณด์‹œ์ฃ .

๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

โ† ๋ชฉ๋ก์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ

Art Changes Life

๋…ธ๋จธ์Šค์™€ ํ•จ๊ป˜ ์—”ํ„ฐํ…Œํฌ ์‚ฐ์—…์„ ํ˜์‹ ํ•ด๋‚˜๊ฐˆ ๋ฉค๋ฒ„๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค.

์ฑ„์šฉ ์ค‘์ธ ๊ณต๊ณ  ๋ณด๊ธฐ