typescript로 프로젝트를 진행하면서 '도대체 내가 뭘 하고 있는지 모르겠다'는 생각이 들기 시작했다.
tsconfig.json
을 만들기는 만들었는데 뭘 하고 있는지도 모르고 그저 문서에 나와 있는 대로, 또는 문제가 생겼을 때 검색해서 나온 대로 설정만 했을 뿐이었다.
납득이 가지 않는 것을 도저히 참지 못하는 성격이기에 typescript 공식문서를 읽고 그 외 여러 가지 자료를 본 것을 이 글에서 정리하고자 한다.
주의
본 글에서는 typescript 자체에 대한 설명은 하지 않는다.
vscode에서 typescript 프로젝트를 진행한다고 가정하고 typescript 설치 방법 등은 생략한다.
또한 module
방식에 대해서는 ES6
로 방식을 기준으로 설명한다.
우선 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
을 설정하는 것일까?
이유는 간단하다. 매번 명령어에 옵션을 주기가 힘들고 프로젝트에서 일정한 설정을 유지시키기 위해서다.
vscode
는 기본적으로 typescript에 대한 intellisense를 지원한다.
이 intellisense가 .ts
파일을 인식하는 방법을 제어하기 위해서 우리는 tsconfig.json
을 작성해야 한다.
여기서 주의해야 하는 점은 vscode
의 typescript intellisense
와 tsc
는 전혀 상관이 없다는 점이다.
vscode는 intellisense가 .ts
파일을 인식하는 방법을 제어하기 위해서 tsconfig.json
을 사용하는 것이고 tsc
또한 typescript를 javascript로 컴파일하는 과정에서 tsconfig.json
을 사용하는 것일 뿐이다.
vscode
와 tsc
는 분명하게 분리돼 있다.
vscode
의 경우 typescript project directory의 root에 반드시 tsconfig.json
을 넣고 이것을 별도의 세팅을 통해서 위치를 설정하는 방법을 제공하지 않고 있지만 tsc
의 경우는 --build
옵션을 통해서 원하는 설정 파일을 지정하게 할 수 있다.
즉, vscode 에디터 상에는 에러가 나오지 않지만 정작 tsc를 통해서 컴파일을 시도하면 에러가 날 수도 있고 그 반대의 상황이 나올 수도 있다는 말이다.
이 사실을 반드시 인지하고 있어야만 한다.
위에서 얘기한 것과 같이 tsconfig.json
을 설정하는 이유는 크게 2가지가 있다.
vscode
의 intellisense가 typescript 처리하는 방법을 제어하기 위해서tsc
가 typescript를 컴파일하는 방법을 제어하기 위해서1번의 경우는 vscode
에서 코드를 작성할 때 잘못된 코드를 작성하게 하는 것을 방지한다.
그리고 2번의 경우는 1번의 경우는 물론이고 동시에 실제 출력물의 형태에 대한 것 또한 포함된다.
잘 생각해 봐야 한다. 우리가 tsc
를 사용하는 이유는 결론적으로 .ts
파일을 .js
로 변환하기 위해서 이다.
그렇다면 tsconfig.json
을 설정하는 이유는 전체 typescript 프로젝트
최종 결과물이 어떻데 변환되어 출력되는지를 결정하기 위해서가 된다.
우리가 원하는 것이 tsc
를 통해서 실제 결과물을 원하는 것인지 아니면 단순히 vscode
에서 가이드라인을 제시하는 것인지를 잘 생각하면서 설정을 작성해야만 한다.
아래서부터 설명하는 내용은 공식문서를 통해서 더 자세한 내용을 알 수 있다.
모든 속성을 다 설명할 수는 없다. 없는 내용이 있다면 직접 문서에서 찾아보면 된다.
본 글에서 설명하는 속성만 잘 이해해도 추가적인 내용을 이해하는데 큰 문제는 없을 것이라 생각된다.
{
"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
을 설정하는 이유는 일단 vscode
와 tsc
를 위한 것이다. 그 외의 도구에서 tsconfig.json
을 읽어서 동작을 처리하지 않는다면 앞으로 설정하는 것들이 기대한 것과 다르게 동작할 것이라는 것을 유의해야 한다.
예시로 부록1: babel과 typescript을 읽어보길 바란다.
{
"include": ["src/**/*", "tests/**/*"]
}
include
를 통해서 pattern 형태로 원하는 파일 목록을 지정할 수 있다.
include
와 exclude
모두 glob 패턴을 지원한다.
*
없거나 하나 이상의 문자열과 일치 (디렉터리 구분자 제외)?
하나의 문자와 일치 (디렉터리 구분자 제외)**/
단계에 관계없이 아무 디렉터리와 일치만약 glob 패턴에 파일 확장자를 선언하지 않으면 typescript가 지원하는 확장자만을 포함한다. 예시로 .ts
, .tsx
, .d.ts
가 있으며 allowJs
를 활성화 시키면 .js
와 .jsx
도 포함된다.
include
를 지정해도 files
에 지정한 파일들은 제외되지 않는다.
exclude
로 include
에 지정한 파일이나 패턴을 제외시킬 수 있다.
주의할 점은 include
에 지정하지 않은 파일은 적용되지 않는 점이다.
또한 exclude
에 지정하였더라고 import
나 /// <reference
를 통해서 코드 베이스에 추가될 수 있다.
exclude
를 지정하지 않으면 ["node_modules", "bower_components", "jspm_packages"]
와 outDir
에 지정한 경로가 기본값이 된다.
{
"compilerOptions": {
}
}
위의 files
, include
, exclude
는 우리가 원하는 파일을 선택하고 제외하는 설정이라면 compilerOptions
는 선택된 파일들을 처리하는 설정이 된다.
굉장히 많은 옵션들이 존재하나 대표적인 것들만 빠르게 보고 가도록 한다.
target
을 통해서 tsc
가 최종적으로 컴파일하는 결과물의 문법 형태를 결정할 수 있다.
만약 "ES5"
를 선택했다면 코드상에 작성한 () => this
와 같은 화살표 함수는 모두 function
표현법으로 변환될 것이다.
기본 값은 "ES3"
이다. 그렇기에 만약 "ES3"
자체에 없는 기능을 코드에 작성하면 컴파일러는 에러를 출력하게 된다.
target
에 따라서 사용할 수 있는 기능이 제한될 수 있다는 것이다. 예를 들어서 ES6
부터 지원하는 Number.isInteger
를 사용하는데 target이 ES6 보다 낮게 설정돼 있다면 에러가 나게 된다.
만약 tsc
로 결과물을 출력하지 않는다면 현재 코드에서 사용하는 문법을 기준으로 target
을 선택하면 된다.
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"
를 추가하면 더 이상 에러가 발생하지 않고 컴파일도 정상적으로 진행된다.
그러나 실제 runtime
이 ES5
만 지원한다면 런타임 에러가 발생하게 될 것이다.
typescript는 타입 검사와 변환만을 할 뿐이지 polyfill 같은 것은 알아서 해야만 한다.
files
와 include
를 통해서 선택된 파일들의 결과문이 저장되는 디렉터리를 outDir
을 통해서 지정할 수 있다.
만약 타입 체크용으로 사용한다면 필요가 없다.
outFile
이라는 속성도 있는데 이는 모든 파일을 하나의 파일로 합쳐서 출력할 경우 지정하는 파일명이다.
module
이 none
, system
또는 amd
가 아닌 경우 사용할 수 없다.
es6
방식의 module
을 사용한다고 가정하는 이 글에서는 고려할 대상이 아니다.
noEmit
을 true
로 설정하면 최종결과물이 나오지 않게 된다.
이를 통해서 단순 타임 체크용으로 사용할 것인지 아니면 tsc
를 컴파일용으로 사용할 것인지 지정할 수 있게 된다.
declaration
을 true
로 설정하게 되면 해당 .ts
파일의 .d.ts
파일 또한 같이 출력물에 포함되게 된다. declaration 파일들만 따로 출력하게 하고 싶다면 declarationDir
로 별도 지정해 줄 수 있다.
emitDeclarationOnly
가 true
라면 출력물에 declaration 파일만 나오게 된다.
noEmit
과 같이 사용할 수 없다.
sourceMap
을 true
로 지정하면 출력물에 .js.map
이나 .jsx.map
파일을 포함된다.
inlineSourceMap
을 true
을 지정하면 .js
파일 내부에 source map이 포한된다.
두 속성은 같이 사용할 수 없다.
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
를 true
로 지정하면 typescript의 type 검사 옵션 중 strict*
관련된 모든 것을 true
로 만들게 된다.
strictFunctionTypes
, strictNullChecks
등 이와 같은 속성들이 모두 true
가 되고 필요에 따라서 선택하여 false
로 지정하면 된다.
기본값은 false
이며 true
로 설정하기를 권장한다.
컴파일된 결과물이 사용하게 될 module 방식이다.
모듈 해결 전략을 설정하는 것인데 여기서 "node"
로 설정하는 것이 node.js의 node_modules
에서 모듈을 가지고 오는 것이라고 오해하지 않았으면 좋겠다.
node.js
가 사용하는 방식으로 모듈을 찾는 말이다.
외부 모듈이 아닌 이상 상대 경로로 모듈을 참조해야 한다.
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
에서 일차적으로 찾을 것이고 찾기를 실패해 에러를 출력할 수밖게 없다.
모듈 참조를 baseUrl
를 기준으로 다시 매핑시킬 수 있다.
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"app/*": ["app/*"],
"config/*": ["app/_config/*"],
"lib/*": ["lib/*"],
"tests/*": ["tests/*"]
},
}
baseUrl
을 지정하는 것만으로 절대 경로로 모듈 참조를 할 수 있지만 더 상세하게 일을 지어줄 수 있게 된다.
이를 통해서 "../../../lib/some"
과 같이 길고 알아보기 힘든 경로를 "lib/some"
과 같이 단순하게 만들 수 있데 된다.
물론 이 또한 위에서 언급한 것과 같이 외부 bundler를 사용한다면 별도의 설정을 해줘야만 한다.
isolatedModules
을 true
로 설정하면 프로젝트 내에 모든 각각의 소스코드 파일을 모듈로 만들기를 강제한다.
소스코드 파일에서 import
또는export
를 사용하면 그 파일은 모듈이 된다.
만약 import / export
를 하지 않으면 그 파일은 전역 공간으로 정의된다.
isolatedModules
을 true
로 돼 있다면 모듈로 소스코드를 작성하지 않을 경우 에러를 출력한다.
만약 babel
과 같은 외부 도구를 사용한다면 isolatedModules
를 true
로 설정하는 것이 좋다.
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를 할 수 없어 위와 같이 작성해야만 한다.
이러한 문제의 불편함을 해소하기 위해서 esModuleInterop
를 true
를 설정한다.
typescript가 두 방식의 차이를 자동으로 해소할 것이다.
외부 라이브러리의 모듈을 참조할 경우 .d.ts
파일에 타입 정의가 잘못돼 있어서 오류가 나는 경우가 가끔씩 있다.
프로젝트 내부에는 문제가 없는데도 불구하고 외부 라이브러리의 타입 정의가 잘못돼서 오류가 나는 경우이다.
이럴 경우 skipLibCheck
를 true
로 지정하면 tsc
에게 .d.ts
파일의 타입 검사를 생략시킬 수 있다.
이렇게 되면 내부 프로젝트에서 정의된 .d.ts
파일까지 검사가 생략돼서 문제가 발생하지 않냐고 할 수도 있다.
내부 프로젝트에서는 .d.ts
파일 정의를 하지 않으면 된다.
.d.ts
은 내부용이 아니라 외부용으로 사용되는 게 일반적이다.
일반적인 typescript 프로젝트에서는 type 정의를 .ts
파일에서 하고 export
하고 import
해서 사용하기를 권장한다.
위의 내용을 보고 우리가 실제 파일을 출력할 것인지 단순 타입체크만 할 것인지에 따라서 필요한 속성이 될 수도 있고 굳이 쓸 필요가 없는 속성이 될 수도 있다.
만약 단순 타입 체크만 하는 경우라면 rootDir
이나 sourceMap
같은 속성들은 설정할 필요가 아예 없다.
그리고 대부분은 rootDir
의 경우는 정확히 무엇을 하는지도 모르고 사용하는 경우도 많이 보았다.
noEmit
을 true
로 설정하거나 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
에는 더 많고 자세한 설정들이 있다.
여유가 된다면 공식문서에서 하나씩 읽어보면 필요한 설정들을 더 상세히 찾을 수 있을 것이다.
잘못된 점이 있거나 언제든지 부담 없이 말씀해주시기 바랍니다.
보통 typescript로 프로젝트를 진행하게 되면 단순하게 tsc
만을 사용하지는 않을 것이다.
webpack
과 babel
을 많이 사용하게 될 것인데 우리가 여기서 알아야 할 것은 babel
은 tsc
와 상관이 없다는 것이다.
의아하다면 package.json
에서 typescript
를 완전히 제외시킨 뒤 확인해 보면 될 것이다.
아무런 문제가 발생하지 않는다.
우리가 webpack에서 .ts
파일을 처리하기 위해서 ts-loader
를 사용한다면 typescript
가 필요하다. 그러나 우리는 .ts
파일을 처리하기 위해서 babel-loader
를 사용할 것이고 babel은 typescript에 의존하지 않는다.
babel
은 typescript
를 독자적으로 알아서 해석하고 처리한다.
그렇다면 "우리가 여기서 알아야만 하는 점"은 babel
은 .ts
을 파일 처리할 때 전혀 tsconfig.json
을 확인하지 않는다는 점이다.
tsconfig.json
에 무엇을 작성하든 babel이 typescript를 javascript로 변환할 때 어떠한 영향도 주지 않을 것이다.
tsconfig.json
은 vscode
의 intellisense를 제어하기 위한 용이고 babel
이 typescript를 처리하는 방식을 제어하고 싶다면 babel.config.json
을 통해서 설정해야만 한다.
"compilerOptions": {
"isolatedModules": true
}
babel은 typescript를 처리할 때 모든 파일을 module로 취급하기 때문에 다른 것은 몰라도isolatedModules
하나는 꼭 true
로 설정해야 한다.
그래야 vscode
에서 작성 시 import / export
를 하지 않아서 문제가 생기는 실수를 방지할 수 있다.
tsconfig.json
을 통해서 typescript 프로젝트의 코딩 작성 규칙까지는 강제할 수 없다.
tsc
가 확인해주는 것은 설정에 따라서 type이 올바르게 작성돼 있느냐이지 내가 원하는데로 코드를 작성했는가를 확인해주지는 않으니까 말이다.
위의 얘기가 너무 당연하고 굳이 이런 얘기를 왜 하는지 모를 수도 있다.
예를 들어보자.
declare function add(a: number, b: number): number;
위와 같이 add
가 정의돼 있다고 해보자.
그러면 아래와 같은 실제 코드를 작성하면 잘못된 것인가?
function add(a: number, b: number) {
return a + b;
}
typescript의 타입 추론
기능에 의해서 add
는 number
타입을 반환하는 것을 알 수 있고 이는 정상적으로 타입 체크를 통과한다.
그러나 function을 작성 함에 있어서 반드시 출력 타입 작성하게 만들고 싶으면 어떻게 할까?
tsconfig.json
을 통해서는 이런 것을 강제할 수 없다.
그렇기에 이러한 추가적인 코드 스타일 강제는 eslint
와 같은 도구를 사용해야만 한다.
오와아아아앙 멋져용 선댓글 후감상