목록으로

프로그래밍 · TypeScript

TypeScript 완전정복 3화: 컴파일과 트랜스파일 실행 과정 이해하기

BeanCon
TypeScript 코드가 컴파일러를 거쳐 JavaScript로 변환되고 실행되는 과정을 설명하는 대표 이미지

TypeScript 완전정복 3화입니다. TypeScript 코드가 JavaScript로 변환되고 실행되는 과정을 정리했습니다. tsc 컴파일러의 소스 읽기, 문법 검사, 타입 검사, JavaScript 생성, 타입 정보 삭제, 컴파일과 트랜스파일의 차이, target 설정, 컴파일 오류와 실행 오류, noEmit과 watch 모드까지 실습 흐름으로 다룹니다.

목차

메타 설명:TypeScript 코드가 JavaScript로 변환되고 실행되는 전체 과정을 알아봅니다. 컴파일과 트랜스파일의 차이, 타입 검사, 타입 정보 삭제, target 설정, 컴파일 오류와 실행 오류의 차이를 실습 코드와 함께 쉽게 설명합니다.

지난 시간에는 TypeScript 개발에 필요한 장비를 준비했습니다.

* Node.js 설치* npm 확인* VS Code 설치* TypeScript 컴파일러 설치* 첫 번째 `.ts` 파일 작성* `.ts` 파일을 `.js` 파일로 변환* Node.js로 JavaScript 실행

우리가 사용한 핵심 명령어는 다음과 같았습니다.

npx tsc hello.ts
node hello.js

명령어를 입력하자 `hello.ts` 옆에 `hello.js`가 생성됐습니다.

hello.ts
   ↓
hello.js

하지만 아직 풀리지 않은 수수께끼가 있습니다.

`npx tsc hello.ts`를 실행하는 동안 내부에서는 무슨 일이 일어난 걸까요?

TypeScript 컴파일러는 단순히 파일 확장자를 `.ts`에서 `.js`로 바꾼 것이 아닙니다.

코드를 읽고, 문법을 분석하고, 타입을 확인하고, 필요하면 최신 JavaScript 문법을 이전 문법으로 바꾼 뒤 JavaScript 파일을 생성합니다.

오늘은 TypeScript 코드가 지나가는 비밀 통로를 추적해 보겠습니다.

1. 오늘의 추적 미션

이번 편에서는 다음 질문에 답합니다.

* TypeScript 코드는 어떤 단계를 거쳐 실행될까?* 컴파일러는 코드를 어떻게 검사할까?* 타입 정보는 JavaScript 파일에서 왜 사라질까?* 컴파일과 트랜스파일은 무엇이 다를까?* `target`을 바꾸면 결과물이 어떻게 달라질까?* 타입 오류가 있어도 JavaScript 파일이 생성될 수 있을까?* 컴파일 오류와 실행 오류는 무엇이 다를까?* 최신 Node.js는 TypeScript 파일을 직접 실행할 수 있을까?

오늘의 최종 목표는 이 흐름을 머릿속에 그리는 것입니다.

TypeScript 작성
      ↓
문법 분석
      ↓
타입 검사
      ↓
JavaScript 변환
      ↓
JavaScript 실행

이제 `.ts` 파일을 따라 컴파일 공항으로 들어가 보겠습니다.

2. TypeScript 코드는 누가 실행할까?

다음 TypeScript 코드가 있습니다.

const userName: string = "김타입";
const level: number = 3;

console.log(`${userName}님의 레벨은 ${level}입니다.`);

파일 이름은 `player.ts`라고 하겠습니다.

player.ts

이 코드를 실행하려면 먼저 다음 질문에 답해야 합니다.

TypeScript 코드를 실제로 실행하는 주체는 누구일까요?

일반적인 TypeScript 프로젝트에서는 다음과 같이 역할이 나뉩니다.

TypeScript 컴파일러
코드를 검사하고 JavaScript로 변환

Node.js 또는 웹 브라우저
변환된 JavaScript를 실행

Node.js는 기본적으로 JavaScript 실행 환경입니다. TypeScript는 JavaScript에 타입 문법을 추가한 언어이며, `tsc`는 TypeScript 코드를 검사하고 JavaScript 결과물을 생성합니다.

따라서 우리가 배울 기본 실행 흐름은 다음과 같습니다.

npx tsc player.ts
node player.js

첫 번째 명령어는 변환입니다.

npx tsc player.ts

두 번째 명령어는 실행입니다.

node player.js

둘은 서로 다른 작업입니다.

3. TypeScript 코드의 전체 여행 경로

TypeScript 코드의 여행을 조금 더 자세히 펼쳐 보겠습니다.

1. 개발자가 player.ts를 작성한다.
              ↓
2. TypeScript 컴파일러가 코드를 읽는다.
              ↓
3. 문법 구조를 분석한다.
              ↓
4. 변수와 함수의 타입을 확인한다.
              ↓
5. 타입 오류를 개발자에게 알려준다.
              ↓
6. 타입 문법을 제거한다.
              ↓
7. 설정에 맞는 JavaScript를 생성한다.
              ↓
8. Node.js 또는 브라우저가 JavaScript를 실행한다.

TypeScript의 핵심 목표 중 하나는 코드가 실행되기 전에 타입을 검사하는 것입니다. 공식 핸드북에서도 TypeScript를 JavaScript 프로그램을 위한 정적 타입 검사기로 설명합니다.

이제 각 단계를 하나씩 확대해 보겠습니다.

4. 첫 번째 관문: 소스 코드 읽기

먼저 TypeScript 컴파일러는 우리가 작성한 파일을 읽습니다.

const productName: string = "기계식 키보드";
const price: number = 89000;

console.log(productName, price);

컴파일러는 이 코드를 단순한 글자 덩어리로만 보지 않습니다.

다음과 같은 구성 요소로 나누어 이해합니다.

const              변수 선언
productName        변수 이름
string             타입
"기계식 키보드"     값
console.log        함수 호출

우리가 문장을 읽을 때 주어, 목적어, 동사를 구분하는 것과 비슷합니다.

컴파일러도 코드에서 변수 선언, 함수 호출, 연산자, 값 등을 구분합니다.

컴파일러의 첫인상

컴파일러가 코드를 처음 만났다고 상상해 보겠습니다.

const price: number = 89000;

컴파일러의 머릿속은 대략 이렇습니다.

“`price`라는 상수를 만들었군요.”
“타입은 `number`군요.”
“초기값 89000도 숫자군요.”
“현재까지는 평화롭습니다.”

그런데 다음 코드가 등장합니다.

const price: number = "팔만 구천 원";

컴파일러의 표정이 달라집니다.

“잠깐만요. 숫자 좌석에 문자열 승객이 앉았습니다.”

이제 타입 검사가 시작됩니다.

5. 두 번째 관문: 문법 검사

타입을 검사하기 전, 코드 자체가 문법적으로 올바른지 확인해야 합니다.

다음 코드를 살펴보겠습니다.

const message: string = "안녕하세요"
console.log(message);

세미콜론이 없지만 TypeScript와 JavaScript에서는 문맥에 따라 정상적으로 처리될 수 있습니다.

하지만 다음 코드는 문제가 있습니다.

const message: string = ;

값이 들어가야 할 자리가 비어 있습니다.

컴파일하면 문법 오류가 나타납니다.

npx tsc syntax-error.ts

오류 메시지는 다음과 비슷합니다.

Expression expected.

사람의 언어로 바꾸면 다음과 같습니다.

“등호 뒤에 값이 올 줄 알았는데 아무도 오지 않았습니다.”

문법 오류와 타입 오류

문법 오류와 타입 오류는 서로 다릅니다.

문법 오류

코드 문장 자체가 규칙에 맞지 않습니다.

const age: number = ;

타입 오류

문법은 맞지만 값의 종류가 맞지 않습니다.

const age: number = "스물다섯";

두 번째 코드는 문장 구조 자체는 정상입니다.

변수도 있고, 타입도 있고, 값도 있습니다.

다만 `number` 자리에 `string`이 들어왔기 때문에 타입 오류가 발생합니다.

6. 세 번째 관문: 타입 보안검색대

문법 검사를 통과하면 TypeScript의 대표 기능인 타입 검사가 시작됩니다.

다음 코드를 보겠습니다.

function calculateTotal(
  price: number,
  quantity: number
): number {
  return price * quantity;
}

컴파일러는 함수에 대한 정보를 기억합니다.

함수 이름: calculateTotal
첫 번째 매개변수: number
두 번째 매개변수: number
반환값: number

정상적인 호출은 다음과 같습니다.

calculateTotal(10000, 3);

숫자 두 개를 전달했으므로 보안검색대를 통과합니다.

하지만 다음 호출은 붙잡힙니다.

calculateTotal("10000", 3);

첫 번째 값이 문자열이기 때문입니다.

Argument of type 'string' is not assignable
to parameter of type 'number'.

컴파일러 통역기를 작동시키면 다음과 같습니다.

“첫 번째 매개변수에는 숫자가 필요합니다. 문자열은 입장할 수 없습니다.”

7. 타입 검사는 코드를 실행하지 않는다

여기서 반드시 기억해야 할 사실이 있습니다.

TypeScript의 타입 검사는 프로그램을 실제로 실행하는 과정이 아닙니다.

다음 코드를 보겠습니다.

const firstNumber: number = 10;
const secondNumber: number = 20;

console.log(firstNumber + secondNumber);

TypeScript는 두 변수가 숫자인지 확인할 수 있습니다.

하지만 타입 검사 단계에서 실제로 `10 + 20`을 계산해 프로그램의 모든 동작을 대신 실행하는 것은 아닙니다.

타입 검사의 주요 관심사는 다음과 같습니다.

firstNumber는 숫자인가?
secondNumber는 숫자인가?
+ 연산을 적용할 수 있는가?
console.log에 전달할 수 있는가?

실제 계산과 출력은 JavaScript 실행 단계에서 일어납니다.

타입 검사관과 실행 담당자

두 역할을 캐릭터로 나누어 보겠습니다.

타입 검사관

이 값은 숫자인가?
이 함수에 올바른 값을 전달했는가?
이 객체에 해당 속성이 존재하는가?

실행 담당자

숫자를 실제로 계산한다.
함수를 호출한다.
화면이나 터미널에 값을 출력한다.
네트워크 요청을 보낸다.

타입 검사관은 출발 전에 서류를 확인합니다.

실행 담당자는 실제 여행을 떠납니다.

8. 네 번째 관문: JavaScript 생성

타입 검사가 끝나면 TypeScript 컴파일러는 JavaScript 코드를 생성합니다.

다음 TypeScript 파일을 만들어 보겠습니다.

`mission.ts`

const missionName: string = "첫 컴파일";
const completed: boolean = true;

console.log(missionName);
console.log(completed);

컴파일합니다.

npx tsc mission.ts

그러면 `mission.js`가 생성됩니다.

var missionName = "첫 컴파일";
var completed = true;

console.log(missionName);
console.log(completed);

TypeScript 코드에 있었던 타입이 사라졌습니다.

: string
: boolean

JavaScript 결과물에서는 찾아볼 수 없습니다.

9. 사라진 타입의 미스터리

TypeScript에서는 다음처럼 타입을 작성했습니다.

const heroName: string = "타입맨";
const attackPower: number = 100;
const isAlive: boolean = true;

JavaScript로 변환하면 대략 다음과 같은 모습이 됩니다.

var heroName = "타입맨";
var attackPower = 100;
var isAlive = true;

타입 문법은 모두 사라졌습니다.

string   사라짐
number   사라짐
boolean  사라짐

도대체 어디로 간 것일까요?

타입 정보는 TypeScript가 개발 단계에서 코드를 검사하기 위해 사용합니다.

검사가 끝나고 JavaScript를 생성할 때는 대부분의 타입 문법이 제거됩니다.

이를 흔히 타입 지우기, 영어로는 Type Erasure라고 부릅니다.

타입은 리허설 감독이다

타입은 실제 공연 무대에 등장하지 않습니다.

대신 리허설에서 바쁘게 움직입니다.

대사가 올바른가?
등장 순서가 맞는가?
배우가 엉뚱한 소품을 들고 있지 않은가?

리허설이 끝나면 관객 앞에는 배우와 무대만 남습니다.

TypeScript 코드에서 타입은 리허설 감독이고, 생성된 JavaScript는 실제 공연입니다.

10. 타입이 사라지면 실행 중에는 검사하지 못할까?

다음 코드를 살펴보겠습니다.

function printName(name: string): void {
  console.log(name.toUpperCase());
}

printName("TypeScript");

TypeScript는 `name`이 문자열이라고 검사합니다.

JavaScript로 변환하면 다음처럼 됩니다.

function printName(name) {
  console.log(name.toUpperCase());
}

printName("TypeScript");

실행되는 JavaScript에는 `name: string` 정보가 없습니다.

그렇기 때문에 외부에서 들어오는 실제 데이터는 별도의 확인이 필요합니다.

예를 들어 서버에서 다음 값이 들어왔다고 가정해 보겠습니다.

{
  "name": 100
}

TypeScript 코드에 `name: string`이라고 작성했더라도 서버가 실제로 문자열을 보내리라는 보장은 없습니다.

TypeScript 타입은 개발 중의 약속이지, 외부 데이터의 현실을 자동으로 바꾸는 마법은 아닙니다.

택배 상자의 교훈

주문서에는 다음처럼 적혀 있습니다.

상자 내용물: 키보드

하지만 실제 상자를 열었더니 감자가 들어 있을 수도 있습니다.

TypeScript는 주문서 형식을 검사하는 데 강합니다.

실제로 도착한 상자 안에 무엇이 들어 있는지는 실행 중에 직접 확인해야 합니다.

if (
  typeof data === "object" &&
  data !== null &&
  "name" in data &&
  typeof data.name === "string"
) {
  console.log(data.name);
}

외부 API 데이터 검증은 이후 API 회차에서 더 자세히 다룰 예정입니다.

11. 컴파일이란 무엇인가?

컴파일은 일반적으로 한 형태의 소스 코드를 다른 형태로 변환하는 과정을 말합니다.

전통적인 컴파일 언어에서는 다음과 같은 흐름을 떠올릴 수 있습니다.

C 또는 C++ 소스 코드
        ↓
기계어 또는 실행 파일

컴퓨터의 CPU가 실행할 수 있는 낮은 수준의 결과물로 변환됩니다.

TypeScript에서는 다음과 같습니다.

TypeScript
    ↓
JavaScript

TypeScript 컴파일러의 이름도 `tsc`입니다.

TypeScript Compiler

따라서 공식 문서와 실제 개발 현장에서는 TypeScript 코드를 JavaScript로 변환하는 일을 일반적으로 컴파일이라고 부릅니다. `tsc`는 파일을 직접 지정해 컴파일하거나 `tsconfig.json`을 기준으로 프로젝트 전체를 컴파일할 수 있습니다.

12. 트랜스파일이란 무엇인가?

트랜스파일은 한 프로그래밍 언어를 비슷한 수준의 다른 언어로 변환하는 것을 말합니다.

단어를 나누어 보면 다음과 같습니다.

Transform
    +
Compile
    =
Transpile

TypeScript에서 JavaScript로 변환하는 과정은 다음과 같습니다.

고수준 언어 TypeScript
        ↓
고수준 언어 JavaScript

기계어까지 내려가는 것이 아니라, 비슷한 추상화 수준의 언어로 변환됩니다.

그래서 TypeScript에서 JavaScript로 변환하는 과정을 트랜스파일이라고 설명하기도 합니다.

컴파일과 트랜스파일, 무엇이 맞을까?

두 표현 모두 사용됩니다.

컴파일
더 넓고 일반적인 표현

트랜스파일
고수준 언어에서 다른 고수준 언어로 변환한다는 점을 강조

TypeScript 도구 이름이 `TypeScript Compiler`이고 명령어도 `tsc`이므로, 이 연재에서는 주로 컴파일이라는 표현을 사용하겠습니다.

다만 다음 문장을 만나도 같은 흐름을 떠올리면 됩니다.

TypeScript를 JavaScript로 트랜스파일한다.

초보 단계에서는 용어 승부보다 과정 이해가 더 중요합니다.

.ts 코드를 검사하고
실행 가능한 .js 코드로 바꾼다.

이것이 핵심입니다.

13. 단순히 타입만 제거하는 것은 아니다

TypeScript 컴파일러는 타입 정보만 지우는 것이 아닙니다.

설정에 따라 최신 JavaScript 문법을 이전 JavaScript 문법으로 변환할 수도 있습니다.

다음 코드를 보겠습니다.

const greet = (name: string): string => {
  return `안녕하세요, ${name}님`;
};

여기에는 비교적 현대적인 JavaScript 문법이 포함되어 있습니다.

* `const`* 화살표 함수* 템플릿 리터럴

오래된 JavaScript 환경에서 동작하도록 변환하면 대략 다음과 같은 코드가 만들어질 수 있습니다.

var greet = function (name) {
  return "안녕하세요, ".concat(name, "님");
};

다음과 같은 변화가 일어났습니다.

const            → var
화살표 함수       → 일반 함수
템플릿 리터럴     → 문자열 연결
타입 정보         → 제거

이러한 변환을 다운레벨링이라고 부릅니다.

최신 문법을 이전 버전에서도 이해할 수 있는 문법으로 낮추는 과정입니다.

14. target은 JavaScript의 목적지를 정한다

TypeScript 컴파일러에는 `target`이라는 설정이 있습니다.

`target`은 어떤 버전의 JavaScript를 생성할지 결정합니다.

예를 들어 다음과 같은 선택지가 있습니다.

ES5
ES2015
ES2020
ES2022
ESNext

낮은 target을 선택하면 오래된 실행 환경에 맞추기 위해 더 많은 문법이 변환됩니다.

높은 target을 선택하면 최신 JavaScript 문법이 결과물에 더 많이 유지됩니다.

TypeScript 공식 문서에서도 `target`이 어떤 JavaScript 기능을 이전 문법으로 변환하고, 어떤 기능을 그대로 둘지 결정한다고 설명합니다.

시간 여행 엘리베이터

`target`을 JavaScript 시간 여행 엘리베이터라고 생각해 보겠습니다.

ES5 버튼
오래된 환경으로 내려갑니다.

ES2022 버튼
비교적 현대적인 환경으로 이동합니다.

ESNext 버튼
최신 문법을 최대한 유지합니다.

어떤 버튼을 누르느냐에 따라 생성되는 JavaScript 모습이 달라집니다.

15. target에 따른 결과 비교

다음 파일을 만들어 보겠습니다.

`target-test.ts`

const createMessage = (name: string): string => {
  return `환영합니다, ${name}님!`;
};

console.log(createMessage("김타입"));

먼저 ES5를 대상으로 컴파일합니다.

npx tsc target-test.ts --target ES5

결과는 버전에 따라 조금씩 다를 수 있지만 대략 다음과 비슷합니다.

var createMessage = function (name) {
  return "환영합니다, ".concat(name, "님!");
};

console.log(createMessage("김타입"));

이번에는 비교적 최신 JavaScript를 대상으로 변환합니다.

npx tsc target-test.ts --target ES2022

결과는 다음과 비슷합니다.

const createMessage = (name) => {
  return `환영합니다, ${name}님!`;
};

console.log(createMessage("김타입"));

`ES2022`에서는 화살표 함수와 템플릿 리터럴이 그대로 유지됐습니다.

하지만 TypeScript 타입은 두 결과에서 모두 사라집니다.

name: string
): string

타입은 JavaScript 실행 문법이 아니기 때문입니다.

16. 같은 코드, 다른 의상

TypeScript 원본은 하나입니다.

const createMessage = (name: string): string => {
  return `환영합니다, ${name}님!`;
};

하지만 target에 따라 서로 다른 JavaScript 의상을 입을 수 있습니다.

ES5 의상

var createMessage = function (name) {
  return "환영합니다, ".concat(name, "님!");
};

최신 JavaScript 의상

const createMessage = (name) => {
  return `환영합니다, ${name}님!`;
};

행동은 비슷하지만 문법의 모습이 다릅니다.

TypeScript 컴파일러는 배포할 환경에 맞춰 코드를 갈아입히는 의상팀 역할도 합니다.

17. JavaScript가 생성된 뒤에는 무슨 일이 일어날까?

컴파일이 끝나면 JavaScript 파일이 만들어집니다.

target-test.js

이제 실행 환경이 등장합니다.

Node.js에서 실행한다면 다음 명령어를 사용합니다.

node target-test.js

실행 결과는 다음과 같습니다.

환영합니다, 김타입님!

이 단계에서는 TypeScript 컴파일러가 더 이상 주인공이 아닙니다.

JavaScript 실행 환경이 다음 작업을 수행합니다.

변수 생성
함수 호출
문자열 조합
조건문 실행
반복문 실행
결과 출력

전체 역할을 다시 정리하면 다음과 같습니다.

TypeScript 컴파일러
검사와 변환 담당

Node.js
JavaScript 실행 담당

18. 웹 브라우저에서는 어떻게 실행될까?

브라우저에서도 기본 흐름은 비슷합니다.

TypeScript 파일을 먼저 JavaScript로 변환합니다.

npx tsc app.ts

그러면 `app.js`가 생성됩니다.

HTML 파일에서는 JavaScript 파일을 불러옵니다.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>TypeScript 테스트</title>
</head>
<body>
  <h1>TypeScript 실행 실험</h1>

  <script src="app.js"></script>
</body>
</html>

일반적인 브라우저 프로젝트에서는 다음처럼 TypeScript 파일을 직접 연결하지 않습니다.

<script src="app.ts"></script>

대신 변환된 JavaScript를 연결합니다.

<script src="app.js"></script>

흐름은 다음과 같습니다.

app.ts 작성
    ↓
app.js 생성
    ↓
HTML에서 app.js 연결
    ↓
브라우저가 app.js 실행

19. 최신 Node.js는 TypeScript를 직접 실행할 수 있다?

여기에서 최신 환경에 대한 중요한 보충 설명이 필요합니다.

최근 Node.js에는 일부 TypeScript 파일을 직접 실행할 수 있는 타입 제거 기능이 포함되어 있습니다.

다음처럼 실행할 수 있는 환경도 있습니다.

node app.ts

하지만 이것이 `tsc`와 완전히 같은 역할을 한다는 뜻은 아닙니다.

Node.js의 내장 타입 제거 기능은 타입 문법을 제거해 실행하지만 다음과 같은 제한이 있습니다.

* 타입 검사를 수행하지 않습니다.* `tsconfig.json`을 읽지 않습니다.* 최신 JavaScript를 이전 문법으로 변환하지 않습니다.* JavaScript 코드 생성이 필요한 일부 TypeScript 문법은 지원하지 않습니다.* `.tsx` 파일은 지원하지 않습니다.

Node.js 공식 문서에서도 내장 기능은 가벼운 타입 제거 방식이며, 타입 검사를 수행하지 않고 `tsconfig.json`도 무시한다고 설명합니다.

따라서 이 연재에서는 TypeScript의 전체 개발 흐름을 이해하기 위해 계속 다음 방식을 사용합니다.

npx tsc app.ts
node app.js

최신 기능이 있어도 tsc를 배우는 이유

Node.js가 일부 `.ts` 파일을 실행할 수 있는데 왜 굳이 컴파일을 배울까요?

이유는 명확합니다.

타입 검사가 필요하다.
프로젝트 설정이 필요하다.
브라우저용 JavaScript를 만들어야 한다.
target을 조절해야 한다.
배포용 .js 파일이 필요하다.
여러 파일을 한 번에 관리해야 한다.

`node app.ts`는 편리한 실행 방법이 될 수 있지만, TypeScript 프로젝트 전체의 빌드 과정을 대체하는 만능 버튼은 아닙니다.

20. 컴파일 오류란 무엇인가?

컴파일 오류는 TypeScript 코드를 검사하거나 JavaScript로 변환하는 과정에서 발견되는 문제입니다.

다음 파일을 만들어 보겠습니다.

`compile-error.ts`

const score: number = "백 점";

console.log(score);

컴파일합니다.

npx tsc compile-error.ts

오류가 나타납니다.

Type 'string' is not assignable to type 'number'.

이 오류는 프로그램을 실행한 결과가 아닙니다.

실행하기 전 TypeScript가 발견한 문제입니다.

컴파일 오류의 대표 사례

잘못된 타입 대입

let age: number = "스무 살";

함수에 잘못된 값 전달

function double(value: number): number {
  return value * 2;
}

double("10");

존재하지 않는 속성 접근

const user = {
  name: "김타입"
};

console.log(user.email);

반환 타입 불일치

function getPrice(): number {
  return "10000";
}

이러한 문제는 TypeScript 컴파일러가 실행 전에 발견할 수 있습니다.

21. 실행 오류란 무엇인가?

실행 오류는 JavaScript 코드가 실제로 실행되는 도중 발생하는 문제입니다.

다음 TypeScript 코드는 타입 검사에 통과할 수 있습니다.

const user = JSON.parse("{}");

console.log(user.profile.name);

`JSON.parse()`의 결과가 느슨한 타입으로 처리되면 컴파일 단계에서 문제가 발견되지 않을 수 있습니다.

하지만 실행하면 `profile`이 존재하지 않아 오류가 발생합니다.

TypeError:
Cannot read properties of undefined

이것이 실행 오류입니다.

컴파일 단계
통과

실행 단계
폭발

실행 오류의 대표 사례

존재하지 않는 값 사용

const data: any = null;

console.log(data.name);

정의되지 않은 함수 호출

const service: any = {};

service.start();

파일이나 네트워크 문제

// 파일이 존재하지 않거나
// 서버가 응답하지 않을 수 있습니다.

잘못된 JSON 변환

JSON.parse("이것은 JSON이 아닙니다.");

TypeScript가 많은 실수를 막아 주지만 실행 환경에서 발생하는 모든 문제를 제거하지는 않습니다.

22. 논리 오류라는 숨은 보스

컴파일 오류도 없고 실행 오류도 없는데 결과가 잘못될 수 있습니다.

다음 코드를 보겠습니다.

function calculateDiscount(
  price: number,
  discountRate: number
): number {
  return price + discountRate;
}

console.log(calculateDiscount(10000, 1000));

코드는 정상적으로 컴파일됩니다.

실행도 됩니다.

결과는 다음과 같습니다.

11000

하지만 할인 계산이라면 가격에서 할인 금액을 빼야 합니다.

function calculateDiscount(
  price: number,
  discountAmount: number
): number {
  return price - discountAmount;
}

올바른 결과는 다음과 같습니다.

9000

처음 코드는 타입상 문제가 없었습니다.

두 매개변수 모두 숫자였고 반환값도 숫자였습니다.

그러나 계산식이 잘못됐습니다.

이를 논리 오류라고 합니다.

23. 오류 삼형제

세 가지 오류를 정리해 보겠습니다.

오류 종류발견 시점예시
문법 오류컴파일 과정코드 구조가 잘못됨
타입 오류타입 검사 과정숫자 자리에 문자열 전달
실행 오류프로그램 실행 중`undefined`의 속성 접근
논리 오류실행 후 결과 확인할인인데 가격을 더함

TypeScript가 가장 잘 잡는 영역은 타입 오류입니다.

TypeScript가 강한 영역
타입 오류
일부 문법 오류
잘못된 속성 접근
잘못된 함수 사용

TypeScript가 자동으로 모두 해결하지 못하는 영역도 있습니다.

서버 장애
파일 누락
사용자 입력 문제
잘못된 계산 공식
잘못된 업무 규칙
외부 API의 실제 응답 오류

TypeScript는 뛰어난 경비원이지만 미래를 보는 예언자는 아닙니다.

24. 타입 오류가 있으면 JavaScript는 생성되지 않을까?

많은 입문자는 이렇게 예상합니다.

타입 오류 발생
    ↓
컴파일 중단
    ↓
JavaScript 파일 생성 안 됨

하지만 TypeScript 컴파일러는 설정에 따라 타입 오류가 있어도 JavaScript 파일을 생성할 수 있습니다.

다음 코드를 보겠습니다.

`strange.ts`

const count: number = "열 개";

console.log(count);

컴파일합니다.

npx tsc strange.ts

오류 메시지가 나타납니다.

Type 'string' is not assignable to type 'number'.

그런데 폴더를 확인하면 `strange.js`가 생성되어 있을 수도 있습니다.

var count = "열 개";

console.log(count);

TypeScript가 말합니다.

“문제가 있다는 사실은 알려 드렸습니다. 결과물을 만들지는 설정에 따라 결정됩니다.”

25. 오류가 있으면 파일 생성을 막는 방법

타입 오류가 있을 때 JavaScript 파일을 생성하지 않으려면 `--noEmitOnError` 옵션을 사용할 수 있습니다.

npx tsc strange.ts --noEmitOnError

단어를 나누어 보겠습니다.

no       하지 않는다
Emit     결과 파일을 생성한다
OnError  오류가 있을 때

즉, 다음 의미입니다.

“오류가 있으면 JavaScript 파일을 생성하지 마세요.”

코드를 수정합니다.

const count: number = 10;

console.log(count);

다시 컴파일합니다.

npx tsc strange.ts --noEmitOnError

타입 오류가 사라졌으므로 JavaScript 파일이 생성됩니다.

26. 결과 파일 없이 타입 검사만 하기

JavaScript 파일은 필요 없고 타입 오류만 확인하고 싶을 때도 있습니다.

이때는 `--noEmit` 옵션을 사용합니다.

npx tsc app.ts --noEmit

이 명령은 다음 작업을 수행합니다.

TypeScript 코드 읽기
문법 검사
타입 검사
오류 출력
JavaScript 파일은 생성하지 않음

CI 검사나 코드 검증 과정에서 자주 사용하는 방식입니다.

컴파일러 카페 주문법

`tsc` 옵션을 카페 주문처럼 기억해 보겠습니다.

기본 주문

npx tsc app.ts
“검사하고 JavaScript도 만들어 주세요.”

오류 시 제작 중단

npx tsc app.ts --noEmitOnError
“오류가 있으면 JavaScript는 만들지 마세요.”

검사만 하기

npx tsc app.ts --noEmit
“파일은 필요 없고 타입 검사만 해주세요.”

27. 파일을 지정하면 tsconfig.json은 어떻게 될까?

다음 명령어는 특정 파일을 직접 지정합니다.

npx tsc app.ts

이 방식은 간단한 실습에서는 편리합니다.

하지만 주의할 점이 있습니다.

명령줄에서 입력 파일을 직접 지정하면 프로젝트의 `tsconfig.json` 설정이 적용되지 않는 방식으로 동작할 수 있습니다. TypeScript 공식 CLI 문서에서도 파일을 명령줄에 지정하면 `tsconfig.json`이 무시된다고 안내합니다.

프로젝트 설정을 기준으로 컴파일하려면 보통 파일명을 생략합니다.

npx tsc

이때 TypeScript는 가까운 위치의 `tsconfig.json`을 찾아 프로젝트 설정을 사용합니다.

파일 직접 지정
npx tsc app.ts

프로젝트 설정 사용
npx tsc

`tsconfig.json`은 다음 편에서 직접 만들어 보겠습니다.

28. 감시 모드에서 일어나는 과정

지난 편에서 다음 명령어를 사용했습니다.

npx tsc app.ts --watch

또는 다음처럼 짧게 작성할 수 있습니다.

npx tsc app.ts -w

감시 모드를 실행하면 TypeScript 컴파일러는 종료되지 않고 파일 변경을 기다립니다.

컴파일 완료
    ↓
파일 변경을 기다림
    ↓
개발자가 저장
    ↓
다시 타입 검사
    ↓
JavaScript 다시 생성
    ↓
또 기다림

개발자가 코드를 수정합니다.

const message: string = "첫 번째 메시지";

저장하면 컴파일러가 움직입니다.

파일 변경 감지!
다시 검사합니다.

다시 수정합니다.

const message: string = "두 번째 메시지";

저장하면 또 움직입니다.

컴파일러는 저장 버튼 소리에 반응하는 코드 감시견이 됩니다.

감시 모드를 종료하려면 다음 키를 누릅니다.

Ctrl + C

29. 컴파일러 오류 메시지 해부하기

다음 코드를 컴파일해 보겠습니다.

const userAge: number = "25";

오류 메시지는 다음과 비슷합니다.

app.ts(1,7): error TS2322:
Type 'string' is not assignable to type 'number'.

각 부분을 분해해 보겠습니다.

app.ts

app.ts

문제가 발견된 파일입니다.

(1,7)

(1,7)

문제가 발견된 줄과 문자 위치를 나타냅니다.

1번째 줄
7번째 문자 근처

error TS2322

error TS2322

TypeScript 오류 코드입니다.

오류를 검색하거나 문서에서 확인할 때 사용할 수 있습니다.

오류 설명

Type 'string' is not assignable
to type 'number'.

문자열을 숫자 타입에 대입할 수 없다는 의미입니다.

컴파일러 통역 센터

컴파일러 원문:

Type 'string' is not assignable to type 'number'.

사람의 언어:

“문자열이 숫자 행세를 하며 입장하려다 적발됐습니다.”

컴파일러 원문:

Property 'email' does not exist on type...

사람의 언어:

“그 객체를 아무리 뒤져도 email이라는 서랍은 없습니다.”

컴파일러 원문:

Expected 2 arguments, but got 1.

사람의 언어:

“재료를 두 개 달라고 했는데 하나만 가져오셨습니다.”

오류 메시지를 읽는 능력은 TypeScript 실력을 빠르게 올려 주는 중요한 기술입니다.

30. 코드 변신 실험실

이제 TypeScript 코드가 어떻게 바뀌는지 직접 실험해 보겠습니다.

`transform.ts` 파일을 만듭니다.

type User = {
  name: string;
  age: number;
};

const createGreeting = (user: User): string => {
  const nextAge: number = user.age + 1;

  return `${user.name}님은 내년에 ${nextAge}살입니다.`;
};

const user: User = {
  name: "김타입",
  age: 25
};

console.log(createGreeting(user));

ES5를 대상으로 컴파일합니다.

npx tsc transform.ts --target ES5

생성된 JavaScript는 대략 다음과 같습니다.

var createGreeting = function (user) {
  var nextAge = user.age + 1;

  return ""
    .concat(user.name, "님은 내년에 ")
    .concat(nextAge, "살입니다.");
};

var user = {
  name: "김타입",
  age: 25
};

console.log(createGreeting(user));

관찰할 부분이 많습니다.

type User             완전히 사라짐
user: User            타입 제거
): string             반환 타입 제거
nextAge: number       타입 제거
const                  var로 변환
화살표 함수             일반 함수로 변환
템플릿 리터럴           문자열 결합 코드로 변환

`type User`가 사라지는 이유는 실행 시 필요한 값이 아니라 개발 단계에서만 사용하는 타입 선언이기 때문입니다.

31. 값과 타입은 여행 방식이 다르다

TypeScript 코드에는 크게 두 종류의 정보가 존재합니다.

실행할 때 필요한 값
개발할 때 필요한 타입

다음 코드를 보겠습니다.

type Product = {
  name: string;
  price: number;
};

const product: Product = {
  name: "키보드",
  price: 89000
};

JavaScript로 이동하는 정보를 표시해 보겠습니다.

사라지는 것

type Product = {
  name: string;
  price: number;
};

`Product` 타입 선언은 사라집니다.

남는 것

var product = {
  name: "키보드",
  price: 89000
};

실제 객체 값은 실행할 때 필요하므로 남습니다.

타입 유령과 값 배우

타입은 공연 준비가 끝나면 사라지는 유령입니다.

값은 실제 무대에 올라가는 배우입니다.

type
interface
타입 주석

리허설이 끝나면 퇴장
문자열 값
숫자 값
객체
함수

실제 JavaScript 무대에 등장

앞으로 `interface`, 제네릭, 유니언 타입을 배울 때도 이 차이는 매우 중요합니다.

32. TypeScript가 모두 사라지는 것은 아니다

대부분의 타입 문법은 JavaScript 결과물에서 제거됩니다.

하지만 TypeScript의 모든 문법이 단순히 삭제되는 것은 아닙니다.

일부 기능은 실행할 JavaScript 코드를 생성할 수 있습니다.

대표적으로 설정과 사용 방식에 따라 다음과 같은 문법이 영향을 줄 수 있습니다.

enum
namespace
일부 클래스 관련 문법

예를 들어 일반적인 `enum`은 실행 시 사용할 객체 형태의 JavaScript를 생성할 수 있습니다.

enum Direction {
  Up,
  Down
}

이러한 문법은 단순한 타입 주석보다 변환 과정이 복잡합니다.

입문 단계에서는 다음 기준만 기억하면 충분합니다.

string, number 같은 타입 주석
대부분 제거됨

실행 동작을 가진 TypeScript 기능
JavaScript 코드가 생성될 수 있음

`enum`은 이후 별도 회차에서 자세히 살펴보겠습니다.

33. 타입 검사는 현실을 바꾸지 않는다

다음 코드를 살펴보겠습니다.

const temperature: number = 30;

TypeScript는 `temperature`가 숫자라고 판단합니다.

하지만 타입을 강제로 주장한다고 실제 값이 바뀌는 것은 아닙니다.

const input = "30";
const temperature = input as unknown as number;

개발자가 TypeScript에게 숫자라고 억지로 주장할 수는 있습니다.

하지만 실제 JavaScript 값은 여전히 문자열입니다.

console.log(typeof temperature);

결과:

string

TypeScript 타입은 실제 값을 변환하지 않습니다.

문자열을 숫자로 바꾸려면 실행 코드가 필요합니다.

const input = "30";
const temperature: number = Number(input);

명찰을 바꾼다고 직업이 바뀌지는 않는다

감자 상자에 다음 명찰을 붙였다고 가정해 보겠습니다.

고성능 그래픽카드

명찰은 바뀌었지만 상자 안에는 여전히 감자가 있습니다.

타입 단언도 비슷합니다.

value as number

이 코드는 TypeScript에게 그렇게 믿으라고 말할 뿐, 실제 값을 숫자로 변환하지 않습니다.

타입 단언은 이후 별도 회차에서 안전하게 사용하는 방법을 다룰 예정입니다.

34. TypeScript 실행 과정 한눈에 보기

지금까지의 내용을 하나의 큰 흐름으로 합쳐 보겠습니다.

┌─────────────────────────┐
│ 1. TypeScript 코드 작성  │
│        app.ts            │
└────────────┬────────────┘
             ↓
┌─────────────────────────┐
│ 2. 문법 구조 분석         │
│ 변수, 함수, 표현식 확인    │
└────────────┬────────────┘
             ↓
┌─────────────────────────┐
│ 3. 타입 검사              │
│ string, number 등 확인    │
└────────────┬────────────┘
             ↓
┌─────────────────────────┐
│ 4. 타입 정보 제거         │
│ 타입 주석과 타입 선언 삭제 │
└────────────┬────────────┘
             ↓
┌─────────────────────────┐
│ 5. target에 맞게 변환     │
│ 최신 문법을 이전 문법으로  │
└────────────┬────────────┘
             ↓
┌─────────────────────────┐
│ 6. JavaScript 파일 생성   │
│        app.js            │
└────────────┬────────────┘
             ↓
┌─────────────────────────┐
│ 7. 실행 환경에서 실행     │
│ Node.js 또는 브라우저     │
└─────────────────────────┘

35. 실전 프로젝트: 주문 금액 계산기

이번 시간에 배운 흐름을 직접 확인해 보겠습니다.

`order.ts` 파일을 만듭니다.

type Order = {
  productName: string;
  price: number;
  quantity: number;
};

const calculateTotal = (order: Order): number => {
  return order.price * order.quantity;
};

const order: Order = {
  productName: "기계식 키보드",
  price: 89000,
  quantity: 2
};

const total: number = calculateTotal(order);

console.log(`상품명: ${order.productName}`);
console.log(`수량: ${order.quantity}`);
console.log(`총금액: ${total}원`);

컴파일합니다.

npx tsc order.ts --target ES2022

실행합니다.

node order.js

결과는 다음과 같습니다.

상품명: 기계식 키보드
수량: 2
총금액: 178000원

생성된 JavaScript 확인하기

`order.js`를 열어 보면 다음과 비슷합니다.

const calculateTotal = (order) => {
  return order.price * order.quantity;
};

const order = {
  productName: "기계식 키보드",
  price: 89000,
  quantity: 2
};

const total = calculateTotal(order);

console.log(`상품명: ${order.productName}`);
console.log(`수량: ${order.quantity}`);
console.log(`총금액: ${total}원`);

다음 타입 정보가 사라졌습니다.

type Order
: Order
: number

하지만 실제 실행에 필요한 값과 함수는 남아 있습니다.

36. 오류 실험실

이번에는 일부러 오류를 만들어 보겠습니다.

const order: Order = {
  productName: "기계식 키보드",
  price: "89000",
  quantity: 2
};

`price`에 문자열을 넣었습니다.

컴파일합니다.

npx tsc order.ts --noEmitOnError

TypeScript는 다음과 비슷한 오류를 알려줍니다.

Type 'string' is not assignable to type 'number'.

`--noEmitOnError` 옵션을 사용했으므로 오류가 있는 동안 새로운 JavaScript 결과물을 생성하지 않습니다.

수정합니다.

const order: Order = {
  productName: "기계식 키보드",
  price: 89000,
  quantity: 2
};

다시 컴파일합니다.

npx tsc order.ts --noEmitOnError

이번에는 정상적으로 JavaScript 파일이 생성됩니다.

37. 타입 탐정사무소

다음 TypeScript 코드가 있습니다.

type Member = {
  name: string;
  point: number;
};

const member: Member = {
  name: "김타입",
  point: 1000
};

console.log(member.point);

컴파일 후 JavaScript에 남지 않는 것을 찾아보세요.

후보

1. `member` 객체2. `"김타입"` 문자열3. `1000` 숫자4. `type Member`5. `console.log`

정답은 4번 `type Member`입니다.

`Member`는 타입 검사에만 사용되므로 JavaScript 결과물에서는 사라집니다.

나머지는 실제 실행에 필요한 값과 동작이므로 남습니다.

38. 컴파일러의 야간 라디오

오늘의 사연입니다.

“컴파일러님, 저는 분명 TypeScript에 `number`라고 작성했는데 JavaScript 파일에서는 왜 사라지나요?”

컴파일러의 답변입니다.

“`number`는 제가 출근해서 검사할 때 사용하는 정보입니다. Node.js가 야간 근무를 시작할 때는 제 근무가 끝났기 때문에 타입 정보도 함께 퇴근합니다.”

두 번째 사연입니다.

“오류가 났는데 왜 JavaScript 파일이 생겼나요?”

답변입니다.

“경고는 드렸지만 결과물 생성을 막으라는 주문은 받지 못했습니다. 확실히 막고 싶다면 `--noEmitOnError`를 이용해 주세요.”

마지막 사연입니다.

“컴파일과 트랜스파일 중 무엇이 맞나요?”

답변입니다.

“오늘도 용어 토론이 뜨겁군요. 저를 부를 때는 `tsc`, TypeScript Compiler라고 불러 주시면 됩니다.”

39. 초보자 함정 지도

함정 1. 컴파일하면 코드가 바로 실행된다고 생각하기

npx tsc app.ts

이 명령은 주로 검사와 JavaScript 생성 작업을 수행합니다.

실행은 별도로 진행합니다.

node app.js

함정 2. 타입이 실행 중에도 남아 있다고 생각하기

const age: number = 25;

JavaScript에서는 `: number`가 제거됩니다.

타입은 실제 값을 강제로 변경하지 않습니다.

함정 3. 타입 오류가 있으면 무조건 파일이 생성되지 않는다고 생각하기

설정에 따라 오류가 있어도 JavaScript가 생성될 수 있습니다.

확실하게 막으려면 다음 옵션을 사용합니다.

npx tsc app.ts --noEmitOnError

함정 4. target이 TypeScript 버전이라고 생각하기

`target`은 TypeScript 버전을 정하는 설정이 아닙니다.

생성할 JavaScript 문법 수준을 결정합니다.

함정 5. TypeScript가 모든 실행 오류를 막는다고 생각하기

TypeScript는 타입 오류를 줄여 줍니다.

네트워크 실패, 잘못된 외부 데이터, 논리적 계산 오류까지 모두 해결하지는 않습니다.

함정 6. node app.ts가 되면 tsc가 필요 없다고 생각하기

최신 Node.js의 타입 제거 실행은 타입 검사를 하지 않고 `tsconfig.json`도 적용하지 않습니다.

전체 프로젝트 개발에서는 여전히 `tsc` 또는 적절한 TypeScript 빌드 도구가 중요합니다.

40. 미니 퀴즈

문제 1

TypeScript 컴파일러의 기본 역할과 가장 가까운 것은 무엇일까요?

  1. 웹 서버만 실행한다.
  2. TypeScript 코드를 검사하고 JavaScript로 변환한다.
  3. 데이터베이스를 자동 생성한다.
  4. 모든 논리 오류를 수정한다.

정답은 2번입니다.

문제 2

다음 TypeScript 코드에서 JavaScript로 변환할 때 사라지는 부분은 무엇일까요?

const score: number = 100;

정답:

: number

타입 정보는 개발 단계의 검사에 사용되고 JavaScript 결과물에서는 제거됩니다.

문제 3

다음 두 명령어의 차이는 무엇일까요?

npx tsc app.ts
node app.js

정답:

npx tsc app.ts
TypeScript 검사 및 JavaScript 생성

node app.js
생성된 JavaScript 실행

문제 4

오류가 있을 때 JavaScript 파일 생성을 막는 옵션은 무엇일까요?

정답:

npx tsc app.ts --noEmitOnError

문제 5

JavaScript 파일을 생성하지 않고 타입 검사만 수행하는 옵션은 무엇일까요?

정답:

npx tsc app.ts --noEmit

문제 6

`target` 설정의 역할은 무엇일까요?

정답:

생성할 JavaScript의 문법 버전을 결정합니다.

낮은 target은 최신 문법을 이전 문법으로 더 많이 변환하고, 높은 target은 최신 문법을 더 많이 유지합니다.

문제 7

다음 코드에는 타입 오류가 있을까요?

function divide(a: number, b: number): number {
  return a / b;
}

divide(10, 0);

정답:

타입 오류는 없습니다.

두 값 모두 숫자입니다.

하지만 0으로 나누는 상황이 프로그램의 의도에 맞는지는 별도의 논리 검사가 필요합니다.

41. 오늘의 핵심 정리

  • TypeScript 컴파일러는 .ts 코드를 읽고 문법과 타입을 검사합니다.
  • 타입 검사는 프로그램이 실제로 실행되기 전에 이루어집니다.
  • TypeScript 코드는 일반적으로 JavaScript로 변환된 뒤 실행됩니다.
  • 타입 주석과 타입 선언은 JavaScript 결과물에서 대부분 제거됩니다.
  • TypeScript에서 JavaScript로 변환하는 과정을 컴파일 또는 트랜스파일이라고 부릅니다.
  • target은 생성할 JavaScript 문법의 수준을 결정합니다.
  • 낮은 target에서는 최신 문법이 이전 문법으로 변환될 수 있습니다.
  • 컴파일 오류와 실행 오류는 서로 다른 시점에 발생합니다.
  • 타입 오류가 있어도 설정에 따라 JavaScript 파일이 생성될 수 있습니다.
  • --noEmitOnError는 오류가 있을 때 결과물 생성을 막습니다.
  • --noEmit은 JavaScript 파일 없이 타입 검사만 수행합니다.
  • 최신 Node.js는 제한적으로 TypeScript 타입 제거 실행을 지원하지만 타입 검사와 전체 프로젝트 설정을 대신하지는 않습니다.
  • TypeScript는 타입 오류를 줄여 주지만 논리 오류와 모든 실행 오류를 자동으로 해결하지는 않습니다.

오늘 사용한 주요 명령어를 모으면 다음과 같습니다.

# 기본 컴파일
npx tsc app.ts

# JavaScript 실행
node app.js

# ES5를 대상으로 컴파일
npx tsc app.ts --target ES5

# ES2022를 대상으로 컴파일
npx tsc app.ts --target ES2022

# 오류가 있으면 JavaScript 생성 중단
npx tsc app.ts --noEmitOnError

# JavaScript 생성 없이 타입 검사만 수행
npx tsc app.ts --noEmit

# 파일 변경 감시
npx tsc app.ts --watch

42. 다음 편 예고

지금까지는 컴파일할 때마다 옵션을 직접 입력했습니다.

npx tsc app.ts --target ES2022 --noEmitOnError

파일이 많아지고 옵션이 늘어나면 명령어는 점점 길어집니다.

npx tsc app.ts --target ES2022 --module commonjs --strict --outDir dist

매번 이 긴 명령어를 입력하는 것은 번거롭습니다.

프로젝트의 TypeScript 설정을 한곳에 저장할 방법이 필요합니다.

그 역할을 하는 파일이 바로 이것입니다.

tsconfig.json

다음 편에서는 TypeScript 프로젝트의 조종석을 직접 열어 보겠습니다.

다음 이야기

[TypeScript 완전정복 #4] tsconfig.json 처음부터 이해하기 | TypeScript 프로젝트의 조종석

* `tsconfig.json`은 왜 필요할까요?* `target`과 `module`은 무엇을 결정할까요?* `rootDir`과 `outDir`은 어떻게 사용할까요?* `strict`를 켜면 무엇이 달라질까요?* `include`와 `exclude`는 어떤 파일을 선택할까요?

다음 편에서는 흩어져 있던 컴파일 옵션을 하나의 설정 파일에 모아 제대로 된 TypeScript 프로젝트 구조를 만들어 보겠습니다.

댓글

0

댓글을 불러오는 중입니다.