[TypeScript] 타입스크립트 총정리하기

Quartz 쿼츠·2022년 10월 28일
1

약 3 주간 TypeScript를 공부한 개념들을 총정리해보려 한다. 올해 초 React + Typescript 조합을 배우기 위해 수강했던 강의는 유익했지만, 왜 TS를 사용하고 어디에 적용하는게 유용한지 알지 못한 상태로 프로젝트의 코드를 따라 작성하기만 바빴던 기억이 있다. C/C++로 코딩을 시작했기 때문에 정적 타입에 대해 친숙하지만, JS로 더 오랜 기간동안 개발하면서 놓치고 있던 타입 안정성을 보완하기 위해 TS를 제대로 배워보았다.

1. TypeScript란?

1.1 TypeScript를 왜 사용해야 하는가?

웹 프로그래밍에 사용 가능한 언어인 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])]

1.2 TypeScript는 어떻게 동작하는가?

TS는 새로운 언어가 아니라 JS + static type을 더하여 Java, C/C++, Rust과 같이 사용할 수 있게 한다. 브라우저는 JS만 실행할 수 있으므로 TS를 사용하려면 TS ➡️ JS로 컴파일하는 과정이 필요하다. TS에서는 위와 같은 에러가 감지되면 런타임이 아닌 코드 실행 전인 컴파일 시에 감지하여 Compile error를 발생시켜 우리의 코드를 더 강력하게 보호할 수 있다.

또한 TS의 강력한 기능 중 하나인 Type Inference는 우리가 타입을 명시하지 않아도 자동으로 추론한다. 타입 선언 여부는 개발자가 결정하며 필요한 경우를 제외하고는 TS가 추론하게 두는 것이 좋다고 한다.

2. Types

그렇다면 TS에서는 어떻게 타입을 명시할 수 있을까? 아래와 같이 타입을 명시할 변수 이름 옆에 변수명:type으로 표기해주면 된다. TS에서 다루는 타입들을 자세히 알아보자

let names: string = "Quartz"; 

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

2.1 Primitives

우리가 JS에서도 자주 사용하는 string, number 그리고boolean을 그대로 사용할 수 있다.

2.2 Object and Array

객체나 배열의 타입을 선언할 때는 이들이 담고 있는 item들의 타입도 함께 명시해야 한다.

const resultContainer: { res: number } = {
  res: result,
};

const names: string[] = ["Max" ,"Quartz", "Belle"];

배열의 타입을 선언할 때 string[] = Array<string> 로도 나타낼 수 있는데 이에 대한 generic type과 함수의 타입 선언은 추후 4장에서 상세히 설명할 예정이다.

2.3 TS에만 존재하는 types

JS에는 존재하지 않지만 TS에는 존재하는 유용한 타입들에 대해 간략히 정리해보려 한다.

  • any : 모든 타입을 수용한다. TS 를 더이상 사용하지 않고 빠져나오고 싶을 때 쓰는 타입이므로 신중히 사용해야 한다.
  • Tuple: 특정 위치에 특정 타입 있는 custom array 만들 때
    • API에서 데이터 가져올 때 가끔 사용함
const player: [string, number, boolean] = ["nico", 12, false];
  • unknown : 어떤 타입인지 미리 모르는 변수
  • Literal type: 타입을 특정 value로 정하기
  • Union type: 여러 개의 타입 허용 → 더 유연한 타입 설정
    // lieteral & union type
    type PrintMode = "console" | "alert";
  • Enum: Literal + Union 조합의 대안. 여러 모드가 있을 때 사용
    • 다른 언어에는 있지만 JS에는 존재하지 않는다
    				  //0    //1
    enum OutputMode { CONSOLE, ALERT };
    PrintMode === OutputMode.CONSOLE 
  • Optional property ?
    • 속성이 undefined일 수 있는 경우

3. Type을 저장하는 3 가지 방법

타입을 변수로 분리하여 저장하는 것에는 3 가지 방법이 있으며 나는 아래와 같은 표를 근거로 object의 타입을 저장할 때는 interface를 사용하고 나머지는 type을 사용한다는 기준을 세웠다.

3.1 Type Alias

Type Alias는 type 키워드를 사용하여 타입을 별칭을 변수로 저장하는 방법이다. 함수, 원시 타입, 객체, 배열 등 다양한 형태의 타입을 저장할 수 있다.

type Player = { // type alias로 저장하고
	name: string,
}

const quartz: Player = { // 인스턴스 만들어서 사용하기
	name : "quartz"
} 

3.2 Interface

Object의 타입만을 저장할 수 있으며 객체 생성 방법은 type alias와 동일하다.

interface Player { // interface로 저장하고
	name: string,
}

const quartz: Player = { // 인스턴스 만들어서 사용하기
	name : "quartz"
} 

3.3 Abstract class

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}`
    }
}

4. Type of Function

4.1 Call signature

함수의 타입을 설명하기 위해서는 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
}

4.2 Generic

함수의 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;
}

5. tsconfig.json

여기까지 TS의 사용 방법과 예시를 간결하게 정리해보았다. TS ➡️ JS로 컴파일하는 setting을 하기 위해서는 tsconfig.json 파일을 수정하면 되고, React와 같은 라이브러리를 사용하면 기본 설정이 자동으로 되어있다. 이 파일에서는 컴파일할 자바스크립트의 버전, 기본으로 추가되는 라이브러리, JS 파일 혼용 여부, strict mode 등을 설정한다.

6. React with TypeScript

리액트 프로젝트에서 TS를 사용하여 타입을 명시하는 대표적인 예시로 아래의 3 가지가 있다.

6.1 Component porps의 generic type 명시

다음과 같이 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;

6.2 State의 generic type 명시

두 번째useState 또한 generic type을 사용하는 예시이다. 타입을 명시하지 않으면 아래와 같은 에러가 발생할 수 있으므로 타입을 표기해주어야 한다.

const [todos, setTodos] = useState([]); // 타입 안쓰면 never[] type이 됨 = error: 항상 비어있어야 한다는 뜻이 됨

// 사용 예시
const [todos, setTodos] = useState<string[]>([]);
const [item, setItem] = useState<number>([]);

6.3 Event의 type 명시

Click event listener나 form data를 가져올 때도 해당 event data의 타입을 명시해야 하며, React.FormEvent,React.MouseEvent 등 TS가 추론해주는 기능을 활용하여 명시하면 된다.

참고자료(Reference)

1 Statically Typed Vs Dynamically Typed Languages

2 TypeScript 공식 문서

3 노마드 코더 강의: 타입스크립트로 블록체인 만들기

4 Udemy JS, React 강의

profile
Code what we love. 좋아하는 것들을 구현하고 있는 프론트엔드 개발자입니다. 사용자도 함께 만족하는 서비스를 만들고 싶습니다.

0개의 댓글