본문 바로가기
개발공부일지/TypeScript

TypeScript - 컴파일러 & 트랜스파일러 적용하기

by Hynn1429 2023. 4. 19.
반응형

안녕하세요.

Hynn 입니다.

 

이번 포스팅에서는 TypeScript 를 작성하기 전, 환경설정을 하기 위해, 컴파일러, 트랜스파일러를 적용하는 방법에 대해서 알아보도록 하겠습니다.

IDE 는 VisualStudio Code 를 사용하기 때문에, 다른 IDE 에서는 본 포스팅에서 설명하는 부분과 다를 수 있습니다.

 

============

============

사실 TypeScript 에서 JavaScript 로 변환하는 과정은 컴파일러보다는, 트랜스파일러가 더 맞는 단어라고 개인적인 생각이 들기는 합니다.

하지만 부족한(?) 컴퓨터 공학 지식을 간간히 채우기 위해 각 단어가 갖는 의미를 글을 읽으시는 분들에게도 간략하게 설명드리고자, 컴파일러와 트랜스파일러의 정의를 간략하게 정리해보았습니다.

 

컴파일러는 일반적으로 VisualStudio Code 와 같은 IDE 에서 코드를 작성 후 컴파일링 과정에서 오류를 검사합니다. 와 같은 언급을 위에서도 하였지만, 많은 곳에서도 사용하신 바를 볼 수 있습니다. 하지만 여기서 말하는 컴파일링과, 실제 컴파일링의 의미는 조금 다르게 사용됩니다. 위에서 든 예시의 경우, 일반적인 변환 과정을 의미하기 때문입니다.

 

 컴파일러은 고수준 프로그래밍 언어로 작성된 코드를 저수준 언어(ex. 기계어, 어셈블리언어) 와 같은 언어로 변환하는 도구를 뜻합니다.  

즉, 이 도구에서 소스 코드의 문법과 구조를 검사하고, 실행 가능한 바이너리(Bainary) 파일이나 중간 코드(Intermediate code) 로 변환합니다. 이 변환과정에는 최적화 과정도 거치게 되며, 성능을 향상하는데 도움을 줍니다. 대표적으로 C/C+ 언어를 컴파일러는 해당 언어로 작성된 코드를 기계어로 변환하여 실행가능한 바이너리 파일을 생성하게 됩니다.

 

 반면 트랜스파일러는 고수준 프로그래밍 언어로 작성된 코드를 다른 고수준 프로그래밍 언어로 변환하는 도구를 뜻합니다.

트랜스파일러를 사용하는 주 목적은 원래의 언어가 호환되지 않는 환경에서 코드를 실행하기 위해 사용됩니다. 즉 TypeScript 는 JavaScript 환경에서 동작하지 않기 때문에, 이에 적합한 예시라고 할 수 있습니다. 트랜스파일러는 소스 코드의 구문과 구조를 유지하고, 새로운 언어로 변환하는 과정을 거칩니다. 

 

이러한 과정을 거쳐 TypeScript 는 비로소 JavaScript 에서 사용할 수 있도록 "트랜스파일러" 과정이 진행되어야 합니다.

 

트랜스파일러는 아래의 4개의 과정을 거쳐서, TypeScript 를 JavaScript 로 변환할 때 아래의 4개의 단계를 거칩니다.

 

  1. 소스 코드 분석 (Parsing) : 컴파일러가 소스코드를 읽고, 문법 구조를 분석하여 추상구문트리(Abstract Syntax Tree, AST) 를 생성합니다. 이 단계에서는 소스코드와 문법 구조에서 문법 오류를 발견하고 발견되면 오류메시지를 반환합니다.
  2. 타입 검사 (Type Checking) : 이전 단계에서 생성한 추상 구문트리를 사용하여 타입 정보를 추론하고, 타입 오류를 검사합니다. 만약 타입 불일치나 누락된 타입 정보 등의 오류가 발견되면 오류 메시지를 반환합니다.
  3. 트랜스파일 (Transpilation) : 1번째 단계에서 생성한 추상구문트리를 이제 JavaScript 로 변환합니다. 이 과정에서 이제 Typescript 기반으로 작성한 코드의 TypeScript 고유 기능이 호환되는 Javascript 코드로 생성합니다. 이 과정에서 필요한 경우, ECMAScript(ex. ES5, ES6) 버전을 다운그레이드하여 브라우저에서의 호환성을 보장할 수 있습니다. 
  4. 코드 생성 (Code Generation) : 이제 트랜스파일러에서 처리한 JavaScript 코드를 파일로 출력합니다. 이 과정에서 소스맵(SourceMap) 파일을 생성할 수 있고, 이 소스맵 파일을 이용해 원본 TypeScript 코드를 참조할 수 있습니다. 

 

이제 위 4개의 단계를 실제 사용하는 Libarary 와 도구를 이용해  적용해보도록 하겠습니다.

그러기 위해서는 설정을 포함한 몇가지 준비가 필요합니다.

 

먼저 npm 을 이용해 위 과정을 적용할 4개의 모듈을 설치해야 합니다.

각각의 명령어를 작성해보겠습니다.

npm init -y
npm install -d nodemon
npm install -d typescript
npm install -d tsnode
npm install -d tsconfig-paths

"-d" 옵션을 사용했지만, 만약 이러한 개발용 종속성으로 설치를 윈치 않는다면, -d 옵션을 제거하셔도 되고, 전역 설치로 설치하셔도 관계는 없습니다. 다만 일반적으로 이러한 패키지는 개발용 종속성 사용을 하기 때문에, 그에 맞게 "-d" 옵션을 사용했습니다.

 

그리고, 이제 구성파일을 생성하기 위해, 아래의 명령어를 사용하여, tsconfig.json 과 nodemon.json 을 생성해보겠습니다.

npx tsc --init

그렇다면, 이제 루트 디렉토리에 "tsconfig.json" 파일이 생성 된 것을 확인할 수 있습니다.

이제 nodemon.json 파일은 직접 생성하거나 아래의 터미널 명령어를 사용하여 생성하셔도 좋습니다.

 

touch nodemon.json

기본적인 패키지 설치는 모두 완료가 되었고, 각각의 설정파일의 생성도 준비가 끝났습니다.

이제 각각의 파일의 구성요소를 설정하는 요소를 알아보도록 하겠습니다. 

 

위에서 생성한 "tsconfig.json", 그리고 "nodemon.json" 이 두개의 파일이 이제, 우리가 트랜스파일링을 할 때, 어떻게 할 것인지를 정의하는 설정파일입니다.

이 파일에서 설정한 바에 따라, 트랜스파일링이 수행되며, 이 설정을 올바르게 하지 않으면, 당연하게도 원하는 결과물이 나타나지 않을 수 있습니다.

 

이를 살펴보도록 하겠습니다.

 

1) tsconfig.json

먼저 TSC, 즉 Typescript Compiler 의 설정파일의 구성은 기본적으로 아래의 4개의 카테고리로 이루어져 있습니다.

 

{
  "compilerOptions": {}
  "include": [],
  "exclude": [],
  "reference" : []
}

 

이 각 구성요소의 대한 이해를 해야, 올바르게 사용이 가능합니다.

주로 사용하는 속성에 대해서만 알아보도록 하겠습니다.

 

  1. complierOptions : 객체로 구성되어 있으며, TypeScript 컴파일러에 대한 옵션을 설정합니다. 이 설정에서는 Module, Directory, 소스맵, 인터페이스 수정을 비롯한 여러가지 옵션을 정의 합니다.
  2. Include : 이 설정은, Typescript 컴파일러에게 특정 파일이나, 디렉토리를 포함시켜 컴파일하도록 지시하는 배열입니다. 이 속성을 명시적으로 지정하지 않을 경우, 루트 디렉토리 기준의 모든 Typescript 파일을 찾도록 합니다.
  3. exclude : 2번의 설정과 반대로, 명시적으로 특정 파일이나 디렉토리를 제외하여 컴파일 하도록 지시하는 배열입니다. 
  4. reference : 일반적으로 사용하기보다는,  서로 다른 프로젝트를 참조하는데 사용하는 설정입니다. 이를 사용하면 프로젝트 간의 종속성을 명시적으로 선언할 수 있게 하고, 프로젝트 구조를 명확하게 관리할 수 있도록 돕습니다. 예를 들어, 마이크로서비스 아키텍처 형식으로 구성된 서비스는 같은 백앤드에서도, 서비스 별로 프로젝트를 나누어 관리를 하게 되지만, 프로젝트들은 서로 연결되어 있습니다. 이러한 경우, 서로 참조하고 있는 공통 코드나 인터페이스를 분리할 수 있습니다.

 

일반적으로 여기서 reference 를 제외한 3가지를 많이 사용하게 되고, 기본적 구성으로 프로젝트를 작성해보도록 하겠습니다.

 

{
  "compilerOptions": {
    "module": "CommonJS",
    "outDir": "./bundle",
    "target": "ES6",
    "esModuleInterop": true,
    "strict": true,
    "baseUrl": "./src",
    "paths": {
      "@user/*": ["user/*"]
    },
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "src/**/*.test.ts"]
}

위와 같은 기초설정을 구성했습니다. 각 설정의 의미를 알아보도록 하겠습니다.

 

먼저, "complierOptions" 에서는 컴파일러에 대한 옵션을 정의해야 합니다.

각 구성요소별로는 아래의 의미를 나타냅니다.

 

  • Module : CommonJS, ES6 등, 모듈 시스템 중 어떤 모듈 시스템을 사용할지를 정의합니다. 앞에 언급한 2가지 외에도 다른 모듈들이 존재하나, 대표적으로 CommonJS 는 Require 를, ES6 는 Import, Export 문을 사용합니다. 일반적으로 target 과 module 옵션은 동일한 옵션을 사용하는 게 좋습니다. 특정상황에서는 다르게 사용할 수도 있습니다.
  • outDir : 컴파일된 JavaScript 파일을 저장할 디렉토리를 설정합니다.
  • target : 어떤 버전의 JavaScript 를 대상으로 컴파일할지 지정합니다.  일반적으로 target 과 module 옵션은 동일한 옵션을 사용하는 게 좋습니다. 특정상황에서는 다르게 사용할 수도 있습니다.
  • esModuleInterop : Typescript 에서 ES6 모듈과 CommonJS 모듈간의 상호 운용성(interoperability) 를 제공하기 위해 설정하는 옵션입니다. 일반적으로 true 로 설정합니다. 이 값을 False 로 설정할 경우, 상호운용성을 제공하지 않아 호환성을 유지하기 위해 코드 작성단계부터 보다 번거로운 코드 작성이 이루어 지므로, 특별한 상황이 아니라면 true 로 설정합니다.
  • strict : Typescript 의 Typecheck 과정에서 엄격한 타입체크 활성화 여부를 결정합니다. 일반적으로 true 로 설정하지만, 이를 true 로 설정할 경우, 암시적으로 any 타입을 허용하지 않고, null 과 undefined 와 값 역시 명시적으로 처리해야 하는 등, 코드에서 발생할 수 있는 잠재적 오류를 사전에 파악하고 방지하는 도움을 주게 됩니다. 하지만 외부 모듈이나 라이브러리를 사용할 때, 불가피하게 any 타입이나, undefined 값을 반환하는 경우도 존재하기 때문에, 특정한 상황에서는 false 를 사용하게 될 수 도 있지만 권장하지 않습니다.
  • baseUrl & paths : 상대경로를 설정할 때 사용하는 기본 경로를 지정합니다. 즉, 상대경로를 지정함으로서, 모듈을 참조할 때 상대경로를 사용하지 않고 별칭을 부여하여, 경로를 보다 단순하게 표현하여 코드를 작성할 때, 논리적 경로를 사용할 수 있도록 지원합니다. 위의 옵션을 예시로 든다면, 현재 baseUrl 은 루트디렉토리 기준 src 디렉토리 입니다. 그리고 paths 에서 @user 라는 별칭은, src 디렉토리 내의 user 의 모든 파일을 의미합니다. Typescript 에서 *은 모든 파일을, **은 모든 디렉토리를 의미합니다. 
  • SourceMap : 소스맵 생성을 활성화합니다. 이 소스맵을 활성 하면 트랜스파일된 Jvavscript 파일과 Typescript 파일 간의 매핑 정보를 생성하여 디버깅에 도움을 주게 됩니다.  

사실 CompileOptions 에 대부분의 설정 정보가 들어가게 됩니다.

하지만 Include, exclude 역시 매우 중요한 설정정보가 들어가게 됩니다. 이 두개의 설정정보가 하는 역활은 이미 위에서 설명드린 바 있습니다.

위의 설정파일을 기준으로 설명하면, Include 를 사용해 작성자는 "src/**/* " 로 작성했습니다. 위에서 설명드린 바와 같이, src 디렉토리 내의 모든 하위 디렉토리, 모든 파일을 포함하도록 작성이 되었습니다.

 

반면에 exclude 는 node_modules, "src/**/*.test.ts" 라고 명시했습니다. 이는 우리가 잘 아는 Node_modules 디렉토리를 제외하고, src 내의 모든 디렉토리에서 모든 파일명 중 *.test.ts 를 제외하도록 명시한 것입니다. 일반적으로 JavaScript 에서 Jest 를 사용하여 TDD 를 한번이라도 작성해보셨다면 이 의미를 아시리라 생각됩니다.

 

이제 tsconfig.json 파일의 설정이 완료되었습니다.

 

Nodemon.json 의 설정은 위의 tsconfig.json 에 비해서는 비교적 간단한 구성을 가지고 있습니다.

 

{
  "watch": ["src/**/*"],
  "ext": "ts",
  "exec": "ts-node -r tsconfig-paths/register ./src/index.ts",
  "ignore": ["src/**/*.test.ts"]
}

특정한 경우가 아니라면 위의 3가지 옵션을 사용하여 nodemon 파일을 구성할 수 있습니다.

이 구성요소는 아래와 같은 특성을 가지게 됩니다.

 

  • watch : nodemon 에서 실시간으로 감시할 파일 또는 디렉토리를 지정합니다. 설정하지 않는 경우 현재 디렉토리를 감시하지만, 현재 설정에 명시한 디렉토리는 src 디렉토리 내의 모든 디렉토리, 파일입니다.
  • ext : 감시할 파일의 확장자를 지정합니다. 현재 설정에서는 .ts 파일만 감시하도록 명시합니다. 이렇게 할경우 .ts 파일이 변경되면 nodemon 에서 실시간으로 추적합니다.
  • exec : 실행할 명령어를 지정합니다. 일반적으로 여기서 단순히 "실행" 만을 목적으로 한다면 우리가 잘 아는 node src/index.ts 와 같은 명령어를 지정할 수 있습니다.
  • ignore : tsconfig.json 의 exclude 와 같은 역활을 수행합니다.

위의 설정과 함께 이제 마지막으로 package.json 의 명령줄을 하나 추가해야 합니다.

Script 에 아래와 같이 명령어를 입력해줍시다.

 

"dev": "tsc --watch && nodemon"

 

 

 

 

이제 준비가 모두 끝났습니다.

여기서 이제 아래의 명령어만 실행하면, 트랜스파일링 및 nodemon의 실시간 감시가 시작됩니다.

 

npm run dev

 

현재 설정파일은 흐름에 맞추어 작성이 된 예시입니다. 이를 살펴보도록 하겠습니다.

먼저 위의 4개의 트랜스파일링 단계를 다시금 기억해보도록 하겠습니다.

 

  1.  소스코드 분석(Parsing)
  2.  타입 검사(Type checking)
  3.  트랜스파일 (Transpilation)
  4.  코드 생성 (Code Generation)

이제 우리가 사용한 Library 가 각각 어떤 역활을 하는지 알아보겠습니다.

 

  1. Typescript - 일반적으로 이름만 본다면 라이브러리가 아니라, Typescript 언어를 뜻하는 단어이지만, NPM 을 사용한 설치한 라이브러리의 경우, 컴파일러(트랜스파일러)와 관련된 도구를 설치합니다.
  2. TSNode - Typescript 코드를 빠르게 실행할 수 있는 라이브러리입니다. TSNode 는 production 환경이 아니라, developmenet 환경에서만 사용되고, Typescript 코드를 트랜스파일(컴파일) 없이 실행할 수 있도록 돕는 라이브러리 입니다.
  3. TSconfig-paths - Typescript 에서 우리가 트랜스파일(컴파일) 을 할때 각 설정을 정의한 "tsconfig.json" 에서 baseUrl, paths 에 정의된 사용자 정의 경로 별칭, 위의 예시에서는 @user 와 같은 경로 별칭이 올바르게 해석될 수 있도록 처리하는 라이브러리 입니다.
  4. Nodemon - NodeJS 환경에서 사용되는 많이 알려진 라이브러리입니다. 이 라이브러리를 사용하면 코드가 변경됨을 감지하고, 빠르게 피드백(재실행)을 처리해주는 라이브러리 입니다.

 

일반적으로 TypeScript 에서 트랜스파일링 하는 절차에서 Library 에서 관여하는 것은 typescript 내에 내장된 도구인 "tsc" 입니다. 

즉, 단순히 트랜스파일링하는 과정에서는 Typescript 라이브러리만 설치하면, 트랜스파일링이 가능합니다.

하지만 개발환경에서는 보다 빠르게 코드에 대한 피드백, 오류 확인, 실행의 대한 결과등이 제공이 되어야 합니다.

 

그렇지 않으면 트랜스파일링 이후 파일을 실행하여, 트랜스파일링이 올바르게 처리되었는지를 확인해야 하는 번거로움이 존재합니다.

따라서 위의 Json 파일에 구성한 설정이 왜 필요한지를 이해해보아야 합니다.

 

여기서의 추가로 이해가 필요한 핵심은 nodemon.json 에서 존재하는 "exec" 의 설정값입니다.

"exec": "ts-node -r tsconfig-paths/register ./src/index.ts"

이 설정에서 눈여겨 봐야할 점은, "ts-node" , "-r tsconfig-paths/register" , 그리고 "./src/index.ts" 입니다.

먼저 "ts-node" 는 nodemon.json 에서 이전에 명시한 설정에 따라, ".ts" 확장자를 가진 파일이 변경이 감지될 때 마다, 

별도의 트랜스파일링 과정을 거치지 않아도, 파일을 실행합니다.

그리고 "-r" 옵션을 사용해 ts-node 를 실행하기 전, 모듈을 로드하도록 지시합니다.

"-r" 명령뒤에 호출되는 "tsconfig-paths/register" 라는 명령을 사용하면, tsconfig.json 에 사용자가 지정한, paths 내에 있는 사용자정의 경로 인 "@user" 로 정의된 경로를 올바른 경로로 해석 후 처리합니다.

마지막으로 현재 루트 디렉토리의 src 디렉토리 내의 index.ts 파일을 실행하도록 처리합니다.

 

이러한 구성요소의 단계를 흐름으로 표현하면 다음과 같습니다.

 

  1. 사용자는 "npm run dev" 명령을 실행합니다.
  2. 먼저 "tsc" 가 실행됩니다.
  3. tsc 는 변경된 Typescript 파일을 Javascript 파일로 트랜스파일링 합니다.
  4. nodemon 이 실행됩니다. Javascript 파일이 변경된 것을 감지하고 재시작이 이루어집니다.
  5. nodemon.json 의 exec 를 참조하여 ts-node 를 실행합니다.
  6. ts-node 는 "-r" 옵션을 통해 tsconfig-paths/register 를 사용하여 사용자정의 별칭 경로를 해석합니다.
  7. 그리고 "./src/index.ts" 파일을 실행합니다.

하지만, 위에서 사용한 Package.json 에서 사용한 명령어는 콘솔 출력을 볼 수 없습니다. 

즉, 실행중인 것은 맞지만, 오류를 확인하거나 출력의 형태가 어떻게 나타나는지 알 수 없습니다.

그래서 이 단점을 극복하기 위해, 추가적인 라이브러리를 설치하고 이를 반영하도록 하겠습니다.

npm install -d concurrently

이 라이브러리를 설치하고, package.json 에 명령어를 아래와 같이 수정합니다.

"dev": "concurrently \"tsc --watch\" \"nodemon\""

 

이제 이 명령이 어떠한 구성을 하는지 알아보도록 하겠습니다. 

 

"concurrently" 는 개발환경에서 여러 개의 명령어를 동시에 실행하고 관리할 때 사용되는 유틸리티형 라이브러리 입니다. 

위에서 발생하는 단점을 극복하기 위해, 각 명령어를 병렬로 실행하고, 출력을 하나의 콘솔창에 통합하여 보여주는 유틸리티 입니다.

따라서 위의 "dev" 에 concurrenctly 를 사용해 콘솔출력등을 하나의 콘솔창에 통합하여 나타내줍니다.

 

즉, 위의 명령어로 수정하게 되면 concurrently 유틸리티가 "tsc --watch" , "nodemon" 을 각각 병렬로 실행하고, 하나의 콘솔창의 현황을 출력하게 됩니다. 

이를 보다 더 시각적으로 손쉽게 식별하기 위해, 각 모드에 접두사와 색상을 부여할 수도 있습니다.

"dev": "concurrently -n \"TSC,Nodemon\" -c \"bgBlue.bold,bgRed.bold\" \"tsc --watch\" \"nodemon\""

먼저 "-n" 옵션을 사용해, 각각의 명령에 TSC, Nodemon 접두사를 설정합니다.

그리고 난 뒤, "-c" 옵션을 사용해 각각의 명령어에 대한 색상을 설정합니다. 위 설정에서는 "TSC" 는 "bgBlue.bold" 로 설정하고, 

"Nodemon" 은 "bgRed.bold" 로 부여합니다.

 

이렇게 되면 하나의 콘솔에 출력이 되더라도, 각각의 색상이 다르게 표시되므로, 개발 환경에서 보다 손쉽게 어떠한 프로세스에서 실행되고 있는지를 한눈에 파악할 수 있습니다.

 

다음 포스팅에서는 TypeScript 에 본격적인 타입을 비롯한 핵심개념에 대한 학습을 하도록 하겠습니다.

 

감사합니다.

반응형

댓글