Skip to main content

TypeScript

2021-11-02#

List#

  • TypeScript

타입스크립트?#

  • Language
  • Typed Superset of JavaScript
  • compiles to plain JavaScript

자바스크립트 그리고 확장#

TypeScript는 JavaScript에 구문을 추가하여 편집기와 의 긴밀한 통합을 지원하며, 편집기에서 초기에 오류를 포착할 수 있다.

신뢰할 수 있는 결과#

TypeScript 코드는 JavaScript가 실행되는 모든 환경에서 실행되는 JavaScript로 변환 된다. 예) 브라우저, Node.js 또는 Deno 및 앱

규모에 따른 안전#

TypeScript는 JavaScript를 이해하고 유형 추론을 사용 하여 추가 코드 없이도 훌륭한 도구를 제공한다.

JavaScript는 웹 페이지에 사소한 상호작용을 추가하기 위한 작은 스크립팅 언어로 시작하여, 규모에 상관없이 프론트엔드와 백엔드 애플리케이션에서 선택 가능한 언어로 성장했습니다. JavaScript로 작성된 프로그램의 크기, 범위 및 복잡성은 기하급수적으로 커졌지만, 다른 코드 단위 간의 관계를 표현하는 JavaScript 언어의 능력은 그렇지 못했습니다. JavaScript의 다소 특이한 런타임 의미 체계(runtime semantics)와 더불어, 언어와 프로그램 복잡성 간의 불일치는 JavaScript 개발을 규모에 맞게 관리하기 어려운 작업으로 만들었습니다.

  • 프로그래머들이 작성하는 가장 흔한 오류는 타입 오류이다.
  • 다른 종류의 값이 예상되는 곳에 특정한 값이 사용된 경우이다. (이는 단순한 오타, 라이브러리 API를 이해하지 못한 것, 런타임 동작에 대한 잘못된 가정 또는 다른 오류 때문일 수 있다.)
  • TypeScript의 목표는 JavaScript 프로그램의 정적 타입 검사자이다. (즉, 코드가 실행되기 전에 실행하고(정적), 프로그램 타입이 정확한지 확인하는 도구(타입 검사)이다.)

JavaScript Short History#

JavaScript(ECMAScript으로도 알려져있는)는 처음에 브라우저를 위한 스크립팅 언어로 만들어졌습니다. JavaScript가 처음 나왔을 때, 수십 줄 이상의 코드를 작성하는 것은 다소 이례적인 일이었기에 웹 페이지 속 짧은 코드들을 위해 사용할 것으로 여겨졌습니다. 때문에, 초기 웹 브라우저들은 수십 줄 이상의 코드를 실행하는데 오래 걸렸습니다. 그러나 시간이 흘러 JS가 점점 더 유명해지면서, 웹 개발자들은 JS를 이용해 상호작용을 하는 경험을 하기 시작했습니다.

웹 브라우저 개발자들은 위와 같이 늘어나는 JS 사용량에 대하여 실행 엔진(동적 컴파일)을 최적화시키고 최적화 된 것을 이용해 할 수 있는 일(API 추가)을 확장하여 웹 개발자가 더 많이 JS를 사용할 수 있게 했습니다.

현대 웹사이트에서, 브라우저는 수십만 줄의 코드로 구성된 어플리케이션을 자주 실행합니다. 이는 정적 페이지의 간단한 네트워크로 시작해서, 모든 종류의 만족스러울만한 어플리케이션 을 위한 플랫폼으로 성장한 “웹”의 길고 점진적인 성장입니다.

이외에도, JS는 node.js를 사용하여 JS 서버들을 구현하는 것처럼, 브라우저 맥락에서 벗어나는 일에 사용하기 충분할 정도로 유명해졌습니다. “어디서든 작동됨”과 같은 JS의 성질은 교차 플랫폼(cross-platform) 개발을 위한 매력적인 선택지이기도 합니다. 오늘날 많은 개발자들은 오직 JavaScript만을 이용하여 전체 스택을 프로그래밍하고 있다.

요약하자면, 우리에게는 빠른 사용을 위해 설계되었으며 수백만 줄의 어플리케이션들을 작성하기 위해 만들어진 완벽한 도구인 JavaScript가 있습니다. 모든 언어는 각자의 별난 점 - 이상한 점과 놀랄만한 점이 있으며, JavaScript의 자랑스럽지만은 않은 시작은 많은 문제를 만들게 되었습니다.

예를 들어..#

  • JavaScript의 동일 연산자는 (==) 인수를 강제로 변환하여(coerces), 예기치 않은 동작을 유발합니다:
if ("" == 0) {
// 참입니다! 근데 왜죠??
}
if (1 < x < 3) {
// *어떤* x 값이던 참입니다!
}
  • JavaScript는 또한 존재하지 않는 프로퍼티의 접근을 허용합니다:
const obj = { width: 10, height: 15 };
// 왜 이게 NaN이죠? 철자가 어렵네요!
const area = obj.width * obj.heigth;

대부분의 프로그래밍 언어는 이런 종류의 오류들이 발생하면 오류를 표출해주고, 일부는 코드가 실행되기 전인 컴파일 중에 오류를 표출해줍니다. 작은 프로그램을 작성할 때에는, 이런 이상한 점들이 화를 돋구지만 관리는 가능합니다. 그러나 수백 또는 수천 줄의 어플리케이션들을 작성할 때에는, 이러한 지속적 놀라움들은 심각한 문제를 야기한다.

TypeScript: 정적 타입 검사자 (TypeScript: A Static Type Checker)#

앞서 몇 언어는 버그가 많은 프로그램을 아예 실행시키지 않는다고 했습니다. 프로그램을 실행시키지 않으면서 코드의 오류를 검출하는 것을 정적 검사 라고 합니다. 어떤 것이 오류인지와 어떤 것이 연산 되는 값에 기인하지 않음을 정하는 것이 정적 타입 검사입니다.

정적 타입 검사자 인 TypeScript는 프로그램을 실행시키기 전에 값의 종류 를 기반으로 프로그램의 오류를 찾습니다. 예를 들어, 위의 마지막 예시에 오류가 있는 이유는 obj의 타입 때문입니다.

  • 다음은 TypeScript에서 볼 수 있는 오류입니다:
// @errors: 2551
const obj = { width: 10, height: 15 };
const area = obj.width * obj.heigth;

타입이 있는 JavaScript의 상위 집합 (A Typed Superset of JavaScript)#

그렇다면 TypeScript는 JavaScript와 어떤 관계일까요?

구문 (Syntax)

TypeScript는 JS의 구문이 허용되는, JavaScript의 상위 집합 언어입니다. 구문은 프로그램을 만들기 위해 코드를 작성하는 방법을 의미합니다. 예를 들어, 다음 코드는 )이 없으므로 구문 오류입니다:

// @errors: 1005
let a = (4

TypeScript는 독특한 구문 때문에 JavaScript 코드를 오류로 보지 않습니다. 즉, 어떻게 작성돼있는지 모르지만 작동하는 JavaScript 코드를 TypeScript 파일에 넣어도 잘 작동합니다.

타입 (Types)

그러나 TypeScript는 다른 종류의 값들을 사용할 수 있는 방법이 추가된, 타입이 있는 상위 집합입니다. 위의 obj.heigth 오류는 구문 오류가 아닌, 값의 종류(타입)를 잘못 사용해서 생긴 오류입니다.

또 다른 예시로, 아래와 같은 JavaScript 코드가 브라우저에서 실행될 때, 다음과 같은 값이 출력될 것입니다:

console.log(4 / []);

구문적으로 옳은(syntactically-legal) 위 코드는 JavaScript에서 NaN을 출력합니다. 그러나 TypeScript는 배열로 숫자를 나누는 연산이 옳지 않다고 판단하고 오류를 발생시킵니다:

// @errors: 2363
console.log(4 / []);

실제로 어떤 일이 일어나는지 보려는 의도로 숫자를 배열로 나눌 수 있지만, 대부분은 프로그래밍 실수입니다. TypeScript의 타입 검사자는 일반적인 오류를 최대한 많이 검출하면서 올바른 프로그램을 만들 수 있게 설계되었습니다. (TypeScript가 코드를 얼마나 엄격하게 검사할 수 있는지에 대한 설정 또한 존재)

만약 JavaScript 파일의 코드를 TypeScript 코드로 옮기면, 코드를 어떻게 작성했는지에 따라 타입 오류 를 볼 수 있습니다. 이는 코드 상의 문제이거나, TypeScript가 지나치게 보수적인 것일 수 있습니다. 위와 같은 오류를 제거하기 위해 가이드는 다양한 TypeScript 구문을 추가하는 방법을 보여줍니다.

런타임 특성 (Runtime Behavior)

TypeScript는 JavaScript의 런타임 특성 을 가진 프로그래밍 언어입니다. 예를 들어, JavaScript에서 0으로 나누는 행동은 런타임 예외로 처리하지 않고 Infinity값을 반환합니다. 논리적으로, TypeScript는 JavaScript 코드의 런타임 특성을 절대 변화시키지 않습니다.

즉 TypeScript가 코드에 타입 오류가 있음을 검출해도, JavaScript 코드를 TypeScript로 이동시키는 것은 같은 방식으로 실행시킬 것을 보장합니다

JavaScript와 동일한 런타임 동작을 유지하는 것은 프로그램 작동을 중단시킬 수 있는 미묘한 차이를 걱정하지 않고 두 언어 간에 쉽게 전환할 수 있도록 하기 위한 TypeScript의 기본적인 약속입니다.

삭제된 타입 (Erased Types)

개략적으로, TypeScript의 컴파일러가 코드 검사를 마치면 타입을 삭제해서 결과적으로 “컴파일된” 코드를 만듭니다. 즉 코드가 한 번 컴파일되면, 결과로 나온 일반 JS 코드에는 타입 정보가 없습니다.

타입 정보가 없는 것은 TypeScript가 추론한 타입에 따라 프로그램의 특성 을 변화시키지 않는다는 의미입니다. 결론적으로 컴파일 도중에는 타입 오류가 표출될 수 있지만, 타입 시스템 자체는 프로그램이 실행될 때 작동하는 방식과 관련이 없습니다.

마지막으로, TypeScript는 추가 런타임 라이브러리를 제공하지 않습니다. TypeScript는 프로그램은 JavaScript 프로그램과 같은 표준 라이브러리 (또는 외부 라이브러리)를 사용하므로, TypeScript 관련 프레임워크를 추가로 공부할 필요가 없습니다.

컴파일#

  • 컴파일이 필요 O
  • 컴파일러가 필요 O
  • 컴파일하는 시점 O
  • 컴파일된 결과물을 실행

설치#

npm i typescript -g
# npm init 후 npm i typescript 명령어로 프로젝트에만 tsc 사용도 가능

Visual Studio Code 2015, 2017 이후로는 디폴트로 설치됨

환경설정#

tsc --init
# tsconfig.json 컴파일 시 옵션을 설정

TypeScript Compiler#

  • VS Code에 컴파일러가 내장되어 있다.
  • 내장된 컴파일러버전은 VSCode가 업데이트 되면서 올라간다.

만약 VS Code에 내장된 버전이 아닌 프로젝트 환경 전용으로 사용하고 싶다면 npm install typesript를 package.json 쪽에 버전을 명시하여 사용할 수 있다.

레퍼런스#

2021-11-09#

List#

  • 타입 추론
  • 타입 정의하기 (Defining Types)
  • 타입 구성 (Composing Types)

시작하기 전...#

typescriptlang in 5 minutes

위에글의 번역본을 가져와 글을 작성하였다.

TypeScript for JavaScript Programmers#

프로그래밍 언어에서 TypeScript와 JavaScript의 관계는 다소 독특하다. TypeScript은 JavaScript 위에 레이어로서 자리잡고 있는데, JavaScript의 기능들을 제공하면서 그 위에 자체 레이어를 추가합니다. 이 레이어가 TypeScript 타입 시스템이다.

JavaScript는 이미 string, number, object, undefined 같은 원시 타입을 가지고 있지만, 전체 코드베이스에 일관되게 할당되었는지는 미리 확인해 주지 않는다. TypeScript는 이 레이어로서 동작한다.

이는 이미 존재하고 잘 동작하는 JavaScript 코드는 동시에 TypeScript 코드라는 의미지만, TypeScript의 타입 검사기는 사용자가 생각한 일과 JavaScript가 실제로 하는 일 사이의 불일치를 강조할 수 있다.

이 튜토리얼은 TypeScript가 추가하는 타입 시스템 언어 확장을 이해하는데 중점을 두고 타입 시스템에 대한 5분 개요를 제공한다.

타입 추론 (Types by Inference)#

TypeScript는 JavaScript 언어를 알고 있으며 대부분의 경우 타입을 생성해줄 것입니다. 예를 들어 변수를 생성하면서 동시에 특정 값에 할당하는 경우, TypeScript는 그 값을 해당 변수의 타입으로 사용한다.

let helloWorld = "Hello World";
// ^?

JavaScript가 동작하는 방식을 이해함으로써 TypeScript는 JavaScript 코드를 받아들이면서 타입을 가지는 타입 시스템을 구축할 수 있다. 이는 코드에서 타입을 명시하기 위해 추가로 문자를 사용할 필요가 없는 타입 시스템을 제공하며, 위의 예제에서 TypeScript가 helloWorld가 string임을 알게 되는 방식이다.

JavaScript와 함께 VS Code를 사용하고 작업을 할 때 편집기의 자동 완성 기능을 사용해왔을 것이다. 이는 TypeScript에 필수불가결한 JavaScript에 대한 이해가 JavaScript 작업을 개선하기 위해 내부적으로 사용되었기 때문이다.

타입 정의하기 (Defining Types)#

JavaScript는 다양한 디자인 패턴을 가능하게 하는 동적 언어입니다. 몇몇 디자인 패턴은 자동으로 타입을 제공하기 힘들 수 있는데 (동적 프로그래밍을 사용하고 있을 것이기 때문에) 이러한 경우에 TypeScript는 TypeScript에게 타입이 무엇이 되어야 하는지 명시 가능한 JavaScript 언어의 확장을 지원합니다.

다음은 name: stringid: number을 포함하는 추론 타입을 가진 객체를 생성하는 예제입니다.

const user = {
name: "Hayes",
id: 0,
};

이 객체의 형태를 명시적으로 나타내기 위해서는 interface 로 선언합니다.

interface User {
name: string;
id: number;
}

이제 변수 선언 뒤에 : TypeName의 구문을 사용해 JavaScript 객체가 새로운 interface의 형태를 따르고 있음을 선언할 수 있습니다.

interface User {
name: string;
id: number;
}
// ---cut---
const user: User = {
name: "Hayes",
id: 0,
};

해당 인터페이스에 맞지 않는 객체를 생성하면 TypeScript는 경고를 줍니다.

// @errors: 2322
interface User {
name: string;
id: number;
}
const user: User = {
username: "Hayes",
id: 0,
};

JavaScript는 클래스와 객체 지향 프로그래밍을 지원하기 때문에, TypeScript 또한 동일합니다. - 인터페이스는 클래스로도 선언할 수 있습니다.

interface User {
name: string;
id: number;
}
class UserAccount {
name: string;
id: number;
constructor(name: string, id: number) {
this.name = name;
this.id = id;
}
}
const user: User = new UserAccount("Murphy", 1);

인터페이스는 함수에서 매개변수와 리턴 값을 명시하는데 사용되기도 합니다.

// @noErrors
interface User {
name: string;
id: number;
}
// ---cut---
function getAdminUser(): User {
//...
}
function deleteUser(user: User) {
// ...
}

JavaScript에서 사용할 수 있는 적은 종류의 원시 타입이 이미 있습니다.: boolean, bigint, null, number, string, symbol, object와 undefined는 인터페이스에서 사용할 수 있습니다. TypeScript는 몇 가지를 추기해 목록을 확장합니다.

예를 들어, any (무엇이든 허용합니다), unknown (이 타입을 사용하는 사람이 타입이 무엇인지 선언했는가를 확인하십시오), never (이 타입은 발생될 수 없습니다) void (undefined를 리턴하거나 리턴 값이 없는 함수).

타입을 구축하기 위한 두 가지 구문이 있다는 것을 꽤 빠르게 알 수 있을 것입니다.: Interfaces and Types - interface를 우선적으로 사용하고 특정 기능이 필요할 때 type을 사용해야 합니다.

타입 구성 (Composing Types)#

객체들을 조합하여 더 크고 복잡한 객체를 만드는 방법과 유사하게 TypeScript에 타입으로 이를 수행하는 도구가 있습니다. 여러가지 타입을 이용하여 새 타입을 작성하기 위해 일상적인 코드에서 가장 많이 사용되는 두 가지 코드로는 유니언(Union)과 제네릭(Generic)이 있습니다.

유니언 (Unions)#

유니언은 타입이 여러 타입 중 하나일 수 있음을 선언하는 방법입니다. 예를 들어, boolean 타입을 true 또는 false로 설명할 수 있습니다:

type MyBool = true | false;

참고: MyBool위에 마우스를 올린다면, boolean으로 분류된 것을 볼 수 있습니다 - 구조적 타입 시스템의 프로퍼티며, 나중에 살펴보겠습니다.

유니언 타입이 가장 많이 사용된 사례 중 하나는 값이 다음과 같이 허용되는 string 또는 number의 리터럴집합을 설명하는 것입니다:

type WindowStates = "open" | "closed" | "minimized";
type LockStates = "locked" | "unlocked";
type OddNumbersUnderTen = 1 | 3 | 5 | 7 | 9;

유니언은 다양한 타입을 처리하는 방법을 제공하는데, 예를 들어 array 또는 string을 받는 함수가 있을 수 있습니다.

function getLength(obj: string | string[]) {
return obj.length;
}

TypeScript는 코드가 시간에 따라 변수가 변경되는 방식을 이해하며, 이러한 검사를 사용해 타입을 골라낼 수 있습니다.

TypePredicate
stringtypeof s === "string"
numbertypeof n === "number"
booleantypeof b === "boolean"
undefinedtypeof undefined === "undefined"
functiontypeof f === "function"
arrayArray.isArray(a)

예를 들어, typeof obj === "string"을 이용하여 string과 array를 구분할 수 있으며 TypeScript는 객체가 다른 코드 경로에 있음을 알게 됩니다.

function wrapInArray(obj: string | string[]) {
if (typeof obj === "string") {
return [obj];
// ^?
} else {
return obj;
}
}

제네릭 (Generics)#

TypeScript 제네릭 시스템에 대해 자세히 알아볼 수 있지만, 1분 정도의 수준 높은 설명을 하기 위해, 제네릭은 타입에 변수를 제공하는 방법입니다.

배열이 일반적인 예시이며, 제네릭이 없는 배열은 어떤 것이든 포함할 수 있습니다. 제네릭이 있는 배열은 배열 안의 값을 설명할 수 있습니다.

type StringArray = Array<string>;
type NumberArray = Array<number>;
type ObjectWithNameArray = Array<{ name: string }>;

제네릭을 사용하는 고유 타입을 선언할 수 있습니다:

// @errors: 2345
interface Backpack<Type> {
add: (obj: Type) => void;
get: () => Type;
}
// 이 줄은 TypeScript에 `backpack`이라는 상수가 있음을 알리는 지름길이며
// const backpack: Backpack<string>이 어디서 왔는지 걱정할 필요가 없습니다.
declare const backpack: Backpack<string>;
// 위에서 Backpack의 변수 부분으로 선언해서, object는 string입니다.
const object = backpack.get();
// backpack 변수가 string이므로, add 함수에 number를 전달할 수 없습니다.
backpack.add(23);

구조적 타입 시스템 (Structural Type System)#

TypeScript의 핵심 원칙 중 하나는 타입 검사가 값이 있는 형태에 집중한다는 것입니다. 이는 때때로 “덕 타이핑(duck typing)” 또는 “구조적 타이핑” 이라고 불립니다.

구조적 타입 시스템에서 두 객체가 같은 형태를 가지면 같은 것으로 간주됩니다.

interface Point {
x: number;
y: number;
}
function printPoint(p: Point) {
console.log(`${p.x}, ${p.y}`);
}
// "12, 26"를 출력합니다
const point = { x: 12, y: 26 };
printPoint(point);

point변수는 Point타입으로 선언된 적이 없지만, TypeScript는 타입 검사에서 point의 형태와 Point의 형태를 비교합니다. 둘 다 같은 형태이기 때문에, 통과합니다.

형태 일치에는 일치시킬 객체의 필드의 하위 집합만 필요합니다.

// @errors: 2345
interface Point {
x: number;
y: number;
}
function printPoint(p: Point) {
console.log(`${p.x}, ${p.y}`);
}
// ---cut---
const point3 = { x: 12, y: 26, z: 89 };
printPoint(point3); // prints "12, 26"
const rect = { x: 33, y: 3, width: 30, height: 80 };
printPoint(rect); // prints "33, 3"
const color = { hex: "#187ABF" };
printPoint(color);

마지막으로, 정확하게 마무리 짓기 위해, 구조적으로 클래스와 객체가 형태를 따르는 방법에는 차이가 없습니다:

// @errors: 2345
interface Point {
x: number;
y: number;
}
function printPoint(p: Point) {
console.log(`${p.x}, ${p.y}`);
}
// ---cut---
class VirtualPoint {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
const newVPoint = new VirtualPoint(13, 56);
printPoint(newVPoint); // prints "13, 56"

객체 또는 클래스에 필요한 모든 속성이 존재한다면, TypeScript는 구현 세부 정보에 관계없이 일치하게 봅니다.

2021-11-23#

List#

  • The Basic
  • 정적 타입 검사
  • 명시적 타입
  • 지워진 타입
  • 다운레벨링
  • 엄격도

The Basic#

JavaScript의 모든 값은 저마다 다양한 동작들을 내장하고 있으며 이는 다양한 연산(Operation)을 실행하여 확인할 수 있습니다. 이는 다소 추상적으로 들릴 수 있는데, 간단한 예시로 message라는 이름의 변수에 대하여 실행할 수 있는 몇몇 연산들을 살펴보겠습니다.

// 'message'의 프로퍼티 'toLowerCase'에 접근한 뒤
// 이를 호출합니다
message.toLowerCase();
// 'message'를 호출합니다
message();

위 코드를 분석해보면, 우선 첫 번째 실행 코드 줄에서는 toLowerCase라는 프로퍼티에 접근한 뒤 이를 호출합니다. 두 번째 줄에서는 message를 직접 호출하려 하고 있습니다.

하지만 message의 값이 무엇인지 모른다면 - 일반적으로 그렇습니다 - 위 코드의 실행 결과가 무엇인지 확실히 말할 수 없습니다. 각 연산의 동작은 최초에 어떤 값을 가졌는지에 따라 완전히 달라집니다.

  • message가 호출 가능한가?
  • toLowerCase라는 프로퍼티를 가지는가?
  • 만약 가진다면, toLowerCase 또한 호출 가능한가?
  • 만약 두 값이 모두 호출 가능하다면, 각각이 무엇을 반환하는가?
  • 이 질문들은 우리가 JavaScript로 코드를 작성할 때 흔히 고민하게 되는 것들이며, 우리는 이와 관련된 세세한 부분들을 전부 놓치지 않고 있기를 늘 바라게 됩니다.

message가 아래와 같이 정의되었다고 해봅시다.

const message = 'Hello World!';

익히 짐작하셨겠지만 여기서 message.toLowerCase()를 실행하면, 우리는 동일한 문자열이 소문자로만 이루어져 있는 값을 얻을 것입니다.

그렇다면 앞서 본 코드의 두 번째 라인은 어떨까요? JavaScript가 익숙하시다면, 예외와 함께 실행이 되지 않을 것을 아실 겁니다.

TypeError: message is not a function 이와 같은 실수를 미리 방지할 수 있다면 참 좋을 것 같습니다.

JavaScript 런타임은 코드가 실행될 때 자신이 무엇을 해야 할지 결정하기 위하여 값의 타입, 즉 해당 값이 어떤 동작과 능력을 가지고 있는지를 확인합니다. 이것이 바로 TypeError가 암시하는 바입니다. 위 예시에서는 문자열인 "Hello World"가 함수로서 호출될 수 없다고 말하고 있는 것이죠.

일부 값들, 이를테면 string과 number과 같은 원시 타입의 값의 경우 typeof 연산자를 사용하면 각 값들의 타입을 실행 시점에 알 수 있습니다. 하지만 그 밖의 값들, 이를테면 함수값의 경우, 앞서 언급된 방식과 같이 해당 값의 타입을 실행 시점의 메커니즘은 존재하지 않습니다. 예를 들어, 아래와 같은 함수를 살펴보겠습니다.

function fn(x) {
return x.flip();
}

위 코드를 보면, 인자로 전달된 객체가 호출 가능한 프로퍼티인 flip을 가져야만 위 함수가 잘 작동할 것이라는 것을 우리는 코드를 읽음으로써 알 수 있습니다. 하지만 JavaScript는 우리가 알고 있는 이러한 정보를 코드가 실행되는 동안 알지 못합니다. 순수 JavaScript에서 fn가 특정 값과 어떤 동작을 수행하는지 알 수 있는 유일한 방법은 호출하고 무슨 일이 벌어지는지 보는 것입니다. 이와 같은 동작은 코드 실행 전에 예측을 어렵게 만듭니다. 다시 말해 코드가 어떤 동작 결과를 보일지 코드를 작성하는 동안에는 알기 어렵습니다.

이런 측면에서 볼 때, 타입이란 어떤 값이 fn으로 전달될 수 있고, 어떤 값은 실행에 실패할 것임을 설명하는 개념입니다. JavaScript는 오직 동적 타입만을 제공하며, 코드를 실행해야만 어떤 일이 벌어지는지 비로소 확인할 수 있습니다.

이에 대한 대안은 정적 타입 시스템을 사용하여 코드가 실행되기 전에 코드에 대하여 예측하는 것입니다.

정적 타입 검사#

앞서 살펴본, string을 함수로서 호출하고자 했을 때 얻은 TypeError의 이야기로 돌아가 봅시다. 대부분의 사람들은 코드를 실행했을 때 오류를 보고 싶지 않습니다. 그것은 버그로 여겨집니다! 그리고 새로운 코드를 작성할 때 우리는 새로운 버그를 만들어내지 않도록 최선을 다합니다.

여기서 만약 약간의 코드를 추가하고 파일을 저장한 뒤, 코드를 다시 실행했을 때 바로 오류가 확인된다면, 문제를 신속하게 격리시킬 수 있을 것입니다. 하지만 항상 그렇게 되는 것은 아닙니다. 기능을 충분히 테스트하지 않아서, 잠재적인 오류를 미처 발견하지 못할 수도 있습니다! 또는 운 좋게 오류를 발견했더라도, 결국 상당한 규모의 리팩토링을 거치고 새 코드를 추가하면서 의도치 않게 코드를 깊게 파헤치게 될 수도 있습니다.

이상적으로는, 코드를 실행하기 전에 이러한 버그를 미리 발견할 수 있는 도구가 있다면 좋을 것입니다. TypeScript와 같은 정적 타입 검사기의 역할이 바로 그것입니다. 정적 타입 시스템은 우리가 작성한 프로그램에서 사용된 값들의 형태와 동작을 설명합니다. TypeScript와 같은 타입 검사기는 이 정보를 활용하여 프로그램이 제대로 작동하지 않을 때 우리에게 알려줍니다.

const message = 'hello!';
message();
// This expression is not callable.
// Type 'String' has no call signatures.

위의 마지막 예시를 TypeScript로 실행하면, 코드가 실행되기에 앞서 우선 오류 메시지를 확인하게 됩니다.

예외가 아닌 실행 실패#

지금까지 런타임 오류에 대하여 다루었습니다. 이는 JavaScript 런타임이 무언가 이상하다고 우리에게 직접 말해주는 경우에 해당합니다. 이러한 오류는 예기치 못한 문제가 발생했을 때 JavaScript가 어떻게 대응해야 하는지 ECMAScript 명세에서 명시적인 절차를 제공하기 때문에 발생하는 것입니다.

예를 들어, 명세에 따르면 호출 가능하지 않은 것에 대하여 호출을 시도할 경우 오류가 발생합니다. 이는 “당연한 동작”처럼 들릴 수 있겠으나, 누군가는 객체에 존재하지 않는 프로퍼티에 접근을 시도했을 때에도 역시 오류를 던져야 한다고 생각할 수 있습니다. 하지만 그 대신 JavaScript는 전혀 다르게 반응하며 undefined를 반환합니다.

const user = {
name: 'Daniel',
age: 26,
};
user.location; // undefined 를 반환

궁극적으로, 정적 타입 시스템은 어떤 코드가 오류를 발생시키지 않는 “유효한” JavaScript 코드일지라도, 정적 타입 시스템 내에서 오류로 간주되는 경우라면 이를 알려주어야 합니다. TypeScript에서는, 아래의 코드는 location이 정의되지 않았다는 오류를 발생시킵니다.

const user = {
name: 'Daniel',
age: 26,
};
user.location;
// Property 'location' does not exist on type '{ name: string; age: number; }'.

비록 때로는 이로 인하여 표현의 유연성을 희생해야 하겠지만, 이렇게 함으로서 명시적인 버그는 아니지만 버그로 타당히 간주되는 경우를 잡아내는 데에 그 목적이 있습니다. 그리고 TypeScript는 이러한 겉으로 드러나지 않는 버그를 꽤 많이 잡아냅니다.

예를 들어, 오타,

const announcement = 'Hello World!';
// 바로 보자마자 오타인지 아실 수 있나요?
announcement.toLocaleLowercase();
announcement.toLocalLowerCase();
// 아마 아래와 같이 적으려 했던 것이겠죠...
announcement.toLocaleLowerCase();

호출되지 않은 함수,

function flipCoin() {
// 본래 의도는 Math.random()
return Math.random < 0.5;
Operator '<' cannot be applied to types '() => number' and 'number'.
}

또는 기본적인 논리 오류 등이 있습니다.

const value = Math.random() < 0.5 ? "a" : "b";
if (value !== "a") {
// ...
} else if (value === "b") {
This condition will always return 'false' since the types '"a"' and '"b"' have no overlap.
// 이런, 이 블록은 실행되지 않겠군요
}

프로그래밍 도구로서의 타입#

TypeScript는 우리가 코드 상에서 실수를 저질렀을 때 버그를 잡아줍니다. 그거 좋죠, 그런데 TypeScript는 여기서 더 나아가서 우리가 실수를 저지르는 바로 그 순간 이를 막아줍니다.

타입 검사기는 우리가 변수 또는 다른 프로퍼티 상의 올바른 프로퍼티에 접근하고 있는지 여부를 검사할 수 있도록 관련 정보들을 가지고 있습니다. 이 정보를 활용하면 타입 검사기는 우리가 사용할 수 있는 프로퍼티를 제안할 수 있게 됩니다.

즉, TypeScript는 코드 수정에 활용될 수 있고, 우리가 코드를 입력할 때 오류 메시지를 제공하거나 코드 완성 기능을 제공할 수 있습니다. 이는 TypeScript에서 도구(Tooling)를 논할 때에 흔히 언급되는 내용입니다.

import express from 'express';
const app = express();
app.get('/', function (req, res) {
res.sen;
// send
// sendDate
// sendfile
// sendFile
// sendStatus
});
app.listen(3000);

TypeScript는 프로그래밍 도구를 중요하게 생각하며, 여기에는 코드 완성 및 오류 메시지 기능 이외에도 다양한 것이 포함됩니다. TypeScript를 지원하는 코드 편집기는 오류를 자동으로 고쳐주는 “Quick Fixes”, 코드를 간편하게 재조직하는 리팩토링, 변수의 정의로 빠르게 이동하는 유용한 네비게이션, 주어진 변수에 대한 모든 참조 검색 등의 기능들을 제공합니다. 이 모든 기능들은 타입 검사기를 기반으로 하며 완전히 크로스 플랫폼으로 동작하므로, 여러분이 주로 사용하는 코드 편집기가 TypeScript를 지원할 확률이 높습니다.

tsc, TypeScript 컴파일러#

지금까지 계속 타입 검사에 대하여 이야기했지만, 아직 타입 검사기를 사용하지 않았습니다. 우리의 새로운 친구 tsc, TypeScript 컴파일러와 첫인사를 나누도록 합시다. 우선, npm을 사용하여 설치하도록 하겠습니다.

npm install -g typescript

위 코드를 실행하면 TypeScript 컴파일러 tsc가 전역 설치됩니다. tsc를 로컬 node_modules 패키지로부터 실행하고자 한다면 npx 또는 유사한 도구를 사용하면 됩니다.

이제 빈 폴더로 이동하여 첫번째 TypeScript 프로그램인 hello.ts를 작성해보도록 하겠습니다.

// 세상을 맞이하세요.
console.log('Hello world!');

코드 상에 아무런 밑줄도 그어지지 않았음에 유의하세요. 이 “hello world” 프로그램은 JavaScript로 작성하는 “hello world” 프로그램과 동일한 모습을 가집니다. 그리고 이제 typescript 패키지와 함께 설치된 tsc 명령어를 실행하여 타입 검사를 수행합니다.

tsc hello.ts

짜잔!

잠깐, 정확히 무엇이 “짜잔”하고 나왔다는 것이죠? tsc를 실행했지만 아무 일도 일어나지 않았습니다! 뭐, 타입 오류가 없었으니, 아무것도 보고될 것이 없고 그래서 콘솔에도 아무런 출력이 나타나지 않았습니다.

하지만 다시 확인해보면, 우리는 그 대신 파일 출력을 얻었습니다. 현재 디렉토리를 보면, hello.ts 파일 옆에 hello.js 파일이 있는 것을 볼 수 있습니다. 이것이 tsc가 우리의 hello.ts 파일을 JavaScript 파일로 컴파일 또는 변형한 결과물입니다. 그리고 그 내용을 확인해보면, TypeScript가 .ts 파일을 처리한 뒤 뱉어낸 내용을 확인할 수 있습니다.

// 세상을 맞이하세요.
console.log('Hello world!');

위 경우, TypeScript가 변형해야 할 내용은 극히 적었고, 따라서 우리가 처음에 작성한 것과 동일한 결과물이 나왔습니다. 컴파일러는 사람이 작성한 듯이 깔끔하고 읽을 수 있는 코드를 만들어내고자 시도합니다. 물론 그것이 항상 쉬운 것은 아니지만, TypeScript는 일관성 있게 들여 쓰기를 수행하고, 여러 줄에 걸쳐 코드가 작성되는 것을 감안하고, 코드 주변에 작성된 주석도 잘 배치해둡니다.

만약 타입 검사 오류가 주어지면 어떨까요? hello.ts를 다시 작성해보겠습니다.

// 아래는 실무 수준에서 범용적으로 쓰이는 환영 함수입니다
function greet(person, date) {
console.log(`Hello ${person}, today is ${date}!`);
}
greet('Brendan');

여기서 tsc hello.ts를 다시 실행하면, 커맨드 라인 상에서 오류를 얻게 된다는 점에 유의하세요!

Expected 2 arguments, but got 1.

TypeScript는 greet 함수에 인자를 전달하는 것을 깜빡했다고 말해주고 있으며, 실제로 그렇습니다. 지금까지 우리는 오직 표준적인 JavaScript만을 작성했을 뿐인데, 여전히 타입 검사를 통하여 코드 상의 문제를 발견해낼 수 있었습니다. 고마워, TypeScript!

오류 발생시키기#

앞서 살펴 본 예시에서 눈치 채셨는지 모르겠지만, hello.js 파일의 내용이 또 한 번 수정되었습니다. 파일을 열어보면, 입력으로 사용된 코드 파일과 실질적으로 동일하다는 것을 알 수 있습니다. 우리 코드를 보고 tsc가 오류를 발생시켰다는 점을 감안하면 다소 놀랍게 느껴질 수도 있지만, 이는 TypeScript의 핵심 가치 중 하나에 기반한 동작입니다. 그것은 바로, 대부분의 경우 당신이 TypeScript보다 더 잘 알고 있을 것이라는 생각입니다.

앞에서도 말씀드렸듯이 코드에 대한 타입 검사는 프로그램이 실행할 수 있는 동작을 제한합니다. 따라서 타입 검사가 허용 또는 제한하는 동작의 범위에는 어느 정도 절충과 타협이 존재합니다. 대부분의 경우 문제가 발생하지 않지만, 타입 검사가 방해가 되는 시나리오 또한 존재합니다. 예를 들어, JavaScript로 작성된 코드를 TypeScript로 마이그레이션하는 과정에서 타입 검사 오류가 발생하는 경우를 떠올려보세요. 결국에는 타입 검사를 통과하도록 코드를 수정해나가겠지만, 사실 원본 JavaScript 코드는 이미 제대로 잘 작동하고 있는 상태였습니다! TypeScript로 변환하는 작업 때문에 코드 실행이 중단되어야 할 이유가 있을까요?

그래서 TypeScript는 당신을 방해하지 않습니다. 물론, 시간이 흐름에 따라 좀 더 실수에 방어적으로 대응하고, TypeScript가 보다 엄격하게 동작하기를 원할 수도 있습니다. 이 경우 --noEmitOnError 컴파일러 옵션을 사용하면 됩니다. hello.ts 파일을 수정한 뒤 위 플래그 옵션을 사용하여 tsc를 실행해보세요.

tsc --noEmitOnError hello.ts

hello.js가 하나도 수정되지 않는다는 것을 확인할 수 있습니다.

명시적 타입#

지금까지는 아직 TypeScript에게 person 또는 date가 무엇인지 알려주지 않았습니다. 코드를 수정하여 TypeScript가 person이 string이고 date가 Date 객체이어야 한다는 것을 알려주도록 하죠. 또한 date의 toDateString() 메서드를 사용하겠습니다.

function greet(person: string, date: Date) {
console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}

방금 우리는 person과 date에 대하여 타입 표기를 수행하여 greet가 호출될 때 함께 사용될 수 있는 값들의 타입을 설명했습니다. 해당 시그니처는 ”greet는 string 타입의 person과 Date 타입의 date을 가진다”고 해석할 수 있습니다.

이것이 있다면, TypeScript는 우리가 해당 함수를 올바르지 못하게 사용했을 경우 이를 알려줄 수 있게 됩니다. 예를 들어…

function greet(person: string, date: Date) {
console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
greet("Maddison", Date());
Argument of type 'string' is not assignable to parameter of type 'Date'.

어? TypeScript가 두번째 인자에 대하여 오류를 보고했는데요, 왜 그랬을까요?

아마도 놀랍게도, JavaScript에서 Date()를 호출하면 string을 반환합니다. 반면, new Date()를 사용하여 Date 타입을 생성해야 비로소 처음 기대했던 결과를 반환받을 수 있게 됩니다.

어쨌든, 이 오류는 아주 빠르게 고칠 수 있습니다.

function greet(person: string, date: Date) {
console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
greet('Maddison', new Date());

명시적인 타입 표기를 항상 작성할 필요는 없다는 것을 꼭 기억해두세요. 많은 경우, TypeScript는 생략된 타입 정보를 추론할 수 (또는 “알아낼 수”) 있습니다.

let msg = 'hello there!';
let msg: string;

msg가 string 타입을 가진다는 사실을 TypeScript에게 알려주지 않았더라도 TypeScript는 이를 알아낼 수 있습니다. 이는 기본 기능이며, 타입 시스템이 알아서 올바른 타입을 어떻게든 잘 알아낼 수 있다면 타입 표기를 굳이 적지 않는 것이 가장 좋습니다.

참고: 바로 위 코드의 말풍선은 에디터에서 해당 코드를 작성했을 때, 해당 변수에 마우스 호버시 화면에 나타나는 내용입니다.

지워진 타입#

앞서 작성한 함수 greet을 tsc로 컴파일하여 JavaScript 출력을 얻었을 때 어떤 일이 일어나는지 보도록 하겠습니다.

'use strict';
function greet(person, date) {
console.log('Hello ' + person + ', today is ' + date.toDateString() + '!');
}
greet('Maddison', new Date());

여기서 두 가지를 알 수 있습니다.

person과 date 인자는 더 이상 타입 표기를 가지지 않습니다. “템플릿 문자열” - 백틱(` 문자)을 사용하여 작성된 문장 - 은 연결 연산자(+)로 이루어진 일반 문자열로 변환되었습니다. 두번째 항목에 대하여서는 이후 자세히 다로도록 하고 우선 첫번째 항목에 초점을 두도록 하겠습니다. 타입 표기는 JavaScript(또는 엄밀히 말하여 ECMAScript)의 일부가 아니므로, TypeScript를 수정 없이 그대로 실행할 수 있는 브라우저나 런타임을 현재 존재하지 않습니다. 이것이 TypeScript를 사용하고자 할 때 다른 무엇보다도 컴파일러가 필요한 이유입니다. TypeScript 전용 코드를 제거하거나 변환하여 실행할 수 있도록 만들 방법이 필요합니다. 대부분의 TypeScript 전용 코드는 제거되며, 마찬가지로 타입 표기 또한 완전히 지워집니다.

기억하세요: 타입 표기는 프로그램의 런타임 동작을 전혀 수정하지 않습니다.

다운레벨링#

앞서 언급된 또 다른 차이점은, 바로 템플릿 문자열이 아래의 내용에서,

`Hello ${person}, today is ${date.toDateString()}!`;

아래의 내용으로 다시 작성되었다는 점입니다.

'Hello ' + person + ', today is ' + date.toDateString() + '!';

왜 이러한 일이 생겼을까요?

템플릿 문자열은 ECMAScript 2015(a.k.a. ECMAScript 6, ES2015, ES6, 등. 더 묻지 마세요)라고 불리는 버전의 ECMAScript에서 등장한 기능입니다. TypeScript는 새 버전의 ECMAScript의 코드를 ECMAScript 3 또는 ECMAScript 5와 같은 보다 예전 버전의 것들로 다시 작성해 줍니다. 새로운 또는 “상위” 버전의 ECMAScript를 예전의 또는 “하위” 버전의 것으로 바꾸는 과정을 다운레벨링이라 부르기도 합니다.

TypeScript는 ES3라는 아주 구버전의 ECMAScript를 타겟으로 동작하는 것이 기본 동작입니다. --target 플래그를 설정하면 보다 최근 버전을 타겟으로 변환을 수행할 수도 있습니다. --target es2015를 실행하면 TypeScript가 ECMAScript 2015를 타겟으로 동작할 수 있으며, 이는 ECMAScript 2015가 지원되는 런타임이기만 하면 해당 코드가 실행될 수 있도록 변환된다는 의미입니다. 따라서 tsc --target es2015 input.ts를 실행하면 아래와 같은 출력을 얻게 됩니다.

function greet(person, date) {
console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
greet('Maddison', new Date());

타겟 버전의 기본값은 ES3이지만, 현존하는 대다수의 브라우저들은 ES2015를 지원하고 있습니다. 따라서 특정 구버전 브라우저에 대한 호환성 유지가 주요 이슈가 아니라면, 대부분의 경우 안심하고 ES2015 또는 그 이상을 컴파일러 타겟으로 지정할 수 있습니다.

엄격도#

TypeScript의 타입 검사기를 사용하는 목적은 사용자마다 다양합니다. 누군가는 프로그램 일부만 타입 검사를 수행하는 느슨한 수준을 유지하면서도, 유용한 프로그래밍 도구로서의 기능은 온전히 활용하고 싶을 수 있습니다. 이는 TypeScript를 사용할 때 기본으로 제공하고자 하는 경험입니다. 타입 검사는 선택 사항이며, 타입 추론은 가장 관대한 기준으로 이루어지고, 잠재적인 null/undefined 값에 대한 검사는 이루어지지 않습니다. 이러한 기본 경험은 개발 경험을 방해하지 않는 방향으로 이루어집니다.

앞서 언급하였던, 오류 발생 시 tsc가 오류를 처리하는 방식과 유사합니다. 기존의 JavaScript에서 마이그레이션을 하는 입장이라면, 이는 첫발을 디디기 위한 적당한 수준이라고 볼 수 있습니다.

이와 반대로, 대다수의 사용자들은 TypeScript가 최대한으로 타입 검사를 수행해주기를 선호합니다. 이것이 TypeScript에서 엄격도 설정을 제공하는 이유이기도 합니다. 이러한 엄격도 설정을 활용하면 정적 타입 검사기를 마치 (코드 검사가 이루어졌는지 여부만을 단순히 따지는) 스위치 수준의 장치에서 마치 다이얼에 가까운 장치로 만들 수 있습니다. 다이얼을 더 멀리 돌릴수록, TypeScript는 더 많은 것을 검사해줄 겁니다. 그러면 할 일이 조금 더 생기겠지만, 길게 봤을 때 분명 그만한 가치가 있으며, 보다 철저한 검사와 정밀한 도구 기능을 사용할 수 있게 됩니다. 가능하다면, 새로 작성하는 코드에서는 항상 엄격도를 활성화해야 합니다.

TypeScript에는 켜고 끌 수 있는 타입 검사 엄격도 플래그가 몇 가지 존재하며, 앞으로 사용되는 모든 예시 코드는 별도 설명이 없다면 모든 플래그를 활성화한 상태로 작성됩니다. CLI에서 --strict 플래그를 설정하거나 tsconfig.json"strict": true를 추가하면 모든 플래그를 동시에 활성화하게 되지만, 각각의 플래그를 개별적으로 끌 수도 있습니다. 반드시 알아야 하는 두 가지 가장 주요한 옵션은 noImplicitAnystrictNullChecks 입니다.

noImplicitAny#

몇몇 경우에서 TypeScript는 값의 타입을 추론하지 않고 가장 관대한 타입인 any로 간주한다는 사실을 기억하시기 바랍니다. 이는 최악의 경우는 아닙니다. 어쨌든, 타입을 any로 간주하는 것은 일반적인 JavaScript에서는 당연한 일이기도 합니다.

하지만, any를 사용하면 애초에 TypeScript를 사용하는 이유가 무색해지는 경우가 많습니다. 프로그램에서 타입을 더 구체적으로 사용할수록, 더 많은 유효성 검사와 도구 기능을 사용할 수 있으며, 이는 곧 코드 상에서 보다 적은 버그를 만나게 된다는 의미입니다. noImplicitAny 플래그를 활성화하면 타입이 any로 암묵적으로 추론되는 변수에 대하여 오류를 발생시킵니다.

strictNullChecks#

null과 undefined와 같은 값은 다른 타입의 값에 할당할 수 있는 것이 기본 동작입니다. 이는 코드 작성을 쉽게 만들어주지만, null과 undefined의 처리를 잊는 것은 세상의 샐 수 없이 많은 버그들의 원인입니다. 혹자는 이를 백만 불 짜리 실수라고 일컫기도 합니다! strictNullChecks 플래그는 null과 undefined를 보다 명시적으로 처리하며, nullundefined 처리를 잊었는지 여부를 걱정하는 데에서 우리를 해방시켜 줍니다.