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

Younngg·2023년 7월 30일
0

TypeScript

목록 보기
6/6

01. 타입스크립트와 자바스크립트의 관계 이해하기

타입스크립트는 문법적으로도 자바스크립트의 상위집합입니다. 자바스크리브 프로그램에 문법 오류가 없다면, 유효한 타입스크립트 프로그램이라고 할 수 있습니다. 그런데 자바스크립트 프로그램에 어떤 이슈가 존재한다면 문법 오류가 아니라도 타입 체커에게 지적당할 가능성이 높습니다.

자바스크립트 파일이 .js(또는 .jsx) 확장자를 사용하는 반면, 타입스크립트 파일은 .ts(또는 .tsx) 확장자를 사용합니다. 타입스크립트는 자바스크립트의 상위집합이기 때문에 .js 파일에 있는 코드는 이미 타입스크립트라고 할 수 있습니다. 이러한 특성은 기존에 존재하는 자바스크립트 코드를 타입스크립트로 마이그레이션하는 데 엄청난 이점이 됩니다.

모든 자바스크립트 프로그램이 타입스크립트라는 명제는 참이지만, 그 반대는 성립하지 않습니다.

function greet(who:string) {
	console.log('Hello', who);
}

: stirng은 타입스크립트에서 쓰이는 타입 구문입니다. 타입 구문을 사용하는 순간부터 자바스크립트는 타입스크립트 영역으로 들어 가게 됩니다.

타입 시스템의 목표 중 하나는 런타임에 오류를 발생시킬 코드를 미리 찾아내는 것입니다. 타입스크립트가 ‘정적’ 타입 시스템이라는 것은 바로 이런 특징을 말하는 것입니다.

모든 자바스크립트는 타입스크립트이지만, 일부 자바스크립트(그리고 타입스크립트)만이 타입 체크를 통과합니다.

타입스크립트는 자바스크립트의 런타임 동작을 ‘모델링’하는 타입 시스템을 가지고 있기 때문에 런타임 오류를 발생시키는 코드를 찾아내려고 합니다. 그러나 모든 오류를 찾아내리라 기대하면 안 됩니다. 타입 시스템이 정적 타입의 정확성을 보장해줄 것 같지만 그렇지 않습니다. 애초에 타입 시스템은 그런 목적으로 만들어지지도 않았습니다.

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

설정 파일은 tsx —init을 실행하면 생성됩니다.

noImplicitAny는 변수들이 미리 정의된 타입을 가져야 하는지 여부를 제어합니다. 다음 코드는 NoImplicitAny가 해제되어 있을 때에는 유효합니다.

function add(a, b) {
  return a + b;
}
function add(a, b) {
  // 'a' 매개변수에는 암시적으로 'any' 형식이 포함됩니다.
  // 'b' 매개변수에는 암시적으로 'any' 형식이 포함됩니다.
}

noImplicitAny가 설정되었다면 같은 코드임에도 오류가 됩니다. 이 오류들은 명시적으로 : any 라고 선언해 주거나 더 분명한 타입을 사용하면 해결할 수 있습니다.

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

타입스크립트는 타입 정보를 가질 때 가장 효과적이기 때문에 되도록이면 noImplicitAny를 설정해야 합니다. 그러면 타입스크립트가 문제를 발견하기 수월해지고, 코드의 가독성이 좋아지며, 개발자의 생산성이 향상됩니다.

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

다음은 strictNullChecks가 해제되었을 때 유효한 코드입니다.

const x: number = null; // 정상, null은 유효한 값입니다.

그러나 strictNullChecks를 설정하면 오류가 됩니다.

const x: number = null; 
// 'null' 형식은 'number'형식에 할당할 수 없습니다. 

null 대신 undefined를 써도 같은 오류가 납니다. 만약 null을 허용하려고 한다면, 의도를 명시적으로 드러냄으로써 오류를 고칠 수 있습니다.

const x: number | null = null;

strictNullChecks를 설정하려면 noImplicitAny를 먼저 설정해야 합니다.

이 모든 체크를 설정하고 싶다면 strict 설정을 하면 됩니다.

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

큰 그림에서 보면, 타입스크립트 컴파일러는 두 가지 역할을 수행합니다.

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

여기서 놀라운 점은 이 두 가지가 서로 완벽히 독립적이라는 것입니다. 다시 말해서, 타입스크립트가 자바스크립트로 변환될 때 코드 내의 타입에는 영향을 주지 않습니다. 또한 그 자바스크립트의 실행 시점에도 타입은 영향을 미치지 않습니다.

타입 오류가 있는 코드도 컴파일이 가능합니다

컴파일은 타입 체크와 독립적으로 동작하기 때문에, 타입 오류가 있는 코드도 컴파일이 가능합니다.

만약 오류가 있을 때 컴파일 하지 않으려면, tsconfig.json에 noEmitOnError를 설정하거나 빌드 도구에 동일하게 적용하면 됩니다.

런타임에는 타입 체크가 불가능합니다

interface Square {
  width: number;
}
interface Rectangle extends Square {
  height: number;
}
type Shape = Square | Rectangle

function calculateArea(shape: Shape){
  if (shape instanceof Rectangle) {
		// 'Rectangle'은 형식만 참조하지만, 여기서는 값으로 사용되고 있습니다.
    return shape.width * shape.height
		// 'Shape' 형식에 'height' 속성이 없습니다. 
  } else {

  }
}

instanceof 체크는 런타임에 일어나지만, Rectangle은 타입이기 때문에 런타임 시점에 아무런 역할을 할 수 없습니다. 타입스크립트의 타입은 ‘제거 가능’합니다. 실제로 자바스크립트로 컴파일 되는 과정에서 모든 인터페이스, 타입, 타입 구문은 그냥 제거되어 버립니다.

앞의 코드에서 다루고 있는 shape 타입을 명확하게 하려면, 런타임에 타입 정보를 유지하는 방법이 필요합니다. 하나의 방법은 height 속성이 존재하는지 체크해 보는 것입니다.

function calculateArea(shape: Shape){
  if ('height' in shape) {
    shape; // 타입이 Rectangle
    return shape.width * shape.height
  } else {
    shape // 타입이 Square
  }
}

속성 체크는 런타임에 접근 가능한 값에만 관련되지만, 타입 체커 역시도 shape의 타입을 Rectangle로 보정해 주기 때문에 오류가 사라집니다.

타입 정보를 유지하는 또 다른 방법으로는 런타임에 접근 가능한 타입 정보를 명시적으로 저장하는 ‘태그’ 기법이 있습니다.

interface Square {
  kind: 'square';
  width: number;
}
interface Rectangle {
  kind: 'rectangle';
  width: number;
  height: number;
}
type Shape = Square | Rectangle

function calculateArea(shape: Shape){
  if (shape.kind === "rectangle") {
    shape; // 타입이 Rectangle
    return shape.width * shape.height
  } else {
    shape // 타입이 Square
  }
}

여기서 Shape 타입은 ‘태그된 유니온’의 한 예입니다. 이 기법은 런타임에 타입 정보를 손쉽게 유지할 수 있기 때문에, 타입스크립트에서 흔하게 볼 수 있습니다.

타입(런타임 접근 불가)과 값(런타임 접근 가능)을 둘 다 사용하는 기법도 있습니다. 타입을 클래스로 만들면 됩니다.

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) {
    shape; // 타입이 Rectangle
    return shape.width * shape.height
  } else {
    shape // 타입이 Square
  }
}

type Shape = Square | Rectangle 부분에서 Rectangle은 타입으로 참조되지만, shape instanceof Rectangle 부분에서는 값으로 참조됩니다.

타입 연산은 런타임에 영향을 주지 않습니다

값을 정제하기 위해서는 런타임의 타입을 체크해야 하고 자바스크립트 연산을 통해 변환을 수행해야 합니다.

타입스크립트 타입으로는 함수를 오버로드 할 수 없습니다

타입스크립트가 함수 오버로딩 기능을 지원하기는 하지만, 온전히 타입 수준에서만 동작합니다. 하나의 함수에 대해 여러 개의 선언문을 작성할 수 있지만, 구현체는 오직 하나뿐입니다.

타입스크립트 타입은 런타임 성능에 영향을 주지 않습니다

런타임 오버헤드가 없는 대신, 타입스크립트 컴파일러는 ‘빌드타임’ 오버헤드가 있습니다. 오버헤드가 커지면, 빌드 도구에서 ‘transpile only’를 설정하여 타입 체크를 건너뛸 수 있습니다.

04. 구조적 타이핑에 익숙해지기

자바스크립트는 본질적으로 덕 타이핑 기반입니다. 만약 어떤 함수의 매개변수 값이 모두 제대로 주어진다면, 그 값이 어떻게 만들어졌는지 신경 쓰지 않고 사용합니다.

구조적 타이핑을 제대로 이해한다면 오류인 경우와 오류가 아닌 경우의 차이를 알 수 있고, 더욱 견고한 코드를 작성할 수 있습니다.

물리 라이브러리와 2D 벡터 타입을 다루는 경우를 가정해 보겠습니다.

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

벡터의 길이를 계산하는 함수는 다음과 같습니다.

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

이제 이름이 들어간 벡터를 추가합니다.

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

NamedVector는 number타입의 x와 y속성이 있기 때문에 calculateLength 함수로 호출 가능합니다.

const v: NamedVector = {x: 3, y: 4, name: 'Zee'};
calculateLength(v)

타입스크립트 타입 시스템은 자바스크립트의 런타임 동작을 모델링 합니다. NamedVector의 구조가 Vector2D와 호환되기 때문에 calculateLength 호출이 가능합니다. 여기서 ‘구조적 타이핑’ 이라는 용어가 사용됩니다.

구조적 타이핑 때문에 문제가 발생하기도 합니다. 3D 벡터를 만들어 보겠습니다.

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

그리고 벡터의 길이를 1로 만드는 정규화 함수를 작성합니다. 그러나 이 함수는 1보다 조금 더 긴(1.41) 길이를 가진 결과를 출력할 것입니다.

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

normalize({x: 3, y: 4, z: 5})
{x: 0.6, y: 0.8, z: 1}

calculateLength가 2D 벡터를 받도록 선언되었음에도 불구하고 3D 벡터를 받는 데 문제가 없었던 이유는 무엇일까요?

Vector3D와 호환되는 {x, y, z} 객체로 calculateLength를 호출하면, 구조적 타이핑 관점에서 x와 y가 있어서 Vector2D와 호환됩니다. 따라서 오류가 발생하지 않았고, 타입 체커가 문제로 인식하지 않았습니다.

함수를 작성할 때, 호출에 사용되는 매개변수의 속성들이 매개변수의 타입에 선언된 속성만을 가질 거라 생각하기 쉽습니다. 타입은 ‘봉인’되어 있지 않습니다.

any 타입 지양하기

타입스크립트의 타입 시스템은 점진적이고 선택적입니다. 코드에 타입을 조금씩 추가할 수 있기 때문에 점진적이며, 언제든지 타입 체커를 해제할 수 있기 때문에 선택적입니다. 이 기능들의 핵심은 any 타입입니다.

any 타입에는 타입 안정성이 없습니다

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

함수를 작성할 때는 시그니처를 명시해야 합니다. 호출하는 쪽은 약속된 타입의 입력을 제공하고, 함수는 약속된 타입의 출력을 반환합니다. 그러나 any 타입을 사용하면 이런 약속을 어길 수 있습니다.

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

어떤 심벌에 타입이 있다면 타입스크립트 언어 서비스는 자동완성 기능과 적절한 도움말을 제공합니다. 그러나 any 타입인 심벌을 사용하면 아무런 도움을 받지 못합니다.

타입스크립트의 모토는 ‘확장 가능한 자바스크립트’입니다. ‘확장’의 중요한 부분은 바로 타입스크립트 경험의 핵심 요소인 언어 서비스입니다.

any 타입은 코드 리펙터링 때 버그를 감춥니다

any는 타입 설계를 감춰버립니다

any는 타입시스템의 신뢰도를 떨어트립니다

profile
8533283@naver.com

0개의 댓글