Front-End/TypeScript

타입스크립트[TypeScript] | 왜 써야할까? (+ 여러 type에 관하여)

jaeyeong 2023. 3. 28. 14:38

🔍 글을 시작하기 전에

 

해당 글은 타입스크립트 프로그래밍 책과 유데미 타입스크립트 강의를 공부하여 정리한 글입니다.

수정해야 할 부분이 있다면 언제든지 알려주세요!

 

TypeScript를 왜 써야 하는가?

 

책에서는 타입을 사용함으로써 오류를 미리 감지하고 일부 런타임 오류를 방지할 수 있기 때문에 바닐라 JS보다 유용하다고 소개하고 있다.

나는 자바스크립트만을 사용해서 프로젝트를 진행할 때 큰 불편함을 느끼지 못했다.

오류가 발생하면 방법을 찾아 해결하는 것은 당연하게 여기며 프로젝트를 완수했다.

 

그리고 타입스크립트가 거의 필수로 여겨질 때쯤 원티드 프론트엔드 프리온보딩 코스를 진행하면서 반 강제적으로 타입스크립트를 사용하여 프로젝트를 진행했었다.

그 뒤부터는 항상 나의 프로젝트엔 타입스크립트를 필수로 사용하였다.

 

인터넷에서 타입스크립트의 필요성을 말하며 예시로 무조건 나오는 것은 다음과 같다.

 

const result = 10 + '11';
console.log(result); // 1011

 

자바스크립트에서 위 코드대로 숫자와 문자의 합을 구하려고 한다면

숫자가 문자열로 타입이 자동으로 변경된 후 '1011'이라는 문자를 결과로 출력한다.

 

하지만 타입스크립트에서 똑같이 실행한다면 type오류가 발생한다.

이러한 상황을 봤을 때 타입스크립트를 사용해야 하는 이유에 대해 가장 와닿을 수 있는 예시여서 매번 언급되는 것이 아닐까?

 

실제 코드가 많은 프로젝트에서 타입이 암묵적으로 변환되었을 때 어디서 바뀌었는지 하나하나 보며 찾아내야 할 상황을 상상해 본다면..

매우 끔찍하다.

또한 작업자가 한 명이면 그나마 낫지만 여러 명의 개발자가 협업하는 상황이라면 더욱 난감할 것이다.

그래서 애초에 타입스크립트를 사용해서 이런 문제를 사전에 방지하도록 하기 위해 널리 사용되고 있다고 생각된다.


Array(배열)

 

나는 배열을 자바스크립트로 코드를 작성할 때나 코딩 테스트를 풀 때 정말 많이 사용한다.

타입스크립트에서도 배열 타입을 지정하는 것은 간단하다.

 

let favorite: string[];
favorite = ['string'];

 

타입 지정하는 방법은 간단했기 때문에

나는 타입스크립트에서 배열 타입을 사용할 때 어떤 점이 좋을지에 대해 중점적으로 알아보려고 했다.

 

타입스크립트를 사용하면서 변수의 타입이 any로 추론되지 않도록 하는 것이 중요하다고 들어왔고 나 또한 그렇게 생각한다.

왜냐하면 any로 타입을 쓰거나 추론되도록 두는 것은 자바스크립트로 코드를 작성하는 것과 똑같다는 생각이 있었기 때문이다.

 

나는 강의에서 for문을 사용하는 예제를 보며 아, 이래서 타입을 쓰는구나! 를 어렴풋이 느꼈다.

 

const person: {
    name: string;
    age: number;
    hobbies: string[];
} = {
    name: 'JaeYeong',
    age: 100,
    hobbies: ['Sports', 'Cooking'],
};

for (const habby of person.hobbies) {
    console.log(typeof habby) // string, string
    console.log(habby) // 'Sports', 'Cooking'
}

 

person객체에서 hobbies문자열로 구성된 배열을 타입으로 가지고 있다.

타입스크립트는 for문에 사용된 person.hobbies의 타입 정의를 통해 hobby 타입을 string으로 추론하고 있는 것을 볼 수 있다.

그렇기 때문에 IDE에서 habby 뒤에 .을 붙이면 문자열에서 사용할 수 있는 메서드를 보여주기도 한다.

 

타입스크립트는 개발자에게 발생할 수 있는 오류를 알려주기 위해 쓰기도 하지만 편의성도 높여준다는 것을 깨달았고, 나는 이런 부분이 참 마음에 들었다.

 

readonly 속성

 

배열을 이렇게 지나가기엔 아쉬운 감이 있어서 readonly 속성에 대해 짧게 짚고 넘어가려고 한다.

readonly는 말 그대로 데이터를 수정하지 못하고 읽기만 가능한 속성을 뜻한다.

 

let a: readonly number[] = [1,2,3];
let b = a.concat(5);

console.log(a); // [1, 2, 3]
console.log(b); // [1, 2, 3, 5]

a[3] = 4; // readonly 속성이므로 값을 추가하지 못한다

 

a는 숫자로 이루어진 배열이며 readonly 속성을 가졌다. 그래서 a에 대한 내용을 수정할 수 없다.

b는 a에 concat메서드를 사용하여 a배열에 요소를 추가한 새로운 배열로 값이 할당된다.

위 코드에서 알 수 있듯이, 기존 데이터를 수정하지 않는 메서드(concat, slice 등)를 사용하면 읽기 전용 데이터를 활용할 수 있으나 push, pop 등 기존 데이터를 수정하는 메서드는 사용하지 못한다.


Tuple(튜플)

 

나는 작년 초 프론트 엔드 공부를 하기 전에 파이썬을 잠시 공부했었다.

그때 튜플이란 걸 처음 보았고 까먹고 지내다가 책을 보면서 다시 마주했다.

 

타입스크립트에서의 튜플은 배열과 비슷하지만 선언할 때 타입을 명시해야 하는 차이점이 있다.

나는 튜플길이와 타입이 고정된 배열이라고 이해했다.

 

const person: {
    name: string;
    age: number;
    hobbies: string[];
    role: [number, string];
} = {
    name: 'JaeYeong',
    age: 100,
    hobbies: ['Sports', 'Cooking'],
    role: [2, 'author']
};

 

위 코드에서 튜플로 작성된 요소는 role이다.

딱 봐도 첫 번째 요소에는 number, 두 번째 요소에는 string이 오는 배열이 작성될 것임을 알 수 있다.

 

하지만 튜플에도 한 가지 오류 아닌 오류가 있다.

책을 보면서 유데미 강의도 함께 듣고 있는데, 타입스크립트에서 push로 튜플에 요소를 추가할 수 있는 문제 가 있다고 한다.

 

person.role.push('admin');
console.log(person.role); // [2, 'author', 'admin']

 

나도 한번 실행해 보았고 오류 없이 push가 되는 것을 볼 수 있었다.

 

그래도 아래 두 가지 경우에는 문제를 잘 잡아내는 것을 확인했고,

튜플로 정의된 타입에는 push() 메서드를 사용하지 않도록 주의해야겠다는 생각이 들었다.

 

person.role = [0, 'admin', 'user']; // 2개 이상의 요소를 할당시키면 에러가 발생한다
person.role[1] = 10; // 정해진 자리의 타입과 다른 타입을 할당하면 에러가 발생한다

Enum(열거형)

 

enum Language { Korean, English, Russian }

console.log(Language.Korean); // 0
console.log(Language.English); // 1
console.log(Language.Russian); // 2

 

enum은 위 코드를 보면 알 수 있듯이 객체를 숫자로 매핑할 수 있다.

책에서는 이와 같은 enum을 문자열에서 숫자로 매핑하는 열거형이라고 소개하고 있으며,

열거형키를 값에 할당하는 순서가 없는 자료구조라고 한다.

 

별다른 값을 설정하지 않으면 출력된 것과 같이 첫 번째 값은 0, 그다음 값은 1, 그 다음 값은 2로 추론이 된다.

즉, 다음 값은 이전 값에서 +1이 되는 것이다.

 

enum Language { Korean, English = 100, Russian }

 

만약 위와 같이 두 번째 값을 100으로 설정한다면, 그다음 값인 Language.Russian은 101이 되며,

첫 번째 값인 Language.Korean은 그대로 0이다.

그런데 enum을 사용할 때도 주의해야 할 점이 있다.

 

enum Language { Korean, English, Russian }

let a = Language.Korean; // a = 0
let b = Language.Spanish; // Language에 Spanish는 존재하지 않아서 오류 발생
let c = Language[2]; // c = 'Russian'
let d = Language[100]; // ???

 

Language[2]를 통해 값을 찾을 수 있으며, Language에 100이라는 값을 가진 요소가 없지만 오류가 발생하지 않는다.

위 상황에서 console.log(d)를 하면 undefined가 출력된다.

책에서는 이런 상황을 역방향 찾기를 지원한다고 설명해주고 있다.

 

이런 불안정한 상황을 방지하기 위해서는 const enum을 사용하면 된다.

 

const enum Language { Korean, English, Russian }

let a = Language.Korean; // a = 0
let b = Language.Spanish; // Language에 Spanish는 존재하지 않아서 오류 발생
let c = Language[2]; // const enum은 문자열 리터럴로만 접근할 수 있어서 오류 발생
let d = Language[100]; // const enum은 문자열 리터럴로만 접근할 수 있어서 오류 발생

Unknown

 

강의에서는 unknown 타입을 어떤 타입이든지 할당할 수 있지만, 그래서 사용하기 전에 현재 저장되어 있는 타입을 확인하고 나서 사용할 수 있다고 설명해주고 있다.

 

솔직히 처음 봤을 땐 any와 어떤 차이가 있는지 헷갈렸다.

어쨌든 모든 타입을 허용하는 것이면 any랑 마찬가지가 아닌가? 라는 생각이 들었다.

하지만 책에 있는 연습 문제를 통해 명확한 차이가 있는 것을 알게 되었다.

 

let n: unknown = 4
let m = n * 2; // Error TS2571: Object is of type 'unknown'.

 

주석에 있는 에러를 왜 발생시키는지 설명하는 예제문제였다.

 

모르겠어서 답변을 참고했고, 다음과 같은 이유로 에러를 발생시키는 것이라고 설명하고 있었다.

(혹시 풀어보는 사람이 있을 수 있으니 접은 글에 답변을 적어두었다.)

더보기

unknown은 런타임에 임의의 값일 수 있는 값을 나타냅니다.

타입스크립트에 당신이 적은 코드가 안전하다는 것을 증명해야 하고, 그러기 위해서는 먼저 TypeScript에 unknown 값이 실제로 더 구체적인 하위 유형을 가지고 있다는 것을 증명해야 합니다.

타입 쿼리 또는 타입 가드를 사용하여 값을 세분화하여 증명할 수 있습니다.

 

즉, 다음과 같이 타입가드를 사용하면 오류를 해결할 수 있었다.

 

let n: unknown = 4;
if (typeof n === 'number') {
    let m = n * 2;
    console.log(m); // 8
}

 

만약 n이 숫자 타입을 갖는다는 것을 미리 알 수 있다면 unknown 타입이 아니라 1) 명시적으로 number 타입을 작성해 주거나 2) 타입이 자동으로 추론될 수 있도록 값만 할당하는 것이 좋다.

하지만 어떤 타입의 값이 할당될지 모르는 상황이라면 unknown 타입을 지정하는 것이다.

 

any 타입으로 둔다면 주석에 있는 오류 문구처럼 문제점을 알려주지 않고 그대로 실행시킬 것이다.

그렇기 때문에 any보단 unknown 타입이 더 좋은 선택지라고 생각된다.