ECMAScript는 주기별로 새로운 문법을 계속해서 발표하며 자바스크립트의 기능도 지속적으로 추가되고 있지만 애초에 최초의 근본적인 설계 자체가 작은 애플리케이션을 빠르게 개발하기 위해서 만들어졌기에 유연하다는 특성과 그로 인해 발생하는 부작용들은 가져갈 수 밖에 없는 한계가 있었습니다.
이러한 상황에서 타입스크립트가 탄생하게 됩니다. 타입스크립트는 자바스크립트에 타입문법을 추가해서 만든 슈퍼셋입니다. 슈퍼셋이란 기존 자바스크립트에다가 추가적인 기능을 추가했다는 정도의 의미입니다. 단지 타입 시스템을 자바스크립트에 도입했을 뿐이지만, 이 타입시스템이 가져다주는 장점으로 인해 프로덕션 레벨에는 오히려 자바스크립트보다 많이 쓰이는 추세이며, 대세가 되었습니다. 실제로, 2022년 스택오버플로우에서 실시한 설문조사에 따르면 가장 사랑받는 언어 4위, 가장 사용하기 원하는 언어 3위에 랭크되었습니다.(자바스크립트는 각각 16위, 5위에 랭크되었습니다)
강타입 언어는 모든 변수를 선언하는 시점부터 해당 변수의 타입을 반드시 지정해주어야하며, 지정된 타입을 벗어나는 값은 해당 변수에 할당할 수 없도록 만들어진 언어입니다. 이러한 특징으로 인해 강타입언어는 정적타입이라고도 부릅니다.
타입스크립트는 정적타입의 특성을 가지고 있으며, 최종적으로 자바스크립트로 컴파일됩니다. 컴파일이라는 것은 특정한 언어를 다른 언어로 변환하는 것으로 흔히 번역기와 같은 개념으로 생각하면 됩니다. 자바스크립트는 컴파일이 필요없는 인터프리터 언어로서, 코드를 작성하고 바로 실행할 수 있습니다. 하지만 컴파일언어들은 반드시 실행 전 컴파일 과정을 거쳐 다른 형식의 언어로 변환된 후 실행해야한다는 특징을 가지고 있습니다.
Java, C와 같은 전통적인 컴파일언어들은 사람이 읽을 수 있는 프로그래밍 언어를 컴퓨터가 읽기 편한 기계어로 컴파일하지만, 타입스크립트는 이와 다르게 기계어가 아닌 자바스크립트로 컴파일 됩니다.
따라서 자바스크립트를 사용할 수 있는 곳에는 어디든지 활용할 수 있다는 범용성과 더불어, 정적타입언어의 안정성과 표현력 또한 갖출 수 있습니다.
먼저, 정적타입이라는 것은 코드를 작성하는 과정에서 엄격한 규칙을 지켜야 한다는 뜻입니다. 즉, 제약이 발생한다는 뜻입니다. 하지만 프로그래밍 언어는 제약이 많으면 많을수록 개발자가 실수하거나, 권장되지 않은 방식으로 코드를 작성할 확률을 줄일 수 있어 안정성을 높일 수 있습니다.
// 변수
// 변수명: 타입명 = 할당될 값;
let age:number = 1;
이제 age란 변수는 number 타입의 값밖에 할당을 하지 못하게 됩니다.
// 변수
// 변수명: 타입명 = 할당될 값;
let age:number = 1;
age = "2";
이렇게 지정된 타입을 벗어난 경우에 에러를 발생시켜주기에 개발자가 실수로 다른 타입의 값을 할당하거나 다른 타입에서 사용할 수 있는 메서드들을 활용하는 경우를 미연에 방지해주게 됩니다.
이때 에러를 발생시키는 시점이 중요합니다. 만약 실제 코드가 돌아가고 있는, 즉 유저가 애플리케이션을 실행하고 있는 과정에서 타입 에러가 발생해서 애플리케이션이 예기치않게 종료된다면 실제로는 안정성을 갖추지 못한 코드라고 할 수 있습니다. 하지만 타입스크립트는 컴파일언어기에 실행전에 자바스크립트로 컴파일을 시켜줘야 하며, 실제로 애플리케이션에서 실행되는 코드는 자바스크립트로 변환된 코드입니다. 그리고 타입스크립트는 컴파일 시점에 타입에러가 발견될 경우 컴파일을 중단하며 타입에러가 발생했다고 알려줍니다.
이러한 특징으로 인해 만약 타입에 관련된 문제가 있을 시 컴파일 단계에서 개발자가 확인하고 수정할 수 있게 되며, 최종적으로 컴파일된 자바스크립트 코드에는 더이상 타입 문제가 없는 것을 확신할 수 있는 것입니다. 이러한 특징으로 인해 타입스크립트는 “안전하다”라고 할 수 있습니다.
특히 타입은 “추상”과 “구체”를 구분짓고, “추상”만으로 코드를 읽고 이해하고, 상호 소통하도록 할 수 있습니다.
여기서 추상이란 코드의 겉으로 보이는 동작. 즉, 이 코드가 “무엇을 한다”를 표현한다고 할 수 있습니다. 반면에 구체란 이 코드가 “어떻게 동작을 수행한다”를 표현하는 정보라고 할 수 있습니다.
그래서 "추상" 개념만 인지하고 코드를 작성해도 된다.
function sum(인자: 인자의 타입):함수의 리턴 타입 {
}
function sum(x:number, y:number):number {
return x + y;
}
sum이라는 함수는 x, y라는 인자를 받으며 두 인자는 모두 number 타입입니다. 그리고 number 타입의 값을 리턴합니다. 이러한 정보를 타입을 통해서 알 수 있습니다. 사실 이제 이 함수의 구현은 어떻게 이루어졌는지 전혀 신경쓸 필요가 없습니다. 개발자는 함수의 이름, input의 타입, output의 타입을 통해서 이 함수가 어떤 동작을 하는지 유추하고 파악할 수 있습니다. 그리고 이 정보만으로도 충분히 sum 함수를 사용할 수 있게 됩니다.
좀 더 확장된 형태로는 아래와 같이 표현할 수 있습니다.
interface Storage {
setItem:(key:string, item:string) => void;
getItem:(key:string) => string | null;
}
const localStorage:Storage = {
setItem: (key: string, item: string) => window.localStorage.setItem(key, item),
getItem: (key: string) => window.localStorage.getItem(key)
}
const sessionStorage:Storage = {
setItem: (key: string, item: string) => window.sessionStorage.setItem(key, item),
getItem: (key: string) => window.sessionStorage.getItem(key)
}
class LocalStorage implements Storage {
setItem:(key:string, item:string) => {
window.localStorage.setItem(key, item);
}
getItem:(key:string) => {
return window.localStorage.getItem(key);
}
}
class SessionStorage implements Storage {
setItem:(key:string, item:string) => {
window.sessionStorage.setItem(key, item);
}
getItem:(key:string) => {
return window.sessionStorage.getItem(key);
}
}
function saveToken(storage:Storage, token:string) {
storage.setItem("access_token", token);
}
saveToken(localStorage, "JWT");
saveToken(sessionStorage, "JWT");
이런식으로 타입시스템을 이용하면 “구체적인 동작”에 맞춰서 코드간의 상호작용을 설계하는 것이 아닌, “추상”에 맞춰서 코드를 작성할 수 있게 되며 이로 인해 다양한 스타일의 코드 작성 기법을 활용할 수 있게 됩니다. 이것이 타입 시스템이 가진 표현력이라고 할 수 있습니다.
타입스크립트의 타입 지정 방법은 크게 변수와 함수로 나눠 볼 수 있습니다.
// 변수
// 변수명: 타입명 = 할당될 값;
let age:number = 1;
// 함수
// function sum(인자: 인자의 타입):함수의 리턴 타입 {}
function sum(x:number, y:number):number {
return x + y;
}
// 화살표 함수
const sum = (x:number, y:number):number => {
return x + y;
}
하지만 명확한 상황에 대해서는 타입을 명시적으로 지정하지 않아도 자동으로 추론해줍니다.
let age = 1;
// value로 1이 할당되었기에 age의 타입을 number로 추론해서 지정
function sum(x:number, y:number) {
return x + y;
}
// 두개의 number를 받아서 그 둘을 더한 값을 리턴하기에 리턴값을 number로 추론 후 지정
string, number, boolean
any
let age:any = 1;
age = "1"
age = null;
age = undefined;
age = [];
age = {};
age = false;
const numbers: number[] = [1,2,3];
const numbers: Array<number> = [1,2,3]; // Generic
const point: [number, number] = [1,1];
const point: {x:number, y:number} = {x:1, y:1};
const point: {x:number, y:number} = {x:1}; // y 프로퍼티가 없음, 에러
?
const point: {x:number, y?:number} = {x:1, y:1};
const point: {x:number, y?:number} = {x:1}; // y 프로퍼티가 옵셔널, 에러 발생 X
const point: {x:number, y?:number} = {x:1, y:"1"}; // y의 value가 다름, 에러
function sum(x:number, y?:number) {
return x + (y || 0)
}
type Point = {
x:number,
y:number
};
const startPoint:Point = {x:1, y:1};
const endPoint:Point = {x:10 y:15};
interface Point = {
x:number,
y:number
};
const startPoint:Point = {x:1, y:1};
const endPoint:Point = {x:10 y:15};
Type aliases and interfaces are very similar, and in many cases you can choose between them freely
If you would like a heuristic, use
interface
until you need to use features fromtype
.****
let age:number = 1;
age = 5; // OK
// -----
let age:3 = 3;
age = 5; // Error!
let name:string = "yeonuk";
name = "yeonwook" // OK
// ----
let name:"yeonuk" = "yeonuk";
name = "yeonwook" // Error!
// literal type으로 추론
const age = 1;
const name = "yeonuk"
as const
const numbers = [1,2,3] // number[]
const numbers = [1,2,3] as const // [1,2,3]
const obj = {x:1, y:2} // {x:number, y:number}
const obj = {x:1, y:2} as const // {x:1, y:2}
===
타입의 ||
(OR) 연산자let age: string | number = 1;
age = "1";
function map(array:number[], callback:(...args:any[]) => any;) {
const result = [];
for(const element of array){
result.push(callback(element));
};
return result;
}
map([1,2,3,4], x => x + 1); // Good
map(["hello", "world"], x => x.toUpperCase());
// Type 'string' is not assignable to type 'number'.
function map<T>(array:T[], callback:(...args:any[]) => any;) {
const result = [];
for(const element of array){
result.push(callback(element));
};
return result;
}
map<number>([1,2,3,4], x => x + 1); // Good
map<string>(["hello", "world"], x => x.toUpperCase());
type People = {name: string, age:number, gender:"male" | "female", hobby:string};
type KeyOfPeople = keyof People; // name | age | gender | hobby
const yeonuk = {
name:"yeonuk",
age:1,
gender:"male",
hobby:"climbing"
};
type People = typeof yeonuk;
/*
{
name: string;
age: number;
gender: string;
hobby: string;
}
*/
// 결합
type KeyOfPeople = keyof typeof yeonuk;
function toUpper(arg:string | number){
arg.toUpperCase() // Error!
/*
Property 'toUpperCase' does not exist on type 'string | number'.
Property 'toUpperCase' does not exist on type 'number'.
*/
}
// narrowing
function toUpper(arg:string | number){
if(typeof arg === "string"){
arg.toUpperCase() // OK!
}
}
function addFive(num?:number){
return num + 5; // Error!
/*
(parameter) num: number | undefined
Object is possibly 'undefined'.
*/
}
// narrowing
function addFive(num?:number){
if(num){
return num + 5
/*
(parameter) num: number
*/
} else {
throw new Error("Argument num is not a number")
}
}
지금까지 타입스크립트 세션을 들었는데, 만약 오늘 처음으로 타입스크립트를 접해봤다면 자바스크립트에 비해서 너무 복잡한 문법을 가진 것 처럼 보이고, 타입 지정으로 인해 가독성도 안좋아진다고 생각이 들었을 수도 있습니다.
그리고 이런 생각은 남들이 쓰니까 쓴다, 요즘 뜨는 스택이니까, 채용 공고에 올라와 있으니까 등의 이유로 타입스크립트를 배우고 적용했다면 꽤나 오래 지속될 수도 있습니다. 하지만, 만약 타입스크립트의 필요성과 가지고 있는 철학, 동기를 생각해본다면. 그리고 어떤 장점들을 가져올 수 있는지 생각해본다면 좀 더 넓은 시야로 많은 면들을 고려해보면서 타입스크립트에 대해 생각할 수 있게 될 것입니다.
TypeScript began its life as an attempt to bring traditional object-oriented types to JavaScript so that the programmers at Microsoft could bring traditional object-oriented programs to the web. As it has developed, TypeScript’s type system has evolved to model code written by native JavaScripters. The resulting system is powerful, interesting and messy.
실제로 타입스크립트의 공식문서엔 위와 같이 타입스크립트를 왜 만들기 시작했는지에 대한 이야기가 적혀있습니다. 그리고 이러한 측면에서 타입스크립트가 자바스크립트에 OOP 방식을 접하기 위해서 만들어졌구나, OOP는 어떤 방식의 코딩일까, 어떻게 타입스크립트가 OOP 스타일을 가능하게 만드는 것일까? 등의 의도를 가지고 다시 타입스크립트를 살펴보면 왜 interface와 type이 같은 용도로 사용할 수 있지만 다른 의미를 가지고 있는지 등을 생각하게 되고 이해하게 됩니다.
그리고 그 과정에서 어느순간 타입스크립트로 짜여진 코드가 자바스크립트로 짜여진 코드보다 훨씬 잘 이해되고 안정적이라고 느껴질 것입니다.
기술에 대한 견해와 호불호는 개발자라면 가질 수 있고, 당연히 가져야만 합니다. 하지만 새로운 기술이 두렵다는 이유로, 익숙한 기술로 편하게 개발을 하고 싶다는 생각이 들어서 새로운 기술을 거부하고, 그것도 모자라서 그 기술을 핑계를 대면서 매도하는 순간 내적으로 개발자로서의 성장은 멈출것이며 외적으로는 동료들에게 함께 하기 싫은 사람이 될 것 입니다. 지속적으로 성장하고, 많은 것을 받아들이고 흡수하기 위해 여러분들은 새로운 것들을 두려워하지 말고, 핑계를 대며 도망치지 말고 사용법에 대해 익히고, 철학과 동기를 이해한 뒤, 지금까지의 경험과 지식을 근거로 삼아 본인만의 견해를 가질 수 있는 사람이 되시길 바랍니다.
// "Animal" 인터페이스 정의
interface Animal {
name: string;
age: number;
makeSound(): void;
}
// "Dog" 클래스 정의. 이 클래스는 "Animal" 인터페이스를 구현합니다.
class Dog implements Animal {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
makeSound() {
console.log('Woof woof!');
}
}
// "Cat" 클래스 정의. 이 클래스는 "Animal" 인터페이스를 구현합니다.
class Cat implements Animal {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
makeSound() {
console.log('Meow!');
}
}
// "Dog" 클래스와 "Cat" 클래스의 인스턴스를 생성하고 makeSound 메소드를 호출합니다.
const myDog: Animal = new Dog('Rover', 5);
myDog.makeSound(); // Woof woof!
const myCat: Animal = new Cat('Whiskers', 3);
myCat.makeSound(); // Meow!
저도 개발자인데 같이 교류 많이 해봐요 ㅎㅎ! 서로 화이팅합시다!