똑같은 삽질은 2번 하지 말자

Effective Typescript vol.4 (타입설계 Item 28 ~ 37) 본문

카테고리 없음

Effective Typescript vol.4 (타입설계 Item 28 ~ 37)

곽빵 2022. 9. 19. 17:57

개요

이펙티브 타입스크립트라는 책을 읽고 장별(각각의 장안에 아이템별)로 학습한 내용 정리하기

 

이 책의 목표 

독자에게 타입스크립트나 자바스크립트를 가르치는 것이 아니라, 초급자나 중급자가 전문가로 발전할 수 있게 돕는 것

프로그래밍에서 어떤 방법을 가르치는게 아니라 어떤 방법을 사용할 때  그래야 하는지를 알려준다.

 

이 장을 통해 알수 있는 것

다른 장들과는 조금 다르게 타입 자체의 설계에 관점을 두고 있다. 즉, 어떤 데이터를 코드로 구현하려고 할 때 어떤식으로 데이터의 구조(타입)을 설계해야 하는지 그리고 타입 설계를 잘하면 코드의 로직도 역시 쉽게 이해할 수 있게 된다는걸 알 수 있을것이다.

 

Item28. 유효한 상태만 표현하는 타입을 지향하기

조금 추상적인 내용일 수 있는데 요약본을 보면

  • 유효한 상태와 무효한 상태를 둘 다 표현하는 타입은 혼란을 초래하기 쉽고 오류를 유발하게 된다.
  • 유효한 상태만 표현하는 타입을 지향해야 한다. 코드가 길어지거나 표현하기 어렵지만, 결국은 시간을 절약하고 버그 발생으로 인해 생기는 고통을 줄일 수 있을것이다.

여기서 유효와 무효에 대해서 조금 보태서 설명하자면 유효한 상태는 존재할 수 있는 상태이고 무효한 상태는 존재하면 안되는 상태를 의미한다. 무효한 상태를 하나 예를 들면 나는 달리고 있는데 앉아있다와 같이 동적인 상태와 정적인 상태가 동시에 이루어 질 수 없는것 처럼 타입을 설계할 때 말이 안되는 타입을 설계하지 않도록 주의하자. 그리고 그런 타입을 설계하기 위해서는 어떻게 하나? 에 대해서 이후에 나오는 아이템들에서 다루어질 것 같다.

 

Item29. 사용할 때는 너그럽게, 생성할 때는 엄격하게

  • 보통 매개변수 타입은 반환 타입에 비해 범위가 넓은 경향이 있다. 선택적 속성과 유니온 타입은 반환 타입보다 매개변수 타입에 더 일반적인 느낌이 있다.
  • 매개변수와 반환 타입의 재사용을 위해서 기본 형태(반환 타입)와 느슨한 형태(매개변수 타입)를 도입하는 것이 좋다고 한다.

예제의 코드를 보면 매개변수와 반환 타입의 재사용이라는 부분은 A라는 함수의 반환값이 B라는 함수의 매개변수로써 바로 삽입될 때 A의 반환값의 타입은 엄격하게 하고 B함수의 매개변수는 유연하게 하라는 말이다.

 

밑의 setCamera가 B함수, viewportForBounds가 A함수가 된다.

Item30. 문서에 타입 정보를 쓰지 않기

  • 주석과 변수명에 타입 정보를 적는 것은 피해야 한다. 타입 선언이 중복 되는 것으로 끝나면 다행이지만, 최악의 경우는 타입 정보에 모순이 발생하게 된다.
  • 타입이 명확하지 않은 경우는 변수명에 단위 정보를 포함하는 것을 고려해보는 것도 좋다.(timeMS, temperatureC 같이?)

타입스크립트의 타입 구문 시스템은 간결하고, 구체적이며, 쉽게 읽을 수 있도록 설계되어 있으므로 타입은 타입 구문만으로 이미 충분히 정보를 얻을 수 있다. 그러므로 다른 곳에다가 타입정보를 추가하지 말도록 하자.

 

Item31. 타입 주변에 null 값 배치하기

  • 한 값의 null 여부가 다른 값의 null 여부에 암시적으로 관련되도록 설계하는 건 피하자
  • API 작성 시에는 반환 타입을 큰 객체로 만들고 반환 타입 전체가 null이거나 null이 아니게 만들어야 한다. 사람과 타입 체커 모두에게 명료한 코드가 될 수 있다.
  • 클래스를 만들 때는 필요한 모든 값이 준비되었을 때 생성하여 Null이 존재 하지 않도록 하는 것이 좋다. (비동기를 통한 클래스를 만들때의 이야기)
  • strictNullChecks를 설정하면 코드에 많은 오류가 표시되겠지만, null 값과 관련된 문제점을 찾아낼 수 있기 때문에 반드시 필요

null 값을 배치한다는 말은 어떤 관련된 값들의 모임이 있고 그 모임에서 부분적으로 null이나 undefined가 될 수 있는 타입을 배치하면 경우의 수가 많아지기 때문에 그런식으로 암시적으로 관련되도록 설계하는건 피하고, 전체가 null이거나 아니면 전체가 null이 안되게끔 설정하면 더욱 명료하면서 보기 좋은 코드를 만들 수 있다.

 

Item32. 유니온의 인터페이스보다는 인터페이스의 유니온을 사용하기

  • 유니온 타입의 속성을 여러 개 가지는 인터페이스에서는 속성 간의 관계가 분명하지 않기 때문에 실수가 자주 발생하므로 주의해야 하한다.  -> 하나의 인터페이스에서 유니온을 가지면 무효한 상태가 될 가능성이 높지만, 유니온 인터페이스가 되면 하나 하나의 인터페이스가 되며 그것들은 하나의 객체를 표현하기에 아주 적절하며 유효한 상태를 표현할 수 있다.
  • 유니온의 인터페이스 보다 인터페이스의 유니온이 더 정확하고 타입스크립트가 이해하기도 좋다.
  • 타입스크립트가 제어 흐름을 분석할 수 있도록 타입에 태그를 넣는 것을 고려해야 한다. 태그된 유니온은 타입스크립트와 매우 잘 맞기 때문에 자주 볼 수 있는 패턴이다.

태그된 유니온의 예

여기서 type이 태그의 역할을 하며 런타임에 어떤 타입의 Layer가 사용되는지 쉽게 판단할 수 있다.

 

Item33. string 타입보다 더 구체적인 타입 사용하기

  • "문자열을 남발하여 선언된" 코드를 피하자. 모든 문자열을 할당할 수 있는 string 타입보다는 더 구체적인 타입(유니온이나)을 사용하는 것이 좋다.
  • 변수의 범위를 보다 정확하게 표현하고 싶으면 string 타입보다는 문자열 리터럴 타입의 유니온을 사용하면 된다. 타입 체크를 더 엄격히 할 수 있고 생산성을 향상시킬 수 있다.
  • 객체의 속성 이름을 함수 매개변수로 받을 때는 string보다 keyof T를 사용하는 것이 좋다.
  • 그리고 객체의 속성 이름을 keyof T보다는 extends key of T로 하는게 더욱 정확한 추론을 가능하게 한다.
interface Album {
  artist: string;
  title: string;
  releaseDate: Date;
  recordingType: "studio" | "live";
}

const albums: Album[] = [];

function pluck<T>(records: T[], key: keyof T) {
  return records.map((r) => r[key]);
}
function pluck2<T, K extends keyof T>(records: T[], key: K) {
  return records.map((r) => r[key]);
}

const t1 = pluck(albums, "releaseDate"); // const t1: (string | Date)[]
const t2 = pluck(albums, "artist"); // const t2: (string | Date)[]
const t3 = pluck(albums, "recordingType"); // const t3: (string | Date)[]

const t21 = pluck2(albums, "releaseDate"); // const t21: Date[]
const t22 = pluck2(albums, "artist"); // const t22: string[]
const t23 = pluck2(albums, "recordingType"); // const t23: ("studio" | "live")[]

keyof T 와 K extends keyof T 의 차이점은 반환 값이 틀려진다는 점에 있다.

function pluck<T>(records: T[], key: keyof T): T[keyof T][]
function pluck2<T, K extends keyof T>(records: T[], key: K): T[K][]

두번째 친구는 제너릭을 사용함으로써 들어온 키 값에 대한 record를 리턴해주지만, 첫번째 함수의 타입추론은 key가 그대로 들어감으로써 들어간 키값의 타입인 keyof T가 들어간다...!! 

https://stackoverflow.com/questions/53099089/difference-between-of-k-extends-keyof-t-vs-directly-using-keyof-t

 

Difference between of "K extends keyof T" vs. directly using "keyof T"?

Is there any difference between the following typescript definitions: function prop<T, K extends keyof T>(obj: T, key: K) { return obj[key]; } and function prop2<T>(obj: T, key: ...

stackoverflow.com

 

Item34. 부정확한 타입보다는 미완성 타입을 사용하기

  • 타입 안정성에서 *불쾌한 골짜기는 피해야 한다. 타입을 좀더 정확하고 정밀하게 만들려고 하면 오히려 그로 인해 타입이 더 부정확해지는 상황이 발생할 수 있다. 이런 부정확함을 바로잡는 방법을 쓰는 대신에 테스트 세트를 추가하여 놓친 부분이 없는지 확인하는게 더 현실적인 방법이 될 수도 있다.
  • 정확하게 타입을 모델링할 수 없다면, 부정확하게 모델링하지 말아햐 한다. 또한 *any나 *unknown를 구별해서 사용해야 한다.

*불쾌한 골짜기?

불쾌한 골짜기란 로봇 공학과 인공 지능에서 많이 쓰이는 용어로, 어설프게 인간과 비슷한 로봇에서 느끼는 불쾌함을 뜻한다. 위에서 언급한 불쾌한 골짜기는 피해야 한다의 의미는 타입선언에서 어설프게 완벽을 추구하려다가 오히려 역효과가 발생하는 것을 주의하자라는 말이다.

*any

모든 타입을 허용하지만, 타입 체크를 거의 안하는 것과 비슷하게 동작하므로 이후에 문제가 발생할 가능성이 매우 높다.

*unknown

모든 타입을 허용하는데 any 타입과는 다르게 unknown 타입이 씌워진 변수나 함수로 어떤 프로퍼티에 접근하거나 연산을 하는 경우 타입체커한테 경고를 받을 수 있다. (이 타입은 unknown인데 괜찮니? 라는 식으로)

체크를 안하면 에러
체크를 하면 에러가 안남

Item35. 데이터가 아닌, API와 명세를 보고 타입 만들기

  • 코드의 구석 구석까지 타입 안정성을 얻기 위해 API 또는 데이터 형식에 대한 타입 생성을 고려해야 한다.
  • 데이터에 드러나지 않는 예외적인 경우들이 문제가 될 수 있기 때문에 데이터보다는 명세로부터 코드를 생성하는 것이 좋습니다.

 

Item36. 해당 분야의 용어로 타입 이름 짓기

  • 가독성을 높이고, 추상화 수준을 올리기 위해서 해당 분야의 용어를 사용해야 한다.
  • 같은 의미에 다른 이름을 붙이면 안된다. 같은 단어를 두번 반복한다고 해서 뭔가 이상하게 보일 수 있는 경우 정말로 의미적으로 구분이 되어야 하는 경우에만 다른 용어를 사용해야 한다.

전문용어 사용 예시

/**
 * 밑의 코드의 문제점
 * name이 너무 일반적인 용어, 동물의 학명인지 일반적인 명칭인지 구분 불가능
 * endangered 속성이 멸종 위기를 표현하기 위해 boolean 타입을 사용한 것이 이상. 이미 멸종된 동물은 true?가 되는건가
 * habitat 속성은 너무 범위가 넓은 string 타입일 뿐만 아니라 서식지라는 뜻 자체도 불분명
 * 객체의 변수명이 leopard이지만, name의 속성의 값은 'Snow Leopard' 인데 객체의 이름과 속성의 name이 다른 의미를 갖고 있는지가 불분명
 */
interface Animal {
  name: string;
  endangered: boolean;
  habitat: string;
}

const leopard: Animal = {
  name: "Snow Leopard",
  endangered: false,
  habitat: "tundra",
};

/**
 * 밑의 코드의 개선점
 * name은 commonName, genus, species 등 더욱 구체적인 용어로 대체
 * endangered는 동물 보호 등급에 대한 IUCN의 표준 분류 체계인 ConservationStatus 타입의 status로 변경
 * habitat은 기후를 뜻하는 climates로 변경, 쾨펜 기후 분류(Koppen climate classification)을 사용
 */

interface Animal2 {
  commonName: string;
  genus: string;
  species: string;
  status: ConservationStatus;
  climates: KoppenClimate[];
}
type ConservationStatus = "EX" | "EW" | "CR" | "EN" | "VU" | "NT" | "LC";
type KoppenClimate = "Af" | "Am" | "As" | "Aw" | "BSh" | "BSk" | "BWh" | "BWk";

const snowLeopard: Animal2 = {
  commonName: "Snow Leopard",
  genus: "Panthera",
  species: "Uncia",
  status: "VU",
  climates: ["Af", "BSk"],
};

 

Item37. 공식 명칭에는 상표를 붙이기

  • 타입스크립트는 구조적 타이핑(덕 타이핑)을 사용하기 때문에, 값을 세밀하게 구분하지 못하는 경우가 있다. 값을 구분하기 위해 공식 명칭이 필요하다면 상표를 붙이는 것을 고려해야 한다.
  • 상표 기법은 타입 시스템에서 동작하지만 런타임에 상표를 검사하는 것과 동일한 효과를 얻을 수 있다.

is 키워드는 처음 사용해 보는데 이런식으로 타입시스템 내에서 판단하기 어려울 경우에 런타임에서 판단하는걸 타입시스템과 바인딩? 시킬 수 있다.

Comments