📘 TypeScript, 왜 쓰는가
JavaScript만 써본 사람은 있어도 TypeScript 썼다가 JavaScript 쓰는 사람은 없을 것이다… -김지수-
TypeScript를 사용함으로써
- 흔히 발생할 수 있는 실수를 방지한다.
- 자신, 다른 개발자 혹은 미래에 팀에 합류할 개발자에게 문서를 제공한다.
- 리팩토링이 쉬워진다.
- 자동완성… 없이 개발하는 상상해보세요… 악몽..
⇒ 즉, 안전한 프로그램을 구현할 수 있게 하며, 개발 생산성이 매우 높아진다.
⇒ 칼퇴할 가능성이 높아진다…(❁´◡`❁)
여기서 ‘안전한’은 ‘타입 안정성(Type Safety)’을 의미 *타입을 이용해 프로그램이 유효하지 않은 작업을 수행하지 않도록 방지.
JS
- JS는 동적 타이핑 언어 = 프로그램을 실행해야만 특정 데이터의 타입을 알 수 있음
- 타입을 선언하지 않아도 된다 ≠ 타입을 신경쓰지 않아도 된다.
- JS는 정말 고맙게도, 실수가 있는 코드도 최대한 실행하려고 시도하기 때문에 개발자의 의도와 맞지 않는 작업이 실행될 수 있음.
- JS는
프로그램을 실행할 때개발자가 실수 했다고 알려준다.- 즉, 실행하기 전까지는 오류가 있는지 알 수 없다!
TS
- 오류를 알려주는 시점: 코드를 입력하는 순간! ✨
- 개발자는 타입을 신경쓰게 되고, 프로그램 작성 시 타입 수준으로 먼저 생각하게 된다.
- 더욱 유지하기 쉽고 이해하기 쉬운 프로그램을 설계하게 된다.
🔍 TypeScript 자세히 들여다보기
대부분의 언어의 프로그램 실행 과정
- 컴파일러는 작성된 코드를 파싱하여 추상 문법 트리(Abstract Syntax Tree, AST)라는 자료 구조로 변환
- 이 때 공백, 주석, 탭 등의 결과 완전 무시
- AST를 바이트코드(bytecode)라는 하위 수준의 표현으로 변환
- 런타임에 바이트코드를 입력하여 평가하고 결과를 얻음.
즉, 프로그램을 실행한다는 것
= 컴파일러가 소스코드를 파싱해서 AST → bytecode → 런타임 평가하도록 지시하는 것.
타입스크립트는?
2번째 단계에서 AST를 바이트코드가 아닌 자바스크립트로 변환한다.
AST를 만들어 결과 코드를 내놓기 전 타입확인을 거친다. (타입 안정성이 생기는 부분)
- 타입스크립트 코드 파싱→ 타입스크립트 AST
- 타입검사기(typechecker)가 AST를 확인
- 타입스크립트 AST → 자바스크립트 소스로 방출
- 자바스크립트 소스 → 자바스크립트 AST
- AST → 바이트코드
- 런타임이 바이트코드를 평가
*13은 TSC가, 46은 브라우저 혹은 node.js 등 자바스크립트 엔진에서 실행됨.
-
문제) 우리가 작성한 타입은 어디에서까지 사용될까?
1, 2에서 소스코드의 타입들을 사용하고, 타입스크립트를 자바스크립트로 컴파일할 때는 타입을 확인하지 않는다.
이말은 즉, 우리가 작성한 타입은 오로지 타입 검사에만 사용되며, 최종적으로 만들어지는 프로그램에는 아무런 영향을 주지 않는다.
TypeScript의 타입 시스템
타입 검사기(Ttypechecker)가 프로그램에 타입을 할당할 때 사용하는 규칙들. 크게 두 가지로 나뉜다.
- 어떤 타입을 사용하는지 컴파일러에게 명시적으로 알려주는 타입 시스템
- 자동으로 타입을 추론하는 타입 시스템
똑똑한 TS는 두 타입 시스템에 영향을 받아 알아서 추론도 하고, 명시적으로 타입을 지정 가능.
TS는 추론을 꽤 잘 하기 때문에, 필요할 때만 타입을 명시적으로 지정하는 것이 좋다.
-
다른 언어의 타입 시스템?
JavaScript, Python, Ruby : 런타임에 타입 추론
Haskell, OCaml : 컴파일 타임에 추론 및 빠진 타입 검사
Scala, TypeScript: 명시적 타입 지원, 컴파일 타임에 명시되지 않은 타입 추론 및 검사
Java, C: 거의 모든 타입을 명시해야함. 컴파일 타임에 검사
시작은 tsconfig.json로 부터
마지막으로 tsconfig 파일을 만졌던 게 언제인가요?
대부분의 프로젝트에서는 이미 사용되고 있는 options들을 복붙해서 사용. 새로운 프로젝트를 부트스트래핑하지 않는 이상 tsconfig를 하나하나 작성할 일은 잘 없다.
⇒ 정말 tsconfig 옵션들을 잘 알고 있나?
🤨 tsconfig.json, tsconfig.base.json은 뭐고 tsconfig.eslint.json은 뭐지?
혼란에 빠진 채로 EDS의 tsconfig 파일을 수정하며 tsconfig부터 다시 살펴보기로 다짐했고 typescript 스터디 장을 맡게 되어버렸다.
tsconfig란
-
어떤 파일을 컴파일하고 어떤 JavaScript 버전으로 방출할지 등을 결정하는 config 파일.
-
모든 타입스크립트 프로젝트는 root 디렉토리에
tsconfig.json파일이 있어야한다.→
tsconfig.json이 있는 디렉토리가 해당 typescript 프로젝트의 root 디렉토리임을 가리킨다. -
100개 이상의 옵션이 존재 — 모든 옵션은 공식문서 확인 (opens in a new tab)
오늘은 root fields와 compilerOptions 일부만 !
- The root fields (opens in a new tab) : for letting TypeScript know what files are available
- The
compilerOptions(opens in a new tab) fields : this is the majority of the document
root fields
these options relate to how your TypeScript or JavaScript project is set up.
files
glob 문법을 사용하지 못하며, 어떤 파일을 TypeScript 앱에 포함할지 직접 파일 이름 작성.
→ 프로젝트 내 파일이 적어서 globs 문법*을 사용할 필요 없을 때는 유용함.
{
"compilerOptions": {},
"files": [
"core.ts",
"sys.ts",
"types.ts",
"scanner.ts",
"parser.ts",
"utilities.ts",
"binder.ts",
"checker.ts",
"tsc.ts"
]
}files필드를 작성하면,inclues필드의 기본 값은["**/*"]에서 빈 배열로 변경된다.- 이미
includes필드를 사용중이라면,files필드를 추가하는 것을 주의하자.
- 이미
*glob문법 : globs pattern이라고도 하며, 리눅스부터 사용된 파일 패턴 매칭하기 위한 문법. 정규표현식과 유사해보이지만 다름 주의! vscode 검색에서도 사용된다.
Globs (Glob Patterns) 문법 정리 (opens in a new tab)
extends
tsconfig 파일의 path를 지정하여 ts 설정을 상속받아 사용한다.
-
상속받을 base config파일이 먼저 로드되고, 상속받는 파일의 옵션들로 설정이 확장됨.
-
config파일의 모든 상대 경로는 상속받는 파일이 위치한 곳의 상대 경로로 해석됨.
만약 상속 대상인 **
tsconfig.base.json**의 내부 필드에 **./src**와 같은 상대 경로가 지정되어 있는 경우에는 해당 상속 파일을 불러온 주체인tsconfig.json기준으로 상대 경로가 계산된다.⇒ 모노레포 구조에서 유용.
주요 사용 예시: elice-material, elice-design-system과 같은 모노 레포구조
- 모노 레포에 포함된 모든 프로젝트에 공통적으로 설정을 사용하고 싶은 경우.
configs/base.json:
// tsconfig.base.json
{
"files": [
"./src/main.ts"
]
}tsconfig.json:
{
"extends": "./tsconfig.base.json",
"files": [
"./src/index.ts"
]
}적용되는 tsconfig 옵션
{
"files": ["./src/index.ts"],
}includes
파일 이름 혹은 globs 패턴을 사용하여 TypeScript 앱에 포함할 파일을 명시한다.
files필드의 존재 여부에 따라 기본값이 변경됨- 만약
files필드가 선언되어 있다면[] - 선언되어 있지 않다면
["**/*"]
- 만약
globs 패턴은 개발 중 유용하게 쓰이기 때문에 한번 찾아보길 추천합니다.
**: 0개 이상의 하위 디렉토리의 파일에 매칭. 가장 유용하게 쓰이는 와일드카드. 현재 디렉토리 ~ 가장 하위 디렉토리까지 탐색.
{
"include": ["src/**/*", "tests/**/*"]
}.
├── scripts ⨯
│ ├── lint.ts ⨯
│ ├── update_deps.ts ⨯
│ └── utils.ts ⨯
├── src ✓
│ ├── client ✓
│ │ ├── index.ts ✓
│ │ └── utils.ts ✓
│ ├── server ✓
│ │ └── index.ts ✓
├── tests ✓
│ ├── app.test.ts ✓
│ ├── utils.ts ✓
│ └── tests.d.ts ✓
├── package.json
├── tsconfig.json
└── yarn.lockexclude
includes에 매칭된 패턴, 파일 이름 중 TypeScript 앱에 포함하지 않을 파일들을 명시한다.
- 기본값 :
['node_modules', 'bower_components', 'jspm_packages']
외부에서 받아온 패키지도 컴파일이 필요하다면 exclude 필드를 수정하자.
참고 ) **Next.js**는 **node_modules**가 포함되도록 세팅되며, 수정해도 빌드할 때 자동으로 넣어버림
- excludes에 명시했지만 TypeScript 어플리케이션에 포함되는 경우
-
코드 내에서 import 문을 사용하여 해당 모듈을 가져오는 경우
예를 들어, 다음과 같이 설정했지만,
include: ["src/**/*.ts"], exclude: ["src/example/test.ts"]이렇게 프로젝트 내 type check될 파일에서 excludes에 매칭된 파일을 import하는 경우, ‘src/example/test.ts’ 는 exclude 배열에 명시되어있어도 해당 모듈을 가져고 프로젝트에 포함되게 된다.
import {testFunction} from 'src/example/test';만약 import해도 exclude에 명시한 파일이 제외된다면, 결과적으로 컴파일에 포함되지 않아서 프로젝트 빌드할 때 사라지는 파일을 앱 내에서 의존하게 되는 것.
Documentation - Module Resolution (opens in a new tab)
혹시 test 코드를 사용할 때 type check는 되기 원하지만, 컴파일에 포함되지 않게 하고싶다면 이 글 (opens in a new tab)을 참고하세요
-
triple slash directive (opens in a new tab)(**
/// <reference path="..." />)**를 사용하여 특정 모듈을 포함하라고 직접 지시하는 경우 -
files 필드에 직접 컴파일할 파일을 명시하는 경우
-
→ exlucde 필드에 세팅했던 것을 무시하고 해당 모듈을 가져온다.
compiler options - type check, modules, … 😵
compiler option은 종류별로 다음과 같이 나눠진다.
- Type Checking (opens in a new tab)
- Modules (opens in a new tab)
- Emit (opens in a new tab)
- JavaScript Support (opens in a new tab)
- Editor Support (opens in a new tab)
- Interop Constraints (opens in a new tab)
- Backwards Compatibility (opens in a new tab)
- Language and Environment (opens in a new tab)
- Compiler Diagnostics (opens in a new tab)
- Projects (opens in a new tab)
- Output Formatting (opens in a new tab)
- Completeness (opens in a new tab)
- Command Line (opens in a new tab)
- Watch Options (opens in a new tab)
약 80여가지의 option… 시간관계상 엘리스에서 사용중인 옵션들 위주로 확인해보자!
{
"compilerOptions": {
"target": "es2017",
"module": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"experimentalDecorators": true,
"downlevelIteration": true,
"moduleResolution": "node",
"isolatedModules": true,
"resolveJsonModule": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": ".",
"typeRoots": ["node_modules/@types"],
"allowJs": true
},
"include": ["src"],
}- 간략히 설명
baseUrl- 비-상대적 모듈 이름을 해석하기 위한 기본 디렉터리
- 모듈 해석 문서: https://typescript-kr.github.io/pages/module-resolution.html#base-url (opens in a new tab)
skipLibCheck- 모든 선언 파일(
*.d.ts)의 타입 검사를 건너뜁니다
- 모든 선언 파일(
esModuleInterop- 런타임 Babel 생태계 호환성을 위한
__importStar와__importDefaulthelper 를 내보내고 타입 시스템 호환성을 위해-allowSyntheticDefaultImports를 활성화
- 런타임 Babel 생태계 호환성을 위한
allowSyntheticDefaultImportsdefault export가 없는 모듈에서default ifmports를 허용합니다. 코드 방출에는 영향을 주지 않으며, 타입 검사만 수행함
forceConsistentCasingInFileNames- 동일 파일 참조에 대해 일관성 없는 대소문자를 비활성화
noFallthroughCasesInSwitch- 스위치 문에 fallthrough 케이스에 대한 오류를 보고
module/m- 모듈 코드 생성 지정:
None/CommonJS/AND/System/UMD/ES6/ES2015/ESNext (next version of javascript)
- 모듈 코드 생성 지정:
moduleResolution- 모듈 해석 방법 결정:
Node/Classic - 모듈 해석 문서: https://typescript-kr.github.io/pages/module-resolution.html (opens in a new tab)
- 모듈 해석 방법 결정:
resolveJsonModule.json확장자로 import된 모듈을 포함
isolatedModules- 추가 검사를 수행하여 별도의 컴파일 (예를 들어 transfiled module 혹은 @babel/plugin-transform-typescript (opens in a new tab)) 이 안전한지 확인
- transfiled module: https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API#a-simple-transform-function (opens in a new tab)
noEmit- 출력을 내보내지 않음
jsx.tsx파일에서 JSX 지원- JSX: https://typescript-kr.github.io/pages/jsx.html (opens in a new tab)
target
TypeScript 파일을 컴파일 하여 생성할 EcmaScript 버전.
기본값 : es3
가장 최신 버전 : esnext
lib
프로젝트에서 사용할 특정 기능, 문법을 추가. 설정된 target 에 따라 lib 의 기본 값이 달라짐.
웹 브라우저에서 실행될 프로젝트인 경우, DOM API를 호출해야하므로 "dom" 을 필요홀 한다.
"lib": ["dom", "dom.iterable", "esnext"],ex) “dom” 설정값이 없는 경우, document.querySelector → "document는 존재하지 않는다."는 에러
물론, 이 설정값은 TypeScript에 해당 문법, 기능을 알게 해주는 것이며, runtime에 해당 기능을 추가해주는 것은 아니다.
ex) target이 "ES5"인데 ES6 문법을 사용하면 에러가 발생한다. 이때 lib에 "ES6"를 추가하면 에러가 사라지고 컴파일도 정상적으로 된다.
그러나 실제 runtime이 ES5 만 지원한다면 런타임 에러가 발생하게 될 것이다.
allowJs
컴파일 시 JavaScript 파일도 포함될 수 있는지 여부.
elice-web과 같이 javascript 프로젝트를 TypeScript로 점진적으로 바꿔나갈 때 사용하기 좋음.
만약 js파일을 ts, tsx파일에서 import한다면 에러가 발생하지만, allowJS 를 켜놓으면 ts파일에서도 js파일을 import하여 사용가능.
noEmit
noEmit을 true로 설정하면 최종결과물이 나오지 않게 된다.
이를 통해서 단순 타임 체크용으로 사용할 것인지 아니면 tsc를 컴파일용으로 사용할 것인지 지정할 수 있게 된다.
noFallthroughCasesInSwitch
Fallthrough, 즉 switch 문 내에서 break 가 설정되지 않아서 두 번째 케이스 문까지 수행되는 경우가 존재하는 경우 경고를 보여줄 지에 대한 옵션.
default: false (swithc 문에서 Fall through 케이스가 존재해도 무시)
const a: number = 6;
switch (a) {
case 0:
**Fallthrough case in switch.**
console.log("even");
case 1:
console.log("odd");
break;
}FallThrogh Case: switch 문을 break 나 return 으로 종료시키지 않고 다음 케이스로 그냥 흘려버리는 케이스. C++이나 Swift에서는 명시적으로 **[[fallthrough]]**와 같은 키워드로 의도한 fallthrough임을 알릴 수 있지만, 자바스크립트는 명시적으로 Fallthrough case를 표현할 수 없고, 쉽게 실수할 수 있다.
strict
default: false
strict 옵션은 그 자체로 어떤 역할을 하는 것이 아니라, 다른 Strict 관련 옵션들을 일괄적으로 켜고 끌 수 있는 옵션이다.
strict 옵션 = 일명 Strict mode family를 켜고 끄는 옵션
- alwaysStrict (opens in a new tab)
- strictBindCallApply (opens in a new tab)
- strictFunctionTypes (opens in a new tab)
- strictNullChecks (opens in a new tab)
- strictPropertyInitialization (opens in a new tab)
- useUnknownInCatchVariables (opens in a new tab)
- noImplicitAny (opens in a new tab)
- noImplictThis (opens in a new tab)
만약 strict 옵션을 **true**로 설정하면 Strict mode family 옵션들의 값도 함께 **true**로 적용되지만, 만약 개별 옵션을 **false**로 오버라이딩한다면 해당 옵션만 끌 수도 있다.
이 외에도 무궁무진한 tsconfig의 세계… 타입스크립트 문서와 함께하세요 😉
📮 Q&A 노트
typescript 관련 추천 자료
Advanced TypeScript (opens in a new tab)
Documentation - Everyday Types (opens in a new tab)
[tsconfig의 모든 것] Compiler options / Type Checking (opens in a new tab)