약 3 주간 TypeScript를 공부한 개념들을 총정리해보려 한다. 올해 초 React + Typescript 조합을 배우기 위해 수강했던 강의는 유익했지만, 왜 TS를 사용하고 어디에 적용하는게 유용한지 알지 못한 상태로 프로젝트의 코드를 따라 작성하기만 바빴던 기억이 있다. C/C++로 코딩을 시작했기 때문에 정적 타입에 대해 친숙하지만, JS로 더 오랜 기간동안 개발하면서 놓치고 있던 타입 안정성을 보완하기 위해 TS를 제대로 배워보았다.
웹 프로그래밍에 사용 가능한 언어인 JS는 dynamic, weakly typed language이다. JS는 자율성을 가지지만 아래와 같은 버그가 발생하여 우리를 오류로부터 보호해주지 못한다. 두 숫자를 더하는 로직을 만들 때 의도치 않게 number 대신 string이 들어간 경우, JS에서는 에러를 발생시키지 않고 두 string을 concat하여 "53"을 만들어낸다.
console.log(5 + 3) // 8 : add
console.log("5" + "3") // "53" : concat -> bug!
다른 예시로 string + number가 가능하고 함수 호출 시 parameter의 개수가 모자라도 오류가 발생하지 않으며, 타입 체킹을 코드가 실행중일 때 진행하므로 Runtime error만을 발생시킨다. 이렇게 의도치 않은 방식의 버그가 발생할 때 코드를 보호하기 위해 강력한 타입 안정성이 필요하다.
[Fig. 1 Static Typed Vs Dynamic Typed 언어의 타입 체킹(출처: Ref[1])]
TS는 새로운 언어가 아니라 JS + static type을 더하여 Java, C/C++, Rust과 같이 사용할 수 있게 한다. 브라우저는 JS만 실행할 수 있으므로 TS를 사용하려면 TS ➡️ JS로 컴파일하는 과정이 필요하다. TS에서는 위와 같은 에러가 감지되면 런타임이 아닌 코드 실행 전인 컴파일 시에 감지하여 Compile error를 발생시켜 우리의 코드를 더 강력하게 보호할 수 있다.
또한 TS의 강력한 기능 중 하나인 Type Inference는 우리가 타입을 명시하지 않아도 자동으로 추론한다. 타입 선언 여부는 개발자가 결정하며 필요한 경우를 제외하고는 TS가 추론하게 두는 것이 좋다고 한다.
그렇다면 TS에서는 어떻게 타입을 명시할 수 있을까? 아래와 같이 타입을 명시할 변수 이름 옆에 변수명:type
으로 표기해주면 된다. TS에서 다루는 타입들을 자세히 알아보자
let names: string = "Quartz";
function add(a: number, b:number) {
return a + b;
}
우리가 JS에서도 자주 사용하는 string
, number
그리고boolean
을 그대로 사용할 수 있다.
객체나 배열의 타입을 선언할 때는 이들이 담고 있는 item들의 타입도 함께 명시해야 한다.
const resultContainer: { res: number } = {
res: result,
};
const names: string[] = ["Max" ,"Quartz", "Belle"];
배열의 타입을 선언할 때 string[]
= Array<string>
로도 나타낼 수 있는데 이에 대한 generic type과 함수의 타입 선언은 추후 4장에서 상세히 설명할 예정이다.
JS에는 존재하지 않지만 TS에는 존재하는 유용한 타입들에 대해 간략히 정리해보려 한다.
any
: 모든 타입을 수용한다. TS 를 더이상 사용하지 않고 빠져나오고 싶을 때 쓰는 타입이므로 신중히 사용해야 한다.const player: [string, number, boolean] = ["nico", 12, false];
unknown
: 어떤 타입인지 미리 모르는 변수 // lieteral & union type
type PrintMode = "console" | "alert";
//0 //1
enum OutputMode { CONSOLE, ALERT };
PrintMode === OutputMode.CONSOLE
?
타입을 변수로 분리하여 저장하는 것에는 3 가지 방법이 있으며 나는 아래와 같은 표를 근거로 object의 타입을 저장할 때는 interface
를 사용하고 나머지는 type
을 사용한다는 기준을 세웠다.
Type Alias는 type
키워드를 사용하여 타입을 별칭을 변수로 저장하는 방법이다. 함수, 원시 타입, 객체, 배열 등 다양한 형태의 타입을 저장할 수 있다.
type Player = { // type alias로 저장하고
name: string,
}
const quartz: Player = { // 인스턴스 만들어서 사용하기
name : "quartz"
}
Object의 타입만을 저장할 수 있으며 객체 생성 방법은 type alias와 동일하다.
interface Player { // interface로 저장하고
name: string,
}
const quartz: Player = { // 인스턴스 만들어서 사용하기
name : "quartz"
}
JS에 존재하는 개념인 class에 추상화 개념을 얹어서 인스턴스 생성은 불가능하지만 blueprint를 작성하여 다른 클래스에 상속이 가능하다. 클래스처럼 prop, method의 접근 제한을 둘 수 있으며 컴파일 시 일반 클래스로 변환된다.
abstract class User {
constructor(
private firstName: string,
protected lastName: string,
public nickName: string
) { }
abstract getLastName(): void // abstract method
getFullName(){ // 일반 method
return `${this.firstName} ${this.lastName}`
}
}
함수의 타입을 설명하기 위해서는 arguments와 return type을 모두 명시해야 하며 이를 call signature라고 한다. 일반 함수와 화살표형 함수 모두 아래와 같이 나타낼 수 있으며 type alias를 사용하여 더 간편하게 사용이 가능하다.
//arg type. //return type
function playerNaker(name:string) : Player {}
const playerNaker = (name:string) : Player => {}
type Add = (a:number, b:number) => number; // call signature
const add : Add = (a,b) => a+b
함수의 return type 중 리턴이 없는 경우는 void
, 절대 리턴하지 않는 경우는 never
를 사용한다.
함수는 여러 개의 call signature를 가질 수도 있는데 이를 overloading이라고 하며 아래와 같이 표현할 수 있다.
// A. 다른 타입의 같은 개수 파라미터(이 경우가 많음)
type Push = {
(path: string):void // 1
(path: string, state: object): void //2
}
const push:Push = (config) => {
if(typeof config === "string") console.log(config) //1
else console.log(config.path) //2
}
// B.같은 타입의 다른 개수 파라미터(드뭄)
type Add = {
(a:number, b:number) : number
(a:number, b:number, c:number) : number
}
const add:Add = (a, b, c?:number) =>{ //c param이 optional 이라고 표기
if(c) a+b+c
return a+b
}
함수의 call signature를 작성할 때 input type이 명확하지 않은 경우가 있다. 이때 any
를 사용하면 TS의 장점이 사라지는데 이를 해결하기 위해 generic type을 사용한다. 아래의 call signature은 input으로 들어오는 타입을 그대로 return type에 활용할 수 있는 generic type을 사용한 예시이다.
//1. call signature에서 사용
function returnFirst<T>(a: T[]):T {
return a[0]
}
Generic type의 사용 방법은
1. <T>
로 generic 사용을 표기하고 (T가 아닌 어떤 문자도 사용 가능하다)
2. 그 문자(T)를 타입으로 사용하면 된다.
Generic은 함수 뿐만 아니라 type alias, interface에서도 사용이 가능하며, React와 같은 라이브러리나 여러 패키지에서 흔히 마주칠 수 있다.
//2. type alias에서 사용
type ReturnFirst = {
<T>(arr: T[]): T //parameter로 들어오는 타입 유추해줌
}
const returnFirst: ReturnFirst =(arr)=>{
return a[0]
}
//3. interface에서 사용
interface Items<T> {
[key: string]: T;
}
여기까지 TS의 사용 방법과 예시를 간결하게 정리해보았다. TS ➡️ JS로 컴파일하는 setting을 하기 위해서는 tsconfig.json
파일을 수정하면 되고, React와 같은 라이브러리를 사용하면 기본 설정이 자동으로 되어있다. 이 파일에서는 컴파일할 자바스크립트의 버전, 기본으로 추가되는 라이브러리, JS 파일 혼용 여부, strict mode 등을 설정한다.
리액트 프로젝트에서 TS를 사용하여 타입을 명시하는 대표적인 예시로 아래의 3 가지가 있다.
다음과 같이 props를 받는 <UserResultComp />
가 있다고 하자. TS를 사용하는 경우 이 함수형 컴포넌크가 받는 props의 타입을 명시해주어야 한다.
<UserResultComp countCorrect={countCorrect} setIsSubmit={setIsSubmit} getDBUsers={getDBUsers} />
리액트의 함수형 컴포넌트는 React.FC
라는 generic type을 가지며 TS는 이 타입이 명시되면 리액트의 컴포넌트가 받는 default props에 대한 정보들을 갖게 된다. 우리는 기존 React.FC
generic type에 커스텀 props 를 추가하여 React.FC<{ prop1: type }>
으로 명시할 수 있다.
interface UserResultCompProps {
countCorrect: number;
setIsSubmit: React.Dispatch<React.SetStateAction<boolean>>;
getDBUsers(): Promise<void>;
}
const UserResultComp: React.FC<UserResultCompProps> = ({
countCorrect,
setIsSubmit,
getDBUsers,
}) => {
return null;
};
export default UserResultComp;
두 번째useState
또한 generic type을 사용하는 예시이다. 타입을 명시하지 않으면 아래와 같은 에러가 발생할 수 있으므로 타입을 표기해주어야 한다.
const [todos, setTodos] = useState([]); // 타입 안쓰면 never[] type이 됨 = error: 항상 비어있어야 한다는 뜻이 됨
// 사용 예시
const [todos, setTodos] = useState<string[]>([]);
const [item, setItem] = useState<number>([]);
Click event listener나 form data를 가져올 때도 해당 event data의 타입을 명시해야 하며, React.FormEvent
,React.MouseEvent
등 TS가 추론해주는 기능을 활용하여 명시하면 된다.
1 Statically Typed Vs Dynamically Typed Languages
4 Udemy JS, React 강의