ReactNextCentral

기본기 다지기

Published on
타입스크립트 기초부터 고급 기능, 엄격성 설정까지 타입스크립트 사용의 기본 측면을 다룹니다.
Table of Contents

기초

타입스크립트 핸드북의 첫 페이지에 오신 것을 환영합니다.

자바스크립트에서 모든 값은 다양한 연산을 실행할 때 관찰할 수 있는 일련의 행동을 가지고 있습니다. 이것이 추상적으로 들릴 수 있지만 간단한 예로 변수 message에 대해 실행할 수 있는 몇 가지 연산을 고려해 보겠습니다.

// 'message'에서 'toLowerCase' 속성에 접근한 후 호출
message.toLowerCase();
// 'message' 호출
message();

이를 분해하면 첫 번째 실행 가능한 코드 줄은 toLowerCase라는 속성에 접근한 후 호출합니다. 두 번째 줄은 message를 직접 호출하려고 합니다.

그러나 message의 값이 무엇인지 모른다고 가정하면 - 이는 매우 흔한 경우입니다 - 우리는 이 코드를 실행한 결과를 신뢰할 수 없습니다. 각 연산의 동작은 완전히 처음에 어떤 값이 있었는지에 따라 달라집니다.

  • message는 호출 가능한가?
  • toLowerCase라는 속성이 있는가?
  • 있다면, toLowerCase는 호출 가능한가?
  • 이 두 값이 모두 호출 가능하다면, 무엇을 반환하는가?

이러한 질문에 대한 답은 일반적으로 우리가 자바스크립트를 작성할 때 머리 속에 유지하고 모든 세부 사항을 올바르게 파악하기를 바랍니다.

message가 다음과 같이 정의되었다고 가정해 봅시다.

const message = "Hello World!";

아마도 짐작할 수 있듯이 message.toLowerCase()를 실행하면 동일한 문자열이 소문자로만 반환됩니다.

두 번째 코드 줄은 어떨까요? 자바스크립트에 익숙하다면 이것이 예외로 실패한다는 것을 알 것입니다.

TypeError: message is not a function

이러한 실수를 피할 수 있다면 좋을 것입니다.

코드를 실행할 때, 자바스크립트 런타임이 무엇을 할지 결정하는 방법은 값을 기준으로 그 타입 - 어떤 종류의 동작과 기능을 가지고 있는지를 파악함으로써 이루어집니다. 이것이 바로 TypeError가 암시하는 것입니다. 즉, 문자열 "Hello World!"는 함수로 호출될 수 없습니다.

원시 타입인 문자열과 숫자의 경우, typeof 연산자를 사용하여 런타임에 그 타입을 식별할 수 있습니다. 하지만 함수와 같은 다른 것들에 대해서는 그 타입을 식별할 수 있는 해당 런타임 메커니즘이 없습니다. 예를 들어, 다음 함수를 고려해 보세요.

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

코드를 읽음으로써 이 함수가 호출 가능한 flip 속성이 있는 객체를 주어야만 작동한다는 것을 관찰할 수 있지만, 자바스크립트는 이 정보를 코드가 실행되는 동안 확인할 수 있는 방식으로 드러내지 않습니다. 순수한 자바스크립트에서 fn이 특정 값으로 무엇을 하는지 알아내는 유일한 방법은 호출해보고 무슨 일이 일어나는지 보는 것입니다. 이러한 종류의 동작은 코드가 실행되기 전에 코드가 무엇을 할지 예측하기 어렵게 만들어 코드를 작성하는 동안 무엇을 할지 알기가 더 어렵습니다.

이런 방식으로 볼 때, 타입은 fn에 어떤 값들을 전달할 수 있고 어떤 것들이 실패할지를 설명하는 개념입니다. 자바스크립트는 동적 타이핑만을 진정으로 제공합니다. 코드를 실행하여 무슨 일이 일어나는지 보세요.

대안은 코드가 실행되기 전에 코드가 예상되는 동작에 대해 예측할 수 있게 하는 정적 타입 시스템을 사용하는 것입니다.

정적 타입 검사

앞서 문자열을 함수로 호출하려고 할 때 발생했던 TypeError를 생각해 보세요. 대부분의 사람들은 코드를 실행할 때 어떤 종류의 오류도 받고 싶어 하지 않습니다. 그것들은 버그로 간주됩니다! 그리고 새로운 코드를 작성할 때, 우리는 새로운 버그를 도입하지 않도록 최선을 다합니다.

조금의 코드를 추가하고 파일을 저장한 후 코드를 다시 실행하면 바로 오류를 볼 수 있으며 문제를 신속하게 분리할 수 있을지도 모릅니다. 그러나 항상 그런 것은 아닙니다. 충분히 기능을 철저히 테스트하지 않았을 수 있으므로, 발생할 수 있는 잠재적 오류를 절대로 마주치지 않을 수 있습니다! 또는 우리가 운 좋게 오류를 목격했다고 해도, 많은 리팩토링을 수행하고 많은 다른 코드를 추가하게 되어 결국 파고들어야 할 수도 있습니다.

이상적으로는 코드가 실행되기 전에 이러한 버그를 찾아주는 도구가 있었으면 합니다. 타입스크립트와 같은 정적 타입 검사기가 바로 그런 역할을 합니다. 정적 타입 시스템은 프로그램을 실행할 때 우리의 값이 가질 형태와 행동을 설명합니다. 타입스크립트와 같은 타입 검사기는 그 정보를 사용하고 무언가가 잘못되고 있을 수 있다고 우리에게 알려줍니다.

const message = "hello!";
 
message();
// 이 표현식은 호출할 수 없습니다.
// 'String' 타입에는 호출 서명이 없습니다.

타입스크립트로 이 마지막 예제를 실행하면 처음에 코드를 실행하기 전에 오류 메시지를 받게 됩니다.

예외가 아닌 실패

지금까지 우리는 런타임 오류와 같은 특정 사항들에 대해 논의해왔습니다. 자바스크립트 런타임이 무언가 말이 안 된다고 생각하는 경우입니다. 이러한 경우는 ECMAScript 사양이 예상치 못한 상황을 만났을 때 언어가 어떻게 행동해야 하는지에 대한 명시적인 지침이 있기 때문에 발생합니다.

예를 들어, 사양은 호출할 수 없는 것을 시도하려고 하면 오류를 발생시켜야 한다고 말합니다. 아마도 "명백한 행동"처럼 들릴 수 있지만, 객체에 존재하지 않는 속성에 접근하려고 시도하는 것도 오류를 발생시켜야 한다고 상상할 수 있습니다. 대신, 자바스크립트는 다른 행동을 제공하고 undefined 값을 반환합니다.

const user = {
  name: "Daniel",
  age: 26,
};
user.location; // undefined를 반환합니다.

결국 정적 타입 시스템은 시스템에서 오류로 표시해야 하는 코드에 대해 결정을 내려야 합니다. 즉시 오류를 발생시키지 않는 "유효한" 자바스크립트라 할지라도 말입니다. 타입스크립트에서 다음 코드는 location이 정의되지 않았다는 오류를 생성합니다.

const user = {
  name: "Daniel",
  age: 26,
};
 
user.location;
// 'location' 속성은 '{ name: string; age: number; }' 타입에 존재하지 않습니다.

때로는 표현할 수 있는 것에 대한 타협을 의미하기도 하지만, 의도는 우리 프로그램의 정당한 버그를 잡는 것입니다. 그리고 타입스크립트는 많은 정당한 버그를 잡아냅니다.

예를 들어 오타,

const announcement = "Hello World!";
 
// 오타를 얼마나 빨리 찾을 수 있나요?
announcement.toLocaleLowercase();
announcement.toLocalLowerCase();
 
// 우리가 작성하려고 했던 것은 이것이었습니다...
announcement.toLocaleLowerCase();

호출되지 않은 함수,

function flipCoin() {
  // Math.random()을 의미했습니다.
  return Math.random < 0.5;
// '()' => number' 형식과 'number' 형식에 '<' 연산자를 적용할 수 없습니다.
}

또는 기본적인 논리 오류입니다.

const value = Math.random() < 0.5 ? "a" : "b";
if (value !== "a") {
  // ...
} else if (value === "b") {
// '"a"'와 '"b"'에는 겹치는 부분이 없기 때문에 이 비교는 의도하지 않은 것으로 보입니다.
  // 이런, 도달할 수 없는 코드입니다.
}

도구를 위한 타입

타입스크립트는 코드에서 실수할 때 버그를 잡을 수 있습니다. 그것은 훌륭하지만, 타입스크립트는 처음부터 그러한 실수를 하지 않도록 방지할 수도 있습니다.

타입 검사기는 변수와 다른 속성에서 올바른 속성에 접근하고 있는지 등을 검사할 수 있는 정보를 가지고 있습니다. 그 정보를 가지고 나면 사용하고 싶을 수도 있는 속성을 제안하기 시작할 수도 있습니다.

이는 타입스크립트를 코드 편집에도 활용할 수 있음을 의미하며 핵심 타입 검사기는 에디터에서 타이핑하는 동안 오류 메시지와 코드 완성을 제공할 수 있습니다. 이것이 사람들이 타입스크립트에서 도구에 대해 이야기할 때 자주 언급하는 부분입니다.

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

타입스크립트는 도구를 진지하게 받아들이며 이는 타이핑하는 동안의 완성과 오류를 넘어섭니다. 타입스크립트를 지원하는 에디터는 오류를 자동으로 수정하는 "빠른 수정", 코드를 쉽게 재구성하는 리팩터링, 변수의 정의로 점프하거나 주어진 변수에 대한 모든 참조를 찾는 유용한 탐색 기능을 제공할 수 있습니다. 이 모든 것은 타입 검사기 위에 구축되어 있으며 완전히 크로스 플랫폼이므로 좋아하는 에디터에 타입스크립트 지원이 가능합니다.

타입스크립트 컴파일러, tsc

지금까지 타입 검사에 대해 이야기했지만, 아직 타입 검사기를 사용해보지 않았습니다. 이제 우리의 새 친구인 tsc, 타입스크립트 컴파일러와 친해질 시간입니다. 먼저 npm을 통해 가져와 보겠습니다.

npm install -g typescript

이 명령어는 전역적으로 타입스크립트 컴파일러 tsc를 설치합니다. 로컬 node_modules 패키지에서 tsc를 실행하고 싶다면 npx나 비슷한 도구를 사용할 수도 있습니다.

이제 빈 폴더로 이동해 첫 타입스크립트 프로그램인 hello.ts를 작성해봅시다.

// 세계에 인사합니다.
console.log("Hello world!");

여기에는 특별한 것이 없습니다. 이 "hello world" 프로그램은 자바스크립트로 작성한 "hello world" 프로그램과 동일하게 보입니다. 그리고 이제 typescript 패키지에 의해 설치된 tsc 명령어를 실행해 타입 검사를 해봅시다.

tsc hello.ts

타다!

잠깐, 정확히 무슨 "타다"인가요? tsc를 실행했고 아무 일도 일어나지 않았습니다! 타입 오류가 없었기 때문에 콘솔에 아무것도 보고되지 않았습니다.

하지만 다시 확인해보세요. 우리는 파일 출력을 얻었습니다. 현재 디렉토리를 확인하면 hello.ts 옆에 hello.js 파일이 있습니다. 이것은 tsc가 hello.ts 파일을 일반 자바스크립트 파일로 컴파일하거나 변환한 출력물입니다. 그리고 내용을 확인해보면 타입스크립트가 .ts 파일을 처리한 후 내놓은 것을 볼 수 있습니다.

// 세계에 인사합니다.
console.log("Hello world!");

이 경우 타입스크립트가 변환할 것이 거의 없었기 때문에 우리가 작성한 것과 동일하게 보입니다. 컴파일러는 사람이 작성할 것 같은 깔끔하고 읽기 쉬운 코드를 출력하려고 합니다. 항상 그렇게 쉬운 것은 아니지만 타입스크립트는 일관되게 들여쓰기를 하고 코드가 여러 줄에 걸쳐 있을 때를 염두에 두며 주석을 유지하려고 노력합니다.

타입 검사 오류를 도입해보면 어떻게 될까요? hello.ts를 다시 작성해봅시다.

// 산업 등급의 범용 인사 함수입니다.
function greet(person, date) {
  console.log(`Hello ${person}, today is ${date}!`);
}
 
greet("Brendan");

tsc hello.ts를 다시 실행해보면 명령 줄에 오류가 발생하는 것을 알 수 있습니다!

2개의 인수가 필요한데 1개만 받았습니다.

타입스크립트는 greet 함수에 인수를 하나만 전달했다고 알려주고 있으며, 그것은 정당합니다. 지금까지 우리는 표준 자바스크립트만 작성했고 타입 검사는 여전히 우리 코드에 문제를 찾을 수 있었습니다. 감사합니다, 타입스크립트!

오류가 있는 상태에서의 출력

마지막 예제에서 눈치채지 못했을 수 있는 한 가지는 hello.js 파일이 다시 변경되었다는 것입니다. 이 파일을 열어보면 내용이 입력 파일과 거의 같아 보입니다. tsc가 우리 코드에 오류가 있다고 보고했음에도 불구하고 이것이 다소 놀랍게 느껴질 수 있지만, 이는 타입스크립트의 핵심 가치 중 하나에 기반합니다. 대부분의 경우, 당신이 타입스크립트보다 더 잘 알고 있을 것입니다.

앞서 언급했듯이 코드 타입 검사는 실행할 수 있는 프로그램의 종류를 제한하기 때문에 타입 검사기가 수용 가능하다고 판단하는 것들에 대해 타협이 필요합니다. 대부분의 경우에는 괜찮지만, 이러한 검사가 방해가 되는 시나리오도 있습니다. 예를 들어, 자바스크립트 코드를 타입스크립트로 마이그레이션하면서 타입 검사 오류를 도입한다고 상상해보세요. 결국 타입 검사기를 위해 일을 정리하겠지만, 원래의 자바스크립트 코드는 이미 작동하고 있었습니다! 왜 타입스크립트로 전환하는 것이 실행을 중단시켜야 합니까?

그래서 타입스크립트는 당신의 길을 막지 않습니다. 물론 시간이 지나면서 실수에 대해 좀 더 방어적으로 행동하고 싶을 수 있으며 타입스크립트가 좀 더 엄격하게 행동하게 만들고 싶을 수 있습니다. 그런 경우에는 noEmitOnError 컴파일러 옵션을 사용할 수 있습니다. hello.ts 파일을 변경하고 그 플래그와 함께 tsc를 실행해보세요.

tsc --noEmitOnError hello.ts

hello.js가 업데이트되지 않는 것을 알 수 있습니다.

명시적 타입

지금까지 우리는 타입스크립트에 person이나 date가 무엇인지 알려주지 않았습니다. 코드를 수정하여 person을 문자열로, date를 Date 객체로 타입스크립트에 알려주겠습니다. 또한 datetoDateString() 메소드를 사용할 것입니다.

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

우리가 한 일은 persondate에 타입 어노테이션을 추가하여 greet가 호출될 수 있는 값의 타입을 설명한 것입니다. 이 시그니처를 "greet은 타입이 문자열인 person과 타입이 Date인 date를 받습니다"라고 읽을 수 있습니다.

이를 통해 greet이 잘못 호출된 다른 경우에 대해 타입스크립트가 우리에게 알려줄 수 있습니다. 예를 들어...

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

Date()를 자바스크립트에서 호출하면 문자열을 반환합니다. 반면, new Date()로 Date를 생성하면 우리가 기대했던 대로 날짜를 얻을 수 있습니다.

어쨌든, 우리는 쉽게 오류를 수정할 수 있습니다.

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

명시적 타입 어노테이션을 항상 작성할 필요는 없다는 점을 기억하세요. 많은 경우 타입스크립트는 우리가 그것들을 생략하더라도 타입을 추론할 수 있습니다.

let msg = "hello there!";

우리가 타입스크립트에 msg가 문자열 타입이라고 알려주지 않았음에도 타입스크립트는 그것을 알아낼 수 있었습니다. 그것은 기능이며, 타입 시스템이 어차피 같은 타입을 추론하게 될 경우 어노테이션을 추가하지 않는 것이 최선입니다.

지워진 타입

tsc를 사용하여 위의 greet 함수를 컴파일하여 자바스크립트로 출력할 때 어떤 일이 발생하는지 살펴보겠습니다.

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

여기서 두 가지 사항을 주목하세요.

  • 우리의 persondate 매개변수는 더 이상 타입 어노테이션을 가지고 있지 않습니다.
  • 백틱( 문자)을 사용한 “템플릿 문자열”이 일반 문자열과 연결로 변환되었습니다.

두 번째 포인트에 대해서는 나중에 더 자세히 설명하겠지만, 지금은 첫 번째 포인트에 집중해 보겠습니다. 타입 어노테이션은 자바스크립트(또는 엄밀히 말하면 ECMAScript)의 일부가 아니므로, 타입스크립트를 수정하지 않고 실행할 수 있는 브라우저나 다른 런타임이 실제로는 없습니다. 이것이 타입스크립트에 컴파일러가 필요한 이유입니다 - 실행할 수 있도록 타입스크립트 특정 코드를 제거하거나 변환하는 방법이 필요합니다. 대부분의 타입스크립트 특정 코드는 사라지고, 여기서 우리의 타입 어노테이션도 완전히 지워졌습니다.

기억하세요: 타입 어노테이션은 프로그램의 런타임 동작을 결코 변경하지 않습니다.

다운레벨링

위의 변환에서 템플릿 문자열이

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

에서

"Hello ".concat(person, ", today is ").concat(date.toDateString(), "!");

로 바뀐 또 다른 차이점이 있습니다. 이 변환은 왜 일어났을까요?

템플릿 문자열은 ECMAScript 2015(일명 ECMAScript 6, ES2015, ES6 등 - 묻지 마세요)라는 ECMAScript의 버전에서 나온 기능입니다. 타입스크립트는 ECMAScript의 새로운 버전의 코드를 ECMAScript 3 또는 ECMAScript 5(일명 ES3 및 ES5)와 같은 오래된 버전으로 재작성할 수 있는 기능을 가지고 있습니다. 이런 새로운 또는 "높은" 버전의 ECMAScript에서 오래되거나 "낮은" 버전으로 이동하는 과정을 때로는 다운레벨링이라고 합니다.

기본적으로 타입스크립트는 매우 오래된 ECMAScript 버전인 ES3를 대상으로 합니다. target 옵션을 사용하여 조금 더 최근의 것을 선택할 수 있었습니다. --target es2015으로 실행하면 타입스크립트가 ECMAScript 2015를 대상으로 변경되어, ECMAScript 2015가 지원되는 곳이라면 코드가 실행될 수 있음을 의미합니다. 그래서 tsc --target es2015 hello.ts를 실행하면 다음과 같은 출력이 나옵니다.

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

기본 타겟은 ES3이지만, 현재 브라우저의 대다수는 ES2015를 지원합니다. 대부분의 개발자는 특정 고대 브라우저와의 호환성이 중요하지 않다면 안전하게 ES2015 이상을 타겟으로 지정할 수 있습니다.

엄격성

다양한 사용자들이 타입 검사기에서 원하는 것이 다릅니다. 일부 사람들은 프로그램의 일부만 검증하면서도 괜찮은 도구를 제공하는 더 유연한 옵트인 경험을 찾고 있습니다. 이는 타입이 선택 사항이고, 추론이 가장 관대한 타입을 취하며, 잠재적으로 null/undefined 값에 대한 검사가 없는 타입스크립트의 기본 경험입니다. tsc가 오류에 직면했을 때 에밋하는 것처럼, 이러한 기본값은 사용자의 길을 방해하지 않도록 설정되어 있습니다. 기존 자바스크립트를 마이그레이션하는 경우, 그것이 바람직한 첫 단계일 수 있습니다.

반면, 많은 사용자들은 타입스크립트가 가능한 한 많이 검증하기를 바라며 그래서 언어는 엄격한 설정도 제공합니다. 이러한 엄격성 설정은 정적 타입 검사를 스위치(코드가 검사되거나 검사되지 않음)에서 다이얼에 가깝게 변환합니다. 이 다이얼을 더 돌릴수록 타입스크립트는 더 많은 것을 검사합니다. 이는 조금 더 많은 작업을 요구할 수 있지만 일반적으로 장기적으로 그 자체로 보상을 제공하며, 더 철저한 검사와 더 정확한 도구를 가능하게 합니다. 가능한 경우 새 코드베이스는 항상 이러한 엄격한 검사를 켜야 합니다.

타입스크립트는 켜거나 끌 수 있는 여러 타입 검사 엄격성 플래그를 가지고 있으며 모든 예제는 달리 명시되지 않는 한 모두 활성화된 상태로 작성됩니다. CLI의 strict 플래그 또는 tsconfig.json"strict": true는 모두 동시에 켜지지만 개별적으로 선택 해제할 수 있습니다. 알아야 할 가장 큰 두 가지는 noImplicitAnystrictNullChecks입니다.

noImplicitAny

타입스크립트는 일부 위치에서 우리를 위해 타입을 추론하지 않고 대신 가장 관대한 타입인 any로 되돌아갑니다. 이것이 최악의 상황은 아니지만, 종종 타입스크립트를 사용하는 목적을 무너뜨립니다. 프로그램이 더 타입화될수록 더 많은 검증과 도구를 받게 되며, 코딩하는 동안 더 적은 버그에 직면하게 됩니다. noImplicitAny 플래그를 켜면 타입이 암시적으로 any로 추론된 모든 변수에 오류를 발생시킵니다.

strictNullChecks

기본적으로 nullundefined와 같은 값은 다른 타입에 할당할 수 있습니다. 이것은 일부 코드를 작성하기 쉽게 만들지만, nullundefined를 처리하는 것을 잊어버린 것은 세상의 수없이 많은 버그의 원인이 되며 어떤 이들은 그것을 수십억 달러의 실수로 여깁니다! strictNullChecks 플래그는 nullundefined 처리를 더 명시적으로 만들고, 우리가 nullundefined 처리를 잊어버렸는지 걱정할 필요가 없게 해줍니다.