본문 바로가기
프론트엔드

Hello Typescript - part.2

반응형

이전 포스팅에서는 타입스크립트의 기본적인 사양에 대해 훑어보았다면,

이번 포스팅에서는 실제 개발에 도움이 되는 몇 가지 주제를 같이 살펴보도록 합시다. 

1. 진짜 타입스크립트...

출처 : 침착맨

1. Enum 타입

enum을 사용하면 이름이 있는 상수들의 집합을 정의할 수 있습니다. 숫자와 문자, 두 가지 열거형을 지원합니다.  

우선 숫자 열거형부터 살펴봅시다. 

// 숫자 열거형
enum Direction {
    Up = 1,
    Down,
    Left,
    Right,
}

let direction: Direction = Direction.Up

directon = 'Up' // string을 넣으려 하면 에러

 

위의 코드에서 Up이 1로 초기화된 숫자 열거형으로 선언되었습니다. 

그럼 뒤따르던 나머지는 자동으로 증가된 값을 갖게 됩니다. (Down = 2, Left = 3, Right = 4)를 갖게 되죠. 

물론 enum을 대입한 변수에 다른 타입값을 대입하려고 하면 '타입' 스크립트 에러가 나게 되겠죠.

// 문자 열거형
enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}

 

문자열 열거형은 숫자와 다르게 자동으로 증가된 값을 갖지는 않지만,

문자열로 전달된 값과 Enum의 정수값을 비교할 때 편리하게 사용됩니다. 

하지만 이놈( enum )에 대한 사람들의 인식은 마냥 호의적이진 않습니다. 왜일까요?

 

바로 이 Tree - Shaking 기능 때문입니다.

트리 쉐이킹은 간단하게 말해 사용하지 않는 코드를 삭제하는 기능입니다.

export ("출동 준비 완료!") 했지만 아무 곳에서도 import("출동!") 하지 않은 모듈이나

사용하지 않은 코드를 삭제해 번들의 크기를 줄여 페이지 표시 시간을 단축하는 기능이죠. 

 

1 - 1. Enum

// TypeScript
enum Fruits {
    Apple, 
    Banana,
    Tomato
}

▽

// JavaScript
"use strict"
var Fruits;
(function (Fruits) {
    Fruits[Fruits["Apple"] = 0] = "Apple";
    Fruits[Fruits["Banana"] = 1] = "Banana";
    Fruits[Fruits["Tomato"] = 2] = "Tomato";
})(Fruits || (Fruits = {}));

 

위의 예시와 같이 타입을 enum으로 선언하게 되면, Babel로 컴파일되면서 즉시실행함수(IIFE)로 변환됩니다. 

그러니깐, 실제로 쓰이진 않는 코드지만 번들러 입장에서는 IIFE의 사용 여부를 판별할 수 없기 때문에 트리쉐이킹 대상에서 제외되는 거죠. 그럼 이렇게 사용하면 어떨까요?

 

1 - 2. const enum

// TypeScript
const enum Fruits {
    Apple = 'Apple',
    Banana = 'Banana',
    Tomato = 'Tomato'
}
const banana = Fruits.Banana;

▽

// JavaScript
"use strict";
const banana = "Banana" /* Fruits.Banana */;

 

const enum으로 사용하면 실제로 사용하는 코드만 JavaScript 코드로 변환됩니다. 그럼 트리쉐이킹 역시 가능하죠. 

하지만, const enum 은 일반 상수로 선언되어 실제 런타임에서 에러를 유발할 수도 있고,

이를 방지하기 위한 방법을 사용하면 const enum 또한 IIFE 가 생성되어 트리쉐이킹이 불가능합니다. 

 

1 - 3. Union Types

// TypeScript
const Fruits = {
    Apple: 'Apple',
    Banana: 'Banana',
    Tomato: 'Tomato'
} as const;
type Fruits = typeof Fruits[keyof typeof Fruits]; // 'Appla' | 'Banana' | 'Tomato'

▽

// JavaScript
"use strict";
const Fruits = {
    Apple: 'Apple',
    Banana: 'Banana',
    Tomato: 'Tomato'
};

 

위의 예시처럼 Union Types를 사용하면 enum의 이점을 가지면서

JavaScript의 IIFE가 생성되지 않기 때문에 트리쉐이킹이 가능해 번들의 사이즈를 줄이는 게 가능합니다. 

그럼, 트리쉐이킹의 관점에서 보았을 때 enum타입은 Union Types > const enum > enum으로 정리될 수 있겠네요.

 

2. 제네릭 타입

제네릭 타입은 그 안에서 사용하는 타입을 추상화해 외부로부터 구체적인 타입을 지정할 수 있는 기능입니다. 

function getText<T>(text: T): T {
	return text;
}

getText<string>('hello');
getText<number>(30);
getText<boolean>(true);

 

자, 예제를 보시면 getText 함수 안에서 사용하는 타입을 외부에서(함수를 호출할 때) 지정하고 있습니다. 

이렇게 정의한 것과 같은 거죠. 

getText<string>('hello')

▽

function getText<string>(text: string): string {
	return text;
}

 

3. Union 타입과 Intersection 타입

타입스크립트의 타입은 '조합'해서 사용할 수 있습니다. 

여러 타입을 합집합(Union) 할 수도 있고 교집합(Intersection)해서 사용할 수 있죠.

합집합은 " & "를 사용하고 교집합은 " | "를 사용해 정의할 수 있습니다. 아래 예시를 보고 오류 사항을 찾아볼까요?

type Info = {
    id: number | string,
    name: string
}

type Contact = {
    name: string,
    email: string,
    number: number
}

type User = Info & Contact

const UserContact: User = {
    name: 'Merry',
    email: 'pool1129@naver.com',
    number: 123456789
}

 

UserContact는 Contact의 정보만으로 변수로 정의될 수 없습니다. id가 필요하죠. 

User 타입은 Info 타입과 Contact 타입의 합집합이기 때문에 id가 필수값이 된 것입니다.

이런 식으로 타입의 합집합과 교집합을 사용해 여러 타입의 조합을 만들어 사용할 수 있습니다. 

 

4. 리터럴 타입

그럼 타입은 string, number, function, boolean 등등 이런 형식으로 밖에 사용할 수 없는 걸까요?

아닙니다. 이렇게도 쓸 수 있죠. 

type User = {
    id: number,
    name: string,
    isCheck: '미확인' | '확인중' | '확인완료'
}

 

리터럴 타입을 사용해서 정해진 문자열이나 수치'만' 대입할 수 있는 타입으로 설정할 수도 있습니다. 

 

5. never 타입

never 타입은 '불가능'을 나타내는 타입입니다.

'불가능'이라는 단어는 너무 모호하니 TypeScript-Kr.gitbook의 설명을 빌리겠습니다. 

  • 절대 발생할 수 없는 타입
  • 함수에서 항상 오류를 발생시키거나 절대 반환하지 않는 반환 타입
  • 모든 타입에 할당 가능한 하위 타입

더 모호해졌습니다. "절대 발생할 수 없다" 면 이 never 타입은 도대체 왜 존재하는 것일까요?

아래 세 가지 예시를 확인해 봅시다.

// (1)
function error(): never {
    throw new Error("Error!!!")
}

// (2)
function test(name: string) {
    if (typeof name === "string") {
        console.log("문자입니다");
    } else {
        name // type: never
    }
}

// (3)
function infinite(): never {
    while(true) {
        ...
    }
}

 

(1) 번 예시는 항상 에러가 반환되는 함수로 절대로 값이 정상으로 반환되지 않죠.

그때 우리는 never 타입으로 명시할 수 있습니다. 

 

(2) 번 예시는 name 파라미터 값에 string 타입만 할당 가능한 함수를 나타냅니다. 

하지만 if문을 살펴보면 else가 존재 불가능 하죠. 이때 name은 never 타입이 됩니다. 

 

(3) 번 예시의 경우 무한 루프로 인해 함수가 절대 반환되지 않으므로 역시 never을 타입으로 명시할 수 있죠. 

 

간단히 말해, never 타입은 "여기에 값이 올 수 없다"라는 의미를 가지며, 비정상적인 상황을 표현하는 데 사용됩니다. 


2. 이건 더 진짜 타입스크립트...

출처 : 침착맨

 

1. 옵셔널 체이닝

옵셔널 체이닝은 지난 포스팅에서도 등장한 타입스크립트의 편리한 기술 중 하나입니다. 

객체의 속성이 존재하는가에 관한 조건의 분기를 '? '를 사용해 간단하게 기술하는 거죠. 

interface Position {
    id: number
    direction?: {
        x: number,
        y: number
    }
}

 

2. 논-널 어서션 연산자

물음표(옵셔널 체이닝)가 나왔는데 느낌표가 안 나오면 섭섭하지 않을까요?

컴파일 옵션을 --strictNullChecks로 지정하면 타입스크립트는 귀신같이 null일 가능성이 있는 객체를 에러로 취급합니다. 

이때 null이 아님을 나타내고 싶을 때 논-널 어서션이라는 기능을 사용할 수 있습니다. 

function getUser(user?: user) {
	let name: user!.name
}

 

옵셔널 체이닝과 비슷한 구석이 있지만, 논-널 어서션은 "에러를 발생시키지 말아 달라"는 것일 뿐

실행 시 에러가 발생할 가능성이 있습니다. 

 

3. 타입 가드

function setNumber(value: string | number) {
    if (typeof value === string) {
	return Number(value)
    }

    return value
}

 

위의 예시를 보면 value 인수에 들어올 수 있는 타입은 string 또는 number 타입일 것입니다.

그래서 if 블록 이후 인수인 value는 자동으로 number 타입으로 취급됩니다. 이것이 바로 타입 가드입니다. 

해당 기능을 활용해 null을 안전한 속성으로 다루거나 에러를 발생시키기 쉬운 as를 사용하는 타입 어서션을 보다 안전하게 사용할 수 있습니다. 

 

4. keyof 연산자

interface User {
    name: string;
    age: number;
    email: string;
}

function getProperty<T, K extends keyof T>(obj: T. key: K): T[K] {
    return obj[key]
}

const user: User = {
    name: 'Eddy',
    age: 32,
    email: 'test@test.com'
}

const userName = getProperty(user, 'name') // 'Eddy'

 

keyof 연산자를 사용하면 해당 타입이 가진 각 속성의 타입의 Union 타입을 반환합니다. 

그러니깐 예를 들면 User 타입에 keyof 연산자를 쓰면 'name' |  'age' | 'email'이라는 Union 타입이 되는 거죠.

이러한 이유로 userName의 값은 getProperty의 obj는 user, key는 'name'으로 user ["name"] = 'Eddy'가 될 것입니다. 

 

만약 getProperty(user, 'gender')와 같이 정의한다면 'gender'은 객체의 키로 존재하지 않기 때문에 컴파일 시 에러가 나게 됩니다. 

 

5. 인덱스 타입

인덱스 타입을 각 속성에 대응하는 타입을 정의할 수 없을 때 간단한 게 사용할 수 있습니다. 

간혹 타입을 만들다 보면 이런 케이스가 있을 텐데요. 

인덱스 타입을 사용하면 string 값을 가지는 속성명을 다루는 타입으로 정리할 수 있을 것 같습니다. 

type User = {
    name: string;
    address: string;
    memo: string;
    gender: string;
}

▽

type User = {
    [env: string]: string
}

let UserInfo: User = {
    'name': "Eddy",
    'address': "Seoul",
    'memo': "Backend",
    'gender': "Male"
}

 

6. readonly

타입 스크립트에서는 타입 앨리어스, 인터페이스, 클래스에 대해 readonly 속성을 지정할 수 있습니다. 

 

input 태그의 readonly 속성을 생각해 봅시다.

readonly 속성으로 지정된 input 값은 변경할 수 없죠. 타입스크립트의 readonly 속성도 동일합니다. 

type User = {
    readonly name: string;
    readonly gender: string;
}	

let user: User = {
    name: 'Eddy',
    gender: 'Male'
}

// readonly 속성을 지정해 두었음으로 컴파일시 에러가 발생
user.gender = 'Female';

 

또한, Readonly 타입이라는 제너릭 타입도 있습니다. 이렇게 쓸 수도 있죠.

type User = {
    name: string;
    gender: string;
}	

type UserReadonly = Readonly<User>

let user: User = {name: 'Merry', gender: 'Female'}
let userReadonly: UserReadonly = {name: 'Eddy', gender: 'Male'}

user.name = 'Teddy'
userReadonly.name = 'Ann' // Error

 

7. unknown

타입스크립트 3.0에서 도입된 unknown은 any와 동일하게 모든 값을 허용하지만,

할당된 값이 어떤 타입인지 모르기 때문에 함부로 프로퍼티나 연산을 할 수 없습니다. 아래 예시를 한번 볼까요?

let userAge = unknown = 10;
let userName = unknown = 'Eddy';

// error TS2339: Property 'length' does not exist on type 'unknown'
console.log(userAge.length) 
console.log(userName.length)

 

이렇게 unknown은 임의의 타입을 대입할 수 있는 any와 같은 특성을 가지면서

보다 불명확한 값을 나타내는 기능을 강조합니다. 

또한, 컴파일 시 에러를 사전에 감지할 수도 있으므로 any를 사용하는 것보다 안전한 코드를 작성할 수 있습니다. 

 

8. 비동기 Async/Await

동기 / 비동기

 

자바스크립트 특성상 작업을 순서대로 처리하는 특성(동기식 언어)을 가졌기 때문에

"응답 시간만큼의 지연 시간이 존재하는 것"이 가장 큰 고민거리일 것입니다.

하지만 우리는 자바스크립트의 비동기 처리 패턴 async & await을 해결책으로 사용할 수 있습니다. 

async function getDate() {
    const response = await fetch('https://url.example.com');
    const data = await response.json();

    return data;
}

 

async function은 Promise 객체를 반환하고, await 키워드는 Promise가 처리될 때까지 기다리기에(동기식)

fetch 함수의 비동기적인 호출 및 처리를 수행할 수 있을 것입니다. 

이렇게 타입스크립트에서도 비동기 처리를 수행할 때 자바스크립트와 동일한 방식으로 다룰 수 있습니다. 

 

9. 타입 정의 파일

타입스크립트로 프로젝트를 하나 생성한다고 생각해 봅시다.

정의할 수 있는 타입이 엄청 많을 것 같지 않나요? 아래 예시는 실제 토이프로젝트에서 사용했던 타입들입니다.

interface TicketDetails {
    membership_seq: string, // 회원권일련번호,
    user_member_seq: string, // 사용자일련번호(회원),
    user_member_id: string, // 사용자ID(회원),
    user_member_nm: string, // 사용자명(회원),
    class_seq: number, // 강사수업일련번호,
    .
    .
    .
}

interface TicketClasses {
    tmem_numb: number,
    user_numb: number,
    tmem_name: string,
}

 

이런 타입들이 프로젝트 내에서 반복적으로 사용된다면 우리는 타입 정의 파일을 작성하여 공통으로 관리할 수 있습니다. 

아래 예시와 같이. d.ts라는 확장자를 가진 타입 정의 파일을 생성하고 로딩해서 사용할 수 있죠.

// ./lib/ticket.d.ts
export interface TicketDetails {
    membership_seq: string, // 회원권일련번호,
    user_member_seq: string, // 사용자일련번호(회원),
    user_member_id: string, // 사용자ID(회원),
    user_member_nm: string, // 사용자명(회원),
    class_seq: number, // 강사수업일련번호,
    .
    .
    .
}

// 타입 정의 파일에 정의된 타입을 사용할 시
import { TicketDetails } from './lib/ticket'

 

또, 외부 라이브러리를 로딩하고 싶은 경우엔

@types/[라이브러리명]으로 공개된 타입 정의 파일을 설치해 타입 정보를 가져다 사용할 수 있죠.

설치하지 않더라도 라이브러리에 포함된 경우도 있으니 확인해 보시길 바랍니다. 


자, 이제 해당 에 나와있는 타입스크립트에 관한 내용은 끝이 났습니다. 

 

저는 책을 읽고 포스팅을 하면서 기존에 알고 있던 개념들을 정리하고,

새로운 부분을 알게 되는 좋은 시간이 된 것 같습니다. 

 

다음에는 Next.js 프로젝트를 만들어 보는 포스팅으로 돌아오겠습니다. 

출처 : 무한도전


저희는 스터디를 통해 글을 기록하고 있습니다. 피드백은 언제나 환영입니다 :)

반응형

'프론트엔드' 카테고리의 다른 글

자 이제 시작이야, Next 세계로  (3) 2024.01.04
React 맛보기 - React Hooks  (1) 2024.01.03
React 맛보기 - React Component  (0) 2023.12.29
Hello Typescript - part.1  (1) 2023.12.27
우리가 Next.js를 공부하게 된 이유  (1) 2023.12.26