2024-06-17.md

🏑

DIL: μ΄νŽ™ν‹°λΈŒ νƒ€μž…μŠ€ν¬λ¦½νŠΈ

μŠ€ν„°λ””: μ›”κ°„ CS, https://github.com/monthly-cs/2024-05-effective-typescript
μž‘μ„±μΌ: 2024-06-17
μž‘μ„±μž: dusunax


μ½”λ“œλ₯Ό μž‘μ„±ν•˜κ³  μ‹€ν–‰ν•˜κΈ°

μ•„μ΄ν…œ 53: νƒ€μž…μŠ€ν¬λ¦½νŠΈ κΈ°λŠ₯λ³΄λ‹€λŠ” ECMAScript κΈ°λŠ₯을 μ‚¬μš©ν•˜κΈ° Prefer ECMAScript Features to TypeScript Features

μ˜›λ‚  μ˜›μ μ—

λ°”μ•Όνλ‘œ νƒ€μž…μŠ€ν¬λ¦½νŠΈ 초기 2010λ…„ κ²½, μžλ°”μŠ€ν¬λ¦½νŠΈλŠ” 클래슀, λ°μ½”λ ˆμ΄ν„°, λͺ¨λ“ˆ μ‹œμŠ€ν…œ 같은 κΈ°λŠ₯이 μ—†μ—ˆλ‹€...

μžλ°”μŠ€ν¬λ¦½νŠΈλŠ” ν”„λ ˆμž„μ›Œν¬λ‚˜ 트랜슀파일러둜 이λ₯Ό λ³΄μ™„ν•˜μ˜€κ³ , νƒ€μž…μŠ€ν¬λ¦½νŠΈλ„ 초기 λ²„μ „μ—λŠ” λ…λ¦½μ μœΌλ‘œ κ°œλ°œν•œ 클래슀, enum, λͺ¨λ“ˆ μ‹œμŠ€ν…œμ„ ν¬ν•¨μ‹œν‚¬ 수 밖에 μ—†μ—ˆλ‹€...

μ‹œκ°„μ΄ 흐λ₯΄λ©°... μžλ°”μŠ€ν¬λ¦½νŠΈ ν‘œμ€€ 기ꡬ인 TC39λŠ” λΆ€μ‘±ν–ˆλ˜ 점듀을 λ‚΄μž₯ κΈ°λŠ₯으둜 μΆ”κ°€ν–ˆλŠ”λ° μ΄λŠ” νƒ€μž…μŠ€ν¬λ¦½νŠΈ 초기 λ²„μ „μ—μ„œ λ…λ¦½μ μœΌλ‘œ κ°œλ°œν–ˆλ˜ κΈ°λŠ₯κ³Ό, μžλ°”μŠ€ν¬λ¦½νŠΈμ— μƒˆλ‘œ μΆ”κ°€λœ κΈ°λŠ₯이 ν˜Έν™˜μ„± 문제λ₯Ό λ°œμƒμ‹œν‚€λŠ” 원인이 λœλ‹€...

νƒ€μž…μŠ€ν¬λ¦½νŠΈ μ§„μ˜μ€ (1) νƒ€μž…μŠ€ν¬λ¦½νŠΈ 초기 버전 ν˜•νƒœλ₯Ό μœ μ§€ν•˜κΈ° μœ„ν•΄ μžλ°”μŠ€ν¬λ¦½νŠΈ μ‹ κ·œ κΈ°λŠ₯을 λ³€ν˜•ν•˜μ—¬ λΌμ›Œ λ§žμΆ”κ±°λ‚˜, (2) μžλ°”μŠ€ν¬λ¦½νŠΈμ˜ μ‹ κ·œ κΈ°λŠ₯을 κ·ΈλŒ€λ‘œ μ±„νƒν•˜κ³  νƒ€μž…μŠ€ν¬λ¦½νŠΈ 초기 버전을 ν¬κΈ°ν•˜λŠ” 두 가지 μ „λž΅ 쀑 ν•˜λ‚˜λ₯Ό μ„ νƒν•˜κ²Œ λœλ‹€...

λŒ€λΆ€λΆ„μ˜ μ§„μ˜μ΄ 두 번째 μ „λž΅μ„ μ„ νƒν•˜μ˜€κ³ , κ·Έ μ΄ν›„λ‘œ TC39λŠ” λŸ°νƒ€μž„ κΈ°λŠ₯을 λ°œμ „ μ‹œν‚€κ³ , νƒ€μž…μŠ€ν¬λ¦½νŠΈ νŒ€μ€ νƒ€μž… κΈ°λŠ₯만 λ°œμ „μ‹œν‚¨λ‹€λŠ” λͺ…ν™•ν•œ 원칙을 μ„Έμš°κ³  이λ₯Ό μ§€μΌœμ˜€κ³  μžˆλ‹€.

그런데 이 원칙이 μ„Έμ›Œμ§€κΈ° μ „ 이미 μ‚¬μš©λ˜κ³  있던 λͺ‡ 가지 κΈ°λŠ₯이 μžˆμ—ˆλ‹€. 이 κΈ°λŠ₯듀은 νƒ€μž… 곡간과 κ°’ κ³΅κ°„μ˜ 경계λ₯Ό ν˜Όλž€μŠ€λŸ½κ²Œ λ§Œλ“€κ²Œ λœλ‹€... λ‹€μŒ 화에 μ΄μ–΄μ„œ...

μ—΄κ±°ν˜• enum

  • κ°’μ˜ λͺ¨μŒ! μ—΄κ±°ν˜•!
    • κ°’μ˜ λͺ¨μŒμ„ λ‚˜νƒ€λ‚΄κΈ° μœ„ν•΄μ„œ λ§Žμ€ μ–Έμ–΄μ—μ„œ μ‚¬μš©ν•¨
enum Flavor {
  Vanilla = 0,
  Chocolate = 1,
  Strawberry = 2,
}

let flavor = Flavor.Chocolate;
//  ^? let flavor: Flavor

Flavor; // Autocomplete shows: Vanilla, Chocolate, Strawberry
Flavor[0]; // Value is "Vanilla"

νƒ€μž…μŠ€ν¬λ¦½νŠΈ μ—΄κ±°ν˜• enum의 문제점

  • μˆ«μžν˜• μ—΄κ±°ν˜•μ—μ„œλŠ” μ •μ˜λœ 숫자 μ™Έμ˜ 값이 할당될 수 μžˆλŠ” κ°€λŠ₯μ„± => μ˜λ„ν•˜μ§€ μ•Šμ€ λ™μž‘ 초래
let flavor: Flavor = 3; // 였λ₯˜κ°€ λ°œμƒν•˜μ§€ μ•ŠμŒ, μ˜λ„ν•˜μ§€ μ•Šμ€ κ°’ ν• λ‹Ή
  • μƒμˆ˜ μ—΄κ±°ν˜•(const enum)은 λŸ°νƒ€μž„μ— μ™„μ „νžˆ μ œκ±°λ˜μ–΄ μ΅œμ ν™”λ¨(μ—΄κ±°ν˜• 값이 사라짐)
const enum Flavor {
  Vanilla = 0,
  Chocolate = 1,
  Strawberry = 2,
}

let flavor = Flavor.Chocolate;
  • js 컴파일 image

  • tsconfig.json의 preserveConstEnums ν”Œλž˜κ·Έ μ‚¬μš©

{
  "compilerOptions": {
    "preserveConstEnums": true,
    // ...
  }
}

// js
var Flavor;
(function (Flavor) {
    Flavor[Flavor["Vanilla"] = 0] = "Vanilla";
    Flavor[Flavor["Chocolate"] = 1] = "Chocolate";
    Flavor[Flavor["Strawberry"] = 2] = "Strawberry";
})(Flavor || (Flavor = {}));
var flavor = Flavor.Chocolate;

λ¬Έμžμ—΄ μ—΄κ±°ν˜•

  • λ¬Έμžμ—΄ μ—΄κ±°ν˜•μ€ λŸ°νƒ€μž„μ—λ„ μ•ˆμ •μ„± & 투λͺ…μ„± 제곡
    • νƒ€μž…μŠ€ν¬λ¦½νŠΈμ˜ λ‹€λ₯Έ νƒ€μž…κ³Ό 달리 ꡬ쑰적 타이핑(duck typing)이 μ•„λ‹Œ λͺ…λͺ©μ  타이핑(nominally typing) μ‚¬μš©
      • 라이브러리λ₯Ό κ³΅κ°œν•  λ•Œ ν•„μš”
enum Flavor {
  Vanilla = "Vanilla",
  Chocolate = "Chocolate",
  Strawberry = "Strawberry",
}

let flavor: Flavor = Flavor.Chocolate;
console.log(flavor); // 좜λ ₯κ°’: "Chocolate"
  • μžλ°”μŠ€ν¬λ¦½νŠΈμ™€ νƒ€μž…μŠ€ν¬λ¦½νŠΈμ˜ λ™μž‘ 차이
function scoop(flavor: Flavor) {
  /* ... */
}

// μžλ°”μŠ€ν¬λ¦½νŠΈ
scoop("vanilla");

// νƒ€μž…μŠ€ν¬λ¦½νŠΈ: μ—΄κ±°ν˜•μ„ μž„ν¬νŠΈν•˜κ³  λ¬Έμžμ—΄ λŒ€μ‹  μ‚¬μš©ν•΄μ•Ό ν•œλ‹€
import { Flavor } from "ice-cream";
scoop(Flavor.VANILLA);
  • JS와 TS의 λ™μž‘μ΄ λ‹€λ₯΄κΈ° λ•Œλ¬Έμ— λ¬Έμžμ—΄ μ—΄κ±°ν˜•μ„ μ‚¬μš©ν•˜μ§€ μ•ŠλŠ” 것이 μ’‹λ‹€!
    • λ¦¬ν„°λŸ΄ μœ λ‹ˆμ˜¨μ„ μ‚¬μš©ν•˜μž: μ•ˆμ „ν•˜κ³ , JS와 ν˜Έν™˜λ˜κ³ , μžλ™μ™„μ„± κΈ°λŠ₯을 μ‚¬μš©ν•  수 μžˆλ‹€.
type Flavor = "vanilla" | "chocolate" | "strawberry";
let flavor: Flavor = "vanilla";

λ§€κ°œλ³€μˆ˜ 속성

  • 클래슀λ₯Ό μ΄ˆκΈ°ν™”ν•  λ•Œ, 속성 할당을 μœ„ν•΄ μƒμ„±μžμ˜ λ§€κ°œλ³€μˆ˜ μ‚¬μš©
// JS
class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

// TS
class Person {
  constructor(public name: string) {} // 멀버 λ³€μˆ˜λ‘œ name을 μ„ μ–Έν•œ 것과 λ™μΌν•˜κ²Œ λ™μž‘ν•œλ‹€.
}

λ§€κ°œλ³€μˆ˜ μ†μ„±μ˜ 문제점

  • 일반적으둜 TS μ»΄νŒŒμΌμ€ νƒ€μž… μ œκ±°κ°€ μ΄λ€„μ§€λ―€λ‘œ μ½”λ“œκ°€ μ€„μ–΄λ“€μ§€λ§Œ => λ§€κ°œλ³€μˆ˜ 속성은 μ½”λ“œκ°€ λŠ˜μ–΄λ‚œλ‹€
  • λ§€κ°œλ³€μˆ˜ 속성이 λŸ°νƒ€μž„μ—λŠ” μ‚¬μš©λ˜μ§€λ§Œ => TS κ΄€μ μ—μ„œ μ•ˆ μ“°λŠ” κ²ƒμ²˜λŸΌ λ³΄μž„
  • λ§€κ°œλ³€μˆ˜ 속성과 일반 속성을 μ„žμ–΄μ„œ μ‚¬μš© μ‹œ 클래슀 섀계가 ν˜Όλž€μŠ€λŸ½λ‹€
class Person {
  first: string;
  last: string;
  constructor(public name: string) {
    [this.first, this.last] = name.split(" ");
  }
}
// Person ν΄λž˜μŠ€μ— first, last, name 속성이 μžˆλ‹€.
// ν΄λž˜μŠ€μ— λ§€κ°œλ³€μˆ˜ μ†μ„±λ§Œ μ‘΄μž¬ν•œλ‹€λ©΄? 클래슀 λŒ€μ‹  μΈν„°νŽ˜μ΄μŠ€λ‘œ λ§Œλ“€κ³  객체 λ¦¬ν„°λŸ΄μ„ μ‚¬μš©ν•˜μž

class PersonClass {
  constructor(public name: string) {}
}
const p: PersonClass = { name: "Jed Bartlet" }; // λ§€κ°œλ³€μˆ˜ 속성 OK

interface Person {
  name: string;
}
const jed: Person = new PersonClass("Jed Bartlet");

λ„€μž„μŠ€νŽ˜μ΄μŠ€μ™€ νŠΈλ¦¬ν”Œ μŠ¬λž˜μ‹œ μž„ν¬νŠΈ (/// )

  • ES6μ΄μ „μ—λŠ” JS에 곡식적 λͺ¨λ“ˆ μ‹œμŠ€ν…œμ΄ μ—†μ—ˆλ‹€.
    • Node.jsλŠ” required와 module.exportsλ₯Ό μ‚¬μš©
    • AMDλŠ” define ν•¨μˆ˜μ™€ 콜백
    • νƒ€μž…μŠ€ν¬λ¦½νŠΈ μ—­μ‹œ 자체적 λͺ¨λ“ˆ μ‹œμŠ€ν…œ: module ν‚€μ›Œλ“œμ™€ νŠΈλ¦¬ν”Œ μŠ¬λž˜μ‹œ
      • λͺ¨λ“ˆ μ‹œμŠ€ν…œκ³Ό μΆ©λŒμ„ ν”Όν•˜κΈ° μœ„ν•΄μ„œ module λŒ€μ‹  namespace ν‚€μ›Œλ“œλ₯Ό 좔가함
  • moduleκ³Ό /// μ‚¬μš©ν•˜μ§€ μ•ŠκΈ° => ES6의 λͺ¨λ“ˆ μ‚¬μš©ν•΄μ•Ό 함
module Foo
// index.ts
/// <reference path="other.ts"/>
foo.bar();

λ°μ½”λ ˆμ΄ν„°

  • 클래슀, λ©”μ„œλ“œ, 속성에 annotation 뢙이기!
    • μ•΅κ·€λŸ¬ ν”„λ ˆμž„μ›Œν¬λ₯Ό μ§€μ›ν•˜κΈ° μœ„ν•΄ μΆ”κ°€λ˜μ—ˆμ—ˆλ‹€.
    • tsconfig.json의 experimentalDecorators μ„€μ • ν™œμ„±ν™” ν•„μš”
  • λ°μ½”λ ˆμ΄ν„°λŠ” ν‘œμ€€ν™”κ°€ μ™„λ£Œλ˜μ§€ μ•Šμ•˜μ§€λ§Œ, νƒ€μž…μŠ€ν¬λ¦½νŠΈμ—μ„œ μ‹€ν—˜μ μœΌλ‘œ μ‚¬μš©ν•  수 있음
class Greeter {
  greeting: string;

  constructor(message: string) {
    this.greeting = message;
  }

  @logged // <-- λ°μ½”λ ˆμ΄ν„°
  greet() {
    return `Hello, ${this.greeting}`;
  }
}

function logged(originalFn: any, context: ClassMethodDecoratorContext) {
  return function (this: any, ...args: any[]) {
    console.log(`Calling ${String(context.name)}`);
    return originalFn.call(this, ...args);
  };
}

console.log(new Greeter("Dave").greet());
// 좜λ ₯ κ²°κ³Ό:
// Calling greet
// Hello, Dave
  • ex) NestJSλŠ” λ°μ½”λ ˆμ΄ν„°λ₯Ό κ΄‘λ²”μœ„ν•˜κ²Œ μ‚¬μš©ν•˜λŠ” ν”„λ ˆμž„μ›Œν¬ => λͺ¨λ“ˆ, 컨트둀러, μ„œλΉ„μŠ€ 등을 μ •μ˜ν•  λ•Œ λ°μ½”λ ˆμ΄ν„° μ‚¬μš©

Things to Remember

  • By and large, you can convert TypeScript to JavaScript by removing all the types from your code.
  • Enums, parameter properties, triple-slash imports, experimental decorators, and member visibility modifiers are historical exceptions to this rule.
    • 일반적으둜 νƒ€μž… 정보λ₯Ό μ œκ±°ν•˜λ©΄ μžλ°”μŠ€ν¬λ¦½νŠΈκ°€ λ˜μ§€λ§Œ μ—΄κ±°ν˜•, λ§€κ°œλ³€μˆ˜ 속성, νŠΈλ¦¬ν”Œ μŠ¬λž˜μ‹œ μž„ν¬νŠΈ, λ°μ½”λ ˆμ΄ν„°μ™€ 같이 νƒ€μž… 정보λ₯Ό μ œκ±°ν•œλ‹€κ³  μžλ°”μŠ€ν¬λ¦½νŠΈκ°€ λ˜μ§€λŠ” μ•ŠλŠ” κ²½μš°κ°€ μžˆλ‹€.
  • To keep TypeScript’s role in your codebase as clear as possible and to avoid future compatibility issues, avoid nonstandard features.
    • μ•žμ„œ λ‚˜μ—΄ν•œ κΈ°λŠ₯ => νƒ€μž… 슀크립트의 역할을 λͺ…ν™•ν•˜κ²Œ ν•˜κΈ° μœ„ν•΄ μ‚¬μš©ν•˜μ§€ μ•ŠλŠ” 것이 μ’‹λ‹€.