React를 공부하다보면, TypeScript라는 것으로 코딩할 수 있다고 알게된다.
그러면 이번시간에는 TypeScript가 무엇이며 왜 사용해야하는지에 대해서 알아보자.
What is TypeScript?
TypeScript는 JavaScript의 superset
언어로 JavaScript를 기반으로 하되 보다 확장된 프로그래밍 언어라는 뜻이다.
이는 아주 중요한 특징 중 하나로, TypeScript의 뿌리에는 여전히 JavaScript가 있다는 것을 의미한다.
그렇기에 JavaScript의 기본 문법들, 코드 작성법, 조건문, 반복문, 객체 사용법 등등 그대로 사용 가능할 것이다.
다만, 몇가지 기능이 추가된 것이다. 또한, TypeScript는 React와 달리 JavaScript의 library가 아니다.
때문에 React에서 JavaScript의 기존 기능을 기반으로 새로운 기능을 만들거나 확장하지 않아도 된다.
대신, JavaScript의 주요 문법보다 확장된 문법을 가지게 된다.
Statically Typed
TypeScript에서 가장 중요하다고 할 수 있는 것은 정적 타입(Statically Typed)의 특징을 갖는다는 것이다.
이는 TypeScript의 이름에서 볼 수 있듯이, TypeScript 명칭의 유래가 될 만큼 중요하다.
원래 JavaScript 언어는 동적 타입(Dynamically Typed)이기 때문에 Statically Typed이 추가됬다.
function add(a, b) {
return a + b ;
}
const result = add(2, 5) ;
console.log(result) ; // 7
JavaScript에서는 자료형(Type)이라는 것이 존재한다. 그 개념이 무엇인지 알고 있다.
위의 코드에서 2와 5는 숫자형(number)이라는 것을 자체적으로 알고 있다.
다만, JavaScript는 Dynamically Typed이기 때문에 함수 선언 시점에서 Type을 특정짓지 않는다.
위의 예시에서는 함수 안에서 두개의 parameter를 받는다는 것만 알고 있을 것이다.
Type이 따로 고정되어 있지 않는다. 고정되어 있지 않는 상태에서 호출로 전달된 parameter를 받아 코드를 실행해보는 것이다.
숫자를 실행했을 때 정상 작동하는 것이다. 하지만 이add()
함수는 문자열에서도 동작한다.
function add(a, b) {
return a + b ;
}
const result = add('2', '5') ;
console.log(result) ; // '25'
이 경우에는 + operator
가 두 parameter의 숫자를 더하는 연산 대신에 두 parameter 문자열을 연결하는 동작으로 수행했기 때문이다.
parameter로 문자열을 받아 수행하는 것을 보면, 부족한 점이 있다는 것이 보인다.
코드를 보완하고 이러한 오류를 방지하는데 Statically Typed가 필요하다.
물론, 오류가 아니라고 말할 수 있을 것이다. 이런 식으로 add()
함수를 호출하지 않고 항상 숫자만을 전달한다면 이런 문제는 발생하지 않는다.
하지만 대규모 project로 가면 코드 파일도 많을 것이고 많은 사람들이 동일한 코드 베이스에서 작업한다.
때문에 함수나 객체를 의도하지 않은 방식으로 사용되는 경우가 발생할 수 있다.
JavaScript는 따로 이렇게 사용하면 안된다고 경고해주지 않기 때문에 계속 이러한 문제와 마주칠 것이다.
실제로 TypeScript로 변환하면 다음과 같이 바꿀 수 있다.
function add(a: number, b: number) {
return a + b ;
}
const result = add('2', '5') ;
// error occurred!:
// Argument of type 'string' is not assignable to parameter of type 'number'.
console.log(result) ; // '25'
add()
함수의 parameter로 모두 숫자형(number)이어야 한다는 것을 표시했으며, 기존의 문자열이 온 경우 error를 표시해준다.
문자열 타입의 parameter를 숫자 타입의 parameter로 할당할 수 없다고 알려준다.
상당히 유용하게 비슷한 종류의 오류와 의도치 않는 방식의 함수 사용을 방지할 수 있다.
이러한 과정들은 오류를 피할 수 있기 때문에 당연히 더 나은 코드를 작성하는 데 도움될 것이다.
run-time 시에 오류의 원인을 찾을 필요없이 코드를 작성하다보면 바로 error를 표시하기 때문이다.
Usage Environment
이제 TypeScript가 무엇인지 알았고 어떻게 사용하는지 알았으니 사용환경을 구축해보자.
웹에 'TypeScript'
를 검색해서 가장 상단에 https://www.typescriptlang.org/라는 사이트에 들어가준다.
이는 TypeScript 공식 웹 페이지로 이와 관해 더 자세히 알아볼 수 있고 설치 방법에 대해서도 볼 수 있다.
npm install typescript --save-dev
사전에 node.js가 설치되어 있어야하며, 이 명령어를 실행하면 특정 project에 한해서 TypeScript가 설치된다.
npm install -g typescript --save-dev
시스템 전체에서 사용할 수 있도록 하기 위해서는 -g 옵션을 넣어야 실행된다.
하지만 보통의 경우, 특정 project에서 설치하는 것으로 충분하다. 그렇게만 해도 해당 project에서 사용 가능하기 때문이다.
여기서 짚고 넘어가야할 부분은 TypeScript가 일반적인 브라우저에서 실행되지 않는다.
브라우저에서 TypeScript를 싱행하려고 해도 실행되지 않는다. 결국에 TypeScript는 JavaScript 형태로 compile해줘야 한다.
compile이 진행되는 동안 표기한 모든 type들은 삭제될 것이다. JavaScript는 작성된 type표기들을 이해하지 못하기 때문이다.
하지만 compile을 진행하면서 IDE에서 표시되는 error notice를 제외한 error가 있다면 알려줄 것이다.
compiler를 사용하려면 아래 명령어를 사용해야한다.
npx tsc [filePath]
TypeScript confilct file이 있다면 npx tsc만 입력해도 무관하며 confilct file이 없다면 위와 같이 file 경로를 직접 지정해줘야한다.
compile을 하게 되면 JavaScript 파일로 변환해준다. 하지만 compile 과정에서 발견된 error가 있더라도 파일 자체는 생성해준다.
Basic Types
Pramitives
JavaScript의 기본형 타입들은 number, string, boolean, null, undefined가 있다. symbol 타입도 있지만 여기서는 다루지 않을 것이다.
숫자형 : number
숫자형에는 소수점 이하 값을 가지는 실수형(float)을 포함한 모든 숫자 값들을 사용 가능하다.
let age: number ; // Not Number
age = 12 ;
// or
let age: number = 12 ;
선언은 위와 같이 할 수 있으며, 변수 선언과 동시에 값을 할당할 수 있다.
중요한 것은 모든 숫자형 표기는 소문자로 시작한다는 것이다.
Number와 같이 대문자를 사용해도 오류는 발생하지 않지만 대문자를 사용하는 경우에는 JavaScript의 Number 객체를 가르킨다.
문자열 : string
number와 마찬가지로 선언하고 사용할 수 있다.
let name: string ; // Not String
name = 'ananiah' ;
// or
let name: string = 'ananiah' ;
string에서도 대문자를 사용한 경우, JavaScript의 String 객체를 가르킨다.
논리형 : boolean
참(true)이나 거짓(false)와 같은 논리값을 지정할 수 있다.
let isDeveloper : boolean ; // Not Boolean
isDeveloper = true ;
// or
let isDeveloper: boolean = true ;
boolean에서도 대문자를 사용한 경우, JavaScript의 Boolean 객체를 가르킨다.
null & undefined
null과 undefined는 변수 선언 방식으로 사용하지 않는다.
let hobbies: null ;
hobbies = 12 ;
// error occuerred!
선언한 이후에 값을 할당하고자 하면 오류가 발생하기 때문이다.
동적 타입 : any
아래와 같이 별다른 type을 지정하지 않은 경우에는 어떻게 될까?
let person ;
기본적으로 어떤 값이든 변수에 저장 가능하다. 여기에는 'any'라는 특별한 type이 생략되어 있기 때문이다.
let person: any ;
이렇게 표기하면 TypeScript에게 이 변수에 저장할 값의 type에 대해 알려줄 것이 없다는 것을 의미하며, 어떤 값이든 저장할 수 있다.
하지만 any type은 예비적으로 사용되는 type으로, TypeScript의 주 사용 목적과 반대되는 type이기에 자주 사용하지 않는 것이 좋다.
그저 무분별하게 any type을 사용하면 일반적인 JavaScript를 사용하는 것과 다를 것이 없기 때문이다.
하지만 애플리케이션 개발 시 어떤 type을 할당해야 할지 모르는 경우, (외부 library나 dynamically content를 사용하는 경우) 이와 같이 any type으로 지정해서 사용할 수 있다.
More Complex Types
Array
let hobbies: string[] ;
hobbies = ['Sports', 'Cooking'] ;
위와 같은 방법으로 특정 type에 대한 배열을 지정할 수 있다. number, boolean 또한 가능하다.
또한, 다른 type을 사용하면 다시 error가 발생한다.
Object
let person: {
name: string ;
age: number ;
} ;
person = {
name: 'Max',
age: 32
}
이렇게 정의된 객체 타입과 동일한 구조를 가진 객체만 해당 변수에 저장될 수 있다고 알릴 수 있다.
Functions & Types
함수를 사용할 때 type을 지정하는 위치가 따로 있다.
앞서서 이미 다뤘듯이 함수의 parameter에서 type을 지정하지만 빠진 것이 있다.
함수는 항상 return type이 존재해야 하기 때문에 함수 선언 당시에 같이 추가해줘야 한다.
function add(a: number, b: number): number {
return a+b ;
}
Features
Type Interface
let course = 'React - 완벽 가이드' ;
course = 1232; // error occurred!
위와 같이 변수 선언과 함께 문자열을 지정해주게 되면, 문자열 type 변수에 숫자를 할당할 수 없다는 error를 출력한다.
그 어디에도 type을 지정해주지 않았는데, 어떻게 이런 오류가 발생할까?
여기에는 TypeScript의 또 다른 핵심 기능인 type interface가 사용되었다.
기본적으로 TypeScript는 가능한 많은 type을 유추하려고 한다. 어떤 type이 어디에 사용할지 알기를 원한다.
비록, 명시적인 type 표기가 없다고 할지라도 할당된 값의 type을 보고 해당 변수의 type을 지정한다.
위와 같은 방식으로 코드를 작성하는 것은 불필요한 작업을 줄여주기 때문에 권장되는 방식이다.
물론, 해당 변수의 type을 강조하기 위해서 사용할 수 있긴 하다.
Union
보통은 하나의 type에 한가지 값만 저장할 것이다. 하지만 다양한 type을 여러 개 저장하는 경우도 있을 것이다.
let course : string | number = 'React - 완벽 가이드' ;
course = 1232; // Not error
그런 경우에 사용할 수 있는 것이 Union이라는 type이다.
Union type은 type을 정의할 때 한 개 이상의 type을 사용할 수 있다. 다른 type들과 마찬가지로 콜론(:)을 변수 이름 뒤에 붙여서 사용한다.
콜론(:) 뒤에는 type을 지정해주고 | (Shift + \)을 사이에 두고 다른 type을 지정하여 사용한다.
위의 예시에서 Union을 사용하기 위해서는 무조건 type을 지정해줘야 하기 때문에 type interface으로 사용할 수 없다.
let userName = string | string[]
// userName 변수에서 이름을 받을 때,
// full name을 받을지, first-last name으로 받을지 정할 수 있다.
Union type은 TypeScript의 핵심 기능 중 하나로 값과 type을 조금 더 유연하게 정의할 수 있게 해준다.
Type Alias
TypeScript로 작성된 project를 진행하다보면 어느 시점부터 동일한 형태의 type을 반복해서 정의내리는 경우가 많아질 것이다.
type 정의를 같게 한다고 문제될 것은 아니지만 대부분의 개발자들은 코드가 중복되는 것을 원치 않아 할 것이다.
그래서 Base type을 만들어 복잡한 type을 미리 지정해두고 그 지정된 정의를 별칭으로 사용하는 것이다.
그 사용 방법으로는 type keyword를 사용한다. 일반적인 JavaScript에는 없는 keyword로 TypeScript에서 추가되었다.
type Person = {
name: string;
age: number;
} ;
let person: Person ;
정의 한번으로, 반복된 정의가 있던 곳, 필요한 모든 곳에 사용할 수 있다.
작성해야하는 코드의 양이 줄어들고 간결해지며, 관리하기 수월해진다.
Generics
우선 예제를 먼저 살펴보자.
function insertAtBeginning(array: any[], value: any) {
const newArray = [value, ...array] ;
return newArray ;
}
const demoArray = [1, 2, 3] ;
const updateArray = insertAtBeginning(demoArray, -1) ; // [-1, 1, 2, 3]
특정 배열과 값을 parameter로 받아와 새로운 배열로 만들어주는 helper 함수를 만들 때, 한 가지 type에만 국한되어 사용하고 싶지 않다.
때문에 이와 같이 any type을 할당해주어 number든 string이든 사용할 수 있게 했다.
하지만 문제는 updateArray
에 있다. type interface에 의해서 추론된 type이 any라고 한다.
TypeScript는 함수에서 any type을 사용했기 때문에 이 배열에 숫자만 들어가있다고 인식하지 못한다.
때문에 IDE에서 updateArray
에 대한 type error를 잡아줄 수가 없다. compile 단계를 거쳐야만 error를 확인할 수 있게 된다.
이러한 문제를 해결하기 위해 Generics이라는 기능을 사용할 수 있다. Generics을 사용하기 위하여 특수한 구문을 사용한다.
function insertAtBeginning<T>(array: T[], value: T) {
const newArray = [value, ...array] ;
return newArray ;
}
const demoArray = [1, 2, 3] ;
const updateArray = insertAtBeginning(demoArray, -1) ; // [-1, 1, 2, 3]
함수 이름과 parameter 괄호 사이에 홑화살괄호(<>)를 추가하여 Generics type을 정의할 수 있다.
이는 일반적인 JavaScript에서 지원하지 않는 기능으로 TypeScript에서 제공하는 기능이다.
보통 Type
의 T
자를 따서 사용하지만 어떤 식별자를 사용해도 무관하다. 해당 식별자를 any를 대신해서 채워넣으면 된다.
TypeScript에서는 type interface로 return 값의 type을 유추하는데, 들어오는 parameter들이 숫자로 구성되어 있다는 것을 파악한다.
이로써, 자연스럽게 updateArray
가 숫자 배열이라는 것을 알게 된다.
Generics type을 이용해서 TypeScript에게 any type이 아니며, 같은 타입을 가져야 한다는 것을 알려줬다.
배열을 구성하는 type들이 value를 구성하는 type과 동일하다는 정보는 아주 중요한 정보로, 논리적으로 추론이 가능해진다.
number type 뿐만 아니라 string type에서도 유연하게 작동하며, 여전히 type 안정성을 확보했다.
사실 Generics type을 사용한 아래 예시는 같은 것을 의미한다.
number[]
Array<number>
또한, 앞서서 말했던 문제는 Generics 문법을 사용하여 다음과 같이 해결할 수도 있다.
function insertAtBeginning(array: any[], value: any) {
const newArray = [value, ...array] ;
return newArray ;
}
const demoArray = [1, 2, 3] ;
const updateArray = insertAtBeginning<number>(demoArray, -1) ; // [-1, 1, 2, 3]