본문 바로가기

[TS] TypeScript 등장 배경 / 장점 / 데이터 타입

[TS] TypeScript 등장 배경 / 장점 / 데이터 타입
*  TypeScript 등장 배경
*  TypeScript 장점
*  TypeScript의 데이터 타입

 

TypeScript(타입스크립트)

TypeScript는 마이크로소프트에서 개발한 JavaScript의 상위 집합(Superset) 언어이다.

  • 상위 집합 언어는 기존 언어의 문법, 기능 및 기능을 모두 포함하면서 추가적인 기능이나 문법을 제공한다.
  • JavaScript에 정적 타입 검사와 클래스 기반 객체 지향 프로그래밍 등의 기능을 추가하여 개발된 언어이다.
    • 정접 타입 검사는 JavaScript의 동적 타입 시스템에 비해 변수의 타입을 명시적으로 선언하고 타입 검사를 수행할 수 있게 해준다는 의미이다.
    • 이는 개발자가 런타임에서 발생할 수 있는 타입 관련 오류를 사전에 감지하고 방지할 수 있게 한다. 
  • JavaScript가 발전하면서 생긴 단점을 보완하기 위해 등장하게 되었다.

 

TypeScript의 등장 배경

JavaScript는 처음에는 브라우저에서만 동작하는 스크립팅 언어로 만들어졌다.

  • 시간이 점점 흐르고, JavaScript로 웹 애플리케이션의 상호작용이 증가하면서, 웹 애플리케이션이 필요로 하는 JavaScript 코드의 양이 폭발적으로 늘어나게 되었다.
  • JavaScript는 태생이 브라우저에서 동작하는 스크립팅 언어였던 만큼, 폭발적으로 사용량이 증가하게 되며 단점 및 한계가 드러나게 되었다. 
    • JavaScript는 동적 타입이 결정되어 유연하고, 다양한 라이브러리와 프레임워크를 사용할 수 있는 장점이 있지만,
    • 타입의 명시성이 부족하다는 단점이 있다.
  • 타입의 명시성이 부족하게 되면 예상치 못한 결과를 초래할 수 있다. 아래는 그 예시이다.
    • add 함수에 숫자 5와 문자열 "7"을 전달인자로 전달하여 덧셈을 수행하고 있다. 이렇게 되면 결과는 "57"이 나온다.
    • 이런 식으로 JavaScript는 문자열과 숫자를 더할 경우, 숫자 타입의 인수 쪽을 강제적으로 타입 변환해 문자열을 만든다.
    • 이렇게 JavaScript는 함수나 변수의 타입을 명시적으로 지정하지 않아도 동작하는 경우가 많아 예상치 못한 결과를 초래하게 된다. 
let add = (x, y) => {
	return x + y;
}

add(5, "7");

 

이런 문제점을 보완하기 위해 TypeScript라는 언어가 등장하게 되었다.

 

TypeScript를 사용했을 시 장점

TypeScript는 정적타입 검사 기능을 제공하며, 코드의 가독성 유지 보수성을 높여준다.

  • 이를 통해 개발자는 런타임 에러를 최소화하고, 코드 작성 시간을 단축하며, 협업 시 코드의 가독성을 높일 수 있다.
  • 또한 TypeScript는 ES6의 문법을 포함한 최신 JavaScript 문법을 지원하며, 인터페이스(Interface), 제네릭(Generic), 데코레이터(Decorators) 등의 기능을 제공하여 객체 지향 프로그래밍을 보다 쉽게 할 수 있도록 도와준다.
  • 아래는 인터페이스(Interface)를 사용하여 코드의 가독성을 높인 예시이다.
    • User 인터페이스를 정의해 User의 정보를 좀 더 쉽게 파악할 수 있다.
    • greetingUser 함수에도 매개변수로 User 타입을 사용해 이 함수가 어떤 타입의 인자를 받고 있는지 명확히 표현하고 있다.
    • 이렇게 TypeScript를 사용하면 코드의 가독성을 높일 수 있다. 
    • 또한 타입을 명시함으로써 코드의 의도 또한 명확해지기 때문에 다른 개발자가 코드를 이해하고 수정하기 쉬워지며, 런타임 에러를 미리 방지할 수 있기 때문에 유지보수성 또한 높아진다.
interface User {
  id: number;
  name: string;
}

function greetingUser(user: User) {
	 console.log(`Hello, ${user.name}!`)
}

const parkUser = {
	id: 1,
  name: "박해커"
};

greetingUser(parkUser);

 


 

TypeScript의 데이터 타입

 

TypeScript는 JavaScript에서 지원하는 타입의 대부분을 지원하고, 그 외에 추가로 타입을 명시하여 JavaScript 언어의 한계를 보완한다.  

 

Boolean(불리언) 타입

가장 기본적인 데이터 타입으로, JavaScript에서도 마찬가지로 boolean 값이라고 불리는 참(true), 거짓(false) 값이다.

let isShow: boolean = true;
let isDone: boolean = false;

 

Number(숫자) 타입

TypeScript에서 Number 타입을 선언하는 방식은 아래와 같다.

  • JavaScript와 마찬가지로 TypeScript 또한 정수와 실수의 구분 없이 Number 타입 하나로 표기한다.
  • TypeScript는 이 외에도 추가로 bigint를 지원한다.
let number1: number = 5;
let number2: number = 0.7;

 

String(문자열) 타입

TypeScript는 JavaScript처럼 큰따옴표(")나 작은따옴표(')를 사용하여 문자열 데이터를 표현한다.

  • 또한 백틱(`)을 사용한 문자열인 템플릿 리터럴을 사용하면 여러 줄에 걸쳐 문자열을 작성할 수 있다.
let firstName: string = "coding";
let lastName: string = 'kim';
let longString: string = `Kimcoding is a developer.
He is 20 years old.`

 

Array(배열) 타입

TypeScript는 JavaScript처럼 값들을 배열로 다룰 수 있게 할 수 있으며, 두 가지 방법으로 배열 타입을 선언해 사용할 수 있다.

  1. 첫 번째 방법은 배열의 요소들을 나타내는 타입 뒤에 배열을 나타내는 []을 쓰는 것이다.
  2. 이어 두 번째 방법은 제네릭 배열 타입을 사용하는 것이다.
    • Array를 먼저 작성한 뒤, <> 안에 배열의 요소들을 나타내는 타입을 작성한다.
    • 배열 타입은 기본적으로 하나의 타입만 작성하게 되어 있으며, 타입을 혼용해서 작성하는 것은 불가능하다.
//첫 번째 방법
let items: string[] = ["apple", "banana", "grape"];

//두 번째 방법
let numberList: Array<number> = [4, 7, 100];

 

Tuple(튜플) 타입

TypeScript에서 튜플 타입을 사용하면 요소의 타입과 개수가 고정된 배열을 표현할 수 있다.

let user: [string, number, boolean] = ["kimcoding", 20, true];

 

모든 요소가 전부 같을 필요는 없지만, 배열의 index마다 타입이 정해져 있기 때문에 정확한 index에 접근할 필요가 있다.

  • 이렇게 user[2]에 접근하게 되면, user[2]에 있는 요소는 boolean 타입이기 때문에 타입 에러가 발생한다.
console.log(user[2].toString());

 

JavaScript에서도 튜플 타입을 지원하며, JavaScript에서의 튜플 또한 여러 개의 값을 가진 배열을 나타내는 데에 사용한다.

  • 그러나 JavaScript에서는 튜플 타입을 명시적으로 선언할 수 없기 때문에 개발자가 직접 튜플의 각 요소의 타입을 확인하고 유추해야 하므로 타입 에러가 더 쉽게 발생한다.
  • TypeScript의 등장 배경 중 하나인 타입 에러를 방지하고자 한 이유 중 하나이기도 하다.

 

Object(객체) 타입

TypeScript에서 객체는 JavaScript와 마찬가지로 원시 타입이 아닌 타입을 나타낸다.

  • JavaScript에서 Object(객체) 타입은 프로퍼티를 가지는 JavaScript의 값을 말하며 typeof 연산자를 사용했을 때 “object”을 반환하는 모든 타입을 의미한다.

JavaScript의 원시 타입에는 number, string, boolean, undefined, null, symbol 이 있다.

 

TypeScript에서 object 타입은 모든 객체를 수용하는 타입으로, 객체의 프로퍼티 타입들이 any로 지정되기 때문에 어떠한 프로퍼티라도 추가할 수 있다.

  • 그러나 이는 타입 안정성을 보장하지 않기 때문에 편하지만, 추천하는 방법은 아니다.
let obj: object = {};

 

따라서 객체의 프로퍼티 타입들을 각기 명시해 주는 것이 훨씬 좋다.

  • 객체는 이런 방식으로 key-value에 구체적인 타입까지도 지정할 수 있다.
let user: {name: string, age: number} = {
	name: "kimcoding",
	age: 20
}

 

Any 타입

간혹 애플리케이션을 만들 때, 알지 못하는 타입을 표현해야 할 때도 있다.

  • 클라이언트에서 유저로부터 받은 데이터 및 서드파티 라이브러리에서 들어오는 값인 경우 개발자가 알지 못하는 타입일 수 있다.
  • 이 경우, 타입 검사를 하지 않고자 할 때 any 타입을 사용할 수 있다.
let maybe: any = 4;

 

any 타입을 사용하게 되면, 변수에 값을 재할당하는 경우 타입을 명시한 변수와 달리 타입에 구애받지 않고 값을 재할당할 수 있게 된다.

let obj: object = {};

//에러 
obj = "hello";

let maybe: any = 4;

//정상적으로 동작 
maybe = true;

 

또한 엄격한 타입 검사를 진행하지 않기 때문에, 실제 할당된 값이 가지지 않는 메서드 및 프로퍼티로 접근해도 에러가 나지 않는다.

  • 대신, 실제 할당된 값이 가지지 않는 메서드 및 프로퍼티이기 때문에 반환되는 값은 undefined이다.
let maybe: any = 4;

//undefined로 출력됩니다.
console.log(maybe.length);

 

또한 any 타입은 타입의 일부만 알고, 전체는 알지 못할 때 유용하다.

  • 예를 들어서 여러 타입이 섞인 배열을 받고자 할 때 유용하다.
let list: any[] = [1, true, "free"];

//any로 다루고 있기 때문에 index 1번째 요소가 boolean 타입이지만 number 타입으로 재할당할 수 있다. 
list[1] = 100;
 

함수

TypeScript에서는 표준 JavaScript 함수가 작업을 수월하게 하도록 몇 가지 새로운 기능이 더 추가되어 있다.

  • JavaScript에서 함수는 모든 애플리케이션의 기본적인 구성 요소이다.
  • JavaScript에서의 함수와 마찬가지로 TypeScript에도 함수는 JavaScript와 마찬가지로 기명 함수(named function)와 화살표 함수(arrow function) 등으로 만들 수 있다.
  • JavaScript에서 함수를 이렇게 작성했다.
//named function
function add(x, y){
	return x + y;
}

//arrow function
let add = (x, y) => {
	return x + y;
}

 

이를 TypeScript로 다시 표현해 보면 아래와 같다.

  • TypeScript에서 함수를 표현할 때는 매개변수의 타입과 반환 타입을 명시해야 한다.
    • 각 매개변수에 해당하는 타입을 작성한 뒤, 반환되는 타입을 괄호 뒤에 작성해준다다.
    • 이는 타입 추론 기능을 활용하지 않는다고 가정했을 때 필수이다
  • . 타입 추론 기능을 활용하지 않는다고 가정했을 때, 아래 함수는 반환값이 존재하는 함수이므로, 괄호 뒤에 콜론(:)을 붙이고 반환되는 타입을 기재한다.
    •  여기서 반환되어야 하는 타입은 number여야 하므로, 콜론(:)을 붙이고 한 칸 띄운 다음 number를 작성한다.
//named function
function add(x: number, y: number):number {
	return x + y;
}

//arrow function
let add = (x: number, y: number): number => {
	return x + y;
}

 

만일 함수에 리턴값이 없다면, void를 사용하여 작성할 수 있다.

  • 이 또한 타입 추론 기능을 활용하지 않는다고 가정했을 때 필수이다.
let printAnswer = (): void => {
	console.log("YES");
}

 

또한 TypeScript는 JavaScript와 달리 매개변수의 개수에 맞춰 전달인자를 전달해야 한다.

let greeting = (firstName: string, lastName: string): string => {
	return `hello, ${firstName} ${lastName}`;
}

//에러
greeting('coding');

//정상적으로 작동
greeting('coding', 'kim'); // hello, coding kim

//너무 많은 매개변수를 보내 에러
greeting('coding', 'kim', 'hacker');

 

만약 개발자가 전달인자를 전달하지 않거나, undefined를 전달했을 때 할당될 매개변수의 값을 정해놓을 수도 있다.

  • 이는 JavaScript에서의 default parameter와 같은 동작을 한다.
  • 뒤의 인자로 undefined를 보내도 값은 “hello, coding kim”으로 반환된다.
let greeting = (firstName: string, lastName="kim"): string => {
	return `hello, ${firstName} ${lastName}`;
}

//정상적으로 작동 
greeting('coding');  // hello, coding kim

//정상적으로 작동 
greeting('coding', undefined); // hello, coding kim

//너무 많은 매개변수를 보내 에러 
greeting('coding', 'kim', 'hacker');

 

  • 혹은 선택적 매개변수를 원한다면 매개변수의 이름 끝에 ?를 붙임으로써 해결할 수도 있다.
  • 그러나 이때는 greating('coding')과 같이 전달인자를 하나만 전달했기 때문에, 뒤의 매개변수는 undefined로 반환이 된다.
let greeting = (firstName: string, lastName?: string): string => {
	return `hello, ${firstName} ${lastName}`;
}

//정상적으로 작동 
greeting('coding'); // hello, coding undefined

//정상적으로 작동 
greeting('coding', 'kim'); // hello, coding kim

//너무 많은 매개변수를 보내 에러 
greeting('coding', 'kim', 'hacker');

 


 

TypeScript는 연산자를 이용해 타입을 정할 수 있다.

  • JavaScript에서도 보았던 ||(OR) 연산자나 && (AND)와 같은 연산자를 이용하여 만들 수 있다.
  • | 연산자를 이용한 타입을 유니온(Union) 타입이라고 하며, & 연산자를 이용한 타입은 인터섹션(Intersection) 타입이라고 부른다.
  •  JavaScript에서는 TypeScript에서처럼 유니온과 인터섹션 타입을 지원하지는 않는다.  
 

유니온(Union) 타입

 

유니온 타입은 둘 이상의 타입을 합쳐서 만들어진 새로운 타입이다.

  • | 연산자를 이용하며, 자바스크립트의 || (OR) 연산자와 같이 “A이거나 B이다”라는 의미의 타입이다.
  • 예를 들어, number | string은 숫자 또는 문자열 타입을 의미한다.

 

아래의 코드는 value 매개변수의 타입을 any로 정의하고, 타입이 number인지 string인지에 따라 if-else 문으로 나누어 출력하고 있다.

  • 그러나 any를 사용하는 것은 JavaScript로 작성하는 것과 큰 차이가 없기 때문에, 유니온 타입을 사용해 TypeScript의 이점을 살리면서 코딩하는 것이 좋다.
function printValue(value: any): void {
  if (typeof value === "number") {
    console.log(`The value is a number: ${value}`);
  } else {
    console.log(`The value is a string: ${value}`);
  }
}

printValue(10); // The value is a number: 10
printValue("hello"); // The value is a string: hello

 

위의 printValue 함수는 숫자 또는 문자열 값을 입력받고 있다.

  • 이때, 유니온 타입을 사용해 number | string 타입으로 지정하고 있다.
  • 이후 입력된 값의 타입을 typeof 연산자를 사용하여 검사한 후, 해당 값이 숫자인 경우와 문자열인 경우 각각 다른 로그를 출력한다.
  • 이처럼 유니온 타입은 다양한 타입의 값을 처리해야 하는 경우 유용하다.
function printValue(value: number|string): void {
  if (typeof value === "number") {
    console.log(`The value is a number: ${value}`);
  } else {
    console.log(`The value is a string: ${value}`);
  }
}

printValue(10); // The value is a number: 10
printValue("hello"); // The value is a string: hello

 

유니온(Union) 타입의 장점

 

유니온 타입을 사용하면 타입을 추론할 수 있기 때문에, 타입에 관련된 API를 쉽게 자동완성으로 얻어낼 수 있다.

  • 그러나 any 타입을 사용하면 타입을 추론할 수 없어, 자동완성 기능을 사용하기가 어렵다.

 

 

또한 코드의 가독성을 높일 수 있다.

  • string | number | boolean 타입으로 선언된 변수는 문자열, 숫자, 불리언 타입 중 하나의 값을 가질 수 있다는 것이 명시적으로 표시되어 코드를 이해하기 쉽게 만들어 준다.
let value: string | number | boolean;

 

유니온(Union) 타입 사용 시 유의할 점

유니온 타입인 값이 있으면, 유니온에 있는 모든 타입에 공통인 멤버들에만 접근할 수 있기 때문에 유의해야 한다.

  • 아래와 같이 인터페이스를 사용하여 Developer와 Person을 정의했을 때를 가정해보자.
interface Developer {
  name: string;
  skill: string;
}

interface Person {
  name: string;
  age: number;
}

 

실질적으로 askSomenone 함수 내부에서는 Developer와 Person이 갖고 있는 공통 프로퍼티인 name에만 접근할 수 있다.

  • 왜냐하면 공통되고 보장된 프로퍼티만 제공해야 하기 때문이다.
  • 만약 나머지 프로퍼티에도 접근하고 싶다면 타입 가드를 사용해야 한다.
function askSomeone(someone: Developer | Person) {
	console.log(someone.name);
}

 

[타입 가드(Type Guard)란?]

TypeScript에서 타입을 보호하기 위해 사용되는 기능 중 하나이다. 타입 가드는 특정 코드 블록에서 타입의 범위를 제한해 해당 코드 블록 안에서 타입 안정성을 보장해 준다.

 

아래 코드는 타입 가드를 사용해 작성된 코드이다.

  • TypeScript에서는 in 연산자를 제공하고 있다.
  • in 연산자는 객체의 프로퍼티 이름과 함께 사용되며, 해당 프로퍼티가 객체 내에 존재하는지 여부를 검사한다.
function askSomeone(someone: Developer | Person) {
  // in 연산자 : 타입스크립트에서 객체의 속성이 존재하는지를 체크하는 연산자
  // in 연산자는 객체의 속성 이름과 함께 사용하여 해당 속성이 객체 내에 존재하는지 여부를 검사
  if ('skill' in someone) {
    console.log(someone.skill);
  }

  if ('age' in someone) {
    console.log(someone.age);
  }
}

 

인터섹션(Intersection) 타입

 

인터섹션(Intersection)은 둘 이상의 타입을 결합하여 새로운 타입을 만드는 방법이다.

  • & 연산자를 사용하여 표현한다.
  • 아래와 같이 타입을 결합해 사용할 수 있다.
  • 여기서 value 변수는 string, number, boolean 타입을 전부 받을 수 있다.
let value: string & number & boolean;

 

인터섹션으로 타입을 연결해 하나의 단일 타입으로 표현할 수 있기 때문에, 타입 가드가 필요치 않다.

  • 아래의 코드는 인터섹션 타입을 사용하여 Developer와 Person을 하나의 타입으로 묶었다.
  • 따라서 askSomeone 함수 내에선 정의된 프로퍼티에 전부 접근할 수 있다.
  • 인터섹션은 교집합이다.
  • 따라서, Developer & Person은 Developer에 있는 속성과, Person에 있는 속성을 다 가지고 있다. 
interface Developer {
  name: string;
  skill: string;
}

interface Person {
  name: string;
  age: number;
}

function askSomeone(someone: Developer & Person) {
    console.log(someone.age);
    console.log(someone.name);
    console.log(someone.skill);
}

 

 

인터섹션 타입 (구조적 타입에서의 집합) 유니온 타입 (구조적 타입에서의 집합)
둘 이상의 타입을 결합해 새로운 합집합을 만들어 내는 것이기 때문에,
전달인자를 전달할 때 모든 프로퍼티를 전부 보내줘야만 한다. 
새로운 교집합을 만들어 내는 것이기 때문에,
유니온 타입은 전달인자를 전달할 때 선택지가 있다.
인터섹션 타입은 타입 가드가 필요하지 않다. 유니온 타입은 유니온 타입으로 정의한 둘 이상의 타입 내의 공통 프로퍼티에만 접근할 수 있어 타입 가드가 따로 필요하다. 

참고한 레퍼런스

 

interface Developer {
  name: string;
  skill: string;
}

interface Person {
  name: string;
  age: number;
}


// 🟣 1. 유니온

function askSomeone(someone: Developer | Person) {
	//이런 식으로 프로퍼티에 접근할 수 있다.
  if ('skill' in someone) {
    console.log(someone.skill);
  }

  if ('age' in someone) {
    console.log(someone.age);
  }
}

//유니온 타입은 전달인자를 전달할 때 선택지가 생긴다.
askSomeone({name: '김코딩', skill: '웹 개발'});
askSomeone({name: '김코딩', age: 20});

// 🟡 2. 인터섹션

function askSomeone2(someone: Developer & Person) {
	//타입 가드를 사용하지 않아도 모든 프로퍼티에 접근할 수 있다.
    console.log(someone.age);
    console.log(someone.name);
    console.log(someone.skill);
}

//그러나 인터섹션 타입으로 결합하게 된다면 전달인자를 전달할 때 선택지가 없다.
askSomeone2({name: '김코딩', skill: '웹 개발', age:20});

 

 

TypeScript로 포팅하기

 

 JavaScript 코드들을 TypeScript로 포팅(*porting) 해보자.

  • 여기서 포팅은 JavaScript 코드를 TypeScript 코드로 변환하는 작업을 의미한다.
[포팅(porting)]

어떤 시스템, 소프트웨어 또는 애플리케이션을 하드웨어나 소프트웨어 환경이 다른 시스템에서도 동작할 수 있도록 변경하는 과정 



1. 옳게 바꾼 사례일까?

let shoes: Array<string, number> = ["Nike", 78, "Asics", 34];
  • No!
  • 제네릭 타입으로 배열 타입을 작성할 때, <> 안에는 하나의 인자만 들어갈 수 있다.
  • 따라서 해당 코드는 알맞게 변환되지 않은 코드이다. 
  • 따라서 해당 코드는 다음과 같은 내용의 에러를 발생시킨다. Generic type 'Array<T>' requires 1 type argument(s).

 

2 옳게 바꾼 사례일까?

let person: {name: string, age: number} = {
    name: "anna",
    age: 15,
    isDeveloper: false
}
  • No!
  • 객체 리터럴은 명시한 프로퍼티만 지정할 수 있다.
  • 현재 명시된 프로퍼티는 name과 age이고, isDeveloper는 명시된 속성이 아니므로 에러가 발생한다.

 

3. Union

아래 에디터에서 코드를 작성한 뒤 터미널에 순서대로 아래와 같이 치면 결과를 확인해볼 수 있다.
1. tsc src/index.ts
2. node src/index.ts

tsc src/index.ts:

  • TypeScript 컴파일러(tsc)를 사용하여 src/index.ts 파일을 컴파일한다.
  • TypeScript 파일을 JavaScript로 변환하여 실행 가능한 형태로 만들어준다.
  •  이 명령을 실행하면 TypeScript 코드가 JavaScript로 변환되고, 에러가 없는 경우 컴파일된 파일이 생성된다.

node src/index.js: 

  • Node.js 런타임 환경에서 src/index.js 파일을 실행한다.

 

 

교집합이 아닌 isMarried 와 isStudent 프로퍼티를 그냥 호출하게 되면, 에러가 뜬다.

 

아래는 TypeScript 코드가 JavaScript로 변환되고, 에러가 없는 경우 컴파일된 파일(오른쪽)이 생성된 경우이다.

  • 아래의 터미널 창을 보면 결과도 정상적으로 출력이 된다. 

 

 

4. Intersection

  • 비교를 위해 3번과 비슷한 예시를 사용했다.

 

728x90
⬆︎