비즈니스에 관심이 많은 개발자입니다.

etc

이펙티브 타입스크립트 1장

타입스크립트와 자바스크립트의 관계

타입스크립트는 자바스크립트의 상위 집합이다. 즉 모든 자바스크립트 프로그램은 타입스크립트 프로그램이다. 하지만 타입스크립트의 경우 타입 선언 등 별도의 문법을 가지고 있기 때문에 유효한 자바스크립트 프로그램이라고는 할 수 없다. 자바스크립트가 타입스크립트 프로그램이기에 마이그레이션 하는데 큰 이점을 가진다.

타입 시스템

타입스크립트는 자바스크립트에 타입 시스템을 더한 것이다. 타입 시스템의 목표 중 하나는 런타임에 오류를 발생시킬 코드를 미리 찾아내는 것이다. '정적'타입 시스템이라는 것은 바로 이런 특징을 말하는 것이다. 하지만 타입 체커가 모든 오류를 찾아내지는 않으니 유의해야한다.

타입을 선언하지 않아도 타입스크립트는 오류는 발생시키지 않지만 의도와 다르게 동작하는 코드를 찾아내기도 한다. 하지만 정확하지 않기에 명시적으로 타입을 선언해야한다.

interface State {
  name: string
  capital: string
}

const states: State[] = [
  { name: "Alaska", capital: "Juneau" },
  { name: "Korea", capitol: "Seoul" },
]
//만일 State 타입을 명시하지 않았다면, 에러를 발생시키지 않지만 State 타입을 명시함으로써 capitol 속성 부분에 에러를 표시해준다. 이는 잠재적으로 생길 수 있는 문제를 해결해준다.

타입스크립트 타입 시스템은 자바스크립트의 런타임 동작을 모델링한다.

const x = 2 + "3"
const a = null + 7
const b = [] + 12
alert("hi", "ts")

변수 x의 경우 문자열 "23"이 되는 자바스크립트 런타임 동작으로 모델링된다. 하지만 변수 a,b, alert 함수의 경우에 자바스크립에서는 정상적으로 동작하지만 타입스크립에서는 타입 체커가 문제점을 표시한다.

이러한 불명확함이 타입스크립트를 사용하는데 의문이 들 수 있지만, 타입스크립트를 사용하면 오류가 적은 프로그램을 만들 수 있다. null+7과 같은 코드가 당연하다고 생각된다면 타입스크립트를 안쓰는게 낫다.

타입스크립트 설정 이해하기

function sum(a, b) {
  return a + b
}

다음 함수가 타입스크립트에서 오류를 발생시키는지, 아닌지 알 수 있을까? 해당 프로그램의 타입스크립트 설정을 보지 않는 이상 알 수 없다. 설정은 커맨드라인에서도 사용할 수 있지만, 설정 파일을 만들어서 동료 개발자나 다른 도구들이 알 수 있도록 해야한다.

주요 설정

noImplicitAny

noImplicitAny는 변수들이 미리 정의된 타입을 가져야 하는지 여부를 제어한다. 위의 sum 함수는 noImplicitAny가 설정되었다면 유효하지 않다. a,b가 any 타입으로 간주되기 때문이다. 되도록이면 noImplicitAny로 설정하도록 하고 새 프로젝트를 한다면 꼭 noImplicitAny로 설정하도록 한다.

strictNullChecks

null과 undefined가 모든 타입에서 허용되는지 확인하는 설정이다.

const x: number = null

위의 예시는 정상이다. 하지만 strictNullChecks 설정하면 오류가 발생한다.

const x: number | null = null

만일 null을 허용하려면 위와같이 명시적으로 null을 타입에 넣어주어야 한다.

만일 null을 허용하지 않으려면 null을 체크하는 코드나 단언문을 추가해야한다.

const el = document.getElementById("status")
//el.textContent = "ready'; -> 개체가 null인 것 같습니다.

if (el) {
  el.textContent = "ready"
}
el!.textContent = "ready"

strictNullChecks 설정은 null, undefined 관련된 오류를 잡는데 큰 도움을 주지만 코드 작성을 어렵게 한다. 새 프로젝트를 시작한다면 가급적 사용을 권장하고 처음이거나 마이그레이션 중이라면 설정하지 않아도 좋다.

strict

모든 체크를 설정하고 싶다면 strict 설정을 하면 된다. 타입스크립트에 strict 설정을 하면 대부분 오류를 잡아낸다.

코드 생성과 타입이 관계없음을 이해하기

타입스크립트의 컴파일러는 두 가지 역할을 수행한다.

  1. 최신 타입스크립트/자바스크립트를 브라우저에서 동작할 수 있도록 구버전의 자바스크립트로 트랜스파일 한다.
  2. 코드의 타입 오류를 체크한다.

여기서 주의해야 할 점은 두 가지가 완벽히 독립적이라는 점이다. 타입스크립트가 자바스크립트로 변환될 때 타입은 영향을 미치지 않는다. 타입과 관련된 코드는 전부 사라진다!

독립적이기에 생기는 특징

1. 타입 오류가 있는 코드도 컴파일 가능

타입 오류가 있어도 컴파일 된다는 것은 엉성해보이지만, 실제로는 도움이 되는 경우가 있다. 애플리케이션에서 어떤 부분이 오류가 발생했다고 했을 때, 컴파일이 가능하기에 다른 부분에 대한 테스트가 가능하다.

만일 오류가 있을 때 컴파일 하지 않으려면 noEmitOnError를 설정하면 된다.

2. 런타임에는 타입 체크가 불가능하다.

만일 어떤 코드에서 instanceof와 타입을 사용하여 분기를 만든다면, 런타임 환경에서 타입은 사라지기에 전혀 유효하지 않다. 런타임에 타입 정보를 유지하는 방법으로는 3가지가 있다.

  1. 속성이 존재하는지 체크 - 속성 체크는 런타임에 접근 가능한 값에만 관련되지만, 타입 체커 역시 보정해주기 때문에 오류가 사라진다.

  2. 타입 정보를 명시적으로 저장하는 태그 기법

    interface Square {
      kind: "square"
      width: number
    }
    
    interface Rectangle {
      kind: "rectangle"
      width: number
      height: number
    }
    

    kind 속성을 추가해서, 명시해준다. 타입스크립트에서 흔하게 볼 수 있는 패턴이다.

  3. 타입을 클래스로 만든다.

    class Square {
      constructor(public width: number) {}
    }
    class Rectangle extends Square {
      constructor(public width: number, public height: number) {
        super(width)
      }
    }
    type Shape = Square | Rectangle
    
    function calculateArea(shape: Shape) {
      if (shape instanceof Rectangle) {
        //...
      }
    }
    

    type Shape = Square | Rectangle 부분에서 Rectangle은 타입으로 참조되지만, shape instanceof Rectangle 부분에서는 값으로 참조되어 분기가 가능한 것이다.

3. 타입 연산은 런타임에 영향을 주지 않는다.

타입이 사라지기 때문에 당연히 타입 연산도 런타임에 어떠한 영향을 주지 않는다.

function asNumber(val: number | string): number {
  return val as number
}
function asNumber(val) {
  return val
}

위의 코드는 아래와 같이 컴파일 되기 때문에 해당 인수가 해당 함수를 통과하더라도 그대로이다. 즉 어떠한 영향도 받지 않는다.

4. 런타임 타입은 선언된 타입과 다를 수 있다.

만약 API값을 수신할 때, API값을 잘못 파악하고 타입을 입력해놨다면 런타임 타입과 선언된 타입이 다르다.

5. 함수 오버로드 할 수 없다.

타입스크립트가 함수 오버로딩 기능을 지원하기는 하지만, 실제 컴파일 되어있을 때 선언문을 여러개 작성하더라도 구현체는 오직 하나뿐이다. 자바스크립트에서 함수 선언문으로 작성할 경우, 함수명이 동일하면 맨 마지막 함수로 덮어써진다. 이는 자바스크립트의 특징이다.

구조적 타이핑 익숙해지기

자바스크립트는 덕 타이핑 기반이다.

덕 타이핑이란, 객체가 어떤 타입에 부합하는 변수와 메소드를 가질 경우 객체를 해당 타입에 속하는 것으로 간주한다. 독수리가 오리처럼 걷고, 헤엄치고, 꽥꽥거린다면 독수리는 오리라고 할 수 있다.

function calculateLength(v: { a: number; b: number }) {
  return v.a + v.b
}

const v1 = { a: 1, b: 2, c: 3 }
calculateLength(v1) //3

calculateLength의 매개변수 v는 a,b의 속성만 가지지만 c의 속성도 가지고 있는 v1을 해당 함수에 넣어도 문제로 인식하지 않는다. 구조적 관점에서 a,b가 있기 때문에 호환되기 때문이다.

함수를 작성할 때 호출에 사용되는 매개변수의 속성들이 매개변수의 타입에 선언된 속성만을 가질거라 생각하기 쉽다. 이러한 타입은 봉인된 또는 정확한 타입이라고 불리며 타입스크립트 타입 시스템에서는 표현할 수 없다. 좋든 싫든 타입은 열려있다. 이러한 특성때문에 발생하는 오류는 아래와 같다.

let sum: number = 0
for (let p of Object.keys(v1)) {
  const num = v1[p]
  //string은 v1의 인덱스로 사용할 수 없다.
  sum += num
}
return sum

Object.keys(v1)을 호출하면 도출되는 배열은 무조건 객체의 속성 중 하나이지만, 타입은 확장될 수 있기 때문에 Object.keys(v1)의 타입은 string[]가 될 수도 있다. 즉 v1이 {a:1,b:2,c:3, d: "this is string"}이라면 NaN을 반환한다. 이럴 경우에 루프보다는, 모든 속성을 각각 더하는 구현이 더 간결하다.

정확한 타입으로 객체를 순회하는 방법은 추후에 다루도록 하겠다.

클래스 역시 구조적 타이핑을 따른다. 클래스의 인스턴스가 예상과 다를 수 있음을 유의해야한다.

구조적 타이핑의 장점

구조적 타이핑은 테스트하는데 유리하다. 함수를 테스트하기 위해서는 모킹한 객체를 생성해야 하는데, 테스트에 필요한 객체의 속성만 작성해주면 되기 때문이다.

any 타입 지양하기

any의 위험성..

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

let age: number = 10
const one = "1" as any
age += one
//age "101:

age는 number 타입이지만, any를 설정함으로써 string 타입을 할당할 수 있게 된다. 런타임에서 age는 string이다..

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

함수를 작성할 때는 시그니처를 명시해야 한다. 호출하는 쪽은 약속된 타입의 입력을 제공하고, 함수는 약속된 타입의 출력을 반환한다. any는 이런 약속을 무시해버릴 수 있다.

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

const a = "3" as number
const b = 5
sum(a, b) //정상적으로 작동함.

언어 서비스가 적용되지 않는다.

자동완성 기능 제공하지 않는다. 또한 Rename Symbol 기능을 사용하면 프로그램 내 모든 속성의 이름을 변경해주지만 any 타입이라면 적용되지 않는다.

타입 설계를 감춘다.

상태 객체의 설계를 감추기 때문에, 설계가 어떻게 되어있는지 전혀 알 수 없다. 내가 아닌 다른 사람이 코드를 본다면 파악하기 위해 코드를 재구성 해봐야 한다.

신뢰도를 떨어트린다.

런타임 오류가 더 자주 발생한다. 또한 타입 오류를 고쳐야하고 머릿속에 실제 타입을 기억해야 하기 때문에 번거롭다.

© 2024 jinwook567, Powered by gatsby-blog