-
Notifications
You must be signed in to change notification settings - Fork 5
Typescript를 적용해보자!
작성자: 김유석
-
CRA에서 설정
npx create-react-app my-app[프로젝트 이름] --template typescript
CRA가 기본적인 것들을 설치하고 설정해주기 때문에 설정을 수정하고 싶다면
yarn eject
를 시행하면 config, scripts 폴더가 생기면서 설정파일들이 보여지게 된다. (다시 돌이킬 수 없다.)
CRA가 자동으로 설정해주지만 기본적인 tsconfig 설정에 대해 알아보자.
// tsconfig.json { "compilerOptions": { "strict": true, "target": "es5", "lib": ["dom", "dom.iterable", "esnext"], "baseUrl": "src", "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx" }, "include": ["src", "public/API"] }
compilerOptions 항목은 tsc 명령 형식에서 옵션을 나타내고, include 항목은 대상 파일 목록을 나타낸다.
위 예시의 include 항목 내 "./src~"는 src 디렉터리는 물론 src의 하위 디렉터리에 있는 모든 파일을 컴파일 대상으로 포함한다는 의미이다.
strict : 엄격모드 사용 여부. true로 설정하지 않으면 Typescript를 사용하는 의미가 퇴색된다.
target : 트랜스파일 시 /build에 생성되는 자바스크립트의 버전을 설정 (es5, es6 등)
lib : 컴파일 할 때 포함 될 라이브러리 목록, esnext는 가장 최신 버전
baseUrl : 모듈 이름을 처리할 기준 디렉토리
import abc from 'src/components/abc' import abc from 'components/abc' // baseUrl을 src로 설정시 이와 같이 접근할 수 있다.
allowJs: TypeScript 컴파일 작업 진행시 js파일도 컴파일 할것인지 여부
skipLibCheck : 정의 파일의 타입 확인을 건너 뛸것인지
esModuleInterop : 타입스크립트 코드는 CommonJS 방식으로 동작하지만 AMD 방식으로 웹 브라우저에서 동작하다는 가정으로 만들어진 라이브러리도 동작시키기 위해 true값을 설정해야한다.
allowSyntheticDefaultImports : default export를 쓰지 않은 모듈도 default import가 가능하게 할건지 여부
forceConsistentCasingInFileNames : 같은 파일에 대한 일관되지 않은 참조를 허용하지 않을지 여부
noFallthroughCasesInSwitch : switch문에서 fallthrough 에 대한 에러 보고
module : 프로그램에서 사용할 모듈 시스템을 결정한다. 즉 모듈 내보내기/불러오기 코드가 어떠한 방식의 코드로 컴파일 될지 결정한다.
- CommonJS (target 프로퍼티가 ES3 혹은 ES5로 지정되었을 때의 기본값)
- ES6/ES2015 (target 프로퍼티가 ES6 혹은 그 이상의 버전으로 지정되었을 때의 기본값)
- 나머지 (ES2020, ESNext, AMD, UMD, System, None)
moduleResolution : 모듈 해석 방법 설정. module 키의 값이 commonjs이면 node로 설정하지만, amd이면 classic으로 설정한다. classic으로 설정할 일은 거의 없을 것이라고 한다.
resolveJsonModule : TypeScript 모듈 내에서 json 모듈을 가져올수 있는 옵션
isolatedModules : 각 파일을 분리된 모듈로 트랜스파일
noEmit : 결과 파일을 내보낼지 여부
jsx : jsx 코드 생성 설정
[그외 추가 옵션들]
noImplicitAny : 타입스크립트 컴파일러는 기본적으로 타입을 명시하지 않은 코드일 경우 암시적으로 any 타입을 설정한 것으로 간주한다. true로 설정하면 오류 메시지를 알려준다.
outDir : baseDir 설정값을 기준으로 했을 때 하위 디렉터리의 이름
paths : 소스 파일의 import 문에서 from 부분을 해석할 때 찾아야 하는 디렉터리를 설정
sourceMap : true이면 트랜스파일 디렉터리에 .js 파일 이외에 .js.map 파일이 만들어진다. 이 소스맵 파일은 변환된 자바스크립트 코드가 타입스크립트 코드의 어디에 해당하는지 알려준다. 주로 디버깅에 사용된다.
downlevelIteration : 생성기(generator)라는 타입스크립트 구문이 정상적으로 동작하려면 반드시 true 값으로 설정해야한다.
-
eslint 설정
eslint linting 라이브러리 eslint, eslint가 typescript를 lint 할 수 있도록 허용해주는 parser인 @typescript-eslint/parser typescript에 구체화된 ESLINT rule들을 잔뜩 포함하는 플러그인인 @typescript-eslint/eslint-plugin
npm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
또 리액트에서 타입스크립트를 사용하려면
eslint-plugin-react
를 설치해야 한다.npm i -D eslint-plugin-react
.eslintrc.js
을 프로젝트 디렉토리 최상단, 즉 root 디렉토리에 만들어준다.// .eslintrc.js module.exports = { parser: "@typescript-eslint/parser", // Specifies the ESLint parser extends: [ "plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react "plugin:@typescript-eslint/recommended" // Uses the recommended rules from @typescript-eslint/eslint-plugin ], parserOptions: { ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features sourceType: "module", // Allows for the use of imports ecmaFeatures: { jsx: true // Allows for the parsing of JSX } }, rules: { }, settings: { react: { version: "detect" // Tells eslint-plugin-react to automatically detect the version of React to use } } };
rules 부분에 Lint할 조건들을 적어주면 된다고 적혀있지만 prettier config를 통해서 바로 formatting으로 해줄 수 있다고 한다.
-
prettier 설정
타입 주석 : 변수 뒤에 콜론(:)과 타입 이름을 기입해 변수에 타입을 지정한다.
🤨 let키워드로 변수 선언시 지정한 타입 외에 다른 값으로 변경할 수 없다!
타입 추론 : 변수의 타입 부분이 생략되면 대입 연산자(=)의 오른쪽 값을 분석해 왼쪽 변수의 타입을 결정한다.
🤨 변수에 타입 표기를 하지 않아도 코드 흐름을 파악하는 데 어려움이 없을 것이기에 변수 선언시 타입을 명시적으로 표기하지 않도록 해보자!
let n: number = 1
let m = 2
The primitives: string, boolean, number
const bool: boolean = false const num: number = 1 const str: string = 'hi'Arrays
두가지 형태로 표시 가능.
type[]
Array<type>
const numArr: num[] = [1, 2, 3] const strArr: string[] = ['hello', 'world'] const boolArr: Array<boolean> = [false, true]🙃
[number]
와 같은 표현으로 나타내진 타입은 Array가 아닌 Tuple이라고 불리는 타입이다.any
특정 값으로 인해 타입체킹 에러가 발생하는 것을 원하지 않을 때 사용한다. 즉, any 타입에는 어떤 값이든 써도 된다. Typescript를 쓰는 이유를 퇴색시킴!
Object Types
객체의 프로퍼티들과 각 프로퍼티의 타입들을 나열하면 된다.
각 프로퍼티를 구분할 때,
,
또는;
를 사용할 수 있고, 마지막에 위치한 구분자의 표기는 선택 사항이다.옵셔널 프로퍼티 활용 가능
function printName(obj: { first: string; last?: string }) {}Null and Undefined
초기화되지 않은 값을 가리키는 두 가지 원시 타입
각 타입의 동작 방식은
strictNullChecks
옵션 설정 여부에 따라 달라진다.
strictNullChecks
가 설정되지 않았다면, 어떤 값이null
또는undefined
일 수 있더라도 해당 값에 평소와 같이 접근할 수 있고,null
과undefined
는 모든 타입의 변수에 대입될 수 있다.Null 검사의 부재는 버그의 주요 원인이 되기도 하므로 별다른 이유가 없으면 코드 전반에 걸쳐
strictNullChecks
옵션을 설정하는 것을 권장한다.Null 아님 단언 연산자 (접미사
!
)명시적인 검사를 하지 않고도 타입에서
null
과undefined
를 제거할 수 있는 특별한 구문을 제공한다.표현식 뒤에
!
를 작성하면 해당 값이null
또는undefined
가 아니라고 타입 단언하는 것이다.function liveDangerously(x?: number | undefined) { // 오류 없음 console.log(x!.toFixed()); }다른 타입 단언과 마찬가지로 코드의 런타임 동작을 변화시키지 않으므로 반드시 해당 값이
null
또는undefiend
가 아닌 경우에만 사용해야 한다.Enums
이름이 있는 상수들의 집합을 정의할 수 있다.
Enum을 사용하면 의도를 문서화 하거나 구분되는 사례 집합을 더 쉽게 만들 수 있다.
enum Direction { Up = 1, Down, Left, Right, }위 코드에서
Up
이 1로 초기화된 숫자 열거형을 선언했다. 그 지점부터 뒤따르는 멤버들은 자동으로 증가된 값을 갖는다.원한다면, 전부 초기화 하지 않을 수 있고, 자동으로 0부터 값이 지정된다.
초기화할 수 있는 값으로는 컴파일 시 알아낼 수 있는 표현식을 포함한다.
Enum의 이름을 사용해 타입을 선언해 사용하면 된다.
enum UserResponse { No = 0, Yes = 1, } function respond(recipient: string, message: UserResponse): void { // ... } respond("Princess Caroline", UserResponse.Yes);**문자열 열거형(String enums)**은 유사한 개념이지만 런타임에서 열거형의 동작이 약간 다르다.
각 멤버들을 문자열 리터럴 또는 다른 문자열 열거형 멤버로 상수 초기화 해야 한다.
enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT", }숫자 열거형처럼 자동 증가하는 기능은 없지만 '직렬화'를 잘한다는 이점이 있다.
숫자만으로는 이것이 어떤 의미인지 유의미한 정보를 제공해주지 않을 수 있기 때문에 문자열 열거형을 통해 유의미하고 읽기 좋은 값을 이용하여 실행할 수 있다.
열거형 타입 자체가 효율적으로 각각의 열거형 멤버의 유니언으로 사용할 수도 있고, 열거형 멤버를 리터럴 타입처럼 사용할 수도 있다.
런타임 시에는 열거형은 실제 존재하는 객체이다. 따라서 함수에 인자로 사용하든지 할 수 있다.
런타임 시 실제 객체라고 할지라도, 컴파일 시
keyof
키워드는 일반적인 객체와는 다르게 동작한다. 대신,keyof
typeof` 를 사용하면 열거형의 모든 키를 문자열로 나타내는 타입을 가져온다.
enum LogLevel { ERROR, WARN, INFO, DEBUG, } type LogLevelStrings = keyof typeof LogLevel; /** * 이것은 아래와 동일합니다. : * type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG'; */다음과 같이 역매핑을 사용할 수 있다.
enum Enum { A, } let a = Enum.A; let nameOfA = Enum[a]; // "A"Unknown
any와 같이 최상위 타입인 unknown은 알 수 없는 타입을 의미한다. 어떤 타입의 값도 할당할 수 있지만, unknown을 다른 타입에 할당할 수 없다.
따라서 any보다는 안전하게 사용할 수 있다.
타입을 단언하면 할당할 수 있다.
Void
void는 일반적으로 값을 반환하지 않는 함수에서 사용햔다.
값을 반환하지 않는 함수는 실제로는 undefined를 반환한다.
Never
Never은 절대 발생하지 않을 값을 나타내며, 어떠한 타입도 적용할 수 없다.
즉, 예외를 반환했거나, 프로그램의 실행이 종료됐다는 것을 의미한다.
function error(message: string): never { throw new Error(message); }보통 다음과 같이 빈 배열을 타입으로 잘못 선언한 경우 Never을 볼 수 있다.
const never: [] = []; never.push(3); //Argument of type '3' is not assignable to parameter of type 'never'Symbol
Tuple
길이가 정해져 있고, 순차적으로 type이 지정되어 있는 배열을 나타낸다.
ex)
const tuple: [string, number, string] = ['']
-
매개변수 타입 표기
매개변수 이름 뒤에 표기한다.
-
반환 타입 표기
반환 타입은 매개변수 목록 뒤에 표기한다.
🙃 변수의 타입 표기와 마찬가지로, 반환 타입은 표기하지 않아도 되는 것이 일반적이라고 한다.
function greet(name: string): void {
console.log("Hello, " + name.toUpperCase() + "!!");
}
유니언 타입은 서로 다른 두개 이상의 타입들을 사용하여 만드는 것으로, 사용된 타입 중 하나를 타입으로 가질 수 있다.
조합에 사용된 타입을 유니언 타입의 멤버라고 부른다.
function printId(id: number | string) {}
유니언 타입을 사용할 시 모든 멤버에 대하여 유효한 작업에 대해서만 허용된다. string | number
라는 유니언 타입에서 string
타입에만 유효한 메서드는 사용할 수 없고, 이를 사용하려면 분기처리가 필요하다.
객체 타입이나 유니언 타입을 사용할 때 직접 다 표기하지 않고 이름을 부여해 표기할 수 있다.
type Point = {
x: number;
y: number;
};
// 앞서 사용한 예제와 동일한 코드입니다
function printCoord(pt: Point) {}
객체 타입을 만드는 또 다른 방법이다.
interface Point {
x: number;
y: number;
}
function printCoord(pt: Point) {}
마치 타입이 없는 임의의 익명 객체를 사용하는 것처럼 동작한다.
타입 별칭과의 차이점 - 타입은 새 프로퍼티를 추가하도록 개방될 수 없는 반면, 인터페이스의 경우 항상 확장될 수 있다.
// 인터페이스 확장
interface Animal {
name: string
}
interface Bear extends Animal {
honey: boolean
}
// 교집합을 이용한 타입 확장
type Animal = {
name: string
}
type Bear = Animal & {
honey: Boolean
}
때로는 Typescript보다 우리가 어떤 값의 타입을 더 잘 아는 경우도 있다.
이런 경우, 타입 단언을 사용해 구체적으로 명시할 수 있다.
타입 단언은 보다 구체적인 또는 덜 구체적인 버전의 타입으로 변환하는 타입 단언만이 허용된다.
즉, 불가능한 강제 변환을 방지한다.
두 가지 형태로 사용할 수 있다.
<타입>객체 (단, jsx 구문 분석에 어려움이 있을 수 있으므로 .tsx 파일에서는 허용하지 않음)
객체 as 타입
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
🙃 타입 단언은 컴파일 시 제거되므로, 타입 단언과 관련된 검사는 런타임 중에 이루어지지 않는다!
const 키워드로 변수를 선언하면 값을 변경할 수 없기 때문에 아래와 같이 타입을 지정할 수 있다.
const constantString: "Hello World" = "Hello World"
리터럴 타입은 그 자체만으로는 그닥 유용하지 않지만 유니언과 함께 사용하면 유용하게 표현할 수 있다.
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
function compare(a: string, b: string): -1 | 0 | 1 {
return a === b ? 0 : a > b ? 1 : -1;
}
타입스크립트에서는 객체의 프로퍼티는 이후에 값이 변화할 수 있다고 가정한다.
따라서 객체의 프로퍼티를 리터럴 타입으로 추론하지 않는다.
const handleRequest(url: string, method: "GET" | "POST") {}
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);
// Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.
위와 같이 함수의 매개변수를 리터럴 타입으로 지정했을 때, 객체의 프로퍼티를 인자로 넘기면 GET
타입으로 인식하지 않고 string
타입으로 인식해 에러가 발생한다.
이러한 경우를 해결하는 데 다음과 같은 두 가지 방법을 사용할 수 있다.
-
둘 중에 한 위치에 타입 단언을 추가
// 수정 1: const req = { url: "https://example.com", method: "GET" as "GET" }; // 수정 2 handleRequest(req.url, req.method as "GET");
수정 1은 ”
req.method
가 항상 리터럴 타입"GET"
이기를 의도하며, 이에 따라 해당 필드에"GUESS"
와 같은 값이 대입되는 경우를 미연에 방지하겠다”는 것을 의미수정 2는 “무슨 이유인지,
req.method
가"GET"
을 값으로 가진다는 사실을 알고 있다”는 것을 의미 -
as const
를 사용하여 객체 전체를 리터럴 타입으로 변환const req = { url: "https://example.com", method: "GET" } as const; handleRequest(req.url, req.method);
as const
접미사는 일반적인const
와 유사하게 작동하는데, 해당 객체의 모든 프로퍼티에string
또는number
와 같은 보다 일반적인 타입이 아닌 리터럴 타입의 값이 대입되도록 보장한다.
재사용 가능한 컴포넌트를 생성할 때, 즉, 다양한 타입에서 작동하는 컴포넌트를 작성할 때 사용할 수 있다.
function identity<Type>(arg: Type): Type {
return arg;
}
타입에 적용되는 타입 변수를 활용한다.
위의 경우, identity 함수에 Type
라는 타입 변수를 추가했다.
Type
는 유저가 준 인수의 타입을 캡처하고 (예 - number
), 이 정보를 나중에 사용할 수 있게 한다. 여기에서는 Type
를 반환 타입으로 다시 사용한다.
위의 제네릭 identity 함수는 두 가지 방법 중 하나로 호출할 수 있다.
첫 번째 방법은 함수에 타입 인수를 포함한 모든 인수를 전달하는 방법이다.
let output = identity<string>("myString"); // 출력 타입은 'string'입니다.
두 번째 방법은 아마 가장 일반적인 방법이다. 여기서는 타입 인수 추론 을 사용한다.
let output = identity("myString"); // 출력 타입은 'string'입니다.
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length); // 오류: Type에는 .length 가 없습니다.
// Property 'length' does not exist on type 'Type'.
return arg;
}
위와 같은 변수 타입은 any나 모든 타입을 의미하기 때문에 .length 멤버가 없는 타입이 포함될 수 있다.
실제로 이 함수가 Type이 아닌 Type의 배열에서 동작하도록 의도했다고 가정하면 아래와 같이 표현할 수 있다.
function loggingIdentity<Type>(arg: Type[]): Type[] {
console.log(arg.length); // 배열은 .length를 가지고 있습니다. 따라서 오류는 없습니다.
return arg;
}
제네릭 인터페이스로 표현 가능
interface GenericIdentityFn {
<Type>(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
제네릭 제약조건(generic constraints)
any와 모든 타입에 동작하는 대신에, .length 프로퍼티가 있는 any와 모든 타입에서 동작하도록 제한하고 싶으면?
extends
키워드로 표현한 인터페이스를 이용해 명시한다.
interface Lengthwise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}