{ tsconfig.json } 제대로 알고 사용하기

Taegyun Sooran Kim·2021년 8월 21일
128
post-thumbnail

typescript로 프로젝트를 진행하면서 '도대체 내가 뭘 하고 있는지 모르겠다'는 생각이 들기 시작했다.
tsconfig.json을 만들기는 만들었는데 뭘 하고 있는지도 모르고 그저 문서에 나와 있는 대로, 또는 문제가 생겼을 때 검색해서 나온 대로 설정만 했을 뿐이었다.
납득이 가지 않는 것을 도저히 참지 못하는 성격이기에 typescript 공식문서를 읽고 그 외 여러 가지 자료를 본 것을 이 글에서 정리하고자 한다.

주의
본 글에서는 typescript 자체에 대한 설명은 하지 않는다.
vscode에서 typescript 프로젝트를 진행한다고 가정하고 typescript 설치 방법 등은 생략한다.
또한 module 방식에 대해서는 ES6로 방식을 기준으로 설명한다.


tsc 사용하기

우선 tsconfig.json을 만들지 않아도 우리는 tsc를 그냥 사용할 수 있다.
tsc를 통해서 원하는 .ts파일을 .js로 컴파일할 수 있다.

$ tsc hello.ts

위와 같이 실행 시 동일한 위치에 hello.js 파일이 생성된 것을 확일 할 수 있다.
이름을 다르게 하고 싶다면 tsc --outFile ho.js hello.ts와 같이 사용할 수도 있다.
--outFile와 같은 옵션들은 tsc --help를 통해서 더 알아볼 수 있다.

이와 같이 tsc는 사실 tsconfig.json 파일 없이도 바로 사용할 수 있다.
그러면 왜 tsconfig.json을 설정하는 것일까?
이유는 간단하다. 매번 명령어에 옵션을 주기가 힘들고 프로젝트에서 일정한 설정을 유지시키기 위해서다.

tsconfig.json

vscode는 기본적으로 typescript에 대한 intellisense를 지원한다.
이 intellisense가 .ts 파일을 인식하는 방법을 제어하기 위해서 우리는 tsconfig.json을 작성해야 한다.

여기서 주의해야 하는 점은 vscodetypescript intellisensetsc는 전혀 상관이 없다는 점이다.
vscode는 intellisense가 .ts 파일을 인식하는 방법을 제어하기 위해서 tsconfig.json을 사용하는 것이고 tsc 또한 typescript를 javascript로 컴파일하는 과정에서 tsconfig.json을 사용하는 것일 뿐이다.

vscodetsc는 분명하게 분리돼 있다.

vscode의 경우 typescript project directory의 root에 반드시 tsconfig.json을 넣고 이것을 별도의 세팅을 통해서 위치를 설정하는 방법을 제공하지 않고 있지만 tsc의 경우는 --build 옵션을 통해서 원하는 설정 파일을 지정하게 할 수 있다.

즉, vscode 에디터 상에는 에러가 나오지 않지만 정작 tsc를 통해서 컴파일을 시도하면 에러가 날 수도 있고 그 반대의 상황이 나올 수도 있다는 말이다.
이 사실을 반드시 인지하고 있어야만 한다.

tsconfig.json 설정하기

위에서 얘기한 것과 같이 tsconfig.json을 설정하는 이유는 크게 2가지가 있다.

  1. vscode의 intellisense가 typescript 처리하는 방법을 제어하기 위해서
  2. tsc가 typescript를 컴파일하는 방법을 제어하기 위해서

1번의 경우는 vscode에서 코드를 작성할 때 잘못된 코드를 작성하게 하는 것을 방지한다.
그리고 2번의 경우는 1번의 경우는 물론이고 동시에 실제 출력물의 형태에 대한 것 또한 포함된다.

잘 생각해 봐야 한다. 우리가 tsc를 사용하는 이유는 결론적으로 .ts 파일을 .js로 변환하기 위해서 이다.
그렇다면 tsconfig.json을 설정하는 이유는 전체 typescript 프로젝트 최종 결과물이 어떻데 변환되어 출력되는지를 결정하기 위해서가 된다.
우리가 원하는 것이 tsc를 통해서 실제 결과물을 원하는 것인지 아니면 단순히 vscode에서 가이드라인을 제시하는 것인지를 잘 생각하면서 설정을 작성해야만 한다.

아래서부터 설명하는 내용은 공식문서를 통해서 더 자세한 내용을 알 수 있다.
모든 속성을 다 설명할 수는 없다. 없는 내용이 있다면 직접 문서에서 찾아보면 된다.
본 글에서 설명하는 속성만 잘 이해해도 추가적인 내용을 이해하는데 큰 문제는 없을 것이라 생각된다.

최상위 속성

- files

{
  "compilerOptions": {},
  "files": [
    "core.ts",
    "sys.ts",
    "types.ts",
    "scanner.ts",
    "parser.ts",
    "utilities.ts",
    "binder.ts",
    "checker.ts",
    "tsc.ts"
  ]
}

files를 통해서 우리가 원하는 파일만 tsc가 처리하도록 만들 수 있다.

$ tsc target.ts

위 같이 cli에서 특정 파일을 지목해서 사용했다면

$ tsc

위와 같이 파일 목록을 넘겨주지 않고 바로 tsc 만 실행시킬 수 있게 된다.
경로는 tsconfig.json의 위치에서 상대 경로로 작성하면 된다.
물론 이 옵션은 타겟 파일이 적을 때 사용하는 게 좋다.

주의
우선 여기서 분명히 다시 한번 얘기하고 가자면 우리가 tsconfig.json을 설정하는 이유는 일단 vscodetsc를 위한 것이다. 그 외의 도구에서 tsconfig.json을 읽어서 동작을 처리하지 않는다면 앞으로 설정하는 것들이 기대한 것과 다르게 동작할 것이라는 것을 유의해야 한다.
예시로 부록1: babel과 typescript을 읽어보길 바란다.

- include

{
  "include": ["src/**/*", "tests/**/*"]
}

include를 통해서 pattern 형태로 원하는 파일 목록을 지정할 수 있다.
includeexclude 모두 glob 패턴을 지원한다.

  • * 없거나 하나 이상의 문자열과 일치 (디렉터리 구분자 제외)
  • ? 하나의 문자와 일치 (디렉터리 구분자 제외)
  • **/ 단계에 관계없이 아무 디렉터리와 일치

만약 glob 패턴에 파일 확장자를 선언하지 않으면 typescript가 지원하는 확장자만을 포함한다. 예시로 .ts, .tsx, .d.ts가 있으며 allowJs를 활성화 시키면 .js.jsx도 포함된다.

include를 지정해도 files에 지정한 파일들은 제외되지 않는다.

- exclude

excludeinclude에 지정한 파일이나 패턴을 제외시킬 수 있다.
주의할 점은 include에 지정하지 않은 파일은 적용되지 않는 점이다.
또한 exclude에 지정하였더라고 import/// <reference를 통해서 코드 베이스에 추가될 수 있다.

exclude를 지정하지 않으면 ["node_modules", "bower_components", "jspm_packages"]outDir에 지정한 경로가 기본값이 된다.

compilerOptions 속성

{
  "compilerOptions": {
    
  }
}

위의 files, include, exclude는 우리가 원하는 파일을 선택하고 제외하는 설정이라면 compilerOptions는 선택된 파일들을 처리하는 설정이 된다.
굉장히 많은 옵션들이 존재하나 대표적인 것들만 빠르게 보고 가도록 한다.

- target

target을 통해서 tsc가 최종적으로 컴파일하는 결과물의 문법 형태를 결정할 수 있다.
만약 "ES5"를 선택했다면 코드상에 작성한 () => this와 같은 화살표 함수는 모두 function 표현법으로 변환될 것이다.

기본 값은 "ES3"이다. 그렇기에 만약 "ES3" 자체에 없는 기능을 코드에 작성하면 컴파일러는 에러를 출력하게 된다.
target에 따라서 사용할 수 있는 기능이 제한될 수 있다는 것이다. 예를 들어서 ES6부터 지원하는 Number.isInteger를 사용하는데 target이 ES6 보다 낮게 설정돼 있다면 에러가 나게 된다.

만약 tsc로 결과물을 출력하지 않는다면 현재 코드에서 사용하는 문법을 기준으로 target을 선택하면 된다.

- lib

lib는 현재 프로젝트에서 사용할 수 있는 특정 기능에 대한 문법(타입)을 추가해준다.
설정하는 target에 따라서 기본으로 설정되는 lib가 달라진다.

만약 프로젝트가 web browser서 실행돼야 하기에 DOM관련 API를 호출해야 한다고 해보자.
그러나 typescript 기본으로 DOM관련 API를 문법에 추가해주지 않는다.
그렇게에 document.querySelector 같이 코드를 작성하면 "document는 존재하지 않는다."라고만 하게 된다.
이때 lib["DOM"]과 같이 지정하면 DOM관련 API의 타입을 사용할 수 있데 된다.

주의할 점은 lib는 typescript가 그런 문법과 기능이 있다는 것을 알게 해주는 것이지 runtime에 해당 기능을 추가해주는 것이 아니라는 것이다.
만약 target"ES5"인데 Number.isInteger와 같은 기능을 사용하게 되면 에러가 나게 된다.
이때 lib"ES6"를 추가하면 더 이상 에러가 발생하지 않고 컴파일도 정상적으로 진행된다.
그러나 실제 runtimeES5 만 지원한다면 런타임 에러가 발생하게 될 것이다.

typescript는 타입 검사와 변환만을 할 뿐이지 polyfill 같은 것은 알아서 해야만 한다.

- outDir

filesinclude를 통해서 선택된 파일들의 결과문이 저장되는 디렉터리를 outDir을 통해서 지정할 수 있다.

만약 타입 체크용으로 사용한다면 필요가 없다.

outFile이라는 속성도 있는데 이는 모든 파일을 하나의 파일로 합쳐서 출력할 경우 지정하는 파일명이다.
modulenone, system 또는 amd가 아닌 경우 사용할 수 없다.
es6 방식의 module을 사용한다고 가정하는 이 글에서는 고려할 대상이 아니다.

- noEmit

noEmittrue로 설정하면 최종결과물이 나오지 않게 된다.
이를 통해서 단순 타임 체크용으로 사용할 것인지 아니면 tsc를 컴파일용으로 사용할 것인지 지정할 수 있게 된다.

- declaration

declarationtrue로 설정하게 되면 해당 .ts 파일의 .d.ts 파일 또한 같이 출력물에 포함되게 된다. declaration 파일들만 따로 출력하게 하고 싶다면 declarationDir로 별도 지정해 줄 수 있다.

- emitDeclarationOnly

emitDeclarationOnlytrue라면 출력물에 declaration 파일만 나오게 된다.

noEmit과 같이 사용할 수 없다.

- sourceMap

sourceMaptrue로 지정하면 출력물에 .js.map 이나 .jsx.map 파일을 포함된다.
inlineSourceMaptrue을 지정하면 .js 파일 내부에 source map이 포한된다.
두 속성은 같이 사용할 수 없다.

- typeRoots

typeRoots는 배열로 설정하며 기본값은 ["node_modules/@types"]이다.
typescript가 정의돼 있는 type을 찾는 공간이 된다.
경로는 tsconfig.json이 있는곳에서 부터 상대 경로로 작성하면 된다.

만약 추가적인 type들을 정의한다면 별도의 type 디렉터리를 만들고 그 안에 .d.ts 파일을 만든 뒤 디렉터리를 typeRoots에 추가해 주면 된다.
예를 들어서 프로젝트의 루트 디렉터리에 @types/ 디렉터리를 만들었다면 ["node_modules/@types", "@types"]와 같이 설정해 주면 된다.

include에 이미 포함된 곳이라면 굳이 추가해줄 필요가 없다.
대부분이 잘못 알고 있는 것 중 하나가 프로젝트 내에서 type 파일을 만들게 되면 typeRoots에 추가해야 한다고 알고 있는데 include에 포함돼 있는 .d.ts 파일은 자동으로 typescript가 인식하므로 넣어줄 필요가 없다.
include 외에 있는 경우만 추가해주면 된다.

또한 추가적인 typeRoots를 설정했는데 "node_modules/@types"을 빼놓게 되면 기본으로 추가가 되지 않으니 문제가 될 수 있음으로 반드시 추가해야 한다.

typeRoots를 통해서 지정한 type 경로는 일반적으로 외부 라이브러리가 제공하는 모듈의 타입을 정의하기 위해서 사용한다.
만약 외부 모듈을 가져오려고 할 때 "Could not find a declaration file for module 'xxxx'."와 같은 에러 메세지가 나게 된다면 @types/xxxx 패키지를 설치하거나 직접 정의를 해줘야만 하는데 직접 정의하게 될 경우 이 경로에 작성하면 된다.
프로젝트 내의 타입을 정의하기 위해서 사용하지 않기를 바란다.

- strict

stricttrue로 지정하면 typescript의 type 검사 옵션 중 strict* 관련된 모든 것을 true로 만들게 된다.
strictFunctionTypes, strictNullChecks 등 이와 같은 속성들이 모두 true가 되고 필요에 따라서 선택하여 false로 지정하면 된다.

기본값은 false이며 true로 설정하기를 권장한다.

- module

컴파일된 결과물이 사용하게 될 module 방식이다.

- moduleResolution

모듈 해결 전략을 설정하는 것인데 여기서 "node"로 설정하는 것이 node.js의 node_modules에서 모듈을 가지고 오는 것이라고 오해하지 않았으면 좋겠다.
node.js가 사용하는 방식으로 모듈을 찾는 말이다.

- baseUrl

외부 모듈이 아닌 이상 상대 경로로 모듈을 참조해야 한다.
baseUrl은 외부 모듈이 아닌 모듈들을 절대 경로 참조할 수 있게 해 준다.
만약 baseUrl"src"로 설정하게 되면 src/를 기준으로 절대 경로로 모듈 참조가 가능해진다.

project-root/
|-- tsconfig.json
|-- src/
|-- |-- index.ts
|-- |-- lib/
|-- |-- |-- some.ts

위와 같이 파일들이 존재한다면 index.ts에서 some.ts를 절대 경로로 참조할 수 있데 된다.

// import { someFunc } from "./lib/some";
import { someFunc } from "lib/some";

someFunc();

그러나 vscode에서 intellisense가 제대로 "lib/some"을 찾아주었고 vsc에서도 정상적으로 타입 검사를 통과했더라도 webpack과 같은 외부 bundler를 사용한다면 별도의 설정을 해줘야만 한다.
별도의 설정을 하지 않는다면 외부 bundler는 "lib/some"node_modules에서 일차적으로 찾을 것이고 찾기를 실패해 에러를 출력할 수밖게 없다.

- paths

모듈 참조를 baseUrl를 기준으로 다시 매핑시킬 수 있다.

{
  "compilerOptions": {
    "baseUrl": "src",
    "paths": {
        "app/*": ["app/*"],
        "config/*": ["app/_config/*"],
        "lib/*": ["lib/*"],
        "tests/*": ["tests/*"]
    },
}

baseUrl을 지정하는 것만으로 절대 경로로 모듈 참조를 할 수 있지만 더 상세하게 일을 지어줄 수 있게 된다.
이를 통해서 "../../../lib/some"과 같이 길고 알아보기 힘든 경로를 "lib/some"과 같이 단순하게 만들 수 있데 된다.

물론 이 또한 위에서 언급한 것과 같이 외부 bundler를 사용한다면 별도의 설정을 해줘야만 한다.

- isolatedModules

isolatedModulestrue로 설정하면 프로젝트 내에 모든 각각의 소스코드 파일을 모듈로 만들기를 강제한다.
소스코드 파일에서 import 또는export를 사용하면 그 파일은 모듈이 된다.
만약 import / export를 하지 않으면 그 파일은 전역 공간으로 정의된다.

isolatedModulestrue로 돼 있다면 모듈로 소스코드를 작성하지 않을 경우 에러를 출력한다.
만약 babel과 같은 외부 도구를 사용한다면 isolatedModulestrue로 설정하는 것이 좋다.

- esModuleInterop

ES6가 아닌 CommonJS의 경우 module의 export 방법이 다르다.
ES6의 경우 export를 할 때 이름을 지정하나 default로 내보내게 되는데 CommonJs의 경우 module.exports = xxx를 통해서 객체를 내보낼 수 있다.
이때 import가 호환이 안되게 된다.

// import moment from "moment";
import * as moment from "moment";
moment();

CommonJs로 작성된 moment는 import moment from "moment"로 import를 할 수 없어 위와 같이 작성해야만 한다.
이러한 문제의 불편함을 해소하기 위해서 esModuleInteroptrue를 설정한다.
typescript가 두 방식의 차이를 자동으로 해소할 것이다.

- skipLibCheck

외부 라이브러리의 모듈을 참조할 경우 .d.ts 파일에 타입 정의가 잘못돼 있어서 오류가 나는 경우가 가끔씩 있다.
프로젝트 내부에는 문제가 없는데도 불구하고 외부 라이브러리의 타입 정의가 잘못돼서 오류가 나는 경우이다.
이럴 경우 skipLibChecktrue로 지정하면 tsc에게 .d.ts 파일의 타입 검사를 생략시킬 수 있다.

이렇게 되면 내부 프로젝트에서 정의된 .d.ts 파일까지 검사가 생략돼서 문제가 발생하지 않냐고 할 수도 있다.
내부 프로젝트에서는 .d.ts 파일 정의를 하지 않으면 된다.
.d.ts은 내부용이 아니라 외부용으로 사용되는 게 일반적이다.
일반적인 typescript 프로젝트에서는 type 정의를 .ts 파일에서 하고 export 하고 import 해서 사용하기를 권장한다.

단순 타입 체크용으로 사용하려면

위의 내용을 보고 우리가 실제 파일을 출력할 것인지 단순 타입체크만 할 것인지에 따라서 필요한 속성이 될 수도 있고 굳이 쓸 필요가 없는 속성이 될 수도 있다.
만약 단순 타입 체크만 하는 경우라면 rootDir이나 sourceMap 같은 속성들은 설정할 필요가 아예 없다.
그리고 대부분은 rootDir의 경우는 정확히 무엇을 하는지도 모르고 사용하는 경우도 많이 보았다.

noEmittrue로 설정하거나 tsc --noEmit 옵션을 활용하면 tsc를 빌드 전 단순 타입 체크를 위한 도구로 만들 수도 있다.
package.json에서 type check용 script를 만들고 build와 연결해서 사용할 수도 있다.

{
  "scripts": {
    "type-check": "tsc --noEmit",
    "build:no-check": "webpack",
    "build": "npm run type-check && npm run build:no-check"
  }
}

마무리

typescript에 프로젝트를 진행하면서 '도대체 나는 무엇을 하고 있는가'라는 괴로움으로 수많은 문서를 읽어서 정리한 내용을 글로 작성하였다.
사실 부족함이 많은 글이지만 적어도 무엇을 하고 있는가에 대해서는 조금이나마 도움을 줄 수 있다고 생각한다.

위에서 설명하지 않았지만 tsconfig.json에는 더 많고 자세한 설정들이 있다.
여유가 된다면 공식문서에서 하나씩 읽어보면 필요한 설정들을 더 상세히 찾을 수 있을 것이다.

잘못된 점이 있거나 언제든지 부담 없이 말씀해주시기 바랍니다.

부록1: babel과 typescript

보통 typescript로 프로젝트를 진행하게 되면 단순하게 tsc 만을 사용하지는 않을 것이다.
webpackbabel을 많이 사용하게 될 것인데 우리가 여기서 알아야 할 것은 babeltsc와 상관이 없다는 것이다.

의아하다면 package.json에서 typescript를 완전히 제외시킨 뒤 확인해 보면 될 것이다.
아무런 문제가 발생하지 않는다.

우리가 webpack에서 .ts 파일을 처리하기 위해서 ts-loader를 사용한다면 typescript가 필요하다. 그러나 우리는 .ts 파일을 처리하기 위해서 babel-loader를 사용할 것이고 babel은 typescript에 의존하지 않는다.
babeltypescript를 독자적으로 알아서 해석하고 처리한다.

그렇다면 "우리가 여기서 알아야만 하는 점"은 babel.ts을 파일 처리할 때 전혀 tsconfig.json을 확인하지 않는다는 점이다.
tsconfig.json에 무엇을 작성하든 babel이 typescript를 javascript로 변환할 때 어떠한 영향도 주지 않을 것이다.

tsconfig.jsonvscode의 intellisense를 제어하기 위한 용이고 babel이 typescript를 처리하는 방식을 제어하고 싶다면 babel.config.json을 통해서 설정해야만 한다.

"compilerOptions": {
  "isolatedModules": true
}

babel은 typescript를 처리할 때 모든 파일을 module로 취급하기 때문에 다른 것은 몰라도isolatedModules 하나는 꼭 true로 설정해야 한다.
그래야 vscode에서 작성 시 import / export를 하지 않아서 문제가 생기는 실수를 방지할 수 있다.

부록2: lint

tsconfig.json을 통해서 typescript 프로젝트의 코딩 작성 규칙까지는 강제할 수 없다.
tsc가 확인해주는 것은 설정에 따라서 type이 올바르게 작성돼 있느냐이지 내가 원하는데로 코드를 작성했는가를 확인해주지는 않으니까 말이다.

위의 얘기가 너무 당연하고 굳이 이런 얘기를 왜 하는지 모를 수도 있다.
예를 들어보자.

declare function add(a: number, b: number): number;

위와 같이 add가 정의돼 있다고 해보자.
그러면 아래와 같은 실제 코드를 작성하면 잘못된 것인가?

function add(a: number, b: number) {
  return a + b;
}

typescript의 타입 추론 기능에 의해서 addnumber 타입을 반환하는 것을 알 수 있고 이는 정상적으로 타입 체크를 통과한다.

그러나 function을 작성 함에 있어서 반드시 출력 타입 작성하게 만들고 싶으면 어떻게 할까?
tsconfig.json을 통해서는 이런 것을 강제할 수 없다.
그렇기에 이러한 추가적인 코드 스타일 강제는 eslint와 같은 도구를 사용해야만 한다.

profile
납득가는 개발을 하고자 하는 납득이입니다.

9개의 댓글

comment-user-thumbnail
2021년 8월 21일

오와아아아앙 멋져용 선댓글 후감상

1개의 답글
comment-user-thumbnail
2021년 8월 21일

이 글을보고 tsconfig에 대한 두려움이 사라졌습니다!

1개의 답글
comment-user-thumbnail
2021년 8월 23일

오 !!!! 글 잘 읽었어요!!

답글 달기
comment-user-thumbnail
2021년 8월 23일

👏👏👏👏👏👏👏

답글 달기
comment-user-thumbnail
2021년 8월 24일

👏👏👏👏👏👏👏

답글 달기
comment-user-thumbnail
2021년 9월 1일

좋은 글 잘 읽었습니다!

답글 달기
comment-user-thumbnail
2023년 3월 19일

좋은 글 감사합니다!

답글 달기