clean-code-typescript

Tweet Tweet

ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋ฅผ ์œ„ํ•œ ํด๋ฆฐ์ฝ”๋“œ

clean-code-javascript์—์„œ ์˜๊ฐ์„ ๋ฐ›์•˜์Šต๋‹ˆ๋‹ค.

๋ชฉ์ฐจ

  1. ์†Œ๊ฐœ
  2. ๋ณ€์ˆ˜
  3. ํ•จ์ˆ˜
  4. ๊ฐ์ฒด์™€ ์ž๋ฃŒ๊ตฌ์กฐ
  5. ํด๋ž˜์Šค
  6. SOLID
  7. ํ…Œ์ŠคํŠธ
  8. ๋™์‹œ์„ฑ
  9. ์—๋Ÿฌ ์ฒ˜๋ฆฌ
  10. ์„œ์‹
  11. ์ฃผ์„
  12. ๋ฒˆ์—ญ
  13. ๋ฒˆ์—ญ์— ๋„์›€์„ ์ฃผ์‹  ๋ถ„๋“ค

์†Œ๊ฐœ

Humorous image of software quality estimation as a count of how many expletives
you shout when reading code

Robert C. Martin์˜ ์ฑ…์ธ ํด๋ฆฐ ์ฝ”๋“œ์— ์žˆ๋Š” ์†Œํ”„ํŠธ์›จ์–ด ๊ณตํ•™ ๋ฐฉ๋ฒ•๋ก ์„ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์— ์ ์šฉํ•œ ๊ธ€์ž…๋‹ˆ๋‹ค. ์ด ๊ธ€์€ ์Šคํƒ€์ผ ๊ฐ€์ด๋“œ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค. ์ด ๊ธ€์€ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์—์„œ ์ฝ๊ธฐ ์‰ฝ๊ณ , ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ๋ฆฌํŒฉํ† ๋ง ๊ฐ€๋Šฅํ•œ ์†Œํ”„ํŠธ์›จ์–ด๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค.

์—ฌ๊ธฐ ์žˆ๋Š” ๋ชจ๋“  ๊ทœ์น™์„ ์—„๊ฒฉํ•˜๊ฒŒ ๋”ฐ๋ฅผ ํ•„์š”๋Š” ์—†์œผ๋ฉฐ, ๋ณดํŽธ์ ์œผ๋กœ ํ†ต์šฉ๋˜๋Š” ๊ทœ์น™์€ ์•„๋‹™๋‹ˆ๋‹ค. ์ด ๊ธ€์€ ํ•˜๋‚˜์˜ ์ง€์นจ์ผ ๋ฟ์ด๋ฉฐ, ํด๋ฆฐ ์ฝ”๋“œ์˜ ์ €์ž๊ฐ€ ์ˆ˜๋…„๊ฐ„ ๊ฒฝํ—˜ํ•œ ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ์ •๋ฆฌํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์†Œํ”„ํŠธ์›จ์–ด ๊ณตํ•™ ๊ธฐ์ˆ ์˜ ์—ญ์‚ฌ๋Š” 50๋…„์ด ์กฐ๊ธˆ ๋„˜์—ˆ๊ณ , ๋ฐฐ์›Œ์•ผ ํ•  ๊ฒƒ์ด ์—ฌ์ „ํžˆ ๋งŽ์Šต๋‹ˆ๋‹ค. ์†Œํ”„ํŠธ์›จ์–ด ์„ค๊ณ„๊ฐ€ ๊ฑด์ถ• ์„ค๊ณ„๋งŒํผ ์˜ค๋ž˜๋˜์—ˆ์„ ๋•Œ๋Š” ์•„๋งˆ๋„ ์•„๋ž˜ ๊ทœ์น™๋“ค๋ณด๋‹ค ์—„๊ฒฉํ•œ ๊ทœ์น™์„ ๋”ฐ๋ผ์•ผ ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ง€๊ธˆ์€ ์ด ์ง€์นจ์„ ๋‹น์‹ ๊ณผ ๋‹น์‹  ํŒ€์ด ์ž‘์„ฑํ•˜๋Š” ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ์ฝ”๋“œ์˜ ํ’ˆ์งˆ์„ ํ‰๊ฐ€ํ•˜๋Š” ๊ธฐ์ค€์œผ๋กœ ์‚ผ์œผ์„ธ์š”.

ํ•œ ๊ฐ€์ง€ ๋” ๋ง์”€๋“œ๋ฆฌ์ž๋ฉด, ์ด ๊ทœ์น™๋“ค์„ ์•Œ๊ฒŒ ๋œ๋‹ค ํ•ด์„œ ๋‹น์žฅ ๋” ๋‚˜์€ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜๋Š” ๊ฒƒ์€ ์•„๋‹ˆ๋ฉฐ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ ์‹ค์ˆ˜๋ฅผ ํ•˜์ง€ ์•Š๊ฒŒ ํ•ด์ฃผ๋Š” ๊ฒƒ์€ ์•„๋‹™๋‹ˆ๋‹ค. ์ –์€ ์ ํ† ๊ฐ€ ์ตœ์ข…์˜ ๊ฒฐ๊ณผ๋ฌผ๋กœ ๋นš์–ด์ง€๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ชจ๋“  ์ฝ”๋“œ๋“ค๋„ ์ฒ˜์Œ ์ž‘์„ฑํ•œ ์ฝ”๋“œ๋กœ ์‹œ์ž‘๋ฉ๋‹ˆ๋‹ค. ๊ฒฐ๊ตญ์€ ๋™๋ฃŒ๋“ค๊ณผ ๋ฆฌ๋ทฐํ•˜๋ฉด์„œ ๊ฒฐ์ ์ด ์ œ๊ฑฐ๋ฉ๋‹ˆ๋‹ค. ๋‹น์‹ ์ด ์ฒ˜์Œ ์ž‘์„ฑํ•œ ์ฝ”๋“œ์— ๊ฐœ์„ ์ด ํ•„์š”ํ•  ๋•Œ ์ž์ฑ…ํ•˜์ง€ ๋งˆ์„ธ์š”. ๋Œ€์‹  ์ฝ”๋“œ๊ฐ€ ๋” ๋‚˜์•„์ง€๋„๋ก ๋‘๋“ค๊ธฐ์„ธ์š”!

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

๋ณ€์ˆ˜

์˜๋ฏธ์žˆ๋Š” ๋ณ€์ˆ˜ ์ด๋ฆ„์„ ์‚ฌ์šฉํ•˜์„ธ์š”

์ฝ๋Š” ์‚ฌ๋žŒ์œผ๋กœ ํ•˜์—ฌ๊ธˆ ๋ณ€์ˆ˜๋งˆ๋‹ค ์–ด๋–ค ์ ์ด ๋‹ค๋ฅธ์ง€ ์•Œ ์ˆ˜ ์žˆ๋„๋ก ์ด๋ฆ„์„ ๊ตฌ๋ณ„ํ•˜์„ธ์š”.

Bad:

function between<T>(a1: T, a2: T, a3: T): boolean {
  return a2 <= a1 && a1 <= a3;
}

Good:

function between<T>(value: T, left: T, right: T): boolean {
  return left <= value && value <= right;
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

๋ฐœ์Œํ•  ์ˆ˜ ์žˆ๋Š” ๋ณ€์ˆ˜ ์ด๋ฆ„์„ ์‚ฌ์šฉํ•˜์„ธ์š”

๋ฐœ์Œํ•  ์ˆ˜ ์—†๋Š” ์ด๋ฆ„์€ ๊ทธ ๋ณ€์ˆ˜์— ๋Œ€ํ•ด์„œ ๋ฐ”๋ณด ๊ฐ™์ด ์†Œ๋ฆฌ๋ฅผ ๋‚ด ํ† ๋ก ํ•  ์ˆ˜๋ฐ–์— ์—†์Šต๋‹ˆ๋‹ค.

Bad:

type DtaRcrd102 = {
  genymdhms: Date;
  modymdhms: Date;
  pszqint: number;
}

Good:

type Customer = {
  generationTimestamp: Date;
  modificationTimestamp: Date;
  recordId: number;
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

๋™์ผํ•œ ์œ ํ˜•์˜ ๋ณ€์ˆ˜๋Š” ๋™์ผํ•œ ๋‹จ์–ด๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”

Bad:

function getUserInfo(): User;
function getUserDetails(): User;
function getUserData(): User;

Good:

function getUser(): User;

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ๋Š” ์ด๋ฆ„์„ ์‚ฌ์šฉํ•˜์„ธ์š”

์ฝ”๋“œ๋ฅผ ์“ธ ๋•Œ๋ณด๋‹ค ์ฝ์„ ๋•Œ๊ฐ€ ๋” ๋งŽ๊ธฐ ๋•Œ๋ฌธ์— ์šฐ๋ฆฌ๊ฐ€ ์“ฐ๋Š” ์ฝ”๋“œ๋Š” ์ฝ์„ ์ˆ˜ ์žˆ๊ณ  ๊ฒ€์ƒ‰์ด ๊ฐ€๋Šฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ”„๋กœ๊ทธ๋žจ์„ ์ดํ•ดํ•  ๋•Œ ์˜๋ฏธ์žˆ๋Š” ๋ณ€์ˆ˜ ์ด๋ฆ„์„ ์ง“์ง€ ์•Š์œผ๋ฉด ์ฝ๋Š” ์‚ฌ๋žŒ์œผ๋กœ ํ•˜์—ฌ๊ธˆ ์–ด๋ ค์›€์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ ์ด๋ฆ„์„ ์ง€์œผ์„ธ์š”. TSLint์™€ ๊ฐ™์€ ๋„๊ตฌ๋Š” ์ด๋ฆ„์ด ์—†๋Š” ์ƒ์ˆ˜๋ฅผ ์‹๋ณ„ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค๋‹ˆ๋‹ค.

Bad:

// 86400000์ด ๋„๋Œ€์ฒด ๋ญ์ง€?
setTimeout(restart, 86400000);

Good:

// ๋Œ€๋ฌธ์ž๋กœ ์ด๋ฃจ์–ด์ง„ ์ƒ์ˆ˜๋กœ ์„ ์–ธํ•˜์„ธ์š”.
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;

setTimeout(restart, MILLISECONDS_IN_A_DAY);

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

์˜๋„๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”

Bad:

declare const users: Map<string, User>;

for (const keyValue of users) {
  // users ๋งต์„ ์ˆœํšŒ
}

Good:

declare const users: Map<string, User>;

for (const [id, user] of users) {
  // users ๋งต์„ ์ˆœํšŒ
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

์•”์‹œํ•˜๋Š” ์ด๋ฆ„์€ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ์„ธ์š”

๋ช…์‹œ์ ์ธ ๊ฒƒ์ด ์•”์‹œ์ ์ธ ๊ฒƒ๋ณด๋‹ค ์ข‹์Šต๋‹ˆ๋‹ค.
๋ช…๋ฃŒํ•จ์€ ์ตœ๊ณ ์ž…๋‹ˆ๋‹ค.

Bad:

const u = getUser();
const s = getSubscription();
const t = charge(u, s);

Good:

const user = getUser();
const subscription = getSubscription();
const transaction = charge(user, subscription);

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

๋ถˆํ•„์š”ํ•œ ๋ฌธ๋งฅ์€ ์ถ”๊ฐ€ํ•˜์ง€ ๋งˆ์„ธ์š”

ํด๋ž˜์Šค/ํƒ€์ž…/๊ฐ์ฒด์˜ ์ด๋ฆ„์— ์˜๋ฏธ๊ฐ€ ๋‹ด๊ฒจ์žˆ๋‹ค๋ฉด, ๋ณ€์ˆ˜ ์ด๋ฆ„์—์„œ ๋ฐ˜๋ณตํ•˜์ง€ ๋งˆ์„ธ์š”.

Bad:

type Car = {
  carMake: string;
  carModel: string;
  carColor: string;
}

function print(car: Car): void {
  console.log(`${car.carMake} ${car.carModel} (${car.carColor})`);
}

Good:

type Car = {
  make: string;
  model: string;
  color: string;
}

function print(car: Car): void {
  console.log(`${car.make} ${car.model} (${car.color})`);
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

short circuiting์ด๋‚˜ ์กฐ๊ฑด๋ฌธ ๋Œ€์‹  ๊ธฐ๋ณธ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”

๊ธฐ๋ณธ ๋งค๊ฐœ๋ณ€์ˆ˜๋Š” short circuiting๋ณด๋‹ค ๋ณดํ†ต ๋ช…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.

Bad:

function loadPages(count?: number) {
  const loadCount = count !== undefined ? count : 10;
  // ...
}

Good:

function loadPages(count: number = 10) {
  // ...
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

์˜๋„๋ฅผ ์•Œ๋ ค์ฃผ๊ธฐ ์œ„ํ•ด enum์„ ์‚ฌ์šฉํ•˜์„ธ์š”

์˜ˆ๋ฅผ ๋“ค์–ด ๊ทธ๊ฒƒ๋“ค์˜ ๊ฐ’ ์ž์ฒด๋ณด๋‹ค ๊ฐ’์ด ๊ตฌ๋ณ„๋˜์–ด์•ผ ํ•  ๋•Œ์™€ ๊ฐ™์ด ์ฝ”๋“œ์˜ ์˜๋„๋ฅผ ์•Œ๋ ค์ฃผ๋Š”๋ฐ์— enum์€ ๋„์›€์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Bad:

const GENRE = {
  ROMANTIC: 'romantic',
  DRAMA: 'drama',
  COMEDY: 'comedy',
  DOCUMENTARY: 'documentary',
}

projector.configureFilm(GENRE.COMEDY);

class Projector {
  // Projector์˜ ์„ ์–ธ
  configureFilm(genre) {
    switch (genre) {
      case GENRE.ROMANTIC:
        // ์‹คํ–‰๋˜์–ด์•ผ ํ•˜๋Š” ๋กœ์ง
    }
  }
}

Good:

enum GENRE {
  ROMANTIC,
  DRAMA,
  COMEDY,
  DOCUMENTARY,
}

projector.configureFilm(GENRE.COMEDY);

class Projector {
  // Projector์˜ ์„ ์–ธ
  configureFilm(genre) {
    switch (genre) {
      case GENRE.ROMANTIC:
        // ์‹คํ–‰๋˜์–ด์•ผ ํ•˜๋Š” ๋กœ์ง
    }
  }
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

ํ•จ์ˆ˜

ํ•จ์ˆ˜์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋Š” 2๊ฐœ ํ˜น์€ ๊ทธ ์ดํ•˜๊ฐ€ ์ด์ƒ์ ์ž…๋‹ˆ๋‹ค

ํ•จ์ˆ˜ ๋งค๊ฐœ๋ณ€์ˆ˜์˜ ๊ฐœ์ˆ˜๋ฅผ ์ œํ•œํ•˜๋Š” ๊ฒƒ์€ ํ•จ์ˆ˜๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์— ๋†€๋ผ์šธ ์ •๋„๋กœ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ํ•จ์ˆ˜ ๋งค๊ฐœ๋ณ€์ˆ˜๊ฐ€ 3๊ฐœ ์ด์ƒ์ธ ๊ฒฝ์šฐ, ๊ฐ๊ธฐ ๋‹ค๋ฅธ ์ธ์ˆ˜๋กœ ์—ฌ๋Ÿฌ ๋‹ค๋ฅธ ์ผ€์ด์Šค๋ฅผ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•˜๋ฏ€๋กœ ๊ฒฝ์šฐ์˜ ์ˆ˜๊ฐ€ ๋งค์šฐ ๋งŽ์•„์ง‘๋‹ˆ๋‹ค.

ํ•œ ๊ฐœ ํ˜น์€ ๋‘ ๊ฐœ์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๊ฐ€ ์ด์ƒ์ ์ธ ๊ฒฝ์šฐ๊ณ , ๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด ์„ธ ๊ฐœ๋Š” ํ”ผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ ์ด์ƒ์˜ ๊ฒฝ์šฐ์—๋Š” ํ•ฉ์ณ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‘ ๊ฐœ ์ด์ƒ์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๊ฐ€์งˆ ๊ฒฝ์šฐ, ํ•จ์ˆ˜๊ฐ€ ๋งŽ์€ ๊ฒƒ์„ ํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์•„์ง‘๋‹ˆ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์€ ๊ฒฝ์šฐ, ๋Œ€๋ถ€๋ถ„ ์ƒ์œ„ ๊ฐ์ฒด๋Š” ํ•˜๋‚˜์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ์ถฉ๋ถ„ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๋งŽ์€ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค๋ฉด ๊ฐ์ฒด ๋ฆฌํ„ฐ๋Ÿด์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”.

ํ•จ์ˆ˜๊ฐ€ ๊ธฐ๋Œ€ํ•˜๋Š” ์†์„ฑ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด, ๊ตฌ์กฐ ๋ถ„ํ•ด ๊ตฌ๋ฌธ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ตฌ๋ฌธ์€ ๋ช‡ ๊ฐœ์˜ ์žฅ์ ์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค:

  1. ์–ด๋–ค ์‚ฌ๋žŒ์ด ํ•จ์ˆ˜ ์‹œ๊ทธ๋‹ˆ์ณ(๋งค๊ฐœ๋ณ€์ˆ˜์˜ ํƒ€์ž…, ๋ฐ˜ํ™˜๊ฐ’์˜ ํƒ€์ž… ๋“ฑ)๋ฅผ ๋ณผ ๋•Œ, ์–ด๋–ค ์†์„ฑ์ด ์‚ฌ์šฉ๋˜๋Š”์ง€ ์ฆ‰์‹œ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  2. ๋ช…๋ช…๋œ ๋งค๊ฐœ๋ณ€์ˆ˜์ฒ˜๋Ÿผ ๋ณด์ด๊ฒŒ ํ•  ๋•Œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  3. ๋˜ํ•œ ๊ตฌ์กฐ ๋ถ„ํ•ด๋Š” ํ•จ์ˆ˜๋กœ ์ „๋‹ฌ๋œ ๋งค๊ฐœ๋ณ€์ˆ˜ ๊ฐ์ฒด์˜ ํŠน์ •ํ•œ ์›์‹œ ๊ฐ’์„ ๋ณต์ œํ•˜๋ฉฐ ์ด๊ฒƒ์€ ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๋ฅผ ๋ฐฉ์ง€ํ•˜๋Š”๋ฐ ๋„์›€์„ ์ค๋‹ˆ๋‹ค. ์œ ์˜์‚ฌํ•ญ: ๋งค๊ฐœ๋ณ€์ˆ˜ ๊ฐ์ฒด๋กœ๋ถ€ํ„ฐ ๊ตฌ์กฐ ๋ถ„ํ•ด๋œ ๊ฐ์ฒด์™€ ๋ฐฐ์—ด์€ ๋ณต์ œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

  4. ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์€ ์†์„ฑ์— ๋Œ€ํ•ด์„œ ๊ฒฝ๊ณ ๋ฅผ ์ฃผ๋ฉฐ, ๊ตฌ์กฐ ๋ถ„ํ•ด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ฒฝ๊ณ ๋ฅผ ๋ฐ›์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Bad:

function createMenu(title: string, body: string, buttonText: string, cancellable: boolean) {
  // ...
}

createMenu('Foo', 'Bar', 'Baz', true);

Good:

function createMenu(options: { title: string, body: string, buttonText: string, cancellable: boolean }) {
  // ...
}

createMenu({
  title: 'Foo',
  body: 'Bar',
  buttonText: 'Baz',
  cancellable: true
});

ํƒ€์ž… ์•จ๋ฆฌ์–ด์Šค๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ฐ€๋…์„ฑ์„ ๋” ๋†’์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:


type MenuOptions = { title: string, body: string, buttonText: string, cancellable: boolean };

function createMenu(options: MenuOptions) {
  // ...
}

createMenu({
  title: 'Foo',
  body: 'Bar',
  buttonText: 'Baz',
  cancellable: true
});

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

ํ•จ์ˆ˜๋Š” ํ•œ ๊ฐ€์ง€๋งŒ ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค

์ด๊ฒƒ์€ ์†Œํ”„ํŠธ์›จ์–ด ๊ณตํ•™์—์„œ ๋‹จ์—ฐ์ฝ” ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ทœ์น™์ž…๋‹ˆ๋‹ค. ํ•จ์ˆ˜๊ฐ€ ํ•œ ๊ฐ€์ง€ ์ด์ƒ์˜ ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•  ๋•Œ ์ž‘์„ฑํ•˜๊ณ  ํ…Œ์ŠคํŠธํ•˜๊ณ  ์ถ”๋ก ํ•˜๊ธฐ ์–ด๋ ค์›Œ์ง‘๋‹ˆ๋‹ค. ํ•จ์ˆ˜๋ฅผ ํ•˜๋‚˜์˜ ํ–‰๋™์œผ๋กœ ์ •์˜ํ•  ์ˆ˜ ์žˆ์„ ๋•Œ, ์‰ฝ๊ฒŒ ๋ฆฌํŒฉํ† ๋งํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ์ฝ”๋“œ๋ฅผ ๋”์šฑ ๋ช…๋ฃŒํ•˜๊ฒŒ ์ฝ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฐ€์ด๋“œ์—์„œ ์ด ๋ถ€๋ถ„๋งŒ ์ž๊ธฐ๊ฒƒ์œผ๋กœ ๋งŒ๋“ค์–ด๋„ ๋‹น์‹ ์€ ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋ณด๋‹ค ์•ž์„ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Bad:

function emailClients(clients: Client[]) {
  clients.forEach((client) => {
    const clientRecord = database.lookup(client);
    if (clientRecord.isActive()) {
      email(client);
    }
  });
}

Good:

function emailClients(clients: Client[]) {
  clients.filter(isActiveClient).forEach(email);
}

function isActiveClient(client: Client) {
  const clientRecord = database.lookup(client);
  return clientRecord.isActive();
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

ํ•จ์ˆ˜๊ฐ€ ๋ฌด์—‡์„ ํ•˜๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ๋„๋ก ํ•จ์ˆ˜ ์ด๋ฆ„์„ ์ง€์œผ์„ธ์š”

Bad:

function addToDate(date: Date, month: number): Date {
  // ...
}

const date = new Date();

// ๋ฌด์—‡์ด ์ถ”๊ฐ€๋˜๋Š”์ง€ ํ•จ์ˆ˜ ์ด๋ฆ„๋งŒ์œผ๋กœ ์œ ์ถ”ํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค
addToDate(date, 1);

Good:

function addMonthToDate(date: Date, month: number): Date {
  // ...
}

const date = new Date();
addMonthToDate(date, 1);

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

ํ•จ์ˆ˜๋Š” ๋‹จ์ผ ํ–‰๋™์„ ์ถ”์ƒํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค

ํ•จ์ˆ˜๊ฐ€ ํ•œ ๊ฐ€์ง€ ์ด์ƒ์„ ์ถ”์ƒํ™”ํ•œ๋‹ค๋ฉด ๊ทธ ํ•จ์ˆ˜๋Š” ๋„ˆ๋ฌด ๋งŽ์€ ์ผ์„ ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์žฌ์‚ฌ์šฉ์„ฑ๊ณผ ์‰ฌ์šด ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด์„œ ํ•จ์ˆ˜๋ฅผ ์ชผ๊ฐœ์„ธ์š”.

Bad:

function parseCode(code: string) {
  const REGEXES = [ /* ... */ ];
  const statements = code.split(' ');
  const tokens = [];

  REGEXES.forEach((regex) => {
    statements.forEach((statement) => {
      // ...
    });
  });

  const ast = [];
  tokens.forEach((token) => {
    // lex...
  });

  ast.forEach((node) => {
    // parse...
  });
}

Good:

const REGEXES = [ /* ... */ ];

function parseCode(code: string) {
  const tokens = tokenize(code);
  const syntaxTree = parse(tokens);

  syntaxTree.forEach((node) => {
    // parse...
  });
}

function tokenize(code: string): Token[] {
  const statements = code.split(' ');
  const tokens: Token[] = [];

  REGEXES.forEach((regex) => {
    statements.forEach((statement) => {
      tokens.push( /* ... */ );
    });
  });

  return tokens;
}

function parse(tokens: Token[]): SyntaxTree {
  const syntaxTree: SyntaxTree[] = [];
  tokens.forEach((token) => {
    syntaxTree.push( /* ... */ );
  });

  return syntaxTree;
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

์ค‘๋ณต๋œ ์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐํ•ด์ฃผ์„ธ์š”

์ฝ”๋“œ๊ฐ€ ์ค‘๋ณต๋˜์ง€ ์•Š๋„๋ก ์ตœ์„ ์„ ๋‹คํ•˜์„ธ์š”. ์ค‘๋ณต๋œ ์ฝ”๋“œ๋Š” ์–ด๋–ค ๋กœ์ง์„ ๋ณ€๊ฒฝํ•  ๋•Œ ํ•œ ๊ณณ ์ด์ƒ์„ ๋ณ€๊ฒฝํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ข‹์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋‹น์‹ ์ด ๋ ˆ์Šคํ† ๋ž‘์„ ์šด์˜ํ•˜๋ฉด์„œ ์žฌ๊ณ ๋ฅผ ์ถ”์ ํ•œ๋‹ค๊ณ  ์ƒ์ƒํ•ด๋ณด์„ธ์š”: ๋ชจ๋“  ํ† ๋งˆํ† , ์–‘ํŒŒ, ๋งˆ๋Š˜, ์–‘๋… ๋“ฑ. ๊ด€๋ฆฌํ•˜๋Š” ๋ชฉ๋ก์ด ์—ฌ๋Ÿฌ๊ฐœ์ผ ๋•Œ ํ† ๋งˆํ† ๋ฅผ ๋„ฃ์€ ์š”๋ฆฌ๋ฅผ ์ œ๊ณตํ•  ๋•Œ๋งˆ๋‹ค ๋ชจ๋“  ๋ชฉ๋ก์„ ์ˆ˜์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ฆฌํ•˜๋Š” ๋ชฉ๋ก์ด ๋‹จ ํ•˜๋‚˜์ผ ๋•Œ๋Š” ํ•œ ๊ณณ๋งŒ ์ˆ˜์ •ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค!

๋‹น์‹ ์€ ์ข…์ข… ๋‘ ๊ฐœ ์ด์ƒ์˜ ์‚ฌ์†Œํ•œ ์ฐจ์ด์ ์ด ์กด์žฌํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•ด์„œ ๊ฑฐ์˜ ๋น„์Šทํ•œ ์ฝ”๋“œ๋ฅผ ์ค‘๋ณต ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๊ทธ ๋ช‡๊ฐ€์ง€ ๋‹ค๋ฅธ ๊ฒƒ์œผ๋กœ ์ธํ•ด ๊ฐ™์€ ์—ญํ• ์„ ํ•˜๋Š” ๋‘ ๊ฐœ ์ด์ƒ์˜ ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ค‘๋ณต๋œ ์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐํ•˜๋Š” ๊ฒƒ์€ ์กฐ๊ธˆ์”ฉ ๋‹ค๋ฅธ ์—ญํ• ์„ ํ•˜๋Š” ๊ฒƒ์„ ๋ฌถ์Œ์œผ๋กœ์จ ํ•˜๋‚˜์˜ ํ•จ์ˆ˜/๋ชจ๋“ˆ/ํด๋ž˜์Šค๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ์ถ”์ƒํ™”๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

์ถ”์ƒํ™”๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํ•˜๋Š” ๊ฒƒ์€ ์ค‘์š”ํ•˜๋ฉฐ, ์ด๊ฒƒ์€ SOLID ์›์น™์„ ๋”ฐ๋ฅด๋Š” ์ด์œ ์ด๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค. ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ์ถ”์ƒํ™”๋Š” ์ค‘๋ณต๋œ ์ฝ”๋“œ๋ณด๋‹ค ๋‚˜์˜๋ฏ€๋กœ ์ฃผ์˜ํ•˜์„ธ์š”! ์ข‹์€ ์ถ”์ƒํ™”๋ฅผ ํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด ๊ทธ๋ ‡๊ฒŒ ํ•˜๋ผ๋Š” ๋ง์ž…๋‹ˆ๋‹ค! ๋ฐ˜๋ณตํ•˜์ง€ ๋งˆ์„ธ์š”. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ํ•˜๋‚˜๋ฅผ ๋ณ€๊ฒฝํ•  ๋•Œ๋งˆ๋‹ค ์—ฌ๋Ÿฌ ๊ณณ์„ ๋ณ€๊ฒฝํ•˜๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

Bad:

function showDeveloperList(developers: Developer[]) {
  developers.forEach((developer) => {
    const expectedSalary = developer.calculateExpectedSalary();
    const experience = developer.getExperience();
    const githubLink = developer.getGithubLink();

    const data = {
      expectedSalary,
      experience,
      githubLink
    };

    render(data);
  });
}

function showManagerList(managers: Manager[]) {
  managers.forEach((manager) => {
    const expectedSalary = manager.calculateExpectedSalary();
    const experience = manager.getExperience();
    const portfolio = manager.getMBAProjects();

    const data = {
      expectedSalary,
      experience,
      portfolio
    };

    render(data);
  });
}

Good:

class Developer {
  // ...
  getExtraDetails() {
    return {
      githubLink: this.githubLink,
    }
  }
}

class Manager {
  // ...
  getExtraDetails() {
    return {
      portfolio: this.portfolio,
    }
  }
}

function showEmployeeList(employee: Developer | Manager) {
  employee.forEach((employee) => {
    const expectedSalary = employee.calculateExpectedSalary();
    const experience = employee.getExperience();
    const extra = employee.getExtraDetails();

    const data = {
      expectedSalary,
      experience,
      extra,
    };

    render(data);
  });
}

๋‹น์‹ ์€ ์ค‘๋ณต๋œ ์ฝ”๋“œ์— ๋Œ€ํ•ด์„œ ๋น„ํŒ์ ์œผ๋กœ ์ƒ๊ฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ€๋”์€ ์ค‘๋ณต๋œ ์ฝ”๋“œ์™€ ๋ถˆํ•„์š”ํ•œ ์ถ”์ƒํ™”๋กœ ์ธํ•œ ๋ณต์žก์„ฑ ๊ฐ„์˜ ๋งž๋ฐ”๊ฟˆ์ด ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์„œ๋กœ ๋‹ค๋ฅธ ๋‘ ๊ฐœ์˜ ๋ชจ๋“ˆ์˜ ๊ตฌํ˜„์ด ์œ ์‚ฌํ•ด ๋ณด์ด์ง€๋งŒ ์„œ๋กœ ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์— ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ, ์ฝ”๋“œ ์ค‘๋ณต์€ ๊ณตํ†ต๋œ ์ฝ”๋“œ์—์„œ ์ถ”์ถœํ•ด์„œ ์ค‘๋ณต์„ ์ค„์ด๋Š” ๊ฒƒ๋ณด๋‹ค ๋‚˜์€ ์„ ํƒ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ์— ์ถ”์ถœ๋œ ๊ณตํ†ต์˜ ์ฝ”๋“œ๋Š” ๋‘ ๋ชจ๋“ˆ ์‚ฌ์ด์—์„œ ๊ฐ„์ ‘์ ์ธ ์˜์กด์„ฑ์ด ๋‚˜ํƒ€๋‚˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

Object.assign ํ˜น์€ ๊ตฌ์กฐ ๋ถ„ํ•ด๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ธฐ๋ณธ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“œ์„ธ์š”

Bad:

type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean };

function createMenu(config: MenuConfig) {
  config.title = config.title || 'Foo';
  config.body = config.body || 'Bar';
  config.buttonText = config.buttonText || 'Baz';
  config.cancellable = config.cancellable !== undefined ? config.cancellable : true;

  // ...
}

createMenu({ body: 'Bar' });

Good:

type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean };

function createMenu(config: MenuConfig) {
  const menuConfig = Object.assign({
    title: 'Foo',
    body: 'Bar',
    buttonText: 'Baz',
    cancellable: true
  }, config);

  // ...
}

createMenu({ body: 'Bar' });

๋Œ€์•ˆ์œผ๋กœ, ๊ธฐ๋ณธ ๊ฐ’์„ ๊ตฌ์กฐ ๋ถ„ํ•ด๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean };

function createMenu({ title = 'Foo', body = 'Bar', buttonText = 'Baz', cancellable = true }: MenuConfig) {
  // ...
}

createMenu({ body: 'Bar' });

์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ์™€ undefined ํ˜น์€ null ๊ฐ’์„ ๋ช…์‹œ์ ์œผ๋กœ ๋„˜๊ธฐ๋Š” ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ํ–‰๋™์„ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด์„œ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ์ปดํŒŒ์ผ๋Ÿฌ์—๊ฒŒ ๊ทธ๊ฒƒ์„ ํ—ˆ๋ฝํ•˜์ง€ ์•Š๋„๋ก ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์—์„œ --strictNullChecks ์˜ต์…˜์„ ํ™•์ธํ•˜์„ธ์š”.

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

ํ•จ์ˆ˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ํ”Œ๋ž˜๊ทธ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ์„ธ์š”

ํ”Œ๋ž˜๊ทธ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ํ•ด๋‹น ํ•จ์ˆ˜๊ฐ€ ํ•œ ๊ฐ€์ง€ ์ด์ƒ์˜ ์ผ์„ ํ•œ๋‹ค๋Š” ๊ฒƒ์„ ๋œปํ•ฉ๋‹ˆ๋‹ค. ํ•จ์ˆ˜๋Š” ํ•œ ๊ฐ€์ง€์˜ ์ผ์„ ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค. boolean ๋ณ€์ˆ˜๋กœ ์ธํ•ด ๋‹ค๋ฅธ ์ฝ”๋“œ๊ฐ€ ์‹คํ–‰๋œ๋‹ค๋ฉด ๊ทธ ํ•จ์ˆ˜๋ฅผ ์ชผ๊ฐœ๋„๋ก ํ•˜์„ธ์š”.

Bad:

function createFile(name: string, temp: boolean) {
  if (temp) {
    fs.create(`./temp/${name}`);
  } else {
    fs.create(name);
  }
}

Good:

function createTempFile(name: string) {
  createFile(`./temp/${name}`);
}

function createFile(name: string) {
  fs.create(name);
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๋ฅผ ํ”ผํ•˜์„ธ์š” (ํŒŒํŠธ 1)

ํ•จ์ˆ˜๋Š” ๊ฐ’์„ ๊ฐ€์ ธ์™€์„œ ๋‹ค๋ฅธ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ ์ด์™ธ์— ๋‹ค๋ฅธ ๊ฒƒ์„ ํ•  ๊ฒฝ์šฐ ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๋Š” ํŒŒ์ผ์„ ์“ด๋‹ค๊ฑฐ๋‚˜, ์ „์—ญ ๋ณ€์ˆ˜๋ฅผ ์กฐ์ž‘ํ•œ๋‹ค๊ฑฐ๋‚˜, ๋œปํ•˜์ง€ ์•Š๊ฒŒ ๋‚ฏ์„  ์‚ฌ๋žŒ์—๊ฒŒ ๋‹น์‹ ์˜ ์ „์žฌ์‚ฐ์„ ์†ก๊ธˆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹น์‹ ์€ ๊ฐ€๋” ํ”„๋กœ๊ทธ๋žจ์—์„œ ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๋ฅผ ๊ฐ€์งˆ ํ•„์š”๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด์ „์˜ ์‚ฌ๋ก€์—์„œ์™€ ๊ฐ™์ด ๋‹น์‹ ์€ ํŒŒ์ผ์„ ์จ์•ผํ•  ๋•Œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹น์‹ ์ด ํ•˜๊ณ  ์‹ถ์€ ๊ฒƒ์€ ์ด๊ฒƒ์„ ์ค‘์•™ํ™”ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ํŠน์ • ํŒŒ์ผ์„ ์“ฐ๊ธฐ ์œ„ํ•ด ๋ช‡ ๊ฐœ์˜ ํ•จ์ˆ˜์™€ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์ง€ ๋งˆ์„ธ์š”. ๊ทธ๊ฒƒ์„ ํ–‰ํ•˜๋Š” ์„œ๋น„์Šค๋ฅผ ๋‹จ ํ•˜๋‚˜๋งŒ ๋งŒ๋“œ์„ธ์š”.

์ค‘์š”ํ•œ ๊ฒƒ์€ ์–ด๋– ํ•œ ๊ตฌ์กฐ๋„ ์—†์ด ๊ฐ์ฒด ์‚ฌ์ด์— ์ƒํƒœ๋ฅผ ๊ณต์œ ํ•˜๊ฑฐ๋‚˜ ์–ด๋–ค ๊ฒƒ์— ์˜ํ•ด์„œ๋“ ์ง€ ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ๋Š” ๋ฐ์ดํ„ฐ ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๊ฐ€ ์ผ์–ด๋‚˜๋Š” ๊ณณ์„ ์ค‘์•™ํ™” ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ๊ณผ ๊ฐ™์€ ์œ„ํ—˜ ์š”์†Œ๋ฅผ ํ”ผํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋งŒ์•ฝ ๊ทธ๋ ‡๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด, ๋‹น์‹ ์€ ๋Œ€๋ถ€๋ถ„์˜ ๋‹ค๋ฅธ ํ”„๋กœ๊ทธ๋ž˜๋จธ๋“ค๋ณด๋‹ค ๋”์šฑ ํ–‰๋ณตํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

Bad:

// ์•„๋ž˜์˜ ํ•จ์ˆ˜์—์„œ ์ฐธ์กฐํ•˜๋Š” ์ „์—ญ ๋ณ€์ˆ˜์ž…๋‹ˆ๋‹ค.
let name = 'Robert C. Martin';

function toBase64() {
  name = btoa(name);
}

toBase64();
// ์ด ์ด๋ฆ„์„ ์‚ฌ์šฉํ•˜๋Š” ๋‹ค๋ฅธ ํ•จ์ˆ˜๊ฐ€ ์žˆ๋‹ค๋ฉด, ๊ทธ๊ฒƒ์€ Base64 ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค

console.log(name); // 'Robert C. Martin'์ด ์ถœ๋ ฅ๋˜๋Š” ๊ฒƒ์„ ์˜ˆ์ƒํ–ˆ์ง€๋งŒ 'Um9iZXJ0IEMuIE1hcnRpbg=='๊ฐ€ ์ถœ๋ ฅ๋จ

Good:

const name = 'Robert C. Martin';

function toBase64(text: string): string {
  return btoa(text);
}

const encodedName = toBase64(name);
console.log(name);

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๋ฅผ ํ”ผํ•˜์„ธ์š” (ํŒŒํŠธ 2)

์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์—์„œ ์›์‹œ๊ฐ’์€ ๊ฐ’์— ์˜ํ•ด ์ „๋‹ฌ๋˜๊ณ  ๊ฐ์ฒด/๋ฐฐ์—ด์€ ์ฐธ์กฐ์— ์˜ํ•ด ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ๊ฐ์ฒด์™€ ๋ฐฐ์—ด์˜ ๊ฒฝ์šฐ ์–ด๋–ค ํ•จ์ˆ˜๊ฐ€ ์‡ผํ•‘ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ฐฐ์—ด์„ ๋ณ€๊ฒฝํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๋ฉด, ๊ตฌ๋งคํ•˜๋ ค๋Š” ์•„์ดํ…œ์ด ์ถ”๊ฐ€๋จ์œผ๋กœ์จ cart ๋ฐฐ์—ด์„ ์‚ฌ์šฉํ•˜๋Š” ๋‹ค๋ฅธ ํ•จ์ˆ˜๋Š” ์ด ์ถ”๊ฐ€์˜ ์˜ํ–ฅ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ์žฅ์ ์ด ๋  ์ˆ˜๋„ ์žˆ์ง€๋งŒ ๋‹จ์ ์ด ๋  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์ตœ์•…์˜ ์ƒํ™ฉ์„ ์ƒ์ƒํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค:

์‚ฌ์šฉ์ž๋Š” ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ์ƒ์„ฑํ•˜๊ณ  ์„œ๋ฒ„์— cart ๋ฐฐ์—ด์„ ์ „์†กํ•˜๋Š” purchase ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋Š” โ€œ๊ตฌ๋งคโ€ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•ฉ๋‹ˆ๋‹ค. ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ๋ถˆ๋Ÿ‰ ๋•Œ๋ฌธ์— purchase ํ•จ์ˆ˜๋Š” ์š”์ฒญ์„ ์žฌ์‹œ๋„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋„คํŠธ์›Œํฌ ์š”์ฒญ์ด ์‹œ์ž‘๋˜๊ธฐ ์ „์— ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜์ง€ ์•Š์€ ์•„์ดํ…œ์„ ์‹ค์ˆ˜๋กœ โ€œ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ถ”๊ฐ€ํ•˜๊ธฐโ€ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ์š”? ๋„คํŠธ์›Œํฌ ์š”์ฒญ์ด ์‹œ์ž‘๋˜๋ฉด, purchase ํ•จ์ˆ˜๋Š” addItemToCart ํ•จ์ˆ˜๊ฐ€ ๋ณ€๊ฒฝํ•œ ์‡ผํ•‘ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ฐฐ์—ด์„ ์ฐธ์กฐํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— purchase ํ•จ์ˆ˜๋Š” ์‹ค์ˆ˜๋กœ ์ถ”๊ฐ€๋œ ์•„์ดํ…œ์„ ๋ณด๋‚ผ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

ํ›Œ๋ฅญํ•œ ํ•ด๋ฒ•์€ addItemToCart ํ•จ์ˆ˜์—์„œ cart ๋ฐฐ์—ด์„ ๋ณต์ œํ•˜๊ณ  ๊ทธ๊ฒƒ์„ ์ˆ˜์ •ํ•˜๊ณ  ๊ทธ ๋ณต์ œํ•œ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ์‡ผํ•‘ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ฐฐ์—ด์„ ์ฐธ์กฐํ•˜๊ณ  ์žˆ๋Š” ๊ฐ’์„ ๋“ค๊ณ  ์žˆ๋Š” ์–ด๋–ค ๋‹ค๋ฅธ ํ•จ์ˆ˜๋„ ๋‹ค๋ฅธ ๋ณ€๊ฒฝ์— ์˜ํ•ด ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š๋Š” ๊ฒƒ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

์ด ์ ‘๊ทผ๋ฒ•์— ๋Œ€ํ•œ ๋‘ ๊ฐ€์ง€ ์ฃผ์˜์‚ฌํ•ญ:

  1. ์‹ค์ œ๋กœ๋Š” ์ž…๋ ฅ๋œ ๊ฐ์ฒด๊ฐ’์„ ๋ณ€๊ฒฝํ•˜๊ธฐ๋ฅผ ์›ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋Ÿฌํ•œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๊ด€๋ก€๋ฅผ ์„ ํƒํ•  ๋•Œ ๋‹น์‹ ์€ ์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ๊ฐ€ ๋งค์šฐ ๋“œ๋ฌผ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋Œ€๋ถ€๋ถ„์€ ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๊ฐ€ ์—†๋„๋ก ๋ฆฌํŒฉํ† ๋ง๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค! (์ˆœ์ˆ˜ ํ•จ์ˆ˜๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”)

  2. ํฐ ๊ฐ์ฒด๋ฅผ ๋ณต์ œํ•˜๋Š” ๊ฒƒ์€ ์„ฑ๋Šฅ ๊ด€์ ์—์„œ ๋น„์šฉ์ด ๋†’์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹คํ–‰ํžˆ๋„ ์ด๋Ÿฌํ•œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์ ‘๊ทผ๋ฒ•์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด์ฃผ๋Š” ํ›Œ๋ฅญํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ํฐ ๋ฌธ์ œ๋Š” ์•„๋‹™๋‹ˆ๋‹ค. ์ด๋Š” ์ˆ˜๋™์œผ๋กœ ๊ฐ์ฒด์™€ ๋ฐฐ์—ด์„ ๋ณต์ œํ•ด์ฃผ๋Š” ๊ฒƒ๋งŒํผ ๋ฉ”๋ชจ๋ฆฌ ์ง‘์•ฝ์ ์ด์ง€ ์•Š๊ฒŒ ํ•ด์ฃผ๊ณ  ๋น ๋ฅด๊ฒŒ ๋ณต์ œํ•ด์ค๋‹ˆ๋‹ค.

Bad:

function addItemToCart(cart: CartItem[], item: Item): void {
  cart.push({ item, date: Date.now() });
};

Good:

function addItemToCart(cart: CartItem[], item: Item): CartItem[] {
  return [...cart, { item, date: Date.now() }];
};

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

์ „์—ญ ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•˜์ง€ ๋งˆ์„ธ์š”

์ „์—ญ์„ ๋”๋Ÿฝํžˆ๋Š” ๊ฒƒ์€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์—์„œ ๋‚˜์œ ๊ด€์Šต์ž…๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด ๋‹ค๋ฅธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ์ถฉ๋Œ๋‚  ์ˆ˜ ์žˆ๊ณ  ๋‹น์‹ ์˜ API์˜ ์‚ฌ์šฉ์ž๋Š” ์ƒ์šฉ์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•  ๋•Œ๊นŒ์ง€ ์ „ํ˜€ ๋ชจ๋ฅผ ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ํ•œ ์˜ˆ์ œ๋ฅผ ์ƒ๊ฐํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค: ๋‹น์‹ ์ด ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋„ค์ดํ‹ฐ๋ธŒ ๋ฐฐ์—ด ๋ฉ”์†Œ๋“œ๋ฅผ ํ™•์žฅํ•ด์„œ ๋‘ ๋ฐฐ์—ด ์‚ฌ์ด์˜ ๋‹ค๋ฅธ ์ ์„ ๋ณด์—ฌ์ฃผ๋Š” diff ๋ฉ”์†Œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์–ด๋–จ๊นŒ์š”? Array.prototype์— ๋‹น์‹ ์˜ ์ƒˆ๋กœ์šด ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋™์ผํ•œ ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ์žˆ๋Š” ๋‹ค๋ฅธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ์ถฉ๋Œ๋‚  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ๋Š” ๋ฐฐ์—ด์—์„œ ์ฒซ ๋ฒˆ์งธ ์š”์†Œ์™€ ๋งˆ์ง€๋ง‰ ์š”์†Œ ์‚ฌ์ด์˜ ๋‹ค๋ฆ„๋งŒ ์ฐพ๊ธฐ ์œ„ํ•ด diff ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ์–ด๋–จ๊นŒ์š”? ์ด๊ฒƒ์ด ๋‹จ์ง€ ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ „์—ญ Array๋ฅผ ์ƒ์†ํ•˜๋Š” ๊ฒƒ์ด ๋” ์ข‹์€ ์ด์œ ์ž…๋‹ˆ๋‹ค.

Bad:

declare global {
  interface Array<T> {
    diff(other: T[]): Array<T>;
  }
}

if (!Array.prototype.diff) {
  Array.prototype.diff = function <T>(other: T[]): T[] {
    const hash = new Set(other);
    return this.filter(elem => !hash.has(elem));
  };
}

Good:

class MyArray<T> extends Array<T> {
  diff(other: T[]): T[] {
    const hash = new Set(other);
    return this.filter(elem => !hash.has(elem));
  };
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

๋ช…๋ นํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ๋ณด๋‹ค ํ•จ์ˆ˜ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ์ง€ํ–ฅํ•˜์„ธ์š”

๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด ์ด๋Ÿฐ ๋ฐฉ์‹์˜ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ์ง€ํ–ฅํ•˜์„ธ์š”.

Bad:

const contributions = [
  {
    name: 'Uncle Bobby',
    linesOfCode: 500
  }, {
    name: 'Suzie Q',
    linesOfCode: 1500
  }, {
    name: 'Jimmy Gosling',
    linesOfCode: 150
  }, {
    name: 'Gracie Hopper',
    linesOfCode: 1000
  }
];

let totalOutput = 0;

for (let i = 0; i < contributions.length; i++) {
  totalOutput += contributions[i].linesOfCode;
}

Good:

const contributions = [
  {
    name: 'Uncle Bobby',
    linesOfCode: 500
  }, {
    name: 'Suzie Q',
    linesOfCode: 1500
  }, {
    name: 'Jimmy Gosling',
    linesOfCode: 150
  }, {
    name: 'Gracie Hopper',
    linesOfCode: 1000
  }
];

const totalOutput = contributions
  .reduce((totalLines, output) => totalLines + output.linesOfCode, 0);

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

์กฐ๊ฑด๋ฌธ์„ ์บก์Šํ™”ํ•˜์„ธ์š”

Bad:

if (subscription.isTrial || account.balance > 0) {
  // ...
}

Good:

function canActivateService(subscription: Subscription, account: Account) {
  return subscription.isTrial || account.balance > 0;
}

if (canActivateService(subscription, account)) {
  // ...
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

๋ถ€์ • ์กฐ๊ฑด๋ฌธ์„ ํ”ผํ•˜์„ธ์š”

Bad:

function isEmailNotUsed(email: string): boolean {
  // ...
}

if (isEmailNotUsed(email)) {
  // ...
}

Good:

function isEmailUsed(email): boolean {
  // ...
}

if (!isEmailUsed(node)) {
  // ...
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

์กฐ๊ฑด๋ฌธ์„ ํ”ผํ•˜์„ธ์š”

๋ถˆ๊ฐ€๋Šฅํ•ด๋ณด์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฒ˜์Œ ์ด๋ฅผ ๋ณธ ๋Œ€๋ถ€๋ถ„์˜ ์‚ฌ๋žŒ๋“ค์€ โ€œ๋Œ€์ฒด if๋ฌธ ์—†์ด ๋ญ˜ ํ•  ์ˆ˜ ์žˆ๋‚˜์š”?โ€ ๋ผ๊ณ  ๋ฐ˜์‘ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋งŽ์€ ๊ฒฝ์šฐ์— ๋‹คํ˜•์„ฑ์„ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ ๋‹ค์Œ ๋ฐ˜์‘์œผ๋กœ๋Š” โ€œ์ข‹์•„์š”. ํ•˜์ง€๋งŒ ์™œ ๊ทธ๋ž˜์•ผํ•˜์ฃ ?โ€ ์ž…๋‹ˆ๋‹ค. ์ด์— ๋Œ€ํ•œ ํ•ด๋‹ต์€ ์šฐ๋ฆฌ๊ฐ€ ์ด์ „์— ๋ฐฐ์šด ํด๋ฆฐ ์ฝ”๋“œ ์ปจ์…‰ ์ค‘ โ€œํ•จ์ˆ˜๋Š” ํ•œ ๊ฐ€์ง€ ์ผ๋งŒ ํ•ด์•ผํ•ฉ๋‹ˆ๋‹คโ€ ์ž…๋‹ˆ๋‹ค. if๋ฌธ์ด ์žˆ๋Š” ํด๋ž˜์Šค์™€ ํ•จ์ˆ˜๊ฐ€ ์žˆ๋‹ค๋ฉด, ๊ทธ ํ•จ์ˆ˜๋Š” ํ•œ ๊ฐ€์ง€ ์ด์ƒ์˜ ์ผ์„ ํ•˜๊ณ  ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ํ•จ์ˆ˜๋Š” ํ•œ ๊ฐ€์ง€ ์ผ๋งŒ ํ•ด์•ผํ•œ๋‹ค๋Š” ๊ฒƒ์„ ๊ธฐ์–ตํ•˜์„ธ์š”.

Bad:

class Airplane {
  private type: string;
  // ...

  getCruisingAltitude() {
    switch (this.type) {
      case '777':
        return this.getMaxAltitude() - this.getPassengerCount();
      case 'Air Force One':
        return this.getMaxAltitude();
      case 'Cessna':
        return this.getMaxAltitude() - this.getFuelExpenditure();
      default:
        throw new Error('Unknown airplane type.');
    }
  }

  private getMaxAltitude(): number {
    // ...
  }
}

Good:

abstract class Airplane {
  protected getMaxAltitude(): number {
    // shared logic with subclasses ...
  }

  // ...
}

class Boeing777 extends Airplane {
  // ...
  getCruisingAltitude() {
    return this.getMaxAltitude() - this.getPassengerCount();
  }
}

class AirForceOne extends Airplane {
  // ...
  getCruisingAltitude() {
    return this.getMaxAltitude();
  }
}

class Cessna extends Airplane {
  // ...
  getCruisingAltitude() {
    return this.getMaxAltitude() - this.getFuelExpenditure();
  }
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

ํƒ€์ž… ์ฒดํ‚น์„ ํ”ผํ•˜์„ธ์š”

ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋Š” ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์˜ ์—„๊ฒฉํ•œ ๊ตฌ๋ฌธ์  ์ƒ์œ„ ์ง‘ํ•ฉ์ด๋ฉฐ ์–ธ์–ด์— ์„ ํƒ์ ์ธ ์ •์  ํƒ€์ž… ๊ฒ€์‚ฌ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์˜ ๊ธฐ๋Šฅ์„ ์ตœ๋Œ€ํ•œ ํ™œ์šฉํ•˜๊ธฐ ์œ„ํ•ด ํ•ญ์ƒ ๋ณ€์ˆ˜์˜ ํƒ€์ž…, ๋งค๊ฐœ๋ณ€์ˆ˜, ๋ฐ˜ํ™˜๊ฐ’์˜ ํƒ€์ž…์„ ์ง€์ •ํ•˜๋„๋ก ํ•˜์„ธ์š”. ๊ทธ๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ฆฌํŒฉํ† ๋ง์ด ๋งค์šฐ ์‰ฌ์›Œ์ง‘๋‹ˆ๋‹ค.

Bad:

function travelToTexas(vehicle: Bicycle | Car) {
  if (vehicle instanceof Bicycle) {
    vehicle.pedal(currentLocation, new Location('texas'));
  } else if (vehicle instanceof Car) {
    vehicle.drive(currentLocation, new Location('texas'));
  }
}

Good:

type Vehicle = Bicycle | Car;

function travelToTexas(vehicle: Vehicle) {
  vehicle.move(currentLocation, new Location('texas'));
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

ํ•„์š” ์ด์ƒ์œผ๋กœ ์ตœ์ ํ™”ํ•˜์ง€ ๋งˆ์„ธ์š”

ํ˜„๋Œ€ ๋ธŒ๋ผ์šฐ์ €๋Š” ๋Ÿฐํƒ€์ž„์—์„œ ๋งŽ์€ ์ตœ์ ํ™”๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ๋งŽ์€ ์‹œ๊ฐ„์„ ์ตœ์ ํ™”ํ•˜๋Š” ๋ฐ์— ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋ฉด ์‹œ๊ฐ„ ๋‚ญ๋น„์ž…๋‹ˆ๋‹ค. ์ตœ์ ํ™”๊ฐ€ ๋ถ€์กฑํ•œ ๋ถ€๋ถ„์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ์ข‹์€ ์ž๋ฃŒ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์„ ์ฐธ์กฐํ•˜์—ฌ ์ตœ์ ํ™”๊ฐ€ ๋ถ€์กฑํ•œ ๋ถ€๋ถ„๋งŒ ์ตœ์ ํ™”ํ•ด์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Bad:

// ์˜ˆ์ „ ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” ์บ์‹œ๋˜์ง€ ์•Š์€ `list.length`๋ฅผ ์‚ฌ์šฉํ•œ ๊ฐ ์ˆœํšŒ๋Š” ๋น„์šฉ์ด ๋งŽ์ด ๋“ค ๊ฒƒ์ž…๋‹ˆ๋‹ค.
// `list.length`์˜ ์žฌ๊ณ„์‚ฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ํ˜„๋Œ€ ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” ์ด ๋ถ€๋ถ„์ด ์ตœ์ ํ™”๋ฉ๋‹ˆ๋‹ค.
for (let i = 0, len = list.length; i < len; i++) {
  // ...
}

Good:

for (let i = 0; i < list.length; i++) {
  // ...
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

ํ•„์š”ํ•˜์ง€ ์•Š๋Š” ์ฝ”๋“œ๋Š” ์ œ๊ฑฐํ•˜์„ธ์š”

์‚ฌ์šฉํ•˜์ง€ ์•Š์€ ์ฝ”๋“œ๋Š” ์ค‘๋ณต๋œ ์ฝ”๋“œ๋งŒํผ ๋‚˜์ฉ๋‹ˆ๋‹ค. ๋‹น์‹ ์˜ ์ฝ”๋“œ์—์„œ ์ด๊ฒƒ์„ ์œ ์ง€ํ•  ์ด์œ ๋Š” ์—†์Šต๋‹ˆ๋‹ค. ํ˜ธ์ถœ๋˜์ง€ ์•Š์€ ์ฝ”๋“œ๊ฐ€ ์žˆ๋‹ค๋ฉด ์ œ๊ฑฐํ•˜์„ธ์š”! ์ง€์šด ์ฝ”๋“œ๋ฅผ ๋‹ค์‹œ ํ™•์ธํ•  ํ•„์š”๊ฐ€ ์žˆ๋‹ค๋ฉด ๋ฒ„์ „ ๊ธฐ๋ก์—์„œ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Bad:

function oldRequestModule(url: string) {
  // ...
}

function requestModule(url: string) {
  // ...
}

const req = requestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');

Good:

function requestModule(url: string) {
  // ...
}

const req = requestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

iterator์™€ generator๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”

์ŠคํŠธ๋ฆผ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉ๋˜๋Š” ๋ฐ์ดํ„ฐ ์ฝœ๋ ‰์…˜์„ ์‚ฌ์šฉํ•  ๋•Œ๋Š” generator์™€ iterable์„ ์‚ฌ์šฉํ•˜์„ธ์š”. ๋ช‡ ๊ฐ€์ง€์˜ ์ข‹์€ ์ด์œ ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค:

Bad:

function fibonacci(n: number): number[] {
  if (n === 1) return [0];
  if (n === 2) return [0, 1];

  const items: number[] = [0, 1];
  while (items.length < n) {
    items.push(items[items.length - 2] + items[items.length - 1]);
  }

  return items;
}

function print(n: number) {
  fibonacci(n).forEach(fib => console.log(fib));
}

// ํ”ผ๋ณด๋‚˜์น˜ ์ˆซ์ž์˜ ์ฒซ ๋ฒˆ์งธ 10๊ฐœ ์ˆซ์ž๋ฅผ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.
print(10);

Good:

// ํ”ผ๋ณด๋‚˜์น˜ ์ˆซ์ž์˜ ๋ฌดํ•œ ์ŠคํŠธ๋ฆผ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
// `generator`๋Š” ๋ชจ๋“  ์ˆซ์ž์˜ ๋ฐฐ์—ด์„ ์œ ์ง€ํ•˜๊ณ  ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
function* fibonacci(): IterableIterator<number> {
  let [a, b] = [0, 1];

  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

function print(n: number) {
  let i = 0;
  for (const fib of fibonacci()) {
    if (i++ === n) break;  
    console.log(fib);
  }  
}

// ํ”ผ๋ณด๋‚˜์น˜ ์ˆซ์ž์˜ ์ฒซ ๋ฒˆ์งธ 10๊ฐœ ์ˆซ์ž๋ฅผ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.
print(10);

map, slice, forEach ๋“ฑ๊ณผ ๊ฐ™์€ ๋ฉ”์†Œ๋“œ๋ฅผ ์—ฐ๊ฒฐํ•จ์œผ๋กœ์จ ๋„ค์ดํ‹ฐ๋ธŒ ๋ฐฐ์—ด์„ ๋น„์Šทํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ iterable๋กœ ์ž‘์—…ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. iterable์˜ ๋ฐœ์ „๋œ ์กฐ์ž‘์˜ ์‚ฌ๋ก€๋ฅผ ์œ„ํ•ด itiriri๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”. (๋˜๋Š” ๋น„๋™๊ธฐ iterable์˜ ์กฐ์ž‘์„ ์œ„ํ•ด์„œ itiriri-async๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.)

import itiriri from 'itiriri';

function* fibonacci(): IterableIterator<number> {
  let [a, b] = [0, 1];
 
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

itiriri(fibonacci())
  .take(10)
  .forEach(fib => console.log(fib));

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

๊ฐ์ฒด์™€ ์ž๋ฃŒ๊ตฌ์กฐ

getter์™€ setter๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”

ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋Š” getter/setter ๊ตฌ๋ฌธ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ํ–‰๋™์„ ์บก์Šํ™”ํ•œ ๊ฐ์ฒด์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•ด getter์™€ setter๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ๊ฐ์ฒด์—์„œ ์†์„ฑ์„ ๋‹จ์ˆœํžˆ ์ฐพ๋Š” ๊ฒƒ๋ณด๋‹ค ๋‚ซ์Šต๋‹ˆ๋‹ค. โ€œ์™œ ๊ทธ๋ ‡์Šต๋‹ˆ๊นŒ?โ€ ๋ผ๊ณ  ๋ฌผ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ด์œ ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค:

Bad:

type BankAccount = {
  balance: number;
  // ...
}

const value = 100;
const account: BankAccount = {
  balance: 0,
  // ...
};

if (value < 0) {
  throw new Error('Cannot set negative balance.');
}

account.balance = value;

Good:

class BankAccount {
  private accountBalance: number = 0;

  get balance(): number {
    return this.accountBalance;
  }

  set balance(value: number) {
    if (value < 0) {
      throw new Error('Cannot set negative balance.');
    }

    this.accountBalance = value;
  }

  // ...
}

// ์ด์ œ `BankAccount`๋Š” ๊ฒ€์ฆ ๋กœ์ง์„ ์บก์Šํ™”ํ•ฉ๋‹ˆ๋‹ค.
// ๋ช…์„ธ๊ฐ€ ๋ฐ”๋€๋‹ค๋ฉด, ์ถ”๊ฐ€์ ์ธ ๊ฒ€์ฆ ๊ทœ์น™์„ ์ถ”๊ฐ€ํ•  ํ•„์š”๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.
// ๊ทธ ๋•Œ, `setter` ๊ตฌํ˜„๋ถ€๋งŒ ์ˆ˜์ •ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.
// ๊ด€๋ จ์žˆ๋Š” ๋‹ค๋ฅธ ์ฝ”๋“œ๋Š” ๋ณ€๊ฒฝํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
const account = new BankAccount();
account.balance = 100;

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

private/protected ๋ฉค๋ฒ„๋ฅผ ๊ฐ–๋Š” ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜์„ธ์š”

ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋Š” ํด๋ž˜์Šค ๋ฉค๋ฒ„๋ฅผ ์œ„ํ•ด public ๊ธฐ๋ณธ, protected, private ์ ‘๊ทผ์ž๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

Bad:

class Circle {
  radius: number;
  
  constructor(radius: number) {
    this.radius = radius;
  }

  perimeter() {
    return 2 * Math.PI * this.radius;
  }

  surface() {
    return Math.PI * this.radius * this.radius;
  }
}

Good:

class Circle {
  constructor(private readonly radius: number) {
  }

  perimeter() {
    return 2 * Math.PI * this.radius;
  }

  surface() {
    return Math.PI * this.radius * this.radius;
  }
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

๋ถˆ๋ณ€์„ฑ์„ ์„ ํ˜ธํ•˜์„ธ์š”

ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์˜ ํƒ€์ž… ์‹œ์Šคํ…œ์€ interface/class์˜ ๊ฐœ๋ณ„ ์†์„ฑ์„ readonly๋กœ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๊ธฐ๋Šฅ์ ์ธ ๋ฐฉ์‹์œผ๋กœ ์ž‘์—…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (์˜ˆ์ƒํ•˜์ง€ ์•Š์€ ๋ณ€์กฐ๋Š” ์œ„ํ—˜ํ•ฉ๋‹ˆ๋‹ค.)
๋”์šฑ ๋‚˜์€ ๋ฐฉ๋ฒ•์œผ๋กœ๋Š” ํƒ€์ž… T๋ฅผ ๊ฐ–๊ณ  mapped types๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ชจ๋“  ๊ฐ ์†์„ฑ์„ ์ฝ๊ธฐ ์ „์šฉ์œผ๋กœ ํ‘œ์‹œํ•˜๋Š” Readonly ๋‚ด์žฅ ํƒ€์ž…์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. (mapped types๋ฅผ ํ™•์ธํ•˜์„ธ์š”.)

Bad:

interface Config {
  host: string;
  port: string;
  db: string;
}

Good:

interface Config {
  readonly host: string;
  readonly port: string;
  readonly db: string;
}

๋ฐฐ์—ด์˜ ๊ฒฝ์šฐ, ReadonlyArray<T>๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ฝ๊ธฐ ์ „์šฉ์˜ ๋ฐฐ์—ด์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ push()์™€ fill()๊ณผ ๊ฐ™์€ ๋ณ€๊ฒฝ์„ ๋ง‰์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๊ฐ’ ์ž์ฒด๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๋Š” concat(), slice()๊ณผ ๊ฐ™์€ ๊ธฐ๋Šฅ์€ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Bad:

const array: number[] = [ 1, 3, 5 ];
array = []; // ์—๋Ÿฌ
array.push(100); // ๋ฐฐ์—ด์€ ๋ณ€๊ฒฝ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

Good:

const array: ReadonlyArray<number> = [ 1, 3, 5 ];
array = []; // ์—๋Ÿฌ
array.push(100); // ์—๋Ÿฌ

TypeScript 3.4 is a bit easier์—์„œ ์ฝ๊ธฐ ์ „์šฉ์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์„ ์–ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

function hoge(args: readonly string[]) {
  args.push(1); // ์—๋Ÿฌ
}

๋ฆฌํ„ฐ๋Ÿด ๊ฐ’์„ ์œ„ํ•ด const assertions๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

Bad:

const config = {
  hello: 'world'
};
config.hello = 'world'; // ๊ฐ’์ด ๋ฐ”๋€๋‹ˆ๋‹ค

const array  = [ 1, 3, 5 ];
array[0] = 10; // ๊ฐ’์ด ๋ฐ”๋€๋‹ˆ๋‹ค

// ์“ธ ์ˆ˜ ์žˆ๋Š” ๊ฐ์ฒด๊ฐ€ ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค
function readonlyData(value: number) {
  return { value };
}

const result = readonlyData(100);
result.value = 200; // ๊ฐ’์ด ๋ฐ”๋€๋‹ˆ๋‹ค

Good:

// ์ฝ๊ธฐ ์ „์šฉ ๊ฐ์ฒด
const config = {
  hello: 'world'
} as const;
config.hello = 'world'; // ์—๋Ÿฌ

// ์ฝ๊ธฐ ์ „์šฉ ๋ฐฐ์—ด
const array  = [ 1, 3, 5 ] as const;
array[0] = 10; // ์—๋Ÿฌ

// ์ฝ๊ธฐ ์ „์šฉ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
function readonlyData(value: number) {
  return { value } as const;
}

const result = readonlyData(100);
result.value = 200; // ์—๋Ÿฌ

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

ํƒ€์ž… vs ์ธํ„ฐํŽ˜์ด์Šค

ํ•ฉ์ง‘ํ•ฉ ๋˜๋Š” ๊ต์ง‘ํ•ฉ์ด ํ•„์š”ํ•  ๋•Œ ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜์„ธ์š”. extends ๋˜๋Š” implements๊ฐ€ ํ•„์š”ํ•  ๋•Œ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”. ์—„๊ฒฉํ•œ ๊ทœ์น™์€ ์—†์ง€๋งŒ ๋‹น์‹ ์—๊ฒŒ ๋งž๋Š” ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•˜์„ธ์š”.
ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์—์„œ type๊ณผ interface ์‚ฌ์ด์˜ ๋‹ค๋ฅธ ์ ์— ๋Œ€ํ•ด์„œ ๋” ์ƒ์„ธํ•œ ์„ค๋ช…์„ ์›ํ•œ๋‹ค๋ฉด ์ด ๋‹ต๋ณ€์„ ์ฐธ๊ณ ํ•˜์„ธ์š”.

Bad:

interface EmailConfig {
  // ...
}

interface DbConfig {
  // ...
}

interface Config {
  // ...
}

//...

type Shape = {
  // ...
}

Good:


type EmailConfig = {
  // ...
}

type DbConfig = {
  // ...
}

type Config  = EmailConfig | DbConfig;

// ...

interface Shape {
  // ...
}

class Circle implements Shape {
  // ...
}

class Square implements Shape {
  // ...
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

ํด๋ž˜์Šค

ํด๋ž˜์Šค๋Š” ์ž‘์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค

ํด๋ž˜์Šค์˜ ํฌ๊ธฐ๋Š” ์ฑ…์ž„์— ์˜ํ•ด ์ธก์ •๋ฉ๋‹ˆ๋‹ค. ๋‹จ์ผ ์ฑ…์ž„ ์›์น™์— ๋”ฐ๋ฅด๋ฉด ํด๋ž˜์Šค๋Š” ์ž‘์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Bad:

class Dashboard {
  getLanguage(): string { /* ... */ }
  setLanguage(language: string): void { /* ... */ }
  showProgress(): void { /* ... */ }
  hideProgress(): void { /* ... */ }
  isDirty(): boolean { /* ... */ }
  disable(): void { /* ... */ }
  enable(): void { /* ... */ }
  addSubscription(subscription: Subscription): void { /* ... */ }
  removeSubscription(subscription: Subscription): void { /* ... */ }
  addUser(user: User): void { /* ... */ }
  removeUser(user: User): void { /* ... */ }
  goToHomePage(): void { /* ... */ }
  updateProfile(details: UserDetails): void { /* ... */ }
  getVersion(): string { /* ... */ }
  // ...
}

Good:

class Dashboard {
  disable(): void { /* ... */ }
  enable(): void { /* ... */ }
  getVersion(): string { /* ... */ }
}

// ๋‹ค๋ฅธ ํด๋ž˜์Šค์— ๋‚จ์€ ๋ฉ”์†Œ๋“œ๋ฅผ ์ด๋™์‹œํ‚ด์œผ๋กœ์จ ์ฑ…์ž„์„ ๋ถ„์‚ฐ์‹œํ‚ค์„ธ์š”
// ...

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

๋†’์€ ์‘์ง‘๋„์™€ ๋‚ฎ์€ ๊ฒฐํ•ฉ๋„

์‘์ง‘๋„๋Š” ํด๋ž˜์Šค ๋ฉค๋ฒ„๊ฐ€ ์„œ๋กœ์—๊ฒŒ ์—ฐ๊ด€๋˜์–ด ์žˆ๋Š” ์ •๋„๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ์ด์ƒ์ ์œผ๋กœ, ํด๋ž˜์Šค ์•ˆ์˜ ๋ชจ๋“  ํ•„๋“œ๋Š” ๊ฐ ๋ฉ”์†Œ๋“œ์— ์˜ํ•ด ์‚ฌ์šฉ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ ๋•Œ ์šฐ๋ฆฌ๋Š” ํด๋ž˜์Šค๊ฐ€ ์ตœ๋Œ€ํ•œ์œผ๋กœ ์‘์ง‘๋˜์–ด์žˆ๋‹ค๋ผ๊ณ  ๋งํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ํ•ญ์ƒ ๊ฐ€๋Šฅํ•˜์ง€๋„ ์•Š๊ณ  ๊ถŒ์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์‘์ง‘๋„๋ฅผ ๋†’์ด๋Š” ๊ฒƒ์„ ์„ ํ˜ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๊ฒฐํ•ฉ๋„๋Š” ๋‘ ํด๋ž˜์Šค๊ฐ€ ์–ผ๋งˆ๋‚˜ ์„œ๋กœ์—๊ฒŒ ๊ด€๋ จ๋˜์–ด์žˆ๊ฑฐ๋‚˜ ์ข…์†์ ์ธ ์ •๋„๋ฅผ ๋œปํ•ฉ๋‹ˆ๋‹ค. ํ•˜๋‚˜์˜ ํด๋ž˜์Šค์˜ ๋ณ€๊ฒฝ์ด ๋‹ค๋ฅธ ํด๋ž˜์Šค์—๊ฒŒ ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š”๋‹ค๋ฉด ๊ทธ ํด๋ž˜์Šค๋“ค์˜ ๊ฒฐํ•ฉ๋„๋Š” ๋‚ฎ๋‹ค๊ณ  ๋งํ•ฉ๋‹ˆ๋‹ค.

์ข‹์€ ์†Œํ”„ํŠธ์›จ์–ด ์„ค๊ณ„๋Š” ๋†’์€ ์‘์ง‘๋„์™€ ๋‚ฎ์€ ๊ฒฐํ•ฉ๋„๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค.

Bad:

class UserManager {
  // Bad: ๊ฐ private ๋ณ€์ˆ˜๋Š” ๋ฉ”์†Œ๋“œ์˜ ํ•˜๋‚˜ ํ˜น์€ ๋˜ ๋‹ค๋ฅธ ๊ทธ๋ฃน์— ์˜ํ•ด ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
  // ํด๋ž˜์Šค๊ฐ€ ๋‹จ์ผ ์ฑ…์ž„ ์ด์ƒ์˜ ์ฑ…์ž„์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๋Š” ๋ช…๋ฐฑํ•œ ์ฆ๊ฑฐ์ž…๋‹ˆ๋‹ค.
  // ์‚ฌ์šฉ์ž์˜ ํŠธ๋žœ์žญ์…˜์„ ์–ป๊ธฐ ์œ„ํ•ด ์„œ๋น„์Šค๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ๋˜๋Š” ๊ฒฝ์šฐ,
  // ์—ฌ์ „ํžˆ `emailSender` ์ธ์Šคํ„ด์Šค๋ฅผ ์ „๋‹ฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  constructor(
    private readonly db: Database,
    private readonly emailSender: EmailSender) {
  }

  async getUser(id: number): Promise<User> {
    return await db.users.findOne({ id });
  }

  async getTransactions(userId: number): Promise<Transaction[]> {
    return await db.transactions.find({ userId });
  }

  async sendGreeting(): Promise<void> {
    await emailSender.send('Welcome!');
  }

  async sendNotification(text: string): Promise<void> {
    await emailSender.send(text);
  }

  async sendNewsletter(): Promise<void> {
    // ...
  }
}

Good:

class UserService {
  constructor(private readonly db: Database) {
  }

  async getUser(id: number): Promise<User> {
    return await this.db.users.findOne({ id });
  }

  async getTransactions(userId: number): Promise<Transaction[]> {
    return await this.db.transactions.find({ userId });
  }
}

class UserNotifier {
  constructor(private readonly emailSender: EmailSender) {
  }

  async sendGreeting(): Promise<void> {
    await this.emailSender.send('Welcome!');
  }

  async sendNotification(text: string): Promise<void> {
    await this.emailSender.send(text);
  }

  async sendNewsletter(): Promise<void> {
    // ...
  }
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

์ƒ์†(inheritance)๋ณด๋‹ค ์กฐํ•ฉ(composition)์„ ์‚ฌ์šฉํ•˜์„ธ์š”

Gang of Four์˜ ๋””์ž์ธ ํŒจํ„ด์— ๋‚˜์™€์žˆ๋“ฏ์ด ํ•  ์ˆ˜ ์žˆ๋Š” ๋Œ€๋กœ ์ƒ์†๋ณด๋‹ค ์กฐํ•ฉ์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ƒ์†๊ณผ ์กฐํ•ฉ์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” ์ข‹์€ ์ด์œ ๋“ค์€ ๊ฐ๊ฐ ๋งŽ์Šต๋‹ˆ๋‹ค. ์ด ๊ตํ›ˆ์—์„œ ์ค‘์š”ํ•œ ์ ์€ ๋‹น์‹ ์˜ ๋งˆ์Œ์ด ๋ณธ๋Šฅ์ ์œผ๋กœ ์ƒ์†์„ ์ถ”๊ตฌํ•œ๋‹ค๋ฉด, ์กฐํ•ฉ์ด ๋‹น์‹ ์˜ ๋ฌธ์ œ๋ฅผ ๋” ์ข‹๊ฒŒ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์„์ง€ ๊ณ ๋ฏผํ•ด๋ณด๋ผ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์–ด๋–ค ๊ฒฝ์šฐ์—๋Š” ๋” ์ข‹์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹น์‹ ์€ โ€œ์–ธ์ œ ์ƒ์†์„ ์‚ฌ์šฉํ•ด์•ผ ํ• ๊นŒ์š”?โ€ ๋ผ๊ณ  ์˜๋ฌธ์ ์„ ๊ฐ€์งˆ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๊ฒƒ์€ ๋‹น๋ฉดํ•œ ๋ฌธ์ œ์— ๋‹ฌ๋ ค ์žˆ์Šต๋‹ˆ๋‹ค. ์กฐํ•ฉ๋ณด๋‹ค ์ƒ์†์ด ๋” ์ข‹์€ ๊ฒฝ์šฐ๊ฐ€ ์•„๋ž˜์— ์žˆ์Šต๋‹ˆ๋‹ค:

  1. โ€œhas-aโ€ ๊ด€๊ณ„๊ฐ€ ์•„๋‹Œ โ€œis-aโ€ ๊ด€๊ณ„์ผ ๋•Œ (์‚ฌ๋žŒ->๋™๋ฌผ vs ์‚ฌ์šฉ์ž->์‚ฌ์šฉ์ž ์ •๋ณด)

  2. ๊ธฐ๋ฐ˜์ด ๋˜๋Š” ํด๋ž˜์Šค๋กœ๋ถ€ํ„ฐ ์ฝ”๋“œ๋ฅผ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์„ ๋•Œ (์‚ฌ๋žŒ์€ ๋ชจ๋“  ๋™๋ฌผ์ฒ˜๋Ÿผ ์›€์ง์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.)

  3. ๊ธฐ๋ฐ˜์ด ๋˜๋Š” ํด๋ž˜์Šค๋ฅผ ๋ณ€๊ฒฝํ•˜์—ฌ ํŒŒ์ƒ๋œ ํด๋ž˜์Šค๋ฅผ ์ „์ฒด์ ์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋ ค๋Š” ๊ฒฝ์šฐ (๋ชจ๋“  ๋™๋ฌผ์€ ์›€์ง์ผ ๋•Œ ์นผ๋กœ๋ฆฌ๊ฐ€ ์†Œ๋น„๋ฉ๋‹ˆ๋‹ค.)

Bad:

class Employee {
  constructor(
    private readonly name: string,
    private readonly email: string) {
  }

  // ...
}

// `Employee`๊ฐ€ ์„ธ๊ธˆ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง€๊ธฐ ๋•Œ๋ฌธ์— ๋‚˜์œ ์—์ž…๋‹ˆ๋‹ค. `EmployeeTaxData`๋Š” `Employee`์˜ ํƒ€์ž…์ด ์•„๋‹™๋‹ˆ๋‹ค.
class EmployeeTaxData extends Employee {
  constructor(
    name: string,
    email: string,
    private readonly ssn: string,
    private readonly salary: number) {
    super(name, email);
  }

  // ...
}

Good:

class Employee {
  private taxData: EmployeeTaxData;

  constructor(
    private readonly name: string,
    private readonly email: string) {
  }

  setTaxData(ssn: string, salary: number): Employee {
    this.taxData = new EmployeeTaxData(ssn, salary);
    return this;
  }

  // ...
}

class EmployeeTaxData {
  constructor(
    public readonly ssn: string,
    public readonly salary: number) {
  }

  // ...
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

๋ฉ”์†Œ๋“œ ์ฒด์ด๋‹์„ ์‚ฌ์šฉํ•˜์„ธ์š”

์ด ํŒจํ„ด์€ ๋งค์šฐ ์œ ์šฉํ•˜๊ณ  ๋งŽ์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ๊ณตํ†ต์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ๋‹น์‹ ์˜ ์ฝ”๋“œ๋ฅผ ํ‘œํ˜„๋ ฅ์ด ์žˆ๊ฒŒ ํ•ด์ฃผ๊ณ  ๋œ ์žฅํ™ฉํ•˜๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ์ด์œ ๋กœ ๋ฉ”์†Œ๋“œ ์ฒด์ด๋‹์„ ์‚ฌ์šฉํ•ด์„œ ๋‹น์‹ ์˜ ์ฝ”๋“œ๊ฐ€ ์–ผ๋งˆ๋‚˜ ๋ช…๋ฃŒํ•ด์ง€๋Š”์ง€ ์‚ดํŽด๋ณด์‹œ๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค.

Bad:

class QueryBuilder {
  private collection: string;
  private pageNumber: number = 1;
  private itemsPerPage: number = 100;
  private orderByFields: string[] = [];

  from(collection: string): void {
    this.collection = collection;
  }

  page(number: number, itemsPerPage: number = 100): void {
    this.pageNumber = number;
    this.itemsPerPage = itemsPerPage;
  }

  orderBy(...fields: string[]): void {
    this.orderByFields = fields;
  }

  build(): Query {
    // ...
  }
}

// ...

const queryBuilder = new QueryBuilder();
queryBuilder.from('users');
queryBuilder.page(1, 100);
queryBuilder.orderBy('firstName', 'lastName');

const query = queryBuilder.build();

Good:

class QueryBuilder {
  private collection: string;
  private pageNumber: number = 1;
  private itemsPerPage: number = 100;
  private orderByFields: string[] = [];

  from(collection: string): this {
    this.collection = collection;
    return this;
  }

  page(number: number, itemsPerPage: number = 100): this {
    this.pageNumber = number;
    this.itemsPerPage = itemsPerPage;
    return this;
  }

  orderBy(...fields: string[]): this {
    this.orderByFields = fields;
    return this;
  }

  build(): Query {
    // ...
  }
}

// ...

const query = new QueryBuilder()
  .from('users')
  .page(1, 100)
  .orderBy('firstName', 'lastName')
  .build();

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

SOLID

๋‹จ์ผ ์ฑ…์ž„ ์›์น™ (SRP)

ํด๋ฆฐ ์ฝ”๋“œ์—์„œ ๋งํ•˜๋“ฏ์ด, โ€œํด๋ž˜์Šค๋ฅผ ๋ณ€๊ฒฝํ•  ๋•Œ๋Š” ๋‹จ ํ•œ ๊ฐ€์ง€ ์ด์œ ๋งŒ ์กด์žฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹คโ€. ์—ฌํ–‰๊ฐˆ ๋•Œ ๊ฐ€๋ฐฉ ํ•˜๋‚˜์— ๋งŽ์€ ๊ฒƒ์„ ์ฑ™๊ธฐ๋Š” ๊ฒƒ๊ณผ ๊ฐ™์ด, ํด๋ž˜์Šค๋ฅผ ๋งŽ์€ ๊ธฐ๋Šฅ์œผ๋กœ ๊ฝ‰ ์ฑ„์šฐ๊ณ  ์‹ถ์€ ์œ ํ˜น์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋Š” ๋‹น์‹ ์˜ ํด๋ž˜์Šค๊ฐ€ ๊ฐœ๋…์ ์œผ๋กœ ์‘์ง‘๋ ฅ์ด ์žˆ์ง€ ์•Š์œผ๋ฉฐ ๋ณ€๊ฒฝ๋  ๋งŽ์€ ์ด์œ ๊ฐ€ ์กด์žฌํ•œ๋‹ค๋Š” ๊ฒƒ์„ ๋งํ•ฉ๋‹ˆ๋‹ค. ํด๋ž˜์Šค๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๋งŽ์€ ์‹œ๊ฐ„์„ ์ค„์ด๋Š” ๊ฒƒ์€ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด ๋„ˆ๋ฌด ๋งŽ์€ ๊ธฐ๋Šฅ์ด ํ•œ ํด๋ž˜์Šค์— ์žˆ๊ณ  ๊ทธ ์•ˆ์—์„œ ํ•˜๋‚˜์˜ ๊ธฐ๋Šฅ์„ ์ˆ˜์ •ํ•œ๋‹ค๋ฉด, ๋‹ค๋ฅธ ์ข…์†๋œ ๋ชจ๋“ˆ์— ์–ด๋–ป๊ฒŒ ์˜ํ–ฅ์„ ์ค„์ง€ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์ด ์–ด๋ ต๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

Bad:

class UserSettings {
  constructor(private readonly user: User) {
  }

  changeSettings(settings: UserSettings) {
    if (this.verifyCredentials()) {
      // ...
    }
  }

  verifyCredentials() {
    // ...
  }
}

Good:

class UserAuth {
  constructor(private readonly user: User) {
  }

  verifyCredentials() {
    // ...
  }
}


class UserSettings {
  private readonly auth: UserAuth;

  constructor(private readonly user: User) {
    this.auth = new UserAuth(user);
  }

  changeSettings(settings: UserSettings) {
    if (this.auth.verifyCredentials()) {
      // ...
    }
  }
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

๊ฐœ๋ฐฉ ํ์‡„ ์›์น™ (OCP)

Bertrand Meyer๊ฐ€ ๋งํ–ˆ๋“ฏ์ด, โ€œ์†Œํ”„ํŠธ์›จ์–ด ์—”ํ„ฐํ‹ฐ(ํด๋ž˜์Šค, ๋ชจ๋“ˆ, ํ•จ์ˆ˜ ๋“ฑ)๋Š” ์ƒ์†์— ๊ฐœ๋ฐฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ˆ˜์ •์—๋Š” ํ์‡„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.โ€ ์ด๊ฒƒ์ด ๋ฌด์—‡์„ ์˜๋ฏธํ• ๊นŒ์š”? ์ด ์›์น™์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ๊ธฐ์กด์˜ ์ฝ”๋“œ๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๊ณ  ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ๊ฒƒ์„ ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค.

Bad:

class AjaxAdapter extends Adapter {
  constructor() {
    super();
  }

  // ...
}

class NodeAdapter extends Adapter {
  constructor() {
    super();
  }

  // ...
}

class HttpRequester {
  constructor(private readonly adapter: Adapter) {
  }

  async fetch<T>(url: string): Promise<T> {
    if (this.adapter instanceof AjaxAdapter) {
      const response = await makeAjaxCall<T>(url);
      // response ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜๊ณ  ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
    } else if (this.adapter instanceof NodeAdapter) {
      const response = await makeHttpCall<T>(url);
      // response ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜๊ณ  ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
    }
  }
}

function makeAjaxCall<T>(url: string): Promise<T> {
  // ์„œ๋ฒ„์— ์š”์ฒญํ•˜๊ณ  ํ”„๋กœ๋ฏธ์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
}

function makeHttpCall<T>(url: string): Promise<T> {
  // ์„œ๋ฒ„์— ์š”์ฒญํ•˜๊ณ  ํ”„๋กœ๋ฏธ์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
}

Good:

abstract class Adapter {
  abstract async request<T>(url: string): Promise<T>;

  // ํ•˜์œ„ ํด๋ž˜์Šค์™€ ๊ณต์œ ํ•˜๋Š” ์ฝ”๋“œ ...
}

class AjaxAdapter extends Adapter {
  constructor() {
    super();
  }

  async request<T>(url: string): Promise<T>{
    // ์„œ๋ฒ„์— ์š”์ฒญํ•˜๊ณ  ํ”„๋กœ๋ฏธ์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  }

  // ...
}

class NodeAdapter extends Adapter {
  constructor() {
    super();
  }

  async request<T>(url: string): Promise<T>{
    // ์„œ๋ฒ„์— ์š”์ฒญํ•˜๊ณ  ํ”„๋กœ๋ฏธ์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  }

  // ...
}

class HttpRequester {
  constructor(private readonly adapter: Adapter) {
  }

  async fetch<T>(url: string): Promise<T> {
    const response = await this.adapter.request<T>(url);
    // response ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜๊ณ  ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  }
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

๋ฆฌ์Šค์ฝ”ํ”„ ์น˜ํ™˜ ์›์น™ (LSP)

๋งค์šฐ ๋‹จ์ˆœํ•œ ๊ฐœ๋…์„ ๋œปํ•˜๋Š” ์–ด๋ ค์›Œ๋ณด์ด๋Š” ์šฉ์–ด์ž…๋‹ˆ๋‹ค. โ€œ๋งŒ์•ฝ S๊ฐ€ T์˜ ํ•˜์œ„ ํƒ€์ž…์ด๋ผ๋ฉด, T ํƒ€์ž…์˜ ๊ฐ์ฒด๋Š” S ํƒ€์ž…์˜ ๊ฐ์ฒด๋กœ ๋Œ€์ฒด๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (์˜ˆ: S ํƒ€์ž… ๊ฐ์ฒด๋Š” T ํƒ€์ž… ๊ฐ์ฒด๋กœ ์น˜ํ™˜๋  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.) ์ด๋Š” ํ”„๋กœ๊ทธ๋žจ์ด ๊ฐ–์ถ”์–ด์•ผํ•  ์†์„ฑ(์ •ํ™•์„ฑ, ์ˆ˜ํ–‰๋˜๋Š” ์ž‘์—… ๋“ฑ)์„ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์•„๋„ ๋Œ€์ฒด๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.โ€ ๋”์šฑ ์–ด๋ ค์›Œ๋ณด์ด๋Š” ์ •์˜์ž…๋‹ˆ๋‹ค.

์ด๋ฅผ ์œ„ํ•œ ์ตœ๊ณ ์˜ ์„ค๋ช…์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๋งŒ์•ฝ ๋ถ€๋ชจ ํด๋ž˜์Šค์™€ ์ž์‹ ํด๋ž˜์Šค๊ฐ€ ์žˆ๋‹ค๋ฉด, ๋ถ€๋ชจ ํด๋ž˜์Šค์™€ ์ž์‹ ํด๋ž˜์Šค๋Š” ์ž˜๋ชป๋œ ๊ฒฐ๊ณผ ์—†์ด ์„œ๋กœ ๊ตํ™˜ํ•˜์—ฌ ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์—ฌ์ „ํžˆ ํ˜ผ๋ž€์Šค๋Ÿฌ์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ณ ์ „์ ์ธ ์ •์‚ฌ๊ฐํ˜•-์ง์‚ฌ๊ฐํ˜• ์˜ˆ์ œ๋ฅผ ์‚ดํŽด๋ณด์„ธ์š”. ์ˆ˜ํ•™์ ์œผ๋กœ, ์ •์‚ฌ๊ฐํ˜•์€ ์ง์‚ฌ๊ฐํ˜•์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ƒ์†์„ ํ†ตํ•ด โ€œis-aโ€ ๊ด€๊ณ„๋กœ ์„ค๊ณ„ํ•œ๋‹ค๋ฉด, ๋‹น์‹ ์€ ๊ณค๊ฒฝ์— ๋น ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Bad:

class Rectangle {
  constructor(
    protected width: number = 0,
    protected height: number = 0) {

  }

  setColor(color: string): this {
    // ...
  }

  render(area: number) {
    // ...
  }

  setWidth(width: number): this {
    this.width = width;
    return this;
  }

  setHeight(height: number): this {
    this.height = height;
    return this;
  }

  getArea(): number {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  setWidth(width: number): this {
    this.width = width;
    this.height = width;
    return this;
  }

  setHeight(height: number): this {
    this.width = height;
    this.height = height;
    return this;
  }
}

function renderLargeRectangles(rectangles: Rectangle[]) {
  rectangles.forEach((rectangle) => {
    const area = rectangle
      .setWidth(4)
      .setHeight(5)
      .getArea(); // BAD: `Square` ํด๋ž˜์Šค์—์„œ๋Š” 25๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. 20์ด ๋ฐ˜ํ™˜๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
    rectangle.render(area);
  });
}

const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);

Good:

abstract class Shape {
  setColor(color: string): this {
    // ...
  }

  render(area: number) {
    // ...
  }

  abstract getArea(): number;
}

class Rectangle extends Shape {
  constructor(
    private readonly width = 0,
    private readonly height = 0) {
    super();
  }

  getArea(): number {
    return this.width * this.height;
  }
}

class Square extends Shape {
  constructor(private readonly length: number) {
    super();
  }

  getArea(): number {
    return this.length * this.length;
  }
}

function renderLargeShapes(shapes: Shape[]) {
  shapes.forEach((shape) => {
    const area = shape.getArea();
    shape.render(area);
  });
}

const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

์ธํ„ฐํŽ˜์ด์Šค ๋ถ„๋ฆฌ ์›์น™ (ISP)

์ธํ„ฐํŽ˜์ด์Šค ๋ถ„๋ฆฌ ์›์น™์€ โ€œํด๋ผ์ด์–ธํŠธ๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ธํ„ฐํŽ˜์ด์Šค์— ์˜์กดํ•˜์ง€ ์•Š๋Š”๋‹คโ€ ๋ผ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด ์›์น™์€ ๋‹จ์ผ ์ฑ…์ž„ ์›์น™๊ณผ ๋งŽ์€ ๊ด€๋ จ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ง์˜ ๋œป์€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋…ธ์ถœ๋œ ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋Œ€์‹ ์— ์ „์ฒด ํŒŒ์ด๋ฅผ ์–ป์ง€ ์•Š๋Š” ๋ฐฉ์‹์œผ๋กœ ์ถ”์ƒํ™”๋ฅผ ์„ค๊ณ„ํ•ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๊ฒƒ์€ ๋˜ํ•œ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์‹ค์ œ๋กœ ํ•„์š”ํ•˜์ง€ ์•Š์€ ๋ฉ”์†Œ๋“œ์˜ ๊ตฌํ˜„์„ ๊ฐ•์š”ํ•˜๋Š” ๊ฒƒ๋„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.

Bad:

interface SmartPrinter {
  print();
  fax();
  scan();
}

class AllInOnePrinter implements SmartPrinter {
  print() {
    // ...
  }  
  
  fax() {
    // ...
  }

  scan() {
    // ...
  }
}

class EconomicPrinter implements SmartPrinter {
  print() {
    // ...
  }  
  
  fax() {
    throw new Error('Fax not supported.');
  }

  scan() {
    throw new Error('Scan not supported.');
  }
}

Good:

interface Printer {
  print();
}

interface Fax {
  fax();
}

interface Scanner {
  scan();
}

class AllInOnePrinter implements Printer, Fax, Scanner {
  print() {
    // ...
  }  
  
  fax() {
    // ...
  }

  scan() {
    // ...
  }
}

class EconomicPrinter implements Printer {
  print() {
    // ...
  }
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

์˜์กด์„ฑ ์—ญ์ „ ์›์น™ (DIP)

์ด ์›์น™์€ ๋‘ ๊ฐ€์ง€ ํ•„์ˆ˜์ ์ธ ์‚ฌํ•ญ์„ ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค:

  1. ์ƒ์œ„ ๋ ˆ๋ฒจ์˜ ๋ชจ๋“ˆ์€ ํ•˜์œ„ ๋ ˆ๋ฒจ์˜ ๋ชจ๋“ˆ์— ์˜์กดํ•˜์ง€ ์•Š์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‘ ๋ชจ๋“ˆ์€ ๋ชจ๋‘ ์ถ”์ƒํ™”์— ์˜์กดํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

  2. ์ถ”์ƒํ™”๋Š” ์„ธ๋ถ€์‚ฌํ•ญ์— ์˜์กดํ•˜์ง€ ์•Š์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์„ธ๋ถ€์‚ฌํ•ญ์€ ์ถ”์ƒํ™”์— ์˜์กดํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ฒ˜์Œ์— ๋ฐ”๋กœ ์ดํ•ดํ•˜๊ธฐ๋Š” ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Angular๋ฅผ ์‚ฌ์šฉํ•ด๋ดค๋‹ค๋ฉด, ์˜์กด์„ฑ ์ฃผ์ž…(DI)์˜ ํ˜•ํƒœ ์•ˆ์—์„œ ์ด ์›์น™์˜ ๊ตฌํ˜„์„ ํ™•์ธํ•ด๋ดค์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋™์ผํ•œ ๊ฐœ๋…์€ ์•„๋‹ˆ์ง€๋งŒ, DIP๋Š” ์ƒ์œ„ ๋ ˆ๋ฒจ์˜ ๋ชจ๋“ˆ์ด ํ•˜์œ„ ๋ ˆ๋ฒจ์˜ ๋ชจ๋“ˆ์˜ ์„ธ๋ถ€์‚ฌํ•ญ์— ์ ‘๊ทผํ•˜๊ณ  ์„ค์ •ํ•˜์ง€ ๋ชปํ•˜๋„๋ก ์ง€ํ‚ต๋‹ˆ๋‹ค. DI๋ฅผ ํ†ตํ•ด์„œ๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์„ฑ์ทจํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์˜ ํฐ ์žฅ์ ์€ ๋ชจ๋“ˆ ์‚ฌ์ด์˜ ๊ฒฐํ•ฉ๋„๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ฒฐํ•ฉ๋„๋Š” ์ฝ”๋“œ๋ฅผ ๋ฆฌํŒฉํ† ๋งํ•˜๊ธฐ ์–ด๋ ต๊ฒŒ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋งค์šฐ ๋‚˜์œ ๊ฐœ๋ฐœ ํŒจํ„ด์ž…๋‹ˆ๋‹ค.

DIP๋Š” ์ฃผ๋กœ IoC ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์‚ฌ์šฉํ•จ์œผ๋กœ์จ ๋‹ฌ์„ฑ๋ฉ๋‹ˆ๋‹ค. ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋ฅผ ์œ„ํ•œ ๊ฐ•๋ ฅํ•œ IoC ์ปจํ…Œ์ด๋„ˆ์˜ ์˜ˆ์ œ๋Š” InversifyJs์ž…๋‹ˆ๋‹ค.

Bad:

import { readFile as readFileCb } from 'fs';
import { promisify } from 'util';

const readFile = promisify(readFileCb);

type ReportData = {
  // ..
}

class XmlFormatter {
  parse<T>(content: string): T {
    // XML ๋ฌธ์ž์—ด์„ T ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜
  }
}

class ReportReader {

  // BAD: ํŠน์ • ์š”์ฒญ์˜ ๊ตฌํ˜„์— ์˜์กดํ•˜๋Š” ๊ฒƒ์„ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.
  // `parse` ๋ฉ”์†Œ๋“œ์— ์˜์กดํ•˜๋Š” `ReportReader`๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  private readonly formatter = new XmlFormatter();

  async read(path: string): Promise<ReportData> {
    const text = await readFile(path, 'UTF8');
    return this.formatter.parse<ReportData>(text);
  }
}

// ...
const reader = new ReportReader();
await report = await reader.read('report.xml');

Good:

import { readFile as readFileCb } from 'fs';
import { promisify } from 'util';

const readFile = promisify(readFileCb);

type ReportData = {
  // ..
}

interface Formatter {
  parse<T>(content: string): T;
}

class XmlFormatter implements Formatter {
  parse<T>(content: string): T {
    // XML ๋ฌธ์ž์—ด์„ T ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜
  }
}


class JsonFormatter implements Formatter {
  parse<T>(content: string): T {
    // JSON ๋ฌธ์ž์—ด์„ T ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜
  }
}

class ReportReader {
  constructor(private readonly formatter: Formatter) {
  }

  async read(path: string): Promise<ReportData> {
    const text = await readFile(path, 'UTF8');
    return this.formatter.parse<ReportData>(text);
  }
}

// ...
const reader = new ReportReader(new XmlFormatter());
await report = await reader.read('report.xml');

// ๋˜๋Š” json ๋ณด๊ณ ์„œ๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ
const reader = new ReportReader(new JsonFormatter());
await report = await reader.read('report.json');

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

ํ…Œ์ŠคํŠธ

ํ…Œ์ŠคํŠธ๋Š” ๋ฐฐํฌ๋ณด๋‹ค ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋ถ€์กฑํ•œ ๊ฒฝ์šฐ, ์ฝ”๋“œ๋ฅผ ๋ฐฐํฌํ•  ๋•Œ๋งˆ๋‹ค ๋‹น์‹ ์€ ์–ด๋–ค ๊ฒƒ์ด ์ž‘๋™ํ•˜์ง€ ์•Š์„์ง€ ํ™•์‹คํ•˜์ง€ ์•Š์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ ์ ˆํ•œ ์–‘์˜ ํ…Œ์ŠคํŠธ๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๊ฒƒ์€ ๋‹น์‹ ์˜ ํŒ€์—๊ฒŒ ๋‹ฌ๋ ค์žˆ์ง€๋งŒ, (๋ชจ๋“  ๋ฌธ์žฅ๊ณผ ๋ธŒ๋žœ์น˜์—์„œ) 100%์˜ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ๊ฐ€์ง„๋‹ค๋ฉด ๋งค์šฐ ๋†’์€ ์ž์‹ ๊ฐ๊ณผ ๋งˆ์Œ์˜ ํ‰ํ™”๋ฅผ ์–ป์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ํ›Œ๋ฅญํ•œ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, ์ข‹์€ ์ปค๋ฒ„๋ฆฌ์ง€ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜์ง€ ์•Š์„ ์ด์œ ๋Š” ์—†์Šต๋‹ˆ๋‹ค. ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์˜ ํƒ€์ž…์„ ์ง€์›ํ•˜๋Š” ๋งŽ์€ ์–‘์˜ ์ข‹์€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ์žˆ์œผ๋ฏ€๋กœ ๋‹น์‹ ์˜ ํŒ€์ด ์„ ํ˜ธํ•˜๋Š” ๊ฒƒ์„ ์ฐพ์•„ ์‚ฌ์šฉํ•˜์„ธ์š”. ๋‹น์‹ ์˜ ํŒ€์— ์ ํ•ฉํ•œ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์ฐพ์•˜๋‹ค๋ฉด, ๋‹น์‹ ์ด ๋งŒ๋“œ๋Š” ๋ชจ๋“  ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ/๋ชจ๋“ˆ์„ ์œ„ํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ํ•ญ์ƒ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•˜์„ธ์š”. ํ…Œ์ŠคํŠธ ๊ธฐ๋ฐ˜ ๊ฐœ๋ฐœ(TDD)์ด ๋‹น์‹ ์ด ์„ ํ˜ธํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋ผ๋ฉด, ๋งค์šฐ ์ข‹์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ค‘์š”ํ•œ ๊ฑด ์–ด๋–ค ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค๊ฑฐ๋‚˜ ๊ธฐ์กด์˜ ๊ฒƒ์„ ๋ฆฌํŒฉํ† ๋งํ•˜๊ธฐ ์ „์— ๋ชฉํ‘œํ•˜๋Š” ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ๋‹ฌ์„ฑํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

TDD์˜ ์„ธ ๊ฐ€์ง€ ๋ฒ•์น™

  1. ์‹คํŒจํ•˜๋Š” ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์ „์—๋Š” ์‹ค์ œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์ง€ ๋งˆ์„ธ์š”.

  2. ์ปดํŒŒ์ผ์€ ์‹คํŒจํ•˜์ง€ ์•Š์œผ๋ฉด์„œ ์‹คํ–‰์ด ์‹คํŒจํ•˜๋Š” ์ •๋„๋กœ๋งŒ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š”.

  3. ์‹คํŒจํ•˜๋Š” ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผํ•  ์ •๋„๋กœ๋งŒ ์‹ค์ œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š”.

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

F.I.R.S.T ๊ทœ์น™

๋ช…๋ฃŒํ•œ ํ…Œ์ŠคํŠธ๋Š” ๋‹ค์Œ ๊ทœ์น™์„ ๋”ฐ๋ผ์•ผ ํ•ฉ๋‹ˆ๋‹ค:

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

ํ…Œ์ŠคํŠธ ํ•˜๋‚˜์— ํ•˜๋‚˜์˜ ๊ฐœ๋…์„ ์ž‘์„ฑํ•˜์„ธ์š”

๋˜ํ•œ, ํ…Œ์ŠคํŠธ๋Š” ๋‹จ์ผ ์ฑ…์ž„ ์›์น™์„ ๋”ฐ๋ผ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ํ•˜๋‚˜๋‹น ํ•˜๋‚˜์˜ assert ๊ตฌ๋ฌธ์„ ์ž‘์„ฑํ•˜์„ธ์š”.

Bad:

import { assert } from 'chai';

describe('AwesomeDate', () => {
  it('handles date boundaries', () => {
    let date: AwesomeDate;

    date = new AwesomeDate('1/1/2015');
    assert.equal('1/31/2015', date.addDays(30));

    date = new AwesomeDate('2/1/2016');
    assert.equal('2/29/2016', date.addDays(28));

    date = new AwesomeDate('2/1/2015');
    assert.equal('3/1/2015', date.addDays(28));
  });
});

Good:

import { assert } from 'chai';

describe('AwesomeDate', () => {
  it('handles 30-day months', () => {
    const date = new AwesomeDate('1/1/2015');
    assert.equal('1/31/2015', date.addDays(30));
  });

  it('handles leap year', () => {
    const date = new AwesomeDate('2/1/2016');
    assert.equal('2/29/2016', date.addDays(28));
  });

  it('handles non-leap year', () => {
    const date = new AwesomeDate('2/1/2015');
    assert.equal('3/1/2015', date.addDays(28));
  });
});

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

ํ…Œ์ŠคํŠธ์˜ ์ด๋ฆ„์€ ํ…Œ์ŠคํŠธ์˜ ์˜๋„๊ฐ€ ๋“œ๋Ÿฌ๋‚˜์•ผ ํ•ฉ๋‹ˆ๋‹ค

ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•  ๋•Œ, ํ…Œ์ŠคํŠธ์˜ ์ด๋ฆ„์€ ์–ด๋–ค ๊ฒƒ์ด ์ž˜๋ชป๋˜์—ˆ๋Š”์ง€ ๋ณผ ์ˆ˜ ์žˆ๋Š” ์ฒซ ๋ฒˆ์งธ ํ‘œ์‹œ์ž…๋‹ˆ๋‹ค.

Bad:

describe('Calendar', () => {
  it('2/29/2020', () => {
    // ...
  });

  it('throws', () => {
    // ...
  });
});

Good:

describe('Calendar', () => {
  it('should handle leap year', () => {
    // ...
  });

  it('should throw when format is invalid', () => {
    // ...
  });
});

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

๋™์‹œ์„ฑ

ํ”„๋กœ๋ฏธ์Šค vs ์ฝœ๋ฐฑ

์ฝœ๋ฐฑ์€ ๋ช…๋ฃŒํ•˜์ง€ ์•Š๊ณ , ์ง€๋‚˜์นœ ์–‘์˜ ์ค‘์ฒฉ๋œ ์ฝœ๋ฐฑ ์ง€์˜ฅ์„ ์œ ๋ฐœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
์ฝœ๋ฐฑ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” ๊ธฐ์กด์˜ ํ•จ์ˆ˜๋ฅผ ํ”„๋กœ๋ฏธ์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜๋กœ ๋ณ€ํ™˜์‹œ์ผœ์ฃผ๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. (Node.js๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด util.promisify๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”. ์ผ๋ฐ˜์ ์ธ ๋ชฉ์ ์ด๋ผ๋ฉด pify, es6-promisify๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.)

Bad:

import { get } from 'request';
import { writeFile } from 'fs';

function downloadPage(url: string, saveTo: string, callback: (error: Error, content?: string) => void) {
  get(url, (error, response) => {
    if (error) {
      callback(error);
    } else {
      writeFile(saveTo, response.body, (error) => {
        if (error) {
          callback(error);
        } else {
          callback(null, response.body);
        }
      });
    }
  });
}

downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html', (error, content) => {
  if (error) {
    console.error(error);
  } else {
    console.log(content);
  }
});

Good:

import { get } from 'request';
import { writeFile } from 'fs';
import { promisify } from 'util';

const write = promisify(writeFile);

function downloadPage(url: string, saveTo: string): Promise<string> {
  return get(url)
    .then(response => write(saveTo, response));
}

downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html')
  .then(content => console.log(content))
  .catch(error => console.error(error));  

ํ”„๋กœ๋ฏธ์Šค๋Š” ์ฝ”๋“œ๋ฅผ ๋”์šฑ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ํ•ด์ฃผ๋Š” ๋ช‡๋ช‡์˜ ํ—ฌํผ ๋ฉ”์†Œ๋“œ๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค:

ํŒจํ„ด ์„ค๋ช…
Promise.resolve(value) ํ•ด๊ฒฐ(resolve)๋œ ํ”„๋กœ๋ฏธ์Šค๋กœ ๊ฐ’์„ ๋ณ€ํ™˜ํ•จ.
Promise.reject(error) ๊ฑฐ๋ถ€(reject)๋œ ํ”„๋กœ๋ฏธ์Šค๋กœ ์—๋Ÿฌ๋ฅผ ๋ณ€ํ™˜ํ•จ.
Promise.all(promises) ์ „๋‹ฌ๋œ ๋ชจ๋“  ํ”„๋กœ๋ฏธ์Šค๊ฐ€ ์ดํ–‰ํ•œ ๊ฐ’์˜ ๋ฐฐ์—ด์„ ์ดํ–‰ํ•˜๋Š” ์ƒˆ ํ”„๋กœ๋ฏธ์Šค ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฑฐ๋‚˜ ๊ฑฐ๋ถ€๋œ ์ฒซ ๋ฒˆ์งธ ํ”„๋กœ๋ฏธ์Šค์˜ ์ด์œ ๋กœ ๊ฑฐ๋ถ€ํ•จ.
Promise.race(promises) ์ „๋‹ฌ๋œ ํ”„๋กœ๋ฏธ์Šค์˜ ๋ฐฐ์—ด์—์„œ ๊ฐ€์žฅ ๋จผ์ € ์™„๋ฃŒ๋œ ๊ฒฐ๊ณผ/์—๋Ÿฌ๋กœ ์ดํ–‰/๊ฑฐ๋ถ€๋œ ์ƒˆ ํ”„๋กœ๋ฏธ์Šค ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•จ.

Promise.all๋Š” ๋ณ‘๋ ฌ์ ์œผ๋กœ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ํ•„์š”๊ฐ€ ์žˆ์„ ๋•Œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค. Promise.race๋Š” ํ”„๋กœ๋ฏธ์Šค๋ฅผ ์œ„ํ•œ ํƒ€์ž„์•„์›ƒ๊ณผ ๊ฐ™์€ ๊ฒƒ์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์„ ์‰ฝ๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค๋‹ˆ๋‹ค.

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

ํ”„๋กœ๋ฏธ์Šค๋ณด๋‹ค async/await๊ฐ€ ๋” ๋ช…๋ฃŒํ•ฉ๋‹ˆ๋‹ค

async/await ๊ตฌ๋ฌธ์„ ์‚ฌ์šฉํ•˜๋ฉด ์—ฐ๊ฒฐ๋œ ํ”„๋กœ๋ฏธ์Šค ๊ตฌ๋ฌธ๋ณด๋‹ค ํ›จ์”ฌ ๋” ๋ช…๋ฃŒํ•˜๊ณ  ์ดํ•ดํ•˜๊ธฐ ์‰ฌ์šด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. async ํ‚ค์›Œ๋“œ๊ฐ€ ์•ž์— ๋ถ™์—ฌ์ง„ ํ•จ์ˆ˜๋Š” await ํ‚ค์›Œ๋“œ์—์„œ ์ฝ”๋“œ์˜ ์‹คํ–‰์„ ๋ฉˆ์ถ˜๋‹ค๋Š” ๊ฒƒ์„ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋Ÿฐํƒ€์ž„์—๊ฒŒ ์•Œ๋ ค์ค๋‹ˆ๋‹ค.

Bad:

import { get } from 'request';
import { writeFile } from 'fs';
import { promisify } from 'util';

const write = util.promisify(writeFile);

function downloadPage(url: string, saveTo: string): Promise<string> {
  return get(url).then(response => write(saveTo, response));
}

downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html')
  .then(content => console.log(content))
  .catch(error => console.error(error));  

Good:

import { get } from 'request';
import { writeFile } from 'fs';
import { promisify } from 'util';

const write = promisify(writeFile);

async function downloadPage(url: string, saveTo: string): Promise<string> {
  const response = await get(url);
  await write(saveTo, response);
  return response;
}

// somewhere in an async function
try {
  const content = await downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html');
  console.log(content);
} catch (error) {
  console.error(error);
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

์—๋Ÿฌ ์ฒ˜๋ฆฌ

์—๋Ÿฌ๋ฅผ ๋˜์ง€๋Š” ๊ฒƒ์€ ์ข‹์€ ๊ฒƒ์ž…๋‹ˆ๋‹ค! ์—๋Ÿฌ๋ฅผ ๋˜์ง„๋‹ค๋Š” ๊ฒƒ์€ ๋Ÿฐํƒ€์ž„์ด ๋‹น์‹ ์˜ ํ”„๋กœ๊ทธ๋žจ์—์„œ ๋ญ”๊ฐ€ ์ž˜๋ชป๋˜์—ˆ์„ ๋•Œ ์‹๋ณ„ํ•˜๊ณ  ํ˜„์žฌ ์Šคํƒ์—์„œ ํ•จ์ˆ˜ ์‹คํ–‰์„ ๋ฉˆ์ถ”๊ณ , (๋…ธ๋“œ์—์„œ) ํ”„๋กœ์„ธ์Šค๋ฅผ ์ข…๋ฃŒํ•˜๋ฉฐ, ์Šคํƒ ํŠธ๋ ˆ์ด์Šค๋ฅผ ์ฝ˜์†”์— ๋ณด์—ฌ์คŒ์œผ๋กœ์จ ๋‹น์‹ ์—๊ฒŒ ํ•ด๋‹น ์—๋Ÿฌ๋ฅผ ์•Œ๋ ค์ฃผ๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

throw ๋˜๋Š” reject ๊ตฌ๋ฌธ์—์„œ ํ•ญ์ƒ Error ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜์„ธ์š”

ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋Š” ์–ด๋–ค ๊ฐ์ฒด๋“ ์ง€ ์—๋Ÿฌ๋ฅผ throw ํ•˜๋Š” ๊ฒƒ์„ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ, ํ”„๋กœ๋ฏธ์Šค๋Š” ์–ด๋–ค ๊ฐ์ฒด๋ผ๋„ ๊ฑฐ๋ถ€๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
Error ํƒ€์ž…์—๋Š” throw ๊ตฌ๋ฌธ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋ฐ”๋žŒ์งํ•ฉ๋‹ˆ๋‹ค. ๋‹น์‹ ์˜ ์—๋Ÿฌ๊ฐ€ ์ƒ์œ„ ์ฝ”๋“œ์˜ catch ๊ตฌ๋ฌธ์—์„œ ์žกํž ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๋ฌธ์ž์—ด ๋ฉ”์‹œ์ง€๊ฐ€ ์žกํžˆ๋Š” ๊ฒƒ์€ ๋งค์šฐ ํ˜ผ๋ž€์Šค๋Ÿฌ์šฐ๋ฉฐ ์ด๋Š” ๋””๋ฒ„๊น…์„ ๋” ๊ณ ํ†ต์Šค๋Ÿฝ๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
์ด์™€ ๊ฐ™์€ ์ด์œ ๋กœ ๋‹น์‹ ์€ Error ํƒ€์ž…์œผ๋กœ ํ”„๋กœ๋ฏธ์Šค๋ฅผ ๊ฑฐ๋ถ€ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

Bad:

function calculateTotal(items: Item[]): number {
  throw 'Not implemented.';
}

function get(): Promise<Item[]> {
  return Promise.reject('Not implemented.');
}

Good:

function calculateTotal(items: Item[]): number {
  throw new Error('Not implemented.');
}

function get(): Promise<Item[]> {
  return Promise.reject(new Error('Not implemented.'));
}

// ๋˜๋Š” ์•„๋ž˜์™€ ๋™์ผํ•ฉ๋‹ˆ๋‹ค:

async function get(): Promise<Item[]> {
  throw new Error('Not implemented.');
}

Error ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜๋Š” ์žฅ์ ์€ try/catch/finally ๊ตฌ๋ฌธ์— ์˜ํ•ด ์ง€์›๋˜๊ณ  ์•”์‹œ์ ์œผ๋กœ ๋ชจ๋“  ์—๋Ÿฌ๊ฐ€ ๋””๋ฒ„๊น…์— ๋งค์šฐ ๊ฐ•๋ ฅํ•œ stack ์†์„ฑ์„ ๊ฐ€์ง€๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
๋˜ ํ•˜๋‚˜์˜ ๋Œ€์•ˆ์€ ์žˆ์Šต๋‹ˆ๋‹ค. throw ๊ตฌ๋ฌธ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๋Œ€์‹ , ํ•ญ์ƒ ์‚ฌ์šฉ์ž ์ •์˜ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋Š” ์ด๊ฒƒ์„ ํ›จ์”ฌ ๋” ์‰ฝ๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์•„๋ž˜์˜ ์˜ˆ์ œ๋ฅผ ํ™•์ธํ•˜์„ธ์š”:

type Result<R> = { isError: false, value: R };
type Failure<E> = { isError: true, error: E };
type Failable<R, E> = Result<R> | Failure<E>;

function calculateTotal(items: Item[]): Failable<number, 'empty'> {
  if (items.length === 0) {
    return { isError: true, error: 'empty' };
  }

  // ...
  return { isError: false, value: 42 };
}

์ด ์•„์ด๋””์–ด์˜ ์ž์„ธํ•œ ์„ค๋ช…์€ ์›๋ฌธ์„ ์ฐธ๊ณ ํ•˜์„ธ์š”.

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

catch ์ ˆ์—์„œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ถ€๋ถ„์„ ๋น„์›Œ๋‘์ง€ ๋งˆ์„ธ์š”

catch ์ ˆ์—์„œ ๋‹จ์ง€ ์—๋Ÿฌ๋ฅผ ๋ฐ›๋Š” ๊ฒƒ๋งŒ์œผ๋กœ๋Š” ํ•ด๋‹น ์—๋Ÿฌ์— ๋Œ€์‘ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ, ์ฝ˜์†”์— ์—๋Ÿฌ๋ฅผ ๊ธฐ๋กํ•˜๋Š” ๊ฒƒ(console.log)์€ ์ฝ˜์†”์— ์ถœ๋ ฅ๋œ ๋งŽ์€ ๊ฒƒ๋“ค ์‚ฌ์ด์—์„œ ๋ฐœ๊ฒฌ๋˜์ง€ ๋ชปํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ทธ๋‹ค์ง€ ์ข‹์€ ์„ ํƒ์€ ์•„๋‹™๋‹ˆ๋‹ค. ๋‹น์‹ ์ด ์–ด๋–ค ์ฝ”๋“œ๋ฅผ try/catch๋กœ ๊ฐ์ŒŒ๋‹ค๋ฉด, ๊ทธ ์ฝ”๋“œ์—์„œ ์—๋Ÿฌ๊ฐ€ ์ผ์–ด๋‚  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ฆ‰ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ์— ๋Œ€ํ•œ ๊ณ„ํš์ด๋‚˜ ์žฅ์น˜๊ฐ€ ์žˆ์–ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

Bad:

try {
  functionThatMightThrow();
} catch (error) {
  console.log(error);
}

// ์•„๋ž˜ ์˜ˆ์ œ๋Š” ํ›จ์”ฌ ๋‚˜์ฉ๋‹ˆ๋‹ค.

try {
  functionThatMightThrow();
} catch (error) {
  // ์—๋Ÿฌ๋ฅผ ๋ฌด์‹œ
}

Good:

import { logger } from './logging'

try {
  functionThatMightThrow();
} catch (error) {
  logger.log(error);
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

์š”์ฒญ์ด ๊ฑฐ๋ถ€๋œ ํ”„๋กœ๋ฏธ์Šค ๊ฐ์ฒด๋ฅผ ๋ฌด์‹œํ•˜์ง€ ๋งˆ์„ธ์š”

์œ„์™€ ๊ฐ™์ด try/catch ์ ˆ์—์„œ ๋ฐ›์€ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ถ€๋ถ„์„ ๋น„์›Œ๋‘๋ฉด ์•ˆ๋ฉ๋‹ˆ๋‹ค.

Bad:

getUser()
  .then((user: User) => {
    return sendEmail(user.email, 'Welcome!');
  })
  .catch((error) => {
    console.log(error);
  });

Good:

import { logger } from './logging'

getUser()
  .then((user: User) => {
    return sendEmail(user.email, 'Welcome!');
  })
  .catch((error) => {
    logger.log(error);
  });

// ๋˜๋Š” async/await ๊ตฌ๋ฌธ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

try {
  const user = await getUser();
  await sendEmail(user.email, 'Welcome!');
} catch (error) {
  logger.log(error);
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

์„œ์‹

์„œ์‹์€ ์ฃผ๊ด€์ ์ž…๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์— ์žˆ๋Š” ๋งŽ์€ ๊ทœ์น™๋“ค๊ณผ ๊ฐ™์ด ๋‹น์‹ ์ด ๋”ฐ๋ฅด๊ธฐ ์–ด๋ ค์šด ๊ทœ์น™์€ ์—†์Šต๋‹ˆ๋‹ค. ์ค‘์š”ํ•œ ์ ์€ ์„œ์‹์— ๋Œ€ํ•ด์„œ ๋…ผ์Ÿํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์„œ์‹์„ ์ž๋™ํ™”ํ•˜๊ธฐ ์œ„ํ•œ ๋„๊ตฌ๋“ค์ด ๋งค์šฐ ๋งŽ์Šต๋‹ˆ๋‹ค. ๊ทธ ์ค‘ ํ•˜๋‚˜๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”! ์„œ์‹์— ๋Œ€ํ•ด ๋…ผ์Ÿํ•˜๋Š” ๊ฒƒ์€ ์—”์ง€๋‹ˆ์–ด์—๊ฒŒ ์‹œ๊ฐ„๊ณผ ๋ˆ ๋‚ญ๋น„์ผ ๋ฟ์ž…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์•ผํ•˜๋Š” ์ผ๋ฐ˜์ ์ธ ๊ทœ์น™์€ ์ผ๊ด€์ ์ธ ์„œ์‹ ๊ทœ์น™์„ ์ง€์ผœ์•ผํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

TSLint๋ผ๊ณ  ๋ถˆ๋ฆฌ๋Š” ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋ฅผ ์œ„ํ•œ ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ๊ทน์ ์œผ๋กœ ๊ฐœ์„ ์‹œํ‚ค๋„๋ก ๋„์™€์ฃผ๋Š” ์ •์  ๋ถ„์„ ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. ํ”„๋กœ์ ํŠธ์— ์ฐธ๊ณ ํ•  ์ˆ˜ ์žˆ๋Š” TSLint ์„ค์ •์„ ์‚ฌ์šฉํ•  ์ค€๋น„๊ฐ€ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค:

๋˜ํ•œ, ํ›Œ๋ฅญํ•œ ์ž๋ฃŒ์ธ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ์Šคํƒ€์ผ ๊ฐ€์ด๋“œ์™€ ์ฝ”๋”ฉ ์ปจ๋ฒค์…˜์„ ์ฐธ๊ณ ํ•ด์ฃผ์„ธ์š”.

์—ญ์ž์ฃผ: TSLint๋Š” deprecated๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Roadmap: TSLint -> ESLint ์ด์Šˆ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.

์ผ๊ด€์ ์œผ๋กœ ๋Œ€์†Œ๋ฌธ์ž๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”

๋Œ€์†Œ๋ฌธ์ž๋ฅผ ๊ตฌ๋ถ„ํ•˜์—ฌ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์€ ๋‹น์‹ ์—๊ฒŒ ๋ณ€์ˆ˜, ํ•จ์ˆ˜ ๋“ฑ์— ๋Œ€ํ•ด์„œ ๋งŽ์€ ๊ฒƒ์„ ์•Œ๋ ค์ค๋‹ˆ๋‹ค. ์ด ๊ทœ์น™์€ ์ฃผ๊ด€์ ์ด์–ด์„œ, ๋‹น์‹ ์˜ ํŒ€์ด ์›ํ•˜๋Š” ๊ฒƒ์„ ์„ ํƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ค‘์š”ํ•œ ์ ์€ ์–ด๋–ค ๊ฑธ ์„ ํƒํ•˜์˜€๋“ ์ง€ ๊ฐ„์— ์ผ๊ด€์ ์ด์–ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

Bad:

const DAYS_IN_WEEK = 7;
const daysInMonth = 30;

const songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
const Artists = ['ACDC', 'Led Zeppelin', 'The Beatles'];

function eraseDatabase() {}
function restore_database() {}

type animal = { /* ... */ }
type Container = { /* ... */ }

Good:

const DAYS_IN_WEEK = 7;
const DAYS_IN_MONTH = 30;

const SONGS = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
const ARTISTS = ['ACDC', 'Led Zeppelin', 'The Beatles'];

function eraseDatabase() {}
function restoreDatabase() {}

type Animal = { /* ... */ }
type Container = { /* ... */ }

ํด๋ž˜์Šค, ์ธํ„ฐํŽ˜์ด์Šค, ํƒ€์ž… ๊ทธ๋ฆฌ๊ณ  ๋„ค์ž„์ŠคํŽ˜์ด์Šค ์ด๋ฆ„์—๋Š” PascalCase๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.
๋ณ€์ˆ˜, ํ•จ์ˆ˜ ๊ทธ๋ฆฌ๊ณ  ํด๋ž˜์Šค ๋ฉค๋ฒ„ ์ด๋ฆ„์—๋Š” camelCase๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

ํ•จ์ˆ˜ ํ˜ธ์ถœ์ž์™€ ํ”ผํ˜ธ์ถœ์ž๋ฅผ ๊ฐ€๊น๊ฒŒ ์œ„์น˜์‹œํ‚ค์„ธ์š”

ํ•จ์ˆ˜๊ฐ€ ๋‹ค๋ฅธ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ, ์ฝ”๋“œ์—์„œ ์ด ํ•จ์ˆ˜๋“ค์„ ์ˆ˜์ง์ ์œผ๋กœ ๊ฐ€๊น๊ฒŒ ์œ ์ง€ํ•˜๋„๋ก ํ•˜์„ธ์š”. ์ด์ƒ์ ์œผ๋กœ๋Š”, ํ˜ธ์ถœํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœ์„ ๋‹นํ•˜๋Š” ํ•จ์ˆ˜ ๋ฐ”๋กœ ์œ„์— ์œ„์น˜์‹œํ‚ค๋Š”๊ฒŒ ์ข‹์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ์‹ ๋ฌธ์ฒ˜๋Ÿผ ์ฝ”๋“œ๋ฅผ ์œ„์—์„œ ์•„๋ž˜๋กœ ์ฝ๋Š” ๊ฒฝํ–ฅ์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ์—๋„ ์ด๋Ÿฐ ๋ฐฉ์‹์œผ๋กœ ์ฝ๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Bad:

class PerformanceReview {
  constructor(private readonly employee: Employee) {
  }

  private lookupPeers() {
    return db.lookup(this.employee.id, 'peers');
  }

  private lookupManager() {
    return db.lookup(this.employee, 'manager');
  }

  private getPeerReviews() {
    const peers = this.lookupPeers();
    // ...
  }

  review() {
    this.getPeerReviews();
    this.getManagerReview();
    this.getSelfReview();

    // ...
  }

  private getManagerReview() {
    const manager = this.lookupManager();
  }

  private getSelfReview() {
    // ...
  }
}

const review = new PerformanceReview(employee);
review.review();

Good:

class PerformanceReview {
  constructor(private readonly employee: Employee) {
  }

  review() {
    this.getPeerReviews();
    this.getManagerReview();
    this.getSelfReview();

    // ...
  }

  private getPeerReviews() {
    const peers = this.lookupPeers();
    // ...
  }

  private lookupPeers() {
    return db.lookup(this.employee.id, 'peers');
  }

  private getManagerReview() {
    const manager = this.lookupManager();
  }

  private lookupManager() {
    return db.lookup(this.employee, 'manager');
  }

  private getSelfReview() {
    // ...
  }
}

const review = new PerformanceReview(employee);
review.review();

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

import ๊ตฌ๋ฌธ์„ ํŠน์ • ์ˆœ์„œ๋Œ€๋กœ ์ •๋ฆฌํ•˜์„ธ์š”

import ๊ตฌ๋ฌธ์„ ์ฝ๊ธฐ ์‰ฝ๊ณ  ๋ช…๋ฃŒํ•˜๊ฒŒ ํ•˜๋ฉด ๋‹น์‹ ์€ ํ˜„์žฌ ์ฝ”๋“œ์˜ ์˜์กด์„ฑ์„ ๋น ๋ฅด๊ฒŒ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ๊ณผ ๊ฐ™์€ import ๊ตฌ๋ฌธ ์ •๋ฆฌ๋ฅผ ์œ„ํ•œ ์ข‹์€ ๋ฐฉ๋ฒ•๋“ค์„ ์ ์šฉํ•ด๋ณด์„ธ์š”:

Bad:

import { TypeDefinition } from '../types/typeDefinition';
import { AttributeTypes } from '../model/attribute';
import { ApiCredentials, Adapters } from './common/api/authorization';
import fs from 'fs';
import { ConfigPlugin } from './plugins/config/configPlugin';
import { BindingScopeEnum, Container } from 'inversify';
import 'reflect-metadata';

Good:

import 'reflect-metadata';

import fs from 'fs';
import { BindingScopeEnum, Container } from 'inversify';

import { AttributeTypes } from '../model/attribute';
import { TypeDefinition } from '../types/typeDefinition';

import { ApiCredentials, Adapters } from './common/api/authorization';
import { ConfigPlugin } from './plugins/config/configPlugin';

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ์•จ๋ฆฌ์–ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”

tsconfig.json์˜ compilerOptions ์„น์…˜ ์•ˆ์—์„œ paths์™€ baseUrl ์†์„ฑ์„ ์ •์˜ํ•ด ๋” ๋ณด๊ธฐ ์ข‹์€ import ๊ตฌ๋ฌธ์„ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”.

์ด ๋ฐฉ๋ฒ•์€ import ๊ตฌ๋ฌธ์„ ์‚ฌ์šฉํ•  ๋•Œ ๊ธด ์ƒ๋Œ€๊ฒฝ๋กœ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์„ ํ”ผํ•˜๊ฒŒ ๋„์™€์ค„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

Bad:

import { UserService } from '../../../services/UserService';

Good:

import { UserService } from '@services/UserService';
// tsconfig.json
...
  "compilerOptions": {
    ...
    "baseUrl": "src",
    "paths": {
      "@services": ["services/*"]
    }
    ...
  }
...

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

์ฃผ์„

์ฃผ์„์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ์ฃผ์„ ์—†์ด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ์‹คํŒจํ–ˆ๋‹ค๋Š” ํ‘œ์‹œ์ž…๋‹ˆ๋‹ค. ์ฝ”๋“œ๋Š” ๋‹จ์ผ ์ง„์‹ค ๊ณต๊ธ‰์›(Single source of truth)์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋‚˜์œ ์ฝ”๋“œ์— ์ฃผ์„๋“ค ๋‹ฌ์ง€ ๋งˆ๋ผ. ์ƒˆ๋กœ ์งœ๋ผ.
โ€” Brian W. Kernighan and P. J. Plaugher

์ฃผ์„ ๋Œ€์‹ ์— ์ž์ฒด์ ์œผ๋กœ ์„ค๋ช… ๊ฐ€๋Šฅํ•œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š”

์ฃผ์„์€ ๋ณ€๋ช…์ผ ๋ฟ, ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ข‹์€ ์ฝ”๋“œ๋Š” ๋Œ€๋ถ€๋ถ„ ๊ทธ ์กด์žฌ ์ž์ฒด๋กœ ๋ฌธ์„œํ™”๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

Bad:

// subscription์ด ํ™œ์„ฑํ™” ์ƒํƒœ์ธ์ง€ ์ฒดํฌํ•ฉ๋‹ˆ๋‹ค.
if (subscription.endDate > Date.now) {  }

Good:

const isSubscriptionActive = subscription.endDate > Date.now;
if (isSubscriptionActive) { /* ... */ }

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

๋‹น์‹ ์˜ ์ฝ”๋“œ๋ฅผ ์ฃผ์„ ์ฒ˜๋ฆฌํ•˜์ง€ ๋งˆ์„ธ์š”

๋ฒ„์ „ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์ด ์กด์žฌํ•˜๋Š” ์ด์œ ์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ฝ”๋“œ๋Š” ๊ธฐ๋ก์— ๋‚จ๊ธฐ์„ธ์š”.

Bad:

type User = {
  name: string;
  email: string;
  // age: number;
  // jobPosition: string;
}

Good:

type User = {
  name: string;
  email: string;
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

์ผ๊ธฐ ๊ฐ™์€ ์ฃผ์„์„ ๋‹ฌ์ง€ ๋งˆ์„ธ์š”

๋ฒ„์ „ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์„ ์‚ฌ์šฉํ•˜์„ธ์š”! ์ฃฝ์€ ์ฝ”๋“œ, ์ฃผ์„ ์ฒ˜๋ฆฌ๋œ ์ฝ”๋“œ, ํŠนํžˆ ์ผ๊ธฐ ๊ฐ™์€ ์ฃผ์„์€ ํ•„์š” ์—†์Šต๋‹ˆ๋‹ค. ๋Œ€์‹ ์— ๊ธฐ๋ก์„ ๋ณด๊ธฐ ์œ„ํ•ด git log ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”!

Bad:

/**
 * 2016-12-20: ์ดํ•ดํ•˜์ง€ ๋ชปํ•ด์„œ ๋ชจ๋‚˜๋“œ๋ฅผ ์ œ๊ฑฐํ•จ (RM)
 * 2016-10-01: ํŠน๋ณ„ํ•œ ๋ชจ๋‚˜๋“œ๋ฅผ ์‚ฌ์šฉํ•ด ๊ฐœ์„ ํ•จ (JP)
 * 2016-02-03: ํƒ€์ž… ์ฒดํ‚น ์ถ”๊ฐ€ํ•จ (LI)
 * 2015-03-14: combine ํ•จ์ˆ˜๋ฅผ ๊ตฌํ˜„ํ•จ (JR)
 */
function combine(a: number, b: number): number {
  return a + b;
}

Good:

function combine(a: number, b: number): number {
  return a + b;
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

์ฝ”๋“œ์˜ ์œ„์น˜๋ฅผ ์„ค๋ช…ํ•˜๋Š” ์ฃผ์„์„ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ์„ธ์š”

์ด๊ฑด ๋ณดํ†ต ์ฝ”๋“œ๋ฅผ ์–ด์ง€๋Ÿฝํžˆ๊ธฐ๋งŒ ํ•ฉ๋‹ˆ๋‹ค. ํ•จ์ˆ˜์™€ ๋ณ€์ˆ˜ ์ด๋ฆ„์„ ์ ์ ˆํ•œ ๋“ค์—ฌ์“ฐ๊ธฐ์™€ ์„œ์‹์œผ๋กœ ๋‹น์‹ ์˜ ์ฝ”๋“œ์— ์‹œ๊ฐ์ ์ธ ๊ตฌ์กฐ๊ฐ€ ๋ณด์ด๋„๋ก ํ•˜์„ธ์š”.
๋Œ€๋ถ€๋ถ„์˜ IDE(ํ†ตํ•ฉ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ)์—์„œ๋Š” ์ฝ”๋“œ ๋ธ”๋ก์„ ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. (Visual Studio Code์˜ folding regions๋ฅผ ํ™•์ธํ•ด๋ณด์„ธ์š”).

Bad:

////////////////////////////////////////////////////////////////////////////////
// Client ํด๋ž˜์Šค
////////////////////////////////////////////////////////////////////////////////
class Client {
  id: number;
  name: string;
  address: Address;
  contact: Contact;

  ////////////////////////////////////////////////////////////////////////////////
  // public ๋ฉ”์†Œ๋“œ
  ////////////////////////////////////////////////////////////////////////////////
  public describe(): string {
    // ...
  }

  ////////////////////////////////////////////////////////////////////////////////
  // private ๋ฉ”์†Œ๋“œ
  ////////////////////////////////////////////////////////////////////////////////
  private describeAddress(): string {
    // ...
  }

  private describeContact(): string {
    // ...
  }
};

Good:

class Client {
  id: number;
  name: string;
  address: Address;
  contact: Contact;

  public describe(): string {
    // ...
  }

  private describeAddress(): string {
    // ...
  }

  private describeContact(): string {
    // ...
  }
};

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

TODO ์ฃผ์„

์ถ”ํ›„์— ๊ฐœ์„ ์„ ์œ„ํ•ด ์ฝ”๋“œ์— ๋ฉ”๋ชจ๋ฅผ ๋‚จ๊ฒจ์•ผํ•  ๋•Œ, // TODO ์ฃผ์„์„ ์‚ฌ์šฉํ•˜์„ธ์š”. ๋Œ€๋ถ€๋ถ„์˜ IDE๋Š” ์ด๋Ÿฐ ์ข…๋ฅ˜์˜ ์ฃผ์„์„ ํŠน๋ณ„ํ•˜๊ฒŒ ์ง€์›ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ•ด์•ผํ•  ์ผ ๋ชฉ๋ก์„ ๋น ๋ฅด๊ฒŒ ๊ฒ€ํ† ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ TODO ์ฃผ์„์ด ๋‚˜์œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ด์œ ๋Š” ์•„๋‹ˆ๋ผ๋Š” ๊ฒƒ์„ ๋ช…์‹ฌํ•˜์„ธ์š”.

Bad:

function getActiveSubscriptions(): Promise<Subscription[]> {
  // ensure `dueDate` is indexed.
  return db.subscriptions.find({ dueDate: { $lte: new Date() } });
}

Good:

function getActiveSubscriptions(): Promise<Subscription[]> {
  // TODO: ensure `dueDate` is indexed.
  return db.subscriptions.find({ dueDate: { $lte: new Date() } });
}

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

๋ฒˆ์—ญ

์ด ๊ธ€์„ ๋‹ค๋ฅธ ์–ธ์–ด๋กœ๋„ ์ฝ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

๋ฒˆ์—ญ์ด ์™„๋ฃŒ๋˜๋ฉด ์ฐธ๊ณ ๋ฌธํ—Œ์— ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๋‚ด์šฉ๊ณผ ์ง„ํ–‰์ƒํ™ฉ์„ ๋ณด๊ณ ์‹ถ๋‹ค๋ฉด ์ด ๋…ผ์˜๋ฅผ ํ™•์ธํ•˜์„ธ์š”. ๋‹น์‹ ์€ ๋‹น์‹ ์˜ ์–ธ์–ด์— ์ด ๊ธ€์„ ๋ฒˆ์—ญํ•จ์œผ๋กœ์จ ํด๋ฆฐ ์ฝ”๋“œ ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์ค‘์š”ํ•œ ๊ธฐ์—ฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โฌ† ๋งจ ์œ„๋กœ ์ด๋™

๋ฒˆ์—ญ์— ๋„์›€์„ ์ฃผ์‹  ๋ถ„๋“ค

๋ฆฌ๋ทฐ

์ฐธ๊ณ ์ž๋ฃŒ