[Effective Typescript] item4~5 정리

김유진·2023년 3월 27일
0

Effective-TypeScript

목록 보기
2/28
post-thumbnail
post-custom-banner

item4: 구조적 타이핑에 익숙해지기

자바스크립트는 어떤 함수의 매개변수 값이 모두 제대로 주어진다면, 그 값이 어떻게 만들어졌는지 신경 쓰지 않고 사용한다. 이것을 바로 덕 타이핑 기반이라고 한다.
그래서 타입스크립트는 매개변수 값이 요구사항을 만족한다면, 자바스크립트의 타입을 신경 쓰지 않는 동작을 그대로 모델링한다.

예시를 통하여 이해하여 보자.

interface Vector2D {
  x: number;
  y: number;
}

이를 계산하는 함수는 아래와 같다.

function calculateLength(v: Vector2D){
  return Math.sqrt(v.x * v.x + v.y * v.y);
}

이 상황에서 Vector2D 함수를 수정한다면 어떻게 될까?

interface NamedVector {
  name: string;
  x: number;
  y: number;
}

NamedVectorxy가 존재하기 때문에, 위의 함수를 그대로 이해할 수 있다.
그 이유는 타입스크립트가 자바스크립트의 런타임 동작을 모델링하였기 때문인데, NamedVector의 구조가 Vector2D와 호환되기 때문이다.

이 상황을 바로 구조적 타이핑을 사용한다고 이해하면 된다.

하지만 이런 구조적 타이핑 때문에 문제가 발생하기도 한다.

interface Vector3D {
  x: number;
  y: number;
  z: number;
}

벡터의 길이를 1로 만들어주는 함수를 사용하여 보자.

function normalize(v: Vector3D) {
  const length = calculateLength(v);
  return {
    x: v.x / length,
    y: v.y / length,
    z: v.z / length,
  };
}

calculateLength 함수가 애초에 2차원 기반으로 만들어졌으니까, 당연히 원하는 결과를 얻을 수 없다. 그럼 왜 이 문제는 에러가 발생하지 않고 3D백터를 매개변수로 잡아내는 것일까?

구조적 타이핑의 관점

구조적 타이핑은 매개변수 값이 요구사항을 만족한다면, 자바스크립트의 타입을 신경 쓰지 않는 동작을 그대로 모델링한다고 했다. calculateLength입장에서는 x, y가 존재하기 때문에 Vector2D와 호환되는 것이다. 그래서 오류가 발생하지 않은 것이고, 타입 체커가 문제라고 짚지 않은 것이다.

그래서, 타입스크립트의 타입은 항상 열려있다. 매개변수의 속성이 매개변수에 타입에 선언된 속성만을 가질 것이라고 생각하면, 봉인된 타입이라고 생각하는 것인데, 이러한 생각 때문에 실수를 하게 되는 것이다.

function calculateLength1(v: Vector3D) {
  let length = 0;
  for (const axis of Object.keys(v)) {
    const coord = v[axis];
    	//'string'은 'Vector3D'의 인덱스로 사용할 수 없기에 엘리먼트는 임시적으로 '`any'타입입니다.
    length += Math.abs(coord);
  }
  return length;
}

우리는 Vector3D의 프로퍼티에 대한 속성을 number로 지정해뒀다. 그런대 왜 이런 요상한 오류가 뜨는 것일까?
그 이유는 바로 아래와 같이 작성하게 될 수 있기 때문이다.

const vec3D = {x: 3, y: 4, z: 1, address: '123 Broadway'};
calculateLengthL1(vec3D); //정상.. NaN을 반환한다.

함수는 오픈된 친구이기 때문에, axis의 타입이 string이 될수도 있다. 그래서 타입스크립트는 v[axis]의 타입을 Number라고 확정할 수 없는 것이다. 그래서 정확한 타입으로 객체를 루프 돌리는 코드는 어렵다.
이왕이면 아래와 같이 작성하는 것이 깔끔하며 예기치 못한 오류를 줄이는 방법이 되겠다.

function calculateLengthL1(v: Vector3D){
  return Math.abs(v.s) + Math.abs(v.y) + Math.abs(v.z);
}

테스팅에 편리한 구조적 타이핑

interface Author {
  first: string;
  last: string;
}
function getAuthors(database: PostgresDB): Author[] {
  const authorRows = database.runQuery('SELECT FIRST, LAST FROM AUTHORS');
  return authorRows.map(row => ({first: row[0], last: row[1]}));

이 상태에서 원래는 PostgresDB를 생성해야 한다. 하지만 아래와 같이 작성한다면 더욱 편리하게 테스팅을 진행할 수 있다.

interface DB {
  runQuery: (sql: string) => any[];
}
function getAuthors(database: DB): Author[] {
  const authorRows = database.runQuery('SELECT FIRST, LAST FROM AUTHORS');
  return authorRows.map(row => ({first: row[0], last: row[1]}));

이렇게 작성하게 되면 실제 구조적으로 유사한 postgresDBgetAuthors 함수를 사용할 수 있고, DB도 사용이 가능하다. 해당 인터페이스만 충족해도 괜찮기 때문이다.
추상화를 통하여 특정한 구현으로부터 분리할 수 있다는 점이 유용하게 사용할 수 있다.

item5: any 타입 지양하기

저번 프로젝트 때 any 남발했다가 스스로 죄책감 느끼고 후회하며 코드를 많이 고쳤기 때문에 그 기억을 되살리면서 이번 item을 읽었더니 더욱 재미있었다. ㅋ.ㅋ

타입스크립트의 타입 시스템은 점진적이고, 선택적이다.

이 말을 풀어 쓰자면 코드에 타입을 조금씩 추가할 수 있기 때문에 점진적이고, 언제든지 타입 체커를 해제할수 있기 때문에 선택적이다. 이 기능은 any 때문에 설명하게 된 것이라고 할 수 있다.
내가 처음에 any를 썼던 것도, 어떤 타입이 들어가는지 잘 모르겠어서, 그리고 귀찮아서 (..) 그랬었다.
부득이하게 any를 사용한다고 해도 어떤 위험성이 존재하는지 잘 알고 쓰는게 맞지 않겠는가..!!

any 타입에는 타입 안정성이 없다.

let age: number;
age = '12'

any를 사용하게 되면 해당 코드도 에러 없이 잘 수행된다.
직관적으로 age에는 number타입을 사용하는 것이 맞다. 그러나 이러한 상황에는 string을 대입해도 에러가 생기지 않아서 혼돈을 야기한다.

any는 함수 시그니처를 무시해 버린다.

함수를 작성할 때에는 시그니처를 명시하고 꼭 지켜야 한다.

  • 어떤 타입의 입력을 제공받을 것인지?
  • 어떤 타입의 출력을 반환할 것인지?

any를 사용하면 이 약속을 어기게 된다. 오류 없이 실행된다고 해도 다른 곳에서 문제를 일으킬 수 있으므로 주의 깊게 생각하고 사용하자.

any타입에는 언어 서비스가 적용되지 않는다

타입스크립트 언어는 자동완성 기능을 제공해주는데, any를 사용하면 아무 도움을 받지 못한다.
그리고 타입 포맷팅과 이름을 수정해주는 기능도 이용할 수 없다.

any타입은 코드 리팩토링 때 버그를 감춘다.

interface ComponentProps {
  onSelectItem: (item: any) => void;
}

콜백이 있는 컴포넌트일 때,

function renderSelector(props: ComponentProps) {/*...*/}
let selectedId: number = 0;
function handleSelectedItem(item: any){
  selectedId = item.id;
}
renderSelector({onSelectItem: handleSelectItem});

여기서, onSelectItem에 아이템 객체를 필요한 부분만 전달하는 , 즉 id만 전달하게끔 컴포넌트를 개선해보자.

interface ComopnentProps {
  onSelectItem: (id:number) => void;
}

이렇게 개선을 하였는데, handleSelectedItemany를 받아서 id를 전달받아도 문제가 없다고 나온다. 하지만, 타입 체커를 통과를 하기는 하지만, 런타임에는 오류가 발생하게 되는 상황이 발생할 수 있다.
만약, item을 any로 미리 설정해뒀다면, 타입 체커가 컴파일 시간에 사전에 미리 에러를 발견했을 것이다!

any는 타입 설계를 감춘다.

애플리케이션 상태 같은 객체를 정의하는 것은 매우 복잡하다.
상태 객체 안의 타입을 일일이 작성할 때 any를 사용하면 안된다.
특히, 객체를 정의할 때에는 상태 객체의 설계를 감춰버리게 되므로, 협업시에 매우 큰 어려움이 생기며 동료가 코드를 이해하고 설계를 이해하는 데 어렵게 만들어버린다.

any는 타입시스템의 신뢰를 떨어뜨린다.

사람은 실수를 하기 때문에 타입 체커가 실수를 잡아준다.
하지만 any를 사용하게 되면 타입 체커도 타입 오류를 잡지 못하여서 신뢰할 수 없는 상황이 되어버린다.
그렇기 때문에 any타입 사용을 지양하여 타입에 발견될 오류를 미리 잡아서 신뢰도를 높일 수 있다.

post-custom-banner

0개의 댓글