0. 타입스크립트 왜 씀?
말만 탈만 타입스크립트를 공부하기 전에 우리는 이 귀찮고 번거로운 친구를 왜 배워야 하는지 알아둘 필요가 있습니다.
그래야 타입스크립트를 포기하지 않고 사용할 수 있기 때문이죠.
타입스크립트는 마이크로소프트에서 개발하고 있는 오픈소스기반의 언어입니다.
정적 타입, 상위 호환 두가지 키워드로 정의할 수 있을 것 같습니다.
웹 애플리케이션의 발전으로 자바스크립트는 폭발적인 인기를 끌었으나, JS는 아직 불안정하거나 미성숙한 부분이 많습니다.
때문에 표준화 단체인 ECMA에서 명세를 계속 개선하고 있기도 하고요.
그럼, "자바스크립트를 보다 안정적이고 보다 성숙하게 업데이트하면 되는 거 아니야?"라고 생각할 수 있을 것 같은데,
이 친구는 하위 호환을 중시하는 언어입니다. 그래서 사양의 업데이트도 어렵고 '직접' 타입을 삽입하는 것도 현실적이지 않습니다.
문제를 해결하기 위해서 특정 언어를 → 자바스크립트로 변환해 문법적 제약을 피하는 일종의 꼼수를 가진 언어(AltJS)들이 생겨났습니다.
이 꼼수를 가진 언어들 중 대장이 바로 Typescript입니다.
'타입' 이라는건 굉장히 강력한 것 같습니다. 사실 대부분의 언어에서 타입을 명시하게 되어 있기도 하고요.
자바스크립트는 타입의 부재로 컴파일시 에러도 찾을 수 없고, 여러 사람이 개발할 때 (타입이 없으니) 의도치 않은 에러들이 많이 발생됩니다.
그러니 '타입'이라는건 생각보다 좀 더 멋진 거죠.
수많은 AltJs 언어 중 Typescript가 대장 역할을 하게 된 이유는 무엇일까요?
타입스크립트는 개발 생산성이 높은 정적 타입 언어면서, 자바스크립트를 '그대로' 확장해서 사용하는 상위 호환성을 갖고 있습니다.
이 두 가지 이유로 인해 타입스크립트는 현재 프론트엔드의 중심적인 언어로 자리하고 있습니다.
1. 타입스크립트 맛보기
function getName (name: string) {
console.log('My name is' + name)
}
예제를 보면 name에 어떤 타입의 값을 전달할 수 있을까요? 바로 string 타입입니다.
getName 함수의 name은 숫자, 객체, 클래스, 함수 같은 타입을 받을 수 없습니다. 오직 string 타입만 가능하죠.
이렇게 자바스크립트와 다르게 타입에 묶인 변수의 전달을 정적 타입 체크 기능을 통해 안전하게 전달해 주는 것이 타입스크립트의 강력한 기능입니다.
2. 타입...? 그게 뭔데
자, 이제 타입이라는 친구를 어떻게 써야 하는지, 또 어떤 타입이 있는지 한번 확인해 봅시다.
책에서는 변수, 원시타입, 배열, 객체, any, 함수 타입이 정의되어 있습니다.
1. 변수, 원시타입
타입스크립트는 타입 정의 외에는 자바스크립트와 동일한 규칙을 가지고 있습니다. 아주 멋진 언어이죠.
우리가 익숙하게 사용하는 var, let, const를 사용하고 변수명 뒤에 타입을 추가해 타입어노테이션을 합니다. (생략 가능)
스코프 규칙이나 대입 시 변수 작동은 자바스크립트와 동일하기에 해당 내용을 제외합니다.
let age = 30;
let age:number = 30;
자바스크립트에서 자주 사용되는 원시타입인 string, number, boolean 은 타입스크립트에서도 동일한 이름으로 사용되어집니다.
let age: number = 30;
let isCheck: boolean = true;
let name: string = "Merry"
2. 배열
배열 타입을 지정할 때는 배열을 구성하는 타입과 [] 표기를 같이 사용합니다. 아래의 예시를 살펴봅시다.
const array:string[] = [];
array.push('Merry');
array.push(29);
해당 배열에 29를 넣을 수는 없습니다. 배열은 문자열 타입으로 구성되어 있고, 숫자는 넣을 수 없죠.
숫자와 문자를 배열에 같이 넣고 싶다면 어떻게 처리할 수 있을까요?
여러 타입이 있는 배열의 경우, Union 타입이나 튜플을 사용해서 표시해 줄 수 있습니다.
const mixArray1:(string | number)[] = ['foo', 1];
const mixArray2:(string, number)[] = ['foo', 1];
3. 객체
객체는 키(Key)와 값(Value)을 이용한 데이터 형식입니다. 타입스크립트에서는 객체 타입을 아래와 같이 정의할 수 있습니다.
또한 객체 타입의 일부는? 를 사용해 옵셔널(선택 가능) 속성으로 지정할 수 있습니다.
아래와 같이 옵셔널 속성으로 정의하면 해당 속성이 존재하지 않아도 에러가 발생하지 않습니다.
//객체 타입 지정
const user: {name:string, age:number) = {
name: "Merry",
age: 29
};
//객체 타입 지정 + 옵셔널 속성 지정
function joinName(obj: {firstName: string, lastName?: string}) {
...
};
joinName({firstName: Eddy});
4. any
any는 사실 모르는 게 약이긴 합니다. 마법의 'any'.
any는 이름 그대로 모든 타입을 아우르는 '특별한'타입입니다. 특정한 값에 타입 체크를 적용하고 싶지 않을 경우 사용하는데,
타입스크립트의 장점을 사용할 수 없는 마법의 타입이니 기본적으로 사용하지 않는 것이 좋습니다.
(타입스크립트 너무 어려워서 any 남용했다가 진짜 매우 혼남)
5. 함수
타입스크립트의 함수에서는 인수와 반환값의 타입을 지정할 수 있습니다. 해당 예시를 살펴보죠.
function sayUser(name: string, greeting?: string):string {
return `${greeting} ${name}`
}
이런 식으로 함수의 인수와 반환값에 타입을 지정합니다. 물론 위에서 설명한 옵셔널 인수도 사용이 가능하죠.
인수를 정의할 때 기본값을 지정할 수도 있는데, 함수를 호출할 때 인수를 지정하지 않으면 기본값이 설정됩니다.
또한 함수 자체를 인수로 받을 수도 있습니다.
// 인수 기본값 설정
function sayUser(name: string, greeting: string = 'Hello'): string {
return `${greeting} ${name}`
}
sayUser('Merry') // Hello Merry
sayUser('Eddy', 'Hi') // Hi Eddy
// 함수를 인수로 받는 경우
function printUser(firstName: string, fomatter: (name:string) => string) {
console.log(fomatter(firstName));
}
함수의 인수나 반환값에 타입을 붙이는 것 뿐만 아니라 함수 그 자체에 타입을 기입하는 방법도 있습니다.
다음 예시 속에서 wordArray는 인수가 문자열이고 반환값이 배열인 함수를 인수로 받고 있습니다.
function splitWord(word: string): string[]{
return word.split(',');
}
function addWord(wordArray: (x: string) => string[]): string{
return wordArray('사과, 배')[0] + '냠냠'
}
console.log(addWord(splitWord)); // '사과 냠냠'
console.log(addWord('오렌지')); // 타입이 맞지 않으므로 에러
4. 타입으로 꽁꽁 묶여보자
번거로운 타입의 세계로 들어가기 전 심호흡을 세 번 하시는 걸 추천드립니다.
이걸 왜..? 굳이..?라고 생각이 드실수 있는데 그렇다면 포스팅 가장 상단의 '0. 타입스크립트 왜 씀? '을 다시 읽고 오시는 걸 추천드립니다.
이번 포스팅의 마지막에서는 기본적인 타입의 기능에 대해서 설명 후 마무리 짓도록 하겠습니다.
1. 타입 추론
타입스크립트는 정적 타입 언어지만 아주 멋진 타입 추론 기능들이 있습니다. 타입 추론이란 이런 것입니다.
const age = 10;
console.log(age.length);
네, age는 number 타입이므로 length의 속성은 존재하지 않습니다. 고로 오류가 납니다.
이처럼 타입스크립트는 타입과 속성을 잘 생각해보셔야 합니다.
2. 타입 어설션
const myCanvas = document.getElementByIs('canvas');
console.log(myCanvas.width);
// error TS2339: Property 'width' does not exist on type 'HTMLElement'
해당 코드를 실행하면 위의 에러가 발생할 것입니다. 해석하자면 "HTMLElement 유형에는 width 속성이 없다"는 건데,
document.getElementById는 HTMLElement를 반환하고, HTMLCanvasElement를 반환하지 않기 때문에
HTMLCanvasElement가 가지는 width 속성을 사용할 수 없는 거죠.
그래서 우리는 오류가 난 부분을 이렇게 고칠 수 있습니다.
const myCanvas = document.getElementById('canvas') as HTMLCanvasElement;
console.log(myCanvas.width);
타입 어설션은 굉장히 '보수'적이기 때문에 복잡한 부분에서는 표현이 어렵습니다.
때문에 책에서는 먼저 any로 변환한 뒤, 원하는 타입으로 변환하는 2단계 어설션에 대해서도 설명하고 있습니다.
2단계 어설션이란 아래 예시와 같습니다.
const foo: any = 1234;
const bar: number = foo as number;
console.log(bar.toFixed(2))
하지만 2단계 어설션은 지양하는 것이 좋습니다. 왜 지양하는 것이 좋을까요?
우리가 타입스크립트를 쓰는 이유는 "타입 정의로 오류 발생률을 줄이기 위해서"입니다. 하지만 아래와 같이 사용한다면 어떨까요?
const foo: any = 1234;
const bar: number = foo as number;
...
const work: string = foo as string;
이런 식으로 사용한다면 타입의 변환이 무수히 가능하고, 의도하지 않은 에러가 발생할 확률이 높아질 것입니다.
때문에 타입을 최대한 추론해 보시고 정의하는 것이 중요합니다.
3. 타입 앨리어스
하지만 타입스크립트를 위의 예시처럼 '인라인'으로만 사용한다면 코드는 너무 지저분해질 것입니다.
실제로 개발을 해보면 같은 타입들을 여러 번 사용하는 일은 귀찮고 번거로운 일이란 것을 경험하시게 될 것입니다.
그럼 타입들을 어떻게 재사용할 수 있을까요?
type UserType = {
name: string;
age: number:
}
function getUser(user: UserType) {
console.log(user.name);
console.log(user.age);
}
getUser({name: Eddy, age: 42)};
이처럼 type 키워드를 사용해 타입에 별명(UserType)을 설정해 두면 기본 타입뿐만 아니라 복잡한 타입도 쉽게 정의할 수 있습니다.
설정해 둔 이름을 참조해서 같은 타입을 여러 차례 재사용해 코드의 복잡성을 감소시키는 거죠.
또한, 함수 자체의 타입(1)도 타입 앨리어스를 사용해 정의할 수 있고 객체(2)의 키 이름을 명시하지 않고도 타입을 정의할 수 있습니다.
// 함수를 인수로 받는 경우
function printUser(firstName: string, fomatter: (name: string) => string) {
console.log(fomatter(firstName));
}
// 타입앨리어스로 정의하는 경우(1)
type Fomatter = (name: string) => string
function printUser(firstName: string, fomatter: Fomatter) {
console.log(fomatter(firstName));
}
// 타입앨리어스로 정의하는 경우(2)
type Label = {
[key: string]: string
}
const labelArray: Label = {
text1: 'test1',
text2: 'test2',
text3: 'test3',
}
4. 인터페이스
타입스크립트를 공부하다 보면 interface, type 이 두 가지를 보신적이 있으실 거라 생각됩니다.
어떤 상황에서는 type으로 선언되어 있고, 어떤 상황에서는 interface로 선언되어 있는데 이 두가지 차이에 대해서 공부해 봅시다.
타입과 인터페이스의 가장 큰 차이점을 바로 확장 가능여부입니다.
인터페이스는 타입 앨리어스와 비슷한 기능을 가지지만, 보다 확장성이 높은 열린 기능을 가지고 있습니다. 예를 들어,
interface Point {
x: number;
y: number;
}
interface Point {
z: number;
}
정의된 Point 인터페이스에 z를 나중에 추가할 수 있습니다. 선언적인 확장이 가능하죠.
하지만 타입이라면 어떨까요?
type Point = {
x: number;
y: number;
}
type Point = {
z: number;
}
// Error: Duplicate identifier 'Point'
타입은 인터페이스와 다르게 선언적인 확장이 불가능합니다. 그럼 Point로 정의된 type에 z를 추가할 순 없는 걸까요?
아닙니다. 인터페이스와 타입 앨리어스, 두 가지 방법 모두 확장은 가능합니다.
// 인터페이스 확장하는 법
interface Point {
x: number;
y: number;
}
interface Double extends Point {
z: number;
}
// 타입 앨리어스 확장하는 법
type Point = {
x: number;
y: number;
}
type Double = Point & {
z: number
}
하지만 인터페이스에는 '위임'이라는 한 가지 기능이 더 존재합니다.
위임이란, 단순하게 A가 B를 만족하는지에 대한 여부 체크입니다.
interface Point {
x: number;
y: number;
z: number;
}
class NewPoint implements Point {
x: number;
y: number;
}
// Class 'NewPoint' incorrectly implements interface 'Point'.
// Property 'z' is missing in type 'NewPoint' but required in type 'Point'.
해당 오류는 왜 발생하는 것일까요?
NewPoint 인터페이스는 Point 인터페이스를 '위임' 하고 있습니다. NewPoint는 Point를 만족해야 하죠.
하지만 z의 부재로 만족하지 못하고 있기 때문에 해당 오류가 발생하고 있는 것입니다.
그럼 이제 타입앨리어스와 인터페이스 간의 차이점을 보고 드는 생각이 있을 겁니다. 그래서 언제 어떤 걸 써야 하는 건데?
사실 여기에 정답은 없습니다.
혹자는 "좋은 소프트웨어는 언제나 확장이 용이해야 한다는 원칙(OCP)"에 따라 확장 가능한 인터페이스 사용을 권고하며
혹자는 선언적 확장이 가능한 인터페이스는 심각한 부작용을 야기할 수 있다고 합니다.
참고서적에서는 타입 앨리어스는 객체의 타입 그 자체를 의미하고,
클래스나 객체의 일부 속성, 함수를 포함하는 일부 작동을 정의할 경우 인터페이스가 적합하다고 합니다.
여러 의견이 있는 부분이니 만큼 타입스크립트를 사용하는 팀에서 전반적으로 어떤 타입 선언을 사용할지 통일감 있게 정하는 것이 중요해 보입니다.
5. 클래스
타입스크립트 역시 자바스크립트에 ES5에 도입된 클래스 표기법에 타입을 붙일 수 있습니다.
class UserInfo {
name: string;
age: number;
// 인수가 없는 경우의 초기값을 지정한다.
constructor(name: string = '이름', age: number = 0) {
this.name = name
this.age = age
}
// 반환값이 없는 함수를 정의할 때는 void를 지정한다.
setName(n: string): void {
this.x = n + '씨'
}
setAge(n: number): void {
this.y = n + '세'
}
}
const userInfo = new UserInfo()
userInfo.setName('Merry')
console.log(`${userInfo.name}, ${userInfo.age}`); // Merry씨, 0
그럼 이 예시를 확장(extends) 해볼까요?
아래와 같이 UserInfo를 상속해서 새로운 클래스를 만들어 낼 수 있을 것 같습니다.
// extends 상속
class newUserInfo extends UserInfo {
phone: number;
constructor(name: string = '이름', age: number = 0, phone: number = 0) {
// 상속원의 생성자를 호출한다.
super(name, age)
this.phone = phone
}
setPhone(n: number): vode {
tihs.phone = n + '번호'
}
}
또한, 인터페이스에 implements를 사용해 클래스에 대한 구현을 강제할 수도 있습니다. 다음 예시를 살펴보죠.
interface IUser {
name: string;
age: number;
sayHello: () => string;
}
class User implements IUser {
name: string;
age: string;
constructor() {
this.name = ''
this.age = 0
}
sayHello(): string {
return `안녕하세요. 저는 ${this.name}이며, ${this.age}살입니다.`
}
}
const user = new User()
user.name = 'Eddy'
user.age = '29'
consolel.og(user.sayHello()) // '안녕하세요. 저는 Eddy이며, 29살입니다.'
User 클래스가 sayHello 메서드를 사용할 수 있는 이유는 바로, 위임한 IUser에 sayHello 메서드가 정의되어 있기 때문입니다.
또한 타입스크립트의 클래스에서는 접근 수정자(public, private, protected)를 제공합니다.
접근 수정자를 통해서 메서드의 접근 범위를 제어해 사용할 수도 있습니다. (미지정시 public으로 취급됩니다.)
이렇게 Next.js를 배우기 전 꼭! 배워야 할 타입스크립트의 기본적인 내용을 훑고 지나갔습니다.
다음 시간에는 오늘 읽어본 내용을 토대로 실제 개발 환경에서 어떻게 적용되는지 더 세세한 예제를 가지고 돌아오겠습니다.
저희는 스터디를 통해 글을 기록하고 있습니다. 피드백은 언제나 환영입니다 :)
'프론트엔드' 카테고리의 다른 글
자 이제 시작이야, Next 세계로 (3) | 2024.01.04 |
---|---|
React 맛보기 - React Hooks (1) | 2024.01.03 |
Hello Typescript - part.2 (1) | 2024.01.02 |
React 맛보기 - React Component (0) | 2023.12.29 |
우리가 Next.js를 공부하게 된 이유 (1) | 2023.12.26 |