ABOUT ME

-

오늘
-
어제
-
-
  • [Effective Typescript] 타입스크립트 알아보기
    Front-end/Typescript 2021. 10. 5. 02:07

    이 글은 이펙티브 타입스크립트 책을 보며 스터디한 내용입니다.

    타입스크립트의 독특함

    타입스크립트는 사용 방식 면에서 독특한 특징을 가집니다.

    인터프리터로 실행되는 것도 아니고, 저수준 언어로 컴파일 되는 것도 아닙니다. 🤔

     

    또 다른 고수준언어인 자바스크립트로 컴파일 되며, 실행 또한 자바스크립트로 이루어지게 됩니다. 😲

    타입스크립트는 자바스크립트의 상위 호환

    타입스크립트에 대해 알아볼 때 자바스크립트의 상위 호환이라는 말을 굉장히 많이 들어보셨을텐데요

    이유는 다음과 같습니다.

    문법의 상위집합

    문법적으로 상위에 있다는 말은 자바스크립트에서 가지고 있지 않은 부분을 타입스크립트는 가지고 있기 때문입니다. 

    // typescipt
    function greet(who: string) {
      console.log(who);
    }
    
    // javascript
    function greet(who: string) {
      console.log(who); // SyntaxError: Unexpected token
    }

    위 코드처럼 타입이라는 문법을 사용하게 되면 자바스크립트가 아닌 타입스크립트의 영역이 되어버립니다.

    마이그레이션

    index.js -> index.ts 로 바꾸더라도 문제가 없지만, index.ts -> index.js 시 문제가 발생합니다.

    위에서 설명했듯이 문법의 상위집합이기 때문에 상위에서 하위로 가게 되면 문제가 되겠지만, 하위에서 상위로 가는 것은 문제가 발생하지 않습니다.

     

    그래서 자바스크립트에서 타입스크립트로 마이그레이션을 할 때 부분적으로 점진적인 마이그레이션을 도입하기 굉장히 용이한 점이 있습니다.

    타입스크립트의 목표

    타입스크립트는 여러가지의 목표을 가지고 있습니다.

    그 중의 한가지는 런타임 단계에서 에러를 발생시킬 코드를 미리 찾아내는데에 있습니다.

    let city = 'Hello Man';
    console.log(city.map()); // Property 'map' does not exist on type 'string'.

    위 코드처럼 string 타입에는 map이 지원되지 않는 것을 미리 빨간줄을 통해 컴파일 단계에서 알려줍니다.

     

    하지만 이렇게 타입스크립트가 타입 체커를 통해 모든 에러를 찾아내지는 않습니다.

    대표적으로 의도치 않은 동작을 일으킬 여지가 있는 코드에 대해서 찾아내줍니다.

    const persons = [
      {
        name: 'kim',
        age: 29,
      },
      {
        name: 'lim',
        age: 24,
      },
    ];
    
    for (const person of persons) {
        console.log(person.address);
        // Property 'address' does not exist on type '{ name: string; age: number; }'.
    }

    위 코드가 타입스크립트가 아니었다면 undefined가 개발자들을 맞이했을 것입니다. 😨

     

    그래서 이런 현상을 미연에 방지하기 위해 타입 선언을 통해 에러를 찾아냅니다.

    interface Persons {
      name: string;
      age: number;
    }
    
    const persons: Persons[] = [
      {
        name: 'kim',
        age: 29,
      },
      {
        name: 'lim',
        aga: 24, // undefined
      },
    ];
    
    for (const person of persons) {
      console.log(person.age);
    }

    실제로 위 코드는 런타임 단계에서는 문제없이 실행(undefined가 출력)되지만, 이런 이상한 결과가 에러로 이어질 수 있다는 부분 때문에 타입스크립트는 이러한 부분까지 방지합니다.

    const a = null + 7; // Operator '+' cannot be applied to types 'null' and '7'.
    const b = [] + 12; // Operator '+' cannot be applied to types 'undefined[]' and 'number'.

    위 코드는 자바스크립트에서 a는 7, b는 12가 출력되겠지만 타입스크립트는 이런 결과가 문제가 될 수 있다는 가능성 때문에 오류를 표시해줍니다.

    타입스크립트 설정

    타입스크립트는 특정 설정을 통해 제어를 할 수 있습니다.

    tsc --init

    위 명령어를 통해 tsconfig.json파일을 생성하여 여러가지의 설정값들을 설정할 수 있습니다.

     

    이 설정들을 하기 전에, 제대로 설정하기 위해서는 noImplicitAny, strictNullChecks 두 가지를 이해해야 합니다.

    noImplicitAny

    한국말로 직역하면 안돼절대Any라는 뜻을 가지고 있습니다.

    여기서 느낄 수 있듯이 변수들이 미리 정의된 타입을 가져야 하는지 여부를 제어하는 속성입니다.

    {
      "compilerOptions": {
        "noImplicitAny": false
      }
    }

    위 속성 해제시에는 아래와 같은 결과가 일어납니다.

    // 원본
    function add(a, b) {
      return a + b;
    }
    // 암묵적으로 any로 타이핑
    function add(a: any, b: any): any

    위처럼 any타입이 암시적으로 들어가게 되기 때문에 우리는 이를 암시적 any라고 부릅니다.

     

    하지만 속성을 활성화 한다면 임의로 any를 넣어주지 않는 이상 에러가 발생하게 됩니다.

    즉, noImplicitAny를 사용한다면 필수적으로 타입을 넣어주어야 합니다.

    strictNullChecks

    엄격한Null확인이라는 뜻을 가지고 있으며, null과 undefined가 모든 타입에서 허용이 되는지 확인하는 설정입니다.

    {
      "compilerOptions": {
        "noImplicitAny": true,
        "strictNullChecks": false
      }
    }

    해제시에는 아래와 같은 현상이 일어납니다.

    const x: number = null; // 이상 무

    하지만 number로 확실한 타입을 정했으나 null이 들어가는 것 자체가 위험한 부분일 수 있습니다.

    그래서 활성화를 하게 되면

    const x: number | null = null;

    null 값을 사용하기 위해서는 추가로 타입 선언을 해주어야 가능해집니다.

     

    추가적으로 위 두 속성을 함께 사용하고 싶다면 strict속성을 활성화 하면 됩니다.

    {
      "compilerOptions": {
        "strict": true
      }
    }

    NoEmitOnError

    타입 오류가 있더라도 컴파일 여부를 결정하는 속성입니다.

    타입스크립트는 타입 체크컴파일을 독립적으로 동작시키기 때문에, 개발자의 의도에 따라 오류가 있더라도 속성을 통해 컴파일을 실행시킬 수 있습니다.

    {
      "compilerOptions": {
        "strict": true,
        "noEmitOnError": false
      }
    }

     

    let x = 'hello';
    x = 1234;
    
    index.ts:34:1 - error TS2322: Type 'number' is not assignable to type 'string'.
    
    34 x = 1234;
       ~
    
    
    Found 1 error.

    결과는 위와 같이 나오지만, 실제로 컴파일에는 문제가 없습니다.

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

    타입스크립트가 컴파일되는 과정에서 모든 타입 관련된 구문은 제거되고 자바스크립트만 남습니다.

    그래서 아래와 같은 코드는 런타임 환경에서 사용이 불가능합니다.

    interface Citizen {
      money: string;
    }
    
    interface Wizard extends Citizen {
      skill: string;
    }
    
    type Person = Citizen | Wizard;
    
    function getJob(person: Person) {
        if (person instanceof Citizen) {
    	  // 'Citizen' only refers to a type, but is being used as a value here.
        }
    }

    그래서 이를 해결하기 위해 속성의 유무를 기준으로 판단하기도 합니다.

    interface Citizen {
      money: string;
    }
    
    interface Wizard extends Citizen {
      skill: string;
    }
    
    type Person = Wizard | Citizen;
    
    function getJob(person: Person) {
      if ('skill' in person) {
        console.log(person); // Wizard
      }
    }

    혹은 아예 런타임에 접근 가능하도록 명시적으로 저장하는 태그기법을 사용하기도 합니다.

    interface Citizen {
      name: 'citizen';
    }
    
    interface Wizard {
      name: 'wizard';
      skill: string;
    }
    
    type Person = Citizen | Wizard;
    
    function getJob(person: Person) {
      if (person.name === 'wizard') {
        console.log(person); // Wizard
      }
    }

     

     

    런타임 접근 불가와 런타임 접근 가능을 둘다 사용하는 방법이 있습니다. 바로 클래스를 활용하는 방법입니다.

    class Citizen {
      constructor(public name: string) {}
    }
    
    class Wizard extends Citizen {
      constructor(public name: string, public skill: string) {
        super(name);
      }
    }
    
    type Person = Citizen | Wizard;
    
    function getJob(person: Person) {
      if (person instanceof Wizard) {
        console.log(person); // Wizard
      }
    }

    위 처럼 사용하면 타입으로서는 Person 타입을 참조하지만, instanceof에서는 으로 참조됩니다.

    이 부분은 추후 알아보겠습니다.

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

    개발자는 항상 number 타입으로 반환해주고 싶었지만 현실을 그렇지 않습니다.

    function asNumber(val: number | string): number {
      return val as number;
    }
    
    // 컴파일 후
    function asNumber(val) {
        return val;
    }

    as number는 타입 연산이고 런타임에는 아무런 영향도 끼치지 못하며, 영향을 주기 위해서는 런타임의 타입을 체크해주어야 합니다.

    function asNumber(val: number | string): number {
      return typeof val === 'string' ? Number(val) : val;
    }

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

    Java와 같은 언어는 동일한 함수명에 매개변수만 다른 여러 함수를 허용하지만, 타입스크립트에서는 타입과 런타임 환경은 무관하기 때문에 불가능합니다.

    // 불가능
    function add(a: number, b: number) {}
    function add(a: string, b: string) {}
    
    // 가능
    function add(a: number, b: number): number;
    function add(a: string, b: string): string;
    function add(a: any, b: any) {
      return a + b;
    }

    대신 타입의 여러가지 케이스를 통해 타입에 한해서는 동작하지만, 구현체는 하나여야만 합니다.

    타입스크립트는 런타임 성능에 영향을 주지 않는다.

    타입과 타입 연산자는 자바스크립트 변환 시점에 제거가 되기 때문에, 런타임의 성능아무런 영향을 미치지 않습니다.

    대신 컴파일의 과정에서 빌드타임이라는 성능이 따로 존재하기 때문에, 이 컴파일러의 성능을 아주 중요시 여깁니다.

    구조적 타이핑

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

     

    타입스크립트도 이를 모델링하여 매개변수 값이 요구사항을 만족하면 신경을 쓰지 않습니다. 

    그래서 이를 이해한다면 오류의 경우와 오류가 아닌 경우의 차이를 알 수 있고, 더욱 견고한 코드 작성이 가능합니다.

    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;
    }
    
    const v: NamedVector = { x: 3, y: 4, name: 'Zee' };
    
    calculateLength(v); // 5

    위 코드를 보면 Vector2D 타입은 두 가지 타입을 가지고 있으며, calculateLength에 매개변수는 Vector2D에 해당되는 값이 들어갑니다.

     

    거기에 추가로 NamedVector 타입을 추가하여  기존과 다르게 추가로 name 이라는 string 타입을 가진 속성을 추가해서 계산을 했지만, 정상적으로 x, y만 판별하여 계산이 정상적으로 된 모습입니다.

     

    이처럼 해당 타입을 이해하고 알맞는 값을 찾아주는 역할을 합니다.

    하지만 이 때문에 예상치 못한 결과가 나오기도 합니다.

    interface Vector3D {
      x: number;
      y: number;
      z: number;
    }
    
    function nomalize(v: Vector3D) {
      const length = calculateLength(v);
    
      return {
        x: v.x / length,
        y: v.y / length,
        z: v.z / length,
      };
    }
    
    nomalize({ x: 3, y: 4, z: 5 }); // { x: 0.6, y: 0.8, z: 1 }

    calculateLength 함수는 Vector2D지만, 구조적 타이핑으로 인하여 문제는 없는 상황입니다.

    그래서 개발자의 의도는  x, y, z 모든 값이 제곱처리가 되어 값이 나와야하는데 nomalize 함수를 실행하면 x, y가 정상적으로 존재하기 때문에 이상없이 동작하게 되는 것이죠

     

    뿐만 아니라 속성을 반복할 때도 문제가 발생합니다.

    function calculateLengthL1(v: Vector3D) {
      let length = 0;
      
      for (const axis of Object.keys(v)) {
        const coord = v[axis];
                   // ~~~~~~~ Element implicitly has an 'any' type because ...
                   //         'string' can't be used to index type 'Vector3D'
        length += Math.abs(coord);
      }
      
      return length;
    }
    
    const vec3D = {x: 3, y: 4, z: 1, address: '123 Broadway'};
    
    calculateLengthL1(vec3D);  // NaN

    v에는 Vector3D 타입이므로 Object.keys(v) 를 통해 반복한 axis는 x, y, z 가 나올 것을 우리는 기대하고 있습니다.

    하지만 기대와는 달리 coord 변수는 number가 아닌 any로 결정되게 됩니다.

     

    그 이유는 vec3D 변수에 선언한 객체처럼 address 라는 속성을 입력하더라도, 구조적 타이핑 때문에 이상이 없기 때문에 매개변수에서는 어떤 값이 들어올지 확신할 수 없으니 any타입으로 처리가 되는 것입니다.

     

    클래스에서도 이러한 당혹은 계속됩니다. 😨

    class C {
      foo: string;
      constructor(foo: string) {
        this.foo = foo;
      }
    }
    
    const c = new C('instance of C');
    const d: C = { foo: 'object literal' };

    C 라는 클래스를 이용하여 객체를 만들 때 두 문법을 통해 만들 수 있습니다.

     

    생성자로 생성하여 c 라는 인스턴스를 만드는 으로 사용 됐을 때는 구조적으로 필요한 속성생성자가 존재해서 문제가 없지만,

    만약 타입으로 사용했을 때는 단순 할당이 아닌 연산 등의 특정 코드가 필요한 경우 문제가 됩니다.

    any 타입 지양하기

    any는 점진적으로 타입스크립트를 도입하기 참 좋은 선택지이지만 일부 특별한 경우를 제외한 이상 any는 최대한 사용하지 않아아 합니다.

    let age: number;
    age = '12';
    // ~~~ Type '"12"' is not assignable to type 'number'
    
    age = '12' as any;
    age += 1;  // 런타임에서 "121"로 출력

    타입 안전성이 없습니다.

    as any로 타입을 체크하는 순간 혼돈을 걷잡을 수 없게 됩니다. 😇

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

    function calculateAge(birthDate: Date): number {
    	// do something..
    }
    
    let birthDate: any = '1993-09-17';
    calculateAge(birthDate);  // 이상 무

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

    자동완성 및 편집기의 Rename Symbol 등의 기능을 지원하지 않습니다.

    코드 리팩터링 때 버그를 감춥니다.

    구체적인 타입을 적용하면 타입 체커가 발견하지만, any를 사용한다면 런타임 환경에서 에러가 발생합니다.

    타입 설계를 감춰버립니다.

    객체 등의 것들을 설계하는데 있어서 any로 선언시 감추어버리기 때문에 파악하기 쉽지 않습니다.

    타입시스템의 신뢰도를 떨어뜨립니다.

    개발자는 사람이기 때문에 누구나 실수를 합니다. 그래서 타입 체커를 통해 신뢰를 높여야 합니다.

     

    댓글