티스토리 뷰

TypeScript

TypeScript 4.0 릴리즈 노트

한장현 2020. 8. 26. 03:13

 안녕하세요. 한장현입니다.
 그동안 TypeScript는 꾸준히 새 버전이 나오고 있었습니다.
 이번에는 메이저 버전이 변경되는 업데이트라 좀 더 재미있는 내용이 있을까 해서 릴리즈 노트를 찾아봤습니다.

 

 원문은 2020년 8월 20일에 작성되었습니다.

 



 오늘 저희는 TypeScript 4.0을 발표하게 되었습니다! 이 버전은 프로그래밍 언어의 표현력, 생산성, 확장성에 대해 깊이 고민한 결과물이며, TypeScript의 시대를 새롭게 여는 버전이 될 것입니다.

 

 TypeScript에 익숙하지 않은 독자를 위해 간단하게 설명하자면, TypeScript는 JavaScript를 기반으로 정적 타입 문법을 추가한 것입니다. 변수를 선언할 때 타입을 지정하고 이후에 변수를 사용할 때 타입을 다시 지정하면, TypeScript의 타입 검사 로직이 둘 사이의 관계가 적절한지 검사하기 때문에 잘못 작성된 코드를 실행하기 전에 검출할 수 있습니다. 이 기능은 파일을 저장하지 않은 상태에서도 동작합니다. 그리고 코드 작성을 끝낸 후에는 타입과 관련된 코드를 모두 제거해서 간결하며 가독성 높은 JavaScript로 변환할 수 있기 때문에 JavaScript가 실행되는 환경이라면 어디서든 자유롭게 실행할 수 있습니다. 코드 에러 검출 외에도, 정적 타입을 지정하면 에디터에서 코드 자동완성, 코드 이동, 리팩토링 등과 같은 기능을 다양하게 활용할 수 있습니다. 어렵게 생각할 필요는 없습니다. Visual Studio Code나 Visual Studio에서 JavaScript를 다뤄본 경험이 있다면 TypeScript을 사용해본 경험도 있다고 할 수 있습니다. TypeScript에 대해 학습하려면 공식 가이드 문서를 참고하세요.

 

 TypeScript 4.0에 메이저급 큰 변화는 없습니다. 그래서 아직 TypeScript를 접해보지 않았다면 이 언어를 공부하기에 지금이 가장 적절한 시기입니다. 꾸준히 운영되고 있는 커뮤니티가 있으며, 실행해볼 수 있는 예제 코드나 관련글들도 방대하게 존재합니다. TypeScript를 공부하는 동안 한 가지만 기억하세요. TypeScript 4.0에서 활용할 수 있는 기능은 아주 많지만 무엇보다도 TypeScript의 기본을 탄탄하게 알아두는 것이 중요합니다!

 

 TypeScript로 구현한 프로젝트가 있다면 다음 명령을 실행해서 TypeScript 버전을 업그레이드할 수 있습니다.

npm install -D typescript

 

 TypeScript를 활용할 수 있는 에디터도 확인해 보세요.

 

 

4.0 버전이 나오기까지

 

 TypeScript는 요즘 많은 사람들의 JavaScript 스택 대용으로 사용되고 있습니다. npm만 봐도 TypeScript 패키지는 올해 7월 처음으로 5천만 다운로드를 기록했습니다! 그리고 아직 개선의 여지가 분명히 있음에도 불구하고 TypeScript를 사용하는 개발자 대부분이 TypeScript를 즐겁게 사용하고 있다는 소식도 듣게 되었습니다. StackOverflow에서 진행한 설문에서는 TypeScript가 개발자들이 사랑하는 언어 2위에 올랐습니다. 최근 State of JavaScript 설문에서는 TypeScript를 사용해본 개발자 중 89%가 TypeScript를 계속 사용하고 싶다고 응답했습니다.

 

 이 시점에서 TypeScript가 어떻게 개발되어 왔는지 한 번 돌아볼 필요가 있습니다. 1, 2 버전은 생략합시다. 하지만 1, 2 버전에 도입된 기능 중에서도 지금까지 훌륭하게 활용되는 기능이 많으며, 이런 기능들은 TypeScript 4에서도 유지하려고 합니다.

 

 3.0 버전부터 봅시다. 이 버전에 변경된 사항은 셀 수 없이 많지만 그중에서도 튜플 타입과 리스트 타입을 통합한 것이 가장 큰 변경사항이었습니다. 또 3.0 버전은 프로젝트를 참조할 수 있는 기능을 제공했기 때문에 프로젝트를 확장하거나 관리하기에도 편해졌습니다. any 타입을 좀 더 안전하게 처리하기 위해 unknown 타입을 도입한 것도 큰 반응이 있었습니다.

 

 TypeScript 3.1 버전맵핑 타입(mapped types)을 튜플과 배열 타입에도 확장해서 실행시점에 TypeScript 전용 코드 없이도 간단하게 함수에 프로퍼티를 추가할 수 있게 개선되었습니다.

 

 TypeScript 3.2 버전부터는 객체를 분해할 때 제네릭 타입을 사용할 수 있게 되었으며 bind, call, apply 함수의 타입을 엄격하게 지정하도록 개선하면서 좀 더 나은 메타 프로그래밍이 가능한 모델을 구축했습니다. 그리고 TypeScript 3.3 버전은 3.2 버전의 안정성을 개선하는 데에 집중했지만 그 와중에 유니언 타입 함수나 --build 옵션에 파일 증분 빌드 기능을 도입한 것과 같이 다양한 기능이 함께 추가되었습니다.

 

 TypeScript 3.4 버전은 이뮤터블 데이터 구조에 대한 지원이나 고차 제네릭 함수(higher-order generic functinos)에 대한 지원에 집중하면서 함수형 패턴을 지원하는 데에 힘썼습니다. 그리고 이 버전에 도입된 --incremental 옵션을 사용하면 TypeScript 코드를 빌드할 때 증분 빌드를 수행하기 때문에 컴파일 시간을 크게 줄일 수 있었습니다.

 

 TypeScript 3.5 버전3.6 버전은 타입 검사 규칙을 점검하면서 TypeScript의 타입 시스템 전체를 손봤습니다.

 

 TypeScript 3.7 버전은 ECMAScript에 도입된 새 타입 시스템이 추가되었기 때문에 아주 중요한 버전이라고 할 수 있습니다. 이 버전에서 타입과 관련해서는 타입 별칭(alias) 참조 코드를 재귀적으로 참조하도록 구성했고 assertion 스타일의 함수 지원 기능을 추가했습니다. 그리고 JavaScript와 관련해서는 옵셔널 체이닝(optional chaining)이나 새로운 병합 연산자(coalescing)가 추가되었습니다. 두 문법 모두 TypeScript 개발자나 JavaScript 개발자의 도입 요구가 많았던 기능입니다.

 

 최근에 나온 3.8 버전3.9 버전에는 타입만 로드(import)하거나 외부로 공개(export)하는 문법이 추가되었고 ECMAScript에 추가된 private 필드 지정 문법이 추가되었으며, 모듈 계층에서 동작하는 최상위 await 문법, export * 문법이 개선되었습니다. 또 이 버전들은 동작 성능과 확장성 최적화가 이루어진 버전이기도 합니다.

 

 언어 지원 서비스나 인프라구조, 웹사이트, TypeScript 코어 프로젝트에 대해서는 다루지 않았습니다. TypeScript 코어 프로젝트는 수많은 커뮤니티 컨트리뷰터의 도움으로 발전하고 있으며 DefinitelyTyped 프로젝트나 TypeScript 프로젝트도 마찬가지입니다. DefinitelyTyped 프로젝트가 처음 시작되었던 2012년에 풀 리퀘스트는 80건밖에 되지 않았지만, 그 해 말부터 크게 증가해서 2019년에는 8,300개라는 놀라운 숫자의 풀 리퀘스트가 생성되었습니다. 컨트리뷰터들의 수많은 기여들은 TypeScript의 기초를 탄탄하게 하는 데에 큰 도움이 되었으며, TypeScript 생태계를 계속해서 개선해나가는 데에도 큰 도움이 됩니다. 열정적인 커뮤니티 활동에 감사드립니다.

 

 

새로 추가된 기능

 

 4.0 버전은 이런 과정을 거쳤기 때문에 나올 수 있었습니다. 이제 4.0 버전에 새로 추가된 기능에 대해 알아봅시다!

 

  • 가변 튜플 타입
  • 튜플 엘리먼트에 이름 지정하기
  • 생성자에서 클래스 프로퍼티 추론하기
  • 간략 할당 연산자
  • catch 절에 바인딩되는 에러 타입은 unknown입니다.
  • 커스텀 JSX 팩토리
  • --noEmitOnError 옵션을 사용할 때 빌드 속도 개선
  • --incremental--noEmit 함께 사용하기
  • 에디터 지원 개선
    • 옵셔널 체이닝 지원
    • /** @deprecated */ 지원
    • 에디터 시작 시점에 활용할 수 있는 부분 지원 모드
    • 더 똑똑해진 심볼 자동 로드
  • 웹사이트 개편!
  • Breaking Changes

 

가변 튜플 타입

 

 배열이나 튜플 인자 두 개를 받아서 새로운 배열로 결합하는 concat JavaScript 함수가 있다고 합시다.


function concat(arr1, arr2) {
    return [...arr1, ...arr2];
}

 

 그리고 배열이나 튜플을 하나를 인자로 받아서 첫 번째 항목을 제외하고 나머지를 반환하는 tail 함수도 있다고 합시다.


function tail(arg) {
    const [_, ...result] = arg;
    return result
}

 

 이 함수를 대상으로 TypeScript 타입을 지정하려면 어떻게 해야 할까요?
concat의 경우에 이전 버전까지 가능했던 방법은 모든 경우를 고려해서 오버로딩하는 것뿐이었습니다.


function concat(arr1: [], arr2: []): [];
function concat<A>(arr1: [A], arr2: []): [A];
function concat<A, B>(arr1: [A, B], arr2: []): [A, B];
function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];
function concat<A, B, C, D>(arr1: [A, B, C, D], arr2: []): [A, B, C, D];
function concat<A, B, C, D, E>(arr1: [A, B, C, D, E], arr2: []): [A, B, C, D, E];
function concat<A, B, C, D, E, F>(arr1: [A, B, C, D, E, F], arr2: []): [A, B, C, D, E, F];)

 

 음... 그래요. 그런데 오버로딩 함수 7개의 두번째 배열은 모두 빈 배열입니다. arr2에 해당하는 오버로드 함수도 추가해 봅시다.


function concat<A2>(arr1: [], arr2: [A2]): [A2];
function concat<A1, A2>(arr1: [A1], arr2: [A2]): [A1, A2];
function concat<A1, B1, A2>(arr1: [A1, B1], arr2: [A2]): [A1, B1, A2];
function concat<A1, B1, C1, A2>(arr1: [A1, B1, C1], arr2: [A2]): [A1, B1, C1, A2];
function concat<A1, B1, C1, D1, A2>(arr1: [A1, B1, C1, D1], arr2: [A2]): [A1, B1, C1, D1, A2];
function concat<A1, B1, C1, D1, E1, A2>(arr1: [A1, B1, C1, D1, E1], arr2: [A2]): [A1, B1, C1, D1, E1, A2];
function concat<A1, B1, C1, D1, E1, F1, A2>(arr1: [A1, B1, C1, D1, E1, F1], arr2: [A2]): [A1, B1, C1, D1, E1, F1, A2];

 

 코드를 굳이 이렇게 작성해야 할 필요가 있을까요. tail의 경우에도 마찬가지입니다.

 

 이런 식으로 가면 원하는 바를 달성하기 위해 오버로딩 함수를 천 개쯤 구현해야 할 수도 있기 때문에 문제를 근본적으로 해결하는 답이라고 볼 수 없습니다. 그래서 모든 경우를 한 번에 처리하려면 다음과 같이 구성해야 합니다:


function concat<T, U>(arr1: T[], arr2: U[]): Array<T | U>;

 

 하지만 이렇게 정의해도 입력값의 길이나 엘리먼트의 차수(order)를 처리할 수 없었으며 튜플인 경우에도 마찬가지입니다.

 

 그래서 TypeScript 4.0는 타입 추론 개선을 포함한 두 가지 개선사항을 도입하면서 새로운 타입 정의 모델을 마련했습니다.

 

 첫 번째 개선사항은 제네릭에 사용하는 튜플에 전개 연산자(spread operator, ...)를 사용할 수 있게 된 것입니다. 이 기능이 도입되면서 이제는 현재 다루고 있는 데이터의 실제 타입이 무엇인지 알지 못하더라도 튜플이나 배열을 처리할 수 있게 되었습니다. 제네릭에 사용된 전개 연산자에 해당하는 변수의 인스턴스가 생성되면 이 튜플 타입을 기반으로 새로운 튜플이나 배열을 생성할 수 있습니다.

 

 새로운 방식으로 tail 함수를 구현해보면 이렇습니다.


function tail<T extends any[]>(arr: readonly [any, ...T]) {
    const [_ignored, ...rest] = arr;
    return rest;
}

const myTuple = [1, 2, 3, 4] as const;
const myArray = ["hello", "world"];

// type [2, 3, 4]
const r1 = tail(myTuple);

// type [2, 3, 4, ...string[]]
const r2 = tail([...myTuple, ...myArray] as const);

 

 두 번째 개선사항은 튜플 안에 사용하는 전개 연산자의 위치가 꼭 마지막이 아니어도 된다는 것입니다!


type Strings = [string, string];
type Numbers = [number, number];

// [string, string, number, number, boolean]
type StrStrNumNumBool = [...Strings, ...Numbers, boolean];

 

 TypeScript 4.0 이전 버전에서 위와 같은 코드를 작성하면 이런 에러가 발생했습니다:


A rest element must be last in a tuple type.

 

 하지만 TypeScript 4.0부터는 아닙니다.

 

 길이가 고정되지 않은 상태에서 전개 연산자를 사용하면 이제는 해당 타입이 마지막 엘리먼트 전까지 자동으로 지정됩니다.


type Strings = [string, string];
type Numbers = number[]

// [string, string, ...Array<number | boolean>]
type Unbounded = [...Strings, ...Numbers, boolean];

 

 이 두 가지 개선사항을 함께 활용하면 concat 함수를 다음과 같이 아름답게 정의할 수 있습니다:


type Arr = readonly any[];

function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] {
    return [...arr1, ...arr2];
}

 

 이 코드에서 중복된 부분이 불필요하다고 생각할 수 있지만, 이렇게 작성해야 어떠한 배열이나 튜플에 대해서도 타입 추론 기능이 제대로 동작합니다.

 

 이런 방식은 그 자체로도 훌륭하지만 시나리오가 복잡한 경우에 더 빛을 발합니다. 함수를 인자로 받아서 실행하는 partialCall.partialCall 패턴을 생각해 봅시다. 이런 패턴에서는 함수(partialCall)가 다른 함수(f)를 인자로 받아서 f 함수가 처리하는 결과를 또 다른 함수로 반환합니다.


function partialCall(f, ...headArgs) {
    return (...tailArgs) => f(...headArgs, ...tailArgs)
}

 

 TypeScript 4.0에서 개선된 사항을 활용하면 이 함수는 다음과 같이 정의할 수 있습니다.


type Arr = readonly unknown[];

function partialCall<T extends Arr, U extends Arr, R>(
    f: (...args: [...T, ...U]) => R, ...headArgs: T
) {
    return (...tailArgs: U) => f(...headArgs, ...tailArgs)
}

 

 이렇게 작성하면 partialCall 함수가 받는 인자가 어떤 타입이어야 하는지, 반환하는 타입이 어떤 타입이어야 하는지 더 편하게 파악할 수 있습니다.


const foo = (x: string, y: number, z: boolean) => {}

// x 형식이 맞지 않기 때문에 동작하지 않습니다.
const f1 = partialCall(foo, 100);
//                          ~~~
// error! Argument of type 'number' is not assignable to parameter of type 'string'.


// 전달하는 인자의 개수가 맞지 않기 때문에 동작하지 않습니다.
const f2 = partialCall(foo, "hello", 100, true, "oops")
//                                              ~~~~~~
// error! Expected 4 arguments, but got 5.


// 이 코드는 동작합니다! f3는 '(y: number, z: boolean) => void' 타입으로 추론됩니다.
const f3 = partialCall(foo, "hello");

// f3는 이제 어떻게 활용할 수 있을까요?

f3(123, true); // 동작합니다!

f3();
// error! Expected 2 arguments, but got 0.

f3(123, "hello");
//      ~~~~~~~
// error! Argument of type 'string' is not assignable to parameter of type 'boolean'.

 

 가변 튜플 타입을 활용하면 지금까지 없던 패턴을 새롭게 개발할 수 있습니다. 그리고 이 기능이 많이 활용되면서 JavaScript bind를 활용하는 로직이 더 다양하게 확장될 수 있으리라 기대합니다. 타입 추론 기능이 개선되면서 이밖에도 다양한 패턴이 나올 수 있을 것입니다. 자세한 내용은 가변 튜플에 대한 풀 리퀘스트를 참고하세요.

 

 

튜플 엘리먼트에 이름 지정하기

 

 튜플 타입과 인자 목록에 대한 개선사항은 기존 JavaScript 코드에 빈번하게 사용되던 배열 조작 과정에 더 강력한 타입 유효성 검사를 적용할 수 있다는 점에서도 중요합니다. 나머지 매개변수에 튜플 유형을 사용할 수 있다는 점이 특히 중요합니다.

 

전개 연산자에 튜플 타입을 사용하는 함수를 예로 들어 봅시다.


function foo(...args: [string, number]): void {
    // ...
}

 

 이 함수는 아래 함수와 거의 비슷하다고 볼 수 있습니다.


function foo(arg0: string, arg1: number): void {
    // ...
}

 

 사용하는 방식도 그렇습니다.


foo("hello", 42); // 동작

foo("hello", 42, true); // 에러
foo("hello"); // 에러

 

 하지만 두 정의 방식은 가독성 측면에서 좀 다릅니다. 첫 번째 코드처럼 정의하면 함수에 전달된 인자의 이름이 없습니다. 물론 이렇게 작성해도 타입 검사는 통과하겠지만 사용하기에는 조금 불편합니다. 이 함수가 처음 정의된 의도대로 앞으로도 사용될 것인지도 알 수 없습니다.

 

TypeScript 4.0부터는 아래처럼 튜플에 이름을 지정할 수 있습니다.


type Range = [start: number, end: number];

 

 그리고 함수로 전달되는 인자 배열과 튜플 타입의 연결을 강화하기 위해 전개 연산자와 옵션 엘리먼트를 이렇게 사용할 수도 있습니다.


type Foo = [first: number, second?: string, ...rest: any[]];

 

 튜플 엘리먼트에 이름을 지정할 때 지켜야 할 규칙이 몇 가지 있습니다. 첫 번째, 튜플 엘리먼트에 이름을 하나라도 지정하면 모든 엘리먼트에 이름을 지정해야 합니다.


type Bar = [first: string, number];
//                         ~~~~~~
// error! Tuple members must all have names or all not have names.

 

 다만, 이때 이름을 지정했다고 해서 배열을 분해할 때도 그 이름을 똑같이 사용해야 한다는 것은 아닙니다. 이 기능은 개발자에게 좀 더 정확한 정보를 제공하는 것이 목적입니다.


function foo(x: [first: string, second: number]) {
    // ...

    // 참고: `first`, `second`라는 이름을 사용하지 않아도 됩니다.
    let [a, b] = x;

    // ...
}

 

 정리하자면, 튜플 엘리먼트에 이름을 지정하면 함수에 전달되는 인자의 정보를 더 명확하게 지정할 수 있기 때문에 오버로딩 함수를 구현할 때도 타입이 틀어질 일이 줄어듭니다. TypeScript 에디터에서도 오버로딩 함수들을 확인하고 적절한 제안을 해줄 것입니다.

 

 

 더 자세한 내용은 튜플 엘리먼트에 이름 지정하기 관련 풀 리퀘스트를 참고하세요.

 

 

생성자에서 클래스 프로퍼티 추론하기

 

 TypeScript 4.0부터는 클래스 프로퍼티의 타입을 결정할 때 좀 더 개선된 추론 방식을 활용합니다. 이 기능은 noImplicitAny가 활성화된 상태에서 확인할 수 있습니다.


class Square {
    // 이전 버전: any로 추론되기 때문에 에러가 발생합니다!
    // 4.0: `number`로 추론합니다!
    area;
    sideLength;

    constructor(sideLength: number) {
        this.sideLength = sideLength;
        this.area = sideLength ** 2;
    }
}

 

 그리고 생성자에 있는 정보만으로 추론할 수 없는 경우라면 undefined로 간주합니다.


class Square {
    sideLength;

    constructor(sideLength: number) {
        if (Math.random()) {
            this.sideLength = sideLength;
        }
    }

    get area() {
        return this.sideLength ** 2;
        //     ~~~~~~~~~~~~~~~
        // error! Object is possibly 'undefined'.
    }
}

 

 이런 경우가 있다면 생성자보다 initialize와 같은 초기화 메소드를 따로 구현하는 것이 좋습니다. 그리고 strictPropertyInitialization 옵션을 사용한다면 명확하게 !를 사용해야 할 수도 있습니다.


class Square {
    // 타입을 정확하게 지정
    //        v
    sideLength!: number;
    //         ^^^^^^^^
    // type annotation

    constructor(sideLength: number) {
        this.initialize(sideLength)
    }

    initialize(sideLength: number) {
        this.sideLength = sideLength;
    }

    get area() {
        return this.sideLength ** 2;
    }
}

 

 자세한 내용은 관련 풀 리퀘스트를 참고하세요.

 

 

간략(short-circuiting) 할당 연산자

 

 JavaScript 뿐만 아니라 다른 언어에도 복합 할당 연산자(compound assignment operator)라는 개념이 존재합니다. 복합 할당 연산자는 연산자가 실행된 결과를 왼쪽 변수에 다시 할당하기 위해 연산자 두 개를 결합한 것을 의미합니다. 이런 코드가 그렇습니다:


// 더하기
// a = a + b
a += b;

// 빼기
// a = a - b
a -= b;

// 곱하기
// a = a * b
a *= b;

// 나누기
// a = a / b
a /= b;

// 지수 연산
// a = a ** b
a **= b;

// 왼쪽으로 비트 이동
// a = a << b
a <<= b;

 

 연산자 대부분은 할당 연산자와 결합할 수 있습니다. 그런데 최근까지도 and(&&), or(||), null 병합 연산자(??)들은 할당 연산자와 결합할 수 없었습니다.

 

 그래서 TypeScript 4.0은 새로운 ECMAScript 스펙에 맞게 이 연산자들에 대해서도 &&=, ||=, ??=와 같은 복합 할당 연산자를 추가했습니다.

 

 이 연산자들은 순서대로 다음 코드와 동일하게 동작합니다:


a = a && b;
a = a || b;
a = a ?? b;

 

 if 블록으로 표현하면 이렇습니다:


// 'a ||= b'와 동일하게 동작
if (!a) {
    a = b;
}

 

 이 연산자는 다음과 같이 변수 초기화 시점을 지연시키는 용도로도 활용할 수 있습니다.


let values: string[];

// 이전 버전
(values ?? (values = [])).push("hello");

// 4.0
(values ??= []).push("hello");

 (저희가 작성한 코드가 모두 아름다운 것은 아닙니다...)

 

 자주 사용되지는 않겠지만 게터, 세터와 함께 활용하는 방법도 있습니다. 이 방식을 활용하면 어떤 변수의 값이 없을 때만 특정 로직을 실행해서 해당 변수로 할당할 수 있습니다.


obj.prop ||= foo();

// 아래 코드들과 비슷합니다.

obj.prop || (obj.prop = foo());

if (!obj.prop) {
    obj.prop = foo();
}

 

 TypeScript Playground에서 다음 코드가 어떻게 실행되는지 확인해 보세요.


const obj = {
    get prop() {
        console.log("게터가 실행되었습니다.");

        // Replace me!
        return Math.random() < 0.5;
    },
    set prop(_val: boolean) {
        console.log("세터가 실행되었습니다.");
    }
};

function foo() {
    console.log("연산자 오른쪽이 실행되었습니다.");
    return true;
}

console.log("이 코드는 항상 세터를 실행합니다.");
obj.prop = obj.prop || foo();

console.log("이 코드는 *가끔* 세터를 실행합니다.");
obj.prop ||= foo();

 

 이 기능을 추가하는데 기여한 커뮤니티 멤버 Wenlu Wang에게 감사드립니다.

 

 자세한 내용은 관련 풀 리퀘스트를 참고하세요. TC39 제안 문서도 참고할만합니다.

 

 

catch 절에 바인딩되는 에러 타입은 unknown입니다.

 

 TypeScript 4.0 이전 버전까지는 catch 절에 바인딩되는 에러 객체의 타입이 any 였습니다. 그래서 이 에러 객체는 아무렇게나 사용할 수 있었습니다.


try {
    // ...
}
catch (x) {
    // x는 `any` 타입입니다. 갖고 놀아봅시다!
    console.log(x.message);
    console.log(x.toUpperCase());
    x++;
    x.yadda.yadda.yadda();
}

 

 이런 방식은 catch 절에서 받은 에러를 또 다른 에러로 연계해야 할 때 오동작할 여지가 있습니다. 에러 객체가 any 타입으로 간주되기 때문에 이 객체를 다룰 때 타입을 신경 쓰지 않고 아무렇게나 사용할 수 있기 때문입니다.

 

 이제 TypeScript 4.0부터는 catch 절에 전달되는 에러 객체가 unknown으로 간주됩니다. unknown 타입은 타입을 지정해야 사용할 수 있기 때문에 any 타입보다 안전합니다.


try {
    // ...
}
catch (e: unknown) {
    // 에러!
    // `unknown` 타입에는 `toUpperCase`가 존재하지 않습니다.
    console.log(e.toUpperCase());

    if (typeof e === "string") {
        // 동작합니다!
        // `e` 객체가 `string` 타입일 때만 실행됩니다.
        console.log(e.toUpperCase());
    }
}

 

 하지만 이 기능은 개발자들이 충분히 익숙해지기 전까지는 --strict 모드에서만 동작합니다. 조만간 린트(link) 규칙을 통해서 catch 절에 전달되는 에러 타입을 : any: unknown 중 하나로 선택할 수 있을 것입니다.

 

 자세한 내용은 관련 풀 리퀘스트를 참고하세요.

 

 

커스텀 JSX 팩토리

 

 JSX에서 이야기하는 프래그먼트(fragment)는 자식 엘리먼트를 여러 개 반환하는 JSX 엘리먼트 타입을 의미합니다. 그런데 TypeScript에 프래그먼트를 처음 추가할 때는 이 타입이 앞으로 어떻게 활용될지 심각하게 고려하지 않았습니다. 하지만 지금은 많은 라이브러리들이 JSX 사용을 권장하고 있으며 프래그먼트용 API 지원도 늘려가고 있습니다.

 

 이제 TypeScript 4.0부터는 jsxFragmentFactory 옵션을 사용해서 프래그먼트 팩토리를 커스터마이징 할 수 있습니다.

 

 그래서 tsconfig.json 파일을 다음과 같이 작성하면 React에 적합한 JSX 팩토리를 구성할 수 있습니다. 다만, 이렇게 사용하려면 React.createElement 대신 h를 사용해야 하며 React.Fragment 대신 Fragment를 사용해야 합니다.


{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "jsx": "react",
    "jsxFactory": "h",
    "jsxFragmentFactory": "Fragment"
  }
}

 

 그리고 파일마다 JSX 팩토리를 다르게 사용하려면 /** @jsxFrag */ 전처리문(pragma comment)을 사용하면 됩니다.


// 참고: 이 전처리문은 JSDoc-style로 작성해야 합니다.
/** @jsx h */
/** @jsxFrag Fragment */

import { h, Fragment } from "preact";

let stuff = <>
    <div>Hello</div>
</>;

 

 이 파일을 빌드하면 다음과 같은 JavaScript 코드가 됩니다.


// 참고: 이 전처리문은 JSDoc-style로 작성해야 합니다.
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "preact";
let stuff = h(Fragment, null,
    h("div", null, "Hello"));

 

 이 기능을 추가하는데 기여해준 커뮤니티 멤버 Noj Vek에게 감사드립니다.

자세한 내용은 관련 풀 리퀘스트를 참고하세요.

 

 

--noEmitOnError 옵션을 사용할 때 빌드 속도 개선

 

 이전 버전까지는 --incremental 옵션을 사용해서 증분 빌드를 할 때 --noEmitOnError 옵션을 함께 사용하면 속도가 아주 느렸습니다. 이 문제는 --noEmitOnError 플래그를 사용하면 이전에 컴파일 결과에 대한 정보가 .tsbuildinfo 파일에 캐싱되지 않기 때문이었습니다.

 

 TypeScript 4.0은 이 문제를 해결해서 --incremental 옵션과 --noEmitOnError 옵션이 활성화된 상태에서 빌드되는 속도를 크게 개선했습니다.

 

 자세한 내용은 관련 풀 리퀘스트를 참고하세요.

 

 

--incremental--noEmit 함께 사용하기

 

 이전 버전까지는 --incremental 옵션을 사용해서 증분 빌드를 할 때 --noEmitOnError 옵션을 함께 사용하면 속도가 아주 느렸습니다. 이 문제는 --noEmitOnError 플래그를 사용하면 이전에 컴파일했던 정보가 .tsbuildinfo 파일에 캐싱되지 않기 때문이었습니다.

 

 TypeScript 4.0은 이 문제를 해결해서 --incremental 옵션과 --noEmitOnError 옵션이 활성화된 상태에서 빌드되는 속도를 크게 개선했습니다.

 

 자세한 내용은 관련 풀 리퀘스트를 참고하세요.

 

 

에디터 지원 개선

 

 TypeScript 컴파일러는 TypeScript 코드를 컴파일하는 용도 외에 TypeScript를 지원하는 에디터에서 코딩 생산성을 향상시키는 용도로도 활용됩니다. Visual Studio 제품군에서 JavaScript를 개발할 때도 마찬가지입니다. 그래서 이번 버전은 에디터에서 활용할 수 있는 시나리오를 강화하는 데에도 힘썼습니다. 원하는 기능을 개발하는 시간이 크게 줄어들 것입니다.

 

 TypeScript/JavaScript 언어 지원 기능은 에디터마다 다르게 동작할 수 있지만

 

 

 

옵셔널 체이닝 지원

 

 옵셔널 체이닝(optional chaining)은 최근에 등장해서 큰 사랑을 받고 있는 기능입니다. 그래서 TypeScript 4.0에서도 옵셔널 체이닝null 병합 연산자(nullish coalescing)를 사용할 수 있습니다!

 

 

 다만 코드를 이런 방식으로 변경하면 JavaScript 컨텍스트에서 참으로 평가되거나/거짓으로 평가되는 것에 따라 이전과 다르게 동작할 수 있습니다. 타입을 명확하게 지정할수록 의도대로 동작할 것입니다.

 

 자세한 내용은 관련 풀 리퀘스트를 참고하세요.

 

 

/** @deprecated */ 지원

 

 TypeScript 언어 지원 서비스는 이제 JSDoc 스타일로 작성된 /** @deprecated */ 주석을 지원합니다. 그래서 자동완성 기능을 사용할 때 지원이 중단된 것을 바로 확인할 수 있습니다. VS Code를 예로 들면, 지원이 중단된 항목에는 취소선이 표시됩니다.

 

 

 새 기능을 추가하는 데에 기여해주신 Wenlu Wang에게 감사드립니다. 자세한 내용은 관련 풀 리퀘스트를 참고하세요.

 

 

에디터 시작 시점에 활용할 수 있는 부분 지원 모드

 

 에디터 실행 시간이 긴 것에 많은 유저들이 불편을 겪는다는 이야기를 들었습니다. 프로젝트 규모가 클수록 더 그렇습니다. 이 문제의 원인은 프로그램 구축(program construction)이라고 하는 과정 때문입니다. 이 과정은 최상위 폴더에 있는 파일을 찾아와서 파싱하고, 의존성을 분석하며, 의존성의 의존성을 따라가며 분석하는 과정입니다. 그래서 프로젝트 규모가 클수록 이 과정을 처리하는 시간이 오래 걸리기 때문에 에디터를 켠 직후에는 TypeScript 언어 지원 기능을 활용할 수 없었습니다.

 

 TypeScript 4.0은 이 문제를 해결하기 위해 언어 지원 서비스가 전체 로딩되기 전에 활용할 수 있는 부분 지원 모드(partial semantic mode)를 추가했습니다. 이 모드의 핵심은 에디터를 실행했을 때 열리는 현재 파일만이라도 언어 지원 서비스를 지원하는 것입니다.

 

 실행 환경과 프로젝트에 따라 달라질 수 있지만, 일반적으로 Visual Studio Code에서 TypeScript 언어 지원 서비스는 초기화되기 전까지 20초 ~ 1분 동안 동작하지 않았습니다. 하지만 이제 부분 지원 모드를 활용하면 단 몇 초만 있어도 언어 지원 서비스를 활용할 수 있습니다. 아래 영상을 확인해 보세요. 왼쪽 화면은 TypeScript 3.9를 적용한 화면이며, 오른쪽은 TypeScript 4.0을 적용한 화면입니다.

 

 

 코드량이 많은 프로젝트를 열고 에디터를 재시작하면 TypeScript 3.9가 적용된 쪽에서는 코드 자동완성 기능이나 빠른 참조 기능이 제대로 동작하지 않습니다. 하지만 TypeScript 4.0이 적용된 화면은 현재 파일과 관련된 내용이라면 완전히 초기화되지 않은 시점에도 사용자가 원하는 정보를 제공할 수 있습니다.

 

 아직까지 이 모드는 Visual Studio Code Insiders가 설치된 Visual Studio Code에서만 제대로 동작합니다. 그리고 이 모드는 아직 UX나 기능 측면에서 부족함이 있기 때문에 개선할 내용을 정리해서 따로 관리하고 있습니다. 개선할 아이디어가 있다면 피드백을 주셔도 좋습니다.

 

 자세한 내용은 제안 초안이나 관련 풀 리퀘스트, 관련 이슈를 참고하세요.

 

 

더 똑똑해진 심볼 자동 로드

 

 심볼을 자동으로 로드하는 기능은 코딩 생산성을 높여주는 측면에서 아주 훌륭한 기능이지만 이 기능이 언제나 완벽하게 동작하는 것은 아닙니다. 아직은 개발자가 개입해야만 하는 부분이 있습니다. 저희가 확인한 바로는 TypeScript로 작성된 라이브러리라도 프로젝트에 한번도 로드되지 않은 심볼은 자동으로 로드할 수 없는 현상도 있었습니다.

 

 `@types` 패키지에서는 잘 동작하던 자동 로드 기능이 개별 패키지에서는 왜 동작하지 않을까요? 이 현상의 원인은 심볼 자동 로드 기능이 해당 프로젝트에 한 번이라도 로드된 패키지들 대상으로만 동작하기 때문이었습니다. TypeScript는 기특하게도 node_modules/@types에 있는 패키지를 프로젝트에 자동으로 추가하고 이 패키지 대상으로는 심볼 자동 로드 기능이 잘 동작했습니다. 하지만 일반 node_modules에 설치된 npm 패키지를 크롤링하는 과정은 상대적으로 비용이 높은 작업이기 때문에 심볼 자동 로드 기능이 제대로 동작하지 않는 경우가 있었습니다.

 

 이런 상황은 npm 패키지를 새로 설치한 직후에 프로젝트에 로드해서 사용하려고 할 때도 종종 발생합니다.

 

 TypeScript 4.0은 이런 시나리오를 보완하기 위해 package.json 파일의 dependencies 필드와 peerDependencies 필드를 처리하는 로직을 따로 추가했으며, 이 필드를 처리하기 위해 수집한 정보는 심볼 자동 로드 기능에만 활용되고 타입을 검사할 때는 활용되지 않습니다. 이 정보를 활용하면 node_modules 폴더를 전부 뒤지지 않고 설치한 npm 패키지만 대상으로 심볼 자동 로드 기능을 활용할 수 있습니다.

 

 이런 일은 별로 없겠지만 package.json에 추가된 npm 패키지 중 프로젝트에 아직 로드된 적이 없는 타입 정의 관련 패키지가 10개를 넘어가면 프로젝트 로딩을 방해하지 않기 위해 이 기능이 자동으로 비활성화됩니다. 이 경우에 기능을 명시적으로 활성화하거나 전체를 대상으로 비활성화하려면 에디터에서 환경설정 값을 변경하면 됩니다. Visual Studio Code라면 "Include Package JSON AUto Imports" 메뉴(또는 typescript.preferences.includePackageJsonAutoImports)에서 설정할 수 있습니다.

 

 

 자세한 내용은 제안 이슈와 관련 풀 리퀘스트를 참고하세요.

 

 

웹사이트 개편

 

 TypeScript 웹사이트가 새롭게 개편되었습니다!

 

 

 사이트가 개편된 것은 사실 좀 됐습니다. 하지만 이렇게 바뀐 것을 어떻게 생각하실지 지금이라도 이야기를 들어보고 싶습니다! 사용방법이 궁금하거나 개선할 아이디어가 있다면 웹사이트 이슈로 등록해 주세요.

 

 

Breaking Changes

 

lib.d.ts 수정

 

 lib.d.ts 정의 파일이 수정되었습니다. 특히 DOM과 관련된 내용이 변경되었는데, IE의 오래된 버전에서 사용되던 document.origin이 제거되고 Safari MDN이 권장하는 self.origin이 추가되었습니다.

 

 

프로퍼티-게터/세터 오버라이딩 금지

 

 이전 버전까지는 useDefineForClassFields 옵션을 사용한 상태에서 프로퍼티가 게터/세터를 오버라이딩하거나 게터/세터가 프로퍼티를 오버라이딩하는 문법은 에러로 처리했습니다. 이제는 이 옵션이 없어도 이 시나리오에 해당되면 에러를 발생시킵니다. 이 에러는 클래스 상속관계에서만 발생합니다.


class Base {
    get foo() {
        return 100;
    }
    set foo() {
        // ...
    }
}

class Derived extends Base {
    foo = 10;
//  ~~~
// 에러!
// 'foo'는 `Base` 클래스에서 접근자로 정의되어 있습니다.
// `Derived` 클래스에서 프로퍼티 멤버로 오버라이드할 수 없습니다.
}

 


class Base {
    prop = 10;
}

class Derived extends Base {
    get prop() {
    //  ~~~~
    // 에러!
    // 'prop'는 `Base` 클래스에서 프로퍼티로 정의되어 있습니다.
    // `Derived` 클래스에서 접근자로 오버라이드할 수 없습니다.
        return 100;
    }
}

 

 자세한 내용은 관련 풀 리퀘스트를 참고하세요.

 

 

 

delete 대상은 반드시 옵션 항목이어야 합니다.

 

 strictNullChecks 옵션을 활성화한 상태에서 delete 연산자를 사용한다면 이 연산자의 대상이 반드시 any, unknown, never이거나 옵션 항목(이 경우에는 undefined 타입도 가능)이어야 합니다. 그렇지 않다면 에러가 발생합니다.


interface Thing {
    prop: string;
}

function f(x: Thing) {
    delete x.prop;
    //     ~~~~~~
    // 에러! `delete` 연산자의 대상은 반드시 옵션 항목이어야 합니다.
}

 

 자세한 내용은 관련 풀 리퀘스트를 참고하세요.

 

 

TypeScript 노드 팩토리 지원 중단

 

 지금까지 TypeScript는 AST(Abstract Syntax Tree, 추상 구문 트리) 노드 생성을 위해 팩토리 함수들을 제공했습니다. 그리고 TypeScript 4.0부터는 새로운 API 형태로 노드 팩토리를 제공하기로 결정했습니다. 노드 팩토리 사용방법이 변경되니 자세한 내용을 확인하려면 관련 풀 리퀘스트를 참고하세요.

 

 

그다음은?

 

 이제 릴리즈 노트를 마무리할 때가 되었습니다. 하지만 이 시점에도 TypeScript 4.1 개발 계획은 이미 준비되어 있습니다. TypeScript 4.1 버전에 제공될 기능은 최신 빌드 버전에디터 확장 플러그인을 설치하면 확인해볼 수 있습니다. TypeScript 4.0이든 4.1이든 피드백은 언제든 환영합니다! 트위터GitHub 이슈로 등록해 주세요.

 

 다시 한번 언급하자면, 저희는 커뮤니티 멤버들의 헌신에 크게 신세 지고 있다고 생각합니다. 그래서 앞으로도 TypeScript나 JavaScript를 코딩하는 시간이 순수한 즐거움으로 남도록 만들어 드리고 싶습니다. 이런 목적으로 언어 자체를 개선하고 코딩 환경을 개선하며, 동작 성능을 항상 신경쓰고, 개발 UX를 다시 돌아보면서 코딩과 관련된 모든 경험을 개선할 수 있도록 노력하겠습니다.

 

 감사합니다. 이제 TypeScript 4.0을 즐겨보세요!

 

- Daniel Rosenwasser, TypeScript 팀 드림.

'TypeScript' 카테고리의 다른 글

TypeScript를 무서워하지 않아도 되는 이유  (347) 2016.06.09
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함