TypeScript

younghyun·2022년 7월 26일
0

TypeScript

JavaScript 대체 언어 하나로써 자바스크립트(ES5) Superset(상위확장)
C#의 창시자인 덴마크 출신 소프트웨어 엔지니어 Anders Hejlsberg(아네르스 하일스베르)가 개발을 주도한 TypeScript는 Microsoft에서 2012년 발표한 오픈소스.
정적 타이핑 지원하며 ES6(ECMAScript 2015) 클래스, 모듈 등과 ES7의 Decorator 등을 지원.
TypeScript는 ES5 Superset이므로 기존의 자바스크립트(ES5) 문법 그대로 사용 가능.
또한, ES6의 새로운 기능들을 사용하기 위해 Babel과 같은 별도 트랜스파일러(Transpiler)를 사용하지 않아도 ES6의 새로운 기능을 기존의 자바스크립트 엔진(현재의 브라우저 또는 Node.js)에서 실행할 수 있음.
이후 ECMAScript의 업그레이드에 따른 새로운 기능을 지속적으로 추가할 예정이여서 매년 업그레이드될 ECMAScript의 표준을 따라갈 수 있는 좋은 수단

TypeScript 등장 배경

초창기 자바스크립트는 웹페이지의 보조적인 기능을 수행하기 위해 한정적인 용도로 사용. 이 시기에 대부분 로직은 주로 웹서버에서 실행되었고 브라우저(클라이언트)는 서버로부터 전달받은 HTML과 CSS를 렌더링하는 수준.

HTML5가 등장하기 이전까지 웹 애플리케이션은 플래시, 실버라이트, 액티브엑스와 같은 플러그인에 의존하여 인터랙티브한 웹페이지를 구축해옴. 그러다가 HTML5가 등장함으로써 플러그인에 의존하던 구축 방식은 자바스크립트로 대체 됨. 또한 AJAX의 활성화로 SPA(Single Page Application)가 대세가 됨. 이로써 과거 서버 측이 담당하던 업무의 많은 부분이 클라이언트 측으로 이동하게 되었고, 자바스크립트는 웹 어셈블리 언어로 불릴 만큼 중요한 언어로 위상이 높아짐.

모든 프로그래밍 언어에 장단점이 있듯이 자바스크립트도 언어가 잘 정제되기 이전에 서둘러 출시된 문제와 과거 웹페이지의 보조적인 기능을 수행하기 위해 한정적인 용도로 만들어진 태생적 한계로 좋은 점도, 나쁜 점도 많은 것이 사실.

자바스크립트는 C나 Java와 같은 C-family 언어와는 구별되는 아래와 같은 특성이 있음.

  • Prototype-based Object Oriented Language
  • Scope와 this
  • 동적 타입(dynamic typed) 언어 혹은 느슨한 타입(loosely typed) 언어

이와 같은 특성은 클래스 기반 객체지향 언어(Java, C++, C# 등)에 익숙한 개발자를 혼란스럽게 하며 코드가 복잡해질 수 있고 디버그와 테스트 공수가 증가하는 등의 문제를 일으킬 수 있어 특히 규모가 큰 프로젝트에서는 주의하여야 함.
이같은 자바스크립트의 태생적 문제를 극복하고자 TypeScript를 도입하게 됨.

사용 이유(장점)


자바스크립트로 변환해야 실행 가능. 번거로운데 어떤 장점이 있어서 쓰는가.

정적 타입

function sum(a, b) {
  return a + b;
}

함수를 정의한 개발자의 의도는 아마도 2개의 숫자 타입 인수를 전달받아 그 합계를 반환하려는 것으로 추측됨. 하지만 코드상으로는 어떤 타입의 인수를 전달하여야 하는지, 어떤 타입의 반환값을 리턴해야 하는지 명확하지 않음. 따라서 위 함수는 아래와 같이 호출될 수 있음.

function sum(a, b) {
  return a + b;
}

sum('x', 'y'); // 'xy'

위 코드는 JavaScript 문법상 어떠한 문제도 없으므로 JavaScript 엔진은 이의 제기없이 위 코드를 실행할 것.
이러한 상황이 발생한 이유는 변수나 반환값의 타입을 사전에 지정하지 않는 JavaScript의 동적 타이핑(Dynamic Typing)에 의한 것.
위 함수를 TypeScript의 정적 타입을 사용하여 다시 작성.

function sum(a: number, b: number) {
  return a + b;
}

sum('x', 'y');
// error TS2345: Argument of type '"x"' is not assignable to parameter of type 'number'.

JavaScript SuperSet

TypeScript는 JavaScript와 100% 호환. JavaScript 기본 문법에 TypeScript의 문법을 추가한 언어. 프론트엔드 또는 백엔드 어디든 JavaScript를 사용할 수 있는 곳이라면 TypeScript도 쓸 수 있음.
TypeScript는 앱과 웹을 구현하는 JavaScript와 동일한 용도로 사용 가능하며 서버 단에서 개발이 이루어지는 복잡한 대형 프로젝트에서도 빛을 발함.
유효한 JavaScript로 작성한 코드는 확장자를 .js에서 .ts로 변경하고 TypeScript로 컴파일해 변환할 수 있음.

도구 지원

TypeScript를 사용하는 이유는 여러가지 있지만 가장 큰 장점은 IDE(통합개발환경)를 포함한 다양한 도구의 지원을 받을 수 있다는 것. IDE와 같은 도구에 타입 정보를 제공함으로써 높은 수준의 인텔리센스(IntelliSense), 코드 어시스트, 타입 체크, 리팩토링 등을 지원받을 수 있으며 이러한 도구의 지원은 대규모 프로젝트를 위한 필수 요소.

강력한 객체지향 프로그래밍 지원

인터페이스, 제네릭 등과 같은 강력한 객체지향 프로그래밍 지원은 크고 복잡한 프로젝트의 코드 기반을 쉽게 구성할 수 있도록 도우며, Java, C# 등의 클래스 기반 객체지향 언어에 익숙한 개발자가 JavaScript 프로젝트를 수행하는 데 진입 장벽을 낮추는 효과도 있음.

ES6 / ES Next 지원

브라우저만 있으면 컴파일러(Babel 등) 등의 개발환경 구축없이 바로 사용할 수 있는 ES5와 비교할 때, 개발환경 구축 관점에서 다소 복잡해진 측면이 있지만 현재 ES6를 완전히 지원하지 않고 있는 브라우저를 고려하여 Babel 등의 트랜스파일러를 사용해야 하는 현 상황에서 TypeScript 개발환경 구축에 드는 수고는 그다지 아깝지 않을 것. 또한, TypeScript는 아직 ECMAScript 표준에 포함되지는 않았지만 표준화가 유력한 스펙을 선제적으로 도입하므로 새로운 스펙의 유용한 기능을 안전하게 도입하기에 유리.

Angular

마지막으로 Angular는 TypeScript 뿐만 아니라 JavaScript(ES5, ES6), Dart로도 작성할 수 있지만 Angular 문서, 커뮤니티 활동에서 가장 많이 사용되고 있는 것이 TypeScript. Angular 관련 문서의 예제 등도 TypeScript로 작성된 것이 대부분이어서 관련 정보를 얻을 때 이점이 있으며 이러한 현상은 앞으로도 지속될 것으로 예상.

높은 수준의 코드 탐색과 디버깅

TypeScript는 코드에 목적을 명시해 개발자 의도를 명확하게 코드로 기술하고, 목적에 맞지 않는 타입의 변수나 함수들에서 컴파일 시점에 에러를 발생시켜 버그 사전에 제거.
코드 가독성을 높이고 예측할 수 있게 하며 코드 자동 완성이나 실행 전 피드백을 제공해 작업과 동시에 디버깅이 가능해 생산성을 높일 수 있음.
미리 타입을 결정하기 때문에 실행 속도가 매우 빠름.



add함수를 하나 만듦.
인수 전달하지 않아 undefined + undefined = NaN.

add 1,2 를 제외하고는 원하는 사용방식도 아니고 원하는 것도 얻지 못함. JavaScript는 경고를 주지 않았음.

showItems는 배열을 받아서 loop를 보여주는 함수
배열을 전달하면 잘되지만, 배열이 아니면 에러.
자바와 같은 정적 언어는 컴파일 타임에 타입 결정.
코드 작성 시간이 길어지게 되지만 초기 생각을 많이 해서 짜면 안정적이고 빠르게 작업 할 수 있는 장점이 있음.
타입스크립트도 정적 타입언어.

num1은 any타입. 타입을 도저히 모르면 써도 되지만
가급적 쓰지 말아야 함.

2개를 받아야 하는데 1개만 받음.

2개를 받아야 하는데 3개를 받았다.
any타입을 없애기 위해서 number라고 적어줌.
숫자를 받겠다고 유도함. 타입정의를 이렇게 콜론하고 원하는 타입을 적어줌
이렇게 했더니 아까 괜찮았던 Hello, world 에러 발생.
결국 add( 1, 2 )를 제외하고는 모두 에러.
몇 개 인수를 어떤 타입으로 전달해야 하는지 코드 모두 살펴볼 필요 없음.

강력한 생태계

TypeScript는 그리 오래되지 않은 언어임에도 불구하고 강력한 생태계를 가지고 있음. 대부분의 라이브러리들이 TypeScript를 지원하며 마이크로소프트의 비주얼 스튜디오 코드(VSCode)를 비롯해 각종 에디터가 TypeScript 관련 기능과 플러그인을 지원.

점진적 전환 가능

기존의 JavaScript 프로젝트를 TypeScript로 전환하는데 부담이 있다면 추가 기능이나 특정 기능에만 TypeScript를 도입함으로써 프로젝트를 점진적으로 전환할 수 있음. JavaScript에 주석을 추가하는 것에서부터 시작해 시간이 지남에 따라 코드베이스가 완전이 바뀌도록 준비 기간을 가질 수 있음.

TypeScript Syntax

Boolean

참/거짓 나타냄.

let isBoolean: boolean;
let isDone: boolean = false;

number

정적 타입이라 해서 C / JAVA 처럼 int, float, double 타입은 없고, Javascipt number 자료형 그대로 사용.
ES6에 도입된 2진수, 8진수, 10진수, 16진수 리터럴도 지원.

let num: number;
let integer: number = 6;
let float: number = 3.14;

let hex: number = 0xf00d; //61453
let binary: number = 0b1010; // 10
let octal: number = 0o744; // 484

let infinity: number = Infinity;
let nan: number = NaN;


number가 들어와야 하고 number가 return 되어야 함.

문자열 배열에 숫자를 추가하려고 하면 에러

string


타입(Type)은 변수(variables)에 담아 쓸 수 있음.
일반 변수와 구분 하기 위해서 타입은 대 문자로 많이 씀.

타입을 적지 않아도 타입 추론에 의해 타입스크립트가 string이라고 알고 있음.

let red: string = 'Red';
let green: string = 'Green';
let yourColor: string = 'Your color is' + green;

let myColor: string = `My color is ${red}`;

function strings(str1: string, str2: string): string {
	return str1 + str2;
}

작음따옴표, 큰따옴표 뿐만 아니라 ES6 템플릿 문자열도 string Type에 포함.

Array

순차적으로 값을 가지는 일반 배열을 나타냄.
배열은 다음과 같이 두 가지 방법으로 선언할 수 있음.
첫 번째 방법은 배열 요소들을 나타내는 타입 뒤에 []를 쓰는 것.
두 번째 방법은 Array<> 배열 타입을 쓰는 것.

let fruits: string[] = ['Apple', 'Banana', 'Mango'];
// or
let fruits: Array<string> = ['Apple', 'Banana', 'Mango'];
// 문자열만 가지는 배열
let fruits: string[] = ["Apple", "Banana", "Mango"];
let fruits: Array<String> = ["Apple", "Banana", "Mango"];

// 숫자만 가지는 배열
let oneToSeven: number[] = [1, 2, 3, 4, 5, 6, 7];
let oneToSeven: Array<number> = [1, 2, 3, 4, 5, 6, 7];

유니언 타입(다중 타입) '문자열과 숫자를 동시에 가지는 배열'도 선언할 수 있음.

let array: (string | number)[] = ["Apple", 1, 2, "Banana", "Mango", 3];
let array: Array<string | number> = ["Apple", 1, 2, "Banana", "Mango", 3];

배열이 가지는 항목의 값을 단언할 수 없다면 any를 사용할 수 있음.

let someArr: any[] = [0, 1, {}, [], "str", false];

인터페이스(Interface)나 커스텀 타입을 사용할 수도 있음.

interface IUser {
  name: string;
  age: number;
  isVaild: boolean;
}

let userArr: IUser[] = [
  {
    name: "Neo",
    age: 10,
    isVaild: true,
  },
  {
    name: "Lewis",
    age: 64,
    isVaild: false,
  },
  {
    name: "Evan",
    age: 123,
    isVaild: true,
  },
];

읽기 전용 배열을 생성할 수 있음. readonly ReadonlyArray 타입 사용.

let arrA: readonly number[] = [1, 2, 3, 4];
let arrB: ReadonlyArray<number> = [2, 4, 6, 8];
arrA[0] = 123;

나머지 매개변수(스프레드 연산자) 를 이용한 배열 반환 함수.

function getArr(...args: number[]): number[] {
   return args;
}
getArr(1, 2, 3, 4, 5); // [1, 2, 3, 4, 5]

Tuple

배열의 서브 타입
크기와 타입이 고정된 배열

let rgbColor: [number, number, number] = [255, 255, 0];

이 처럼 항상 정해진 갯수의 요소를 가져와야 하는 배열을 지정해 응용할 수 있음.

let x: [string, number]; // 튜플 타입으로 선언

x = ["hello", 10]; // 성공
x = [10, "hello"]; // 오류 (원소 타입이 안맞음)
x = ["hello", 10, 99]; // 오류 (원소 갯수가 안맞음)
let user: [number, string, boolean] = [1234, 'HEROPY', true];
console.log(user[0]); // 1234
console.log(user[1]); // 'HEROPY'
console.log(user[2]); // true

데이터를 개별 변수로 지정하지 않고, 단일 Tuple 타입으로 지정해 사용할 수 있음.

let userId: number = 1234;
let userName: string = 'juyoung';
let isValid: boolean = true;

let userA: [number, string, boolean] = [1234, 'juyoung', true];
console.log(userA[0]) // 1234
console.log(userA[1]) // 'juyoung'
console.log(userA[2]) // true

Tuple 타입 2차원 배열 생성

/* 2차원 튜플 */
let users: [number, string, boolean][];
// or
let users: Array<[number, string, boolean]>;

users = [[1, 'Neo', true], [2, 'Evan', false], [3, 'Lewis', true]];

값으로 타입을 대신할 수 있음. ( 강한 타입 )

let tupleA: [1, number];
tupleA = [1, 2];
tupleA = [2, 3]; // Error - TS2322: Type '2' is not assignable to type '1'.

//0번 째 요소가 1이라는 값으로 고정되어 있으므로 에러

Tuple은 정해진 타입의 고정된 길이 배열을 표현하지만 이는 할당(Assing)에 국한됨.
.push(), .splice() 등을 통해 값을 넣는 행위는 막을 수 없음.

let tuple: [string, number];
tuple = ['a', 1];
tuple = ['b', 2];

tuple.push(3);
console.log(tuple); // ['b', 2, 3];

tuple.push(true); // Error - 그렇다고 해서 튜플에 정의되지 않는 타입을 넣을수는 없음.
// push() 함수를 사용하여 요소를 추가하는 경우 처음 할당된 값의 타입과 정확히 일치해야 함.

readonly를 사용해 읽기 전용 튜플 생성 가능.

let a: readonly [string, number] = ['rest', 123];
a[0] = 'work'; // Error - TS2540: Cannot assign to '0' because it is a read-only property.

Enum

enum은 C, Java와 같은 언어를 다뤄봤으면 한번쯤 들어보는 흔하게 쓰이는 타입으로 특정 값(상수)들의 집합을 의미.

위에서 배운 튜플 타입이 특정 타입이나 값을 고정하는 배열이라면, Enum은 특정 값을 고정하는 또다른 독립된 자료형이라고 보면 됨.

Enum은 JavaScript에는 없는 type.

enum Week {
  Sun,
  Mon,
  Tue,
  Wed,
  Thu,
  Fri,
  Sat
}
console.log(Week.Mon); // 1
console.log(Week.Tue); // 2


Os를 Enum으로 정의.
window = 0,
Ios = 1,
Android = 2
로 값이 증가함. enum에 수동으로 값을 주지 않으면 자동으로 0부터 증가.

enum Week {
  Sun,
  Mon=22,
  Tue,
  Wed,
  Thu,
  Fri,
  Sat
}

console.log(Week.Mon); // 22
console.log(Week.Tue); // 23


window를 3으로 값을 주면 4, 5 이렇게 증가함.

Enum Type은 역방향 매핑 지원함.

컴파일 결과를 보면, os라는 객체가 만들어졌고 window =3, Ios =10, Android =11이 들어와있음.
os 3 = window, 10 = ios 등으로 양방향 매핑이 되어있음.

console.log로 ‘Ios’를 넣어주면 10이 나오고
10을 넣어주면 Ios가 나옴.

console.log(Week['Mon']); // 1
console.log(Week[1]); // 'Mon'

let day: Week = Week.Mon;
const re_Week: string = Week[1];
console.log(day); // 1
console.log(re_Week); // 'Mon'

enum에는 숫자가 아닌 문자열도 입력 가능.(숫자가 아니기에 단방향 매핑만 됨.)
이 방법은 역방향 매핑을 지원하지 않으며 개별적으로 초기화 해야 함.

enum Color {
  Red = 'red',
  Green = 'green',
  Blue = 'blue'
}
console.log(Color.Red); // red
console.log(Color['Green']); //green


myOs Type은 Os이다 선언 해두면,
myOs에는 Window, Ios, Android만 입력할 수 있게 됨.

이렇게 입력할 수 있음. ( 특정 값만 입력하게 하고 싶을 때 )

Object(객체)

기본적으로 typeof 연산자가 'object'로 반환하는 모든 타입을 나타냄.
컴파일러 옵션에서 엄격한 타입 검사(strict)를 true로 설정하면, null은 포함하지 않음.

let obj: object = {};
let arr: object = [];
let func: object = function () {};
let nullValue: object = null;
let date: object = new Date();

이처럼 여러 타입의 상위 타입으로 인식되기 때문에 타입스크립트에서 object를 그대로 타입으로 쓰기에는 애로사항이 많음.
더군다나 다음과 같이 object 타입에 객체값을 주고 실제로 조회해 보면 에러 발생함.

const player: object = { name: 'nico' };
player.name; // Error

정말 객체에 타입을 지정해주고 싶다면 다음과 같이 객체 속성(Properties)들에 대한 타입을 개별적으로 지정하는 식으로 사용.

let userA: { name: string, age: number} = {
  name: 'juyoung',
  age: 27
}

let userB: {name: string, age: number } = {
  name: 'jisu',
  age: false, // Error
  email: 'wndud0647@gmail.com' // Error
}

하지만 이게 타입인지 객체인지 뭔지 가독성이 매우 좋지않은걸 느끼실 텐데, 이를 극복하기 위해 타입스크립트는 type 리터럴이라고 불리우는 alias 기능과 interface 라는 문법을 추가함.
반복적인 사용을 원할 경우, interface 또는 type 사용함.

interface Users {
  name: string,
  age: number
}

let userA: Users = {
  name: 'juyoung',
  age: 27
};

let userB: Users = {
  name: 'jisu',
  age: 28,
  mail: true // Error
}




들어갈 자료들이 많을 때 한 번에 등록 가능.
문자로 들어오는 속성들이 전부 다 string 가짐.
그러면 이제, 글자로 된 모든 Object 속성 타입은 string.


class문법도 똑같이 타입 지정 가능함.

any

모든 타입 의미.
JavaScript에서 사용하던 변수 Type이 사실 any라고 봐도 무방함.
any 타입이 존재하는 이유는 애플리케이션을 만들 때, 알지 못하는 타입을 표현해야 하는 경우가 존재할 수 있기 때문.
예를들어 사용자로부터 받은 데이터나 서드 파티 라이브러리 같은 동적인 컨텐츠에서 변수나 함수를 다룰때, 왠만하면 공식문서에 설명이 잘 되어있겠지만 그래도 알수가 없을때 유용하게 사용됨.

let any: any = 123;
any = 'play game';
any = {};
any = null;

여러 값을 포함하는 배열을 나타낼 때 사용할 수도 있음.

let list: any[] = [1, true, "free"];
list[1] = 100;
// 명시적으로 any 타입 지정
let product_id: any = 124981;
product_id = 'p9023412'; // any 유형이 설정되었으므로 어떤 유형도 값으로 할당 가능

// 암시적으로 any 타입 지정
let product_id;
product_id = 124981;
product_id = 'p9023412';

하지만 그렇다고 any 타입을 남용해서 사용 하면 안됨.
any 타입을 이곳저곳 쓸것이면 자바스크립트로 코딩하면 되지, 굳이 타입스크립트를 쓰는 이유가 없어지기 때문.
그리고 무엇보다 타입스크립트 컴파일의 보호장치를 잃어버릴수도 있게됨.
예를 들어 다음과 같이 잘못된 연산을 컴파일이 경고를 안해주게 됨. (마치 자바스크립트 같음.)

const a: any[] = [1, 2, 3, 4]; // 배열
const b: any = true; // 불리언

a + b; // 배열과 불리언을 더했는데 컴파일이 허용하고 있다..

강한 타입 시스템의 장점을 유지하기 위해 Any 사용을 엄격하게 금지하려면, tsconfig에 컴파일 옵션
"noImplicitAny": true 를 통해 Any 사용 시 에러를 발생시킬 수 있음.

Unknown

Unknown의 Un 이라고 해서 무언가 부정적인 뜻으로 보이지만, 어렵게 생각할 필요 없이 그냥 any와 같음.

말그대로 Unknown은 알 수 없는 타입을 의미하며, any와 같이 모든 데이터 타입을 받을 수 있음.

let a: any = 123;
let u: unknown = 123;

그럼 모든 타입을 허용하면서 왜 any와 unknown을 구분했을까?

  • any는 어떤 것이든지 타입을 허용한다는 뜻.
  • unknown은 알 수 없다, 모른다의 어감이 강함.

이 둘의 차이점은 '엄격함' 에 있음.

예를 들어 any타입 같은 경우 아래와 같이 number 타입의 데이터를 넣고 string 타입의 메소드 length를 사용했을때 어떠한 에러를 발생시키지 않고 그냥 undefined를 반환함.

let value : any = 10;
console.log(value.length); // undefined

하지만 unknown 타입은 any 타입과 동일하게 모든 값을 허용하지만, 할당된 값이 어떤 타입인지 모르기 때문에 함부로 연산을 할 수 없다는 특징을 가지고 있음.

그래서 다음과 같이 미리 에디터 자체에서 에러를 띄워줌.

let valueNum: unknown = 10;
let valueStr: unknown = 'Test';

console.log(valueNum.length); // 문제 발생
console.log(valueStr.length); // 문제 발생

자바스크립트에서 자주 쓰이는 typeof 연산자를 통해 타입을 검사해서 해결할수 있음.
이처럼 any 대신 unknown 타입을 사용하면 체크를 해야 되는 코드는 많아지겠지만, 사전에 문제가 되는 부분을 방지할 수 있으므로 any 타입에 비해 안정적인 애플리케이션을 개발할 수 있게 됨.

null, undefined

기본적으로 null, undefined는 모든 타입의 하위 타입으로, 각 타입에 할당할 수 있음.
null과 undefined를 아무 여러 타입에 할당할 수 있다는 것을 의미.
다만 tsconfig.json에서 strick 모드가 아닐 경우에만 가능.

let nullable: null = null;
let undefinedable: undefined = undefined;
let num: number = undefined;
let str: string = null;
let obj: { a: 1, b: false } = undefined;
let arr: any[] = null;
let und: undefined = null;
let nul: null = undefined;
let voi: void = null;
컴파일 옵션인 --stricks 이나 --strictNullChecks를 사용하면, null과 undefined는 오직 any와 각자 자신들 타입에만 할당 가능하게 된다. (예외적으로 undefined는 void에 할당 가능.)
이 옵션은 불특정한 많은 일반적인 에러를 방지하는 데 도움을 주니, 가능한 경우 --strictNullChecks를 사용할 것을 권장하는 편이다.

strictNullChecks 컴파일 옵션

tsconfig.json에서 컴파일 옵션을 다음과 같이 true로 줬을 경우 일반적인 타입 변수에 null을 할당할수 없게 됨.

{
	"strictNullChecks": true, /* 엄격한 null 검사 사용 */
}
let assign_name:string = null; // [오류] [ts] 'null' 형식은 'string' 형식에 할당할 수 없다.

if (!assign_name) {
  assign_name = '미네랄';
}

만일 string 타입에 어쩔수없이 null 또는 undefined 값이 오게되어, 이들을 허용하고 싶은 경우 유니언 타입인 string | null | undefined를 사용할 수 있음. (any로 처리해도 되겠지만 비권장)

let assign_name: string | null | undefiened = null;

if (!assign_name) {
  assign_name = '미네랄';
}

Never

never 타입은 number나 string 처럼 어떠한 자료형 값을 담기 위한 타입이 아님.

never 타입은 타입스크립트에서 잘못된 것을 알려주기 위한 키워드로써, 단어 그대로 절대 발생할 수 없는 타입을 나타낸다고 보면 됨.

보통 다음과 같이 빈 배열을 타입으로 잘못 선언한 경우, never를 볼 수 있음.

이는 타입스크립트 초보자들이 가장많이 하는 실수

// 에러를 발생시키는 커스텀 never 타입 함수를 생성
function error(message: string): never {
    throw new Error(message); // 함수는 리턴되지 않고 에러를 throw함
}

function fail() {
    return error("Something failed"); // 커스텀 에러함수 호출
}

const result = fail(); // 반환 타입이 never로 추론된다.


never타입 역시 null나 undefiened 처럼 모든 타입에 할당 가능한 하위 타입.
하지만 반대로 어떤 타입도 never에 할당할수 없음 (never 자신은 제외)

function getError(): never {
   throw new Error('ERROR');
}

let never_type: never;

never_type = 99; // 오류 발생: 숫자 값을 never 타입 변수에 할당할 수 없다.

// 함수의 반환 값이 never 타입 이기 때문에 오류가 발생하지 않는다.
never_type = getError();

void

void 라는 단어는 C나 JAVA에서 함수에서 return 값이 없었을때 사용해본 경험이 있음.
그 void가 이 void 타입이 맞음.
void는 어떤 타입도 존재할 수 없음을 나타내기 때문에, any의 반대 타입 같다고 볼수 있음.
따라서 void는 보통 함수에서 반환 값이 없을 때 반환 타입을 표현하기 위해 쓰인다고 보면 됨.

// return 값이 없는 함수
function hello(n: number): void {
  let sum: number = n + 1;
}

const hi: void = hello(1); // 값을 반환하지 않는 함수는 실제로는 undefined를 반환
console.log(hi); // undefined

만일 void를 함수가 아닌 변수 타입으로 정의한다면, void 타입 변수에는 undefined와 null만 할당이 가능해짐.
즉, void를 타입 변수를 선언하는 것은 유용하지 않다고 보면 됨.

let unusable: void = undefined;
unusable = null; // 성공, tsconfig에서 strictNullChecks 컴파일 옵션을 사용하지 않을때만

literal(리터럴)

문자열 리터럴 타입 (String Literal Types)

문자열에 값을 정확하게 지정할때 사용됨.

type Easing = 'ease-in' | 'ease-out' | 'ease-in-out';

function animate(dx: number, dy: number, easing: Easing) {
   if (easing === 'ease-in') {
      // ...
   } else if (easing === 'ease-out') {
   } else if (easing === 'ease-in-out') {
   } else {
      // null이나 undefined를 전달하면 필터링 실행
   }
}

animate(0, 0, 'ease-in');
animate(0, 0, 'uneasy'); // 오류: "uneasy"는 여기서 허용하지 않음.

문자열 리터럴 타입은 나중에 배울 객체 지향 함수의 오버로드를 구별하기 위해 같은 방법으로 사용할 수 있음.

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... 더 많은 오버로드 ...
function createElement(tagName: string): Element {
    // ... 이곳에 코드를 ...
}

Union(유니언)

2개 이상의 타입을 허용하는 경우, 이를 유니언(Union)이라고 한다. (OR 의 의미로도 쓰임.)
| (파이프)를 통해 타입을 구분하며, 괄호는 단일 타입일 때는 안써도 되지만 배열일 경우 씌워야 함.

let union: (string | number);
union = 'Hello World';
union = 777;
union = false; // Error - TS2322: Type 'false' is not assignable to type 'string | number'.
// string, number 타입만 허용하기 때문에 에러.
// 배열에 문자열과 넘버만을 허용하는데 튜플과 달리 자유로움
// 배열 타입을 UNION으로 표현할때 괄호로 묶어 표현 안그러면 string | number[] 이런식으로 됨
let array: (string | number)[] = ['Apple', 1, 2, 'Banana', 'Mango', 3];
// Or
let array: Array<string | number> = ['Apple', 1, 2, 'Banana', 'Mango', 3];
function padLeft(value: string, padding: boolean | number) {
  // ...
}

let indentedString = padLeft("Hello world", true);


자동차와 핸드폰 인터페이스가 있음.
자동차는 출발, 핸드폰은 전화를 할 수 있음.
둘 다 이름과 색상을 가지고 있음.

getGift라는 함수를 만듦. 함수 내에서 색상을 찍어볼 수 있고

gift.start()에는 에러가 발생.

car에만 start함수가 가지고 있어서
start를 사용하기 전에 car인지 확인해줘야 함.
name은 동일한 속성 이지만 값에 타입을 달리 줌. 문자열 리터럴 타입이지 string타입이 아님.
if를 사용했는데 검사할 항목이 많아지면 switch사용하는 게 가독성이 좋음.

Union 주의할 점

다음과 같이 introduce() 함수의 파라미터 타입을 Person, Developer 타입 별칭들을 유니온으로 합침.

유니온 타입을 A도 될 수 있고 B도 될 수 있는 타입이지라고 생각해서, 파라미터의 타입이 Person도 되고 Developer도 될테니까 함수 안에서 당연히 이 인터페이스들이 제공하는 속성들인 age나 skill를 사용할 수 있겠지라고 생각할 수 있겠지만, 타입스크립트 관점에서는 introduce() 함수를 호출하는 시점에 Person타입이 올지 Developer타입이 올지 알 수가 없기 때문에 에러를 내뿜게 됨.

type Person = {
  name: string;
  age: number;
}

type Developer = {
  name: string;
  skill: string;
}

function introduce(someone: Person | Developer) {
  someone.name; // O 정상 동작
  someone.age; // X 타입 오류
  someone.skill; // X 타입 오류
}

즉, tsc는 어느 타입이 들어오든 간에 오류가 안 나는 방향으로 확실하게 타입을 추론한다는 특성이 있음.

그래서 결과적으로 Person과 Developer 두 타입에 공통적으로 들어있는 속성인 name만 접근할 수 있게 되는 것.

좀더 응용하자면 함수에서도 리턴값을 유니온으로 사용하게 되면 에러를 내뿜음.

모든 경우의 수를 고려해 x와 y 매개변수에 number가 들어오느냐 string이 들어오느냐 에따라 리턴값이 달라지니 유니온을 명시했지만, 컴파일러 입장에서는 옳지 않다는 것.

function add(x: string | number, y: string | number): string | number {
   return x + y;
}

add(1, 2);
add('1', '2');
add(1, '2');


이 부분은 곰곰히 생각해보면 당연한 원리.

아래의 코드에서 string 및 number 타입의 result 변수에 add(1, 2) 의 값을 받아봄. 유니온 입장에서도 논리적으로도 옳음.

function add(x: string | number, y: string | number): string | number {
   return x + y;
}

const result: string | number = add(1, 2);
result.charAt(1);


하지만 다음과 같이 이 역시 에러를 내뿜음.

왜냐하면 컴파일러 입장에서는 result 변수가 string인지 number인지 확신이 안되니 문자열 메소드인 charAt() 실행에 대해서 경고를 해주는 것.

이런 부분은 함수 오버로딩을 통해 해결 가능.

type Person = {
    name: string;
    age: number;
}

type Developer = {
    name : string;
    skill: string;   
}

function introduce(someone: Developer ) : string
function introduce(someone: Person ) : number
function introduce(someone: Person | Developer) {
    if ("skill" in someone) {
    return `Hi, my name is ${someone.name} and my skill is ${someone.skill}.`;
  } else {
    return someone.age;
  }
}

let example = {
    name: 'young',
    skill : 'coding'
}

let result = introduce(example);

console.log(result)

위 코드에서, return type을 overload signature(오버로드 시그니처)별로 분할해서 작성하지 않으면 비교 관련 Error 발생. ( 오버로드 시그니처와 구현 시그니처와 매치가 되지 않기 때문. )

overloading

같은 이름을 가진 메소드가 있더라도 매개변수의 개수 또는 타입이 다르면, 같은 이름을 사용해서 함수를 정의할 수 있음.

  • overloading 조건
    메소드의 이름이 같고, 매개변수의 개수나 타입이 달라야 함. 주의할 점은 'Return Value'만 다르면 오버로딩 할 수 없음.

함수 오버로딩

Intersection (인터섹션)

&(ampersand)를 사용해 2개 이상의 타입을 조합하는 경우, 이를 인터섹션이라고 함.
인터섹션은 새로운 타입을 생성하지 않고 기존 타입들을 조합할 수 있기 때문에 유용하지만 자주 사용되지는 않음.

interface User {
  name: string,
  age: number
}

interface Validation {
  isValid: boolean
}

const testCase: User = {
  name: 'juyoung',
  age: 27,
  isValid: true // Error -  TS2322: Type '{ name: string; age: number; isValid: boolean; }' is not assignable to type 'User'.
};

const testCase2: User & Validation = {
  name: 'jisu',
  age: 30,
  isValid: true
}


모든 속성 기입해야 함. 다 적기 전에는 Error가 사라지지 않음.

위 처럼 타입을 & 교집합을 해서 타입을 추론하는 것보다, 그냥 새로운 타입을 하나 만들어 할당하는 것을 권장

타입 - Type Alias

타입 별칭(Type Alias)은 사용자가 정의하는 타입 변수.
다음과 같이 한줄로 복잡하고 기나긴 타입을 정의하면 가독성이 좋지 않기 때문에, type 별칭으로 타입 형태를 묶어둔 뒤 별칭을 타입명으로 선언해서 사용하는 방법이라고 보면 됨.

type alias 와 interface의 변수명은 대문자로 씀.
let Dom: {version:string, el:(selector:string)=>void, css:(prop:string)=>void} = {
  version: '0.0.1',
  el(){},
  css(){}
};
type Operation = {
	version: string, 
    el: (selector:string) => void, 
    css: (prop:string) => void
}

let Dom: Operation = {
  version: '0.0.1',
  el(){},
  css(){}
};

만일 이 타입 별칭이 어떤 타입 묶음이었는지 기억이 안난다면, 에디터 상의 프리뷰 상태로 확인해볼 수 있음.

타입 별칭을 선언하는 방법은 마치 변수와 비슷함.
흔히 C나 JAVA 처럼 앞에 자료형을 선언하고 뒤에 자료형에 부합하는 값을 대입하는 것 처럼, 타입도 type 이라는 자료형을 선언하고 뒤에 타입값을 넣으면 됨.

int num = 123123;
string str = "hello";
// type 변수명 = 타입;
type Name = string;
type Age = number;

let name: Name = 'Tom';
let age: Age = 20;

Interface

인터페이스는 상호 간에 정의한 약속 혹은 규칙을 의미.

좀더 쉽게 말하자면 타입을 정의한 것들을 한데 모은 객체 타입이라고 말할 수 있다. 그래서 객체의 껍데기 혹은 설계도라고 불림.

타입스크립트에서의 인터페이스는 보통 다음과 같은 범주에 대해 약속을 정의할 수 있음.

  • 객체 스펙(속성과 속성의 타입)
  • 함수 파라미터
  • 함수 스펙(파라미터, 반환 타입 등)
  • 배열과 객체를 접근하는 방식
  • 클래스

user라는 객체를 하나 만듦. object타입으로 정의 가능.


user.name은 에러가 남. object에는 특정 속성 값에 대한 정보가 없기 때문임.
프로퍼티를 정의해서 객체를 사용하고자 할 때는 인터페이스 사용함.



.을 찍어주면 어떤 property가 있는지 나옴.
user.age= 10;은 문제가 없음.
user.gender=”male”하면 에러 발생.
gender?(옵셔널 체이닝) 붙여줌. gender는 있어도 되고 없어도 되는 optional한 property.

수정이 가능함. 에러 발생 안 함.

readonly를 붙이면 읽기 전용 속성이기에 수정 안됨.

user객체를 사용하다보면 여러 추가할 속성이 생김. 학년별 점수 기입하고 싶다고 할 때, 1, 2, 3, 4 학년 모두 인터페이스 정의 가능
모두 다 옵셔널 하게 만들면 불편함.

grade단어는 의미 없음. 그냥 key, xx 등 이렇게 적어도 상관없음.
number인 키, 값은 string 인 property여러 개를 받을 수 있음.

성적은 사실 string으로 받기에는 너무 범위가 넓음. 이럴 때 사용할 수 있는게 문자열 리터럴 타입
type Score = ‘A’ | ‘B’ | ‘C’ | ‘F’;
string일 때는 어느것이든 입력할 수 있었지만, 지금은 score에 정의한 것 이외에는 넣을 수 없음.


인터페이스로 함수도 정의 가능. x or y에 마우스 올리면 number라고 뜸.

앞에서 살펴본것 처럼 인자가 3개이거나 문자로 적으면 에러 발생.

나이를 받아서 성인인지 아닌지 리턴해주는 화살표 함수 만들어봄.


인터페이스로 클래스도 정의 가능. implements 키워드 사용 가능.


go..”가 출력이 됨.

인터페이스는 Extends라는 키워드 사용해서 확장이 가능함.
그럼 Car 인터페이스 속성을 그대로 물려받음.
지금은 에러가 나옴. color, wheels, start()속성을 모두 입력해줘야 에러가 사라짐.

확장은 여러 개 가능. Car와 Toy라는 인터페이스 각각을 동시 확장 가능.

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

interface Developer extends Person { // 인터페이스 상속
  skill: string;
}

function logUser(obj: Developer) {
  console.log(obj.name);
  console.log(obj.age);
  console.log(obj.skill);
}

let person = { 
  name: 'Capt', 
  age: 28, 
  skill: 'typescript, javascript' 
};

logUser(person);

[type alias vs interface 사용 선호도]
타입 별칭과 인터페이스는 얼핏보면 비슷해 보이지만, 이들의 가장 큰 차이점은 타입의 확장 가능 / 불가능 여부.
인터페이스는 다양한 방법으로 확장이 가능한데 반해 타입 별칭은 확장이 불가능.
따라서 간단한 타입 집합으로 이용할때는 type alias를 쓰고, 가능한 interface로 선언해서 사용하는 것을 추천되는 편.

인터페이스 추가 학습

Generics(제네릭)

프로그래밍 할때 '변수' 라는 저장소를 사용하는 이유는 데이터 값의 유연성을 위해서. 변수 라는 단어는 변할 수 있는 것을 말하고 그반대인 상수는 항상 고정된 것을 말함.

이러한 개념으로 봤을때 우리가 이때까지 number[] 며 string 이며 사용했던 타입은 항상 고정되어 절대 변하지 않는 타입을 사용해오고 있었던 것. 그리고 여기에 약간의 유연성을 가미한게 number | string | undefiened 유니온 타입.

하지만 이 프로그래밍 환경에서는 상황이 항상 고정되어 의도대로 흘러가지는 않음. 언제 어디서 변할수 있는 변수가 항상 일어나는게 이 업계.

따라서 타입을 직접적으로 고정된 값으로 명시하지말고 '변수' 를 통해 언제든지 변할 수 있는 타입을 통해 보다 유연하게 코딩을 할 수 있는 장치가 필요한데 이것이 바로 오늘 다룰 제네릭(generic) 타입.

간단하게 말하자면 타입을 변수화 한 것.
제네릭의 선언 문법을 <T> 꺾쇠 괄호 기호를 이용한다. 안의 T 는 변수명으로서 아무 문자로 명명해줘도 상관없음.

다음 코드 예제를 통해서 왜 타입스크립트에 제네릭 타입이 빠져서는 안될 녀석인지 간단하게 알아봄.

add() 라는 메소드를 만드는데 이 함수는 숫자도 더해줘서 정수로 만들어주고 문자열도 더해줘서 합쳐진 문자열을 만들어주는 다재다능한 멀티 메소드.

넘버와 스트링 두 타입을 동시에 다루니 유니온 타입을 통해 간단한게 다음과 같이 구성할 수 있음.

function add(x: string | number, y: string | number): string | number {
   return x + y;
}

add(1, 2); // 3
add('hello', 'world'); // 'helloworld'

언뜻 보기에는 문제가 없어보이지만, 여기에는 함정.

바로 유니온이 함정.

우리는 x: string, y: string 또는 x: number, y: number 를 의도했지만 사실 x: string, y: number 또는 x: number, y: string 도 될수 있는 가능성이 있음.

따라서 컴파일러는 이러한 것들을 똑똑하게 캐치하여 우리에게 빨간줄로 에러를 알리게 됨.

이를 어떻게 해결할까?

이러한 멀티 메소드를 유니온 타입으로 선언해서 문제가 된 것. 그러면 함수를 분리해서 하나의 함수 타입에는 하나의 역할만 하도록 분배해주면 됨.

타입스크립트에서는 이걸 함수 오버로딩 Visit Website 라고 부름.

function add(x: string, y: string): string;
function add(x: number, y: number): number;
function add(x: any, y: any) {
   return x + y;
}

add(1, 2); // 3
add('hello', 'world'); // 'helloworld'

// 오버로딩을 통해 다음 함수 호출은 일어날 수가 없다.
// add(1, '2');
// add('1', '2');

그러나 허용될 타입 갯수가 많아질수록 코드가 길어지게 되어 가독성 이 안 좋아지는건 마찬가지.
이러한 한계 때문에 Generic이 나온 것.
다음과 같이 꺾쇠 괄호와 대문자 T 변수로서 지정함으로서, 제네릭을 통해 코드에 선언한 타입을 변수화 하고, 나중에 타입을 정하는 식으로 유연하게 사용이 가능.

function add<T>(x: T, y: T): T { // 제네릭 : 꺾쇠와 문자 T를 이용해 표현. T는 변수명이라고 보면 된다.
   return x + y;
}

add<number>(1, 2); // 제네릭 타입 함수를 호출할때 <number> 라고 정해주면, 함수의 T 부분이 number로 바뀌며 실행되게 된다.
add<string>('hello', 'world'); // 'helloworld'

이처럼 Generic 타입은 함수나 클래스의 선언 시점이 아닌, 사용 시점에 타입을 선언할 수 있는 방법을 제공함.

선언할때 그냥 변수 문자만 적어주고, 생성하는 시점에 사용하는 타입을 결정함으로써 변수나 함수 인터페이스를 다양한 타입으로 재사용할 수 있는 원리.

제네릭 특징

  • 타입이 고정되는 것을 방지하고 재사용 가능한 요소를 선언 가능.
  • 타입 검사를 컴파일 시간에 진행함으로써 타입 안정성을 보장.
  • 캐스팅 관련 코드를 제거할 수 있음.
  • 제네릭 로직을 이용해 타입을 다르게 받을 수 있는 재사용 코드를 만들 수 있음.

제네릭은 원래 C#, Java 등 언어에서 재사용성이 높은 컴포넌트를만들 때 자주 활용되는 타입. 특히, 한가지 타입보다 여러 가지 타입에서 동작하는 컴포넌트를 생성하는데 사용됨.

실제로 타입스크립트 라이브러리 메소드 타입 형태를 보면 모두 제네릭으로 이루어져 있음.

꺾쇠 <> 기호를 변수명, 함수명 앞에다 쓰면 '타입 단언' 이 되게 됨.
따라서 제네릭을 구현하려면 변수명, 함수명 뒤에다가 꺾쇠 괄호를 적어주어야 함.
제네릭명은 꼭 T가 아니어도 됨.
내 마음대로 붙일수는 있지만 관습적으로 대문자 알파벳 한글자로 처리하는 편. (T, U, K ...)

// 인수들을 받아서 배열로 만들어주는 메소드
function toArray<T>(a: T, b: T): T[] {
   return [a, b];
}

// 만약 화살표 함수로 제네릭을 표현한다면 다음과 같이됨.
const toArray2 = <T>(a: T, b: T): T[] => { ... }


toArray<number>(1, 2); // 숫자형 배열
toArray<string>('1', '2'); // 문자형 배열
toArray<string | number>(1, '2'); // 혼합 배열

// 사실 컴파일러는 전달하는 인수의 타입을 보고 스스로 추론하기 때문에 함수 호출할때 제네릭을 안써줘도 알아서 추론.
toArray(1, 2);
toArray('1', '2');
toArray<string | number>(1, '2'); // 하지만 가끔 자동 타입 추론이 잘안되는 경우가 있기 때문에 직접 제네릭을 선언해야 함.

보통 제네릭 함수를 호출할때 제네릭을 생략하는 위의 코드 두 번째 방법이 가독성이 좋기 때문에 흔하게 사용됨.
하지만 코드가 복잡해서 컴파일러가 타입 추론을 잘못한다면 직접 제네릭을 지정해야 하는 경우도 있음.

주의해야할 점은, 만일 제네릭에서 인수를 배열로 받을 경우 따로 제네릭 처리를 T[]Array<T> 로 해주어야 한다는 점. 그외에 일반 타입이나 객체 같은 경우는 따로 처리없이 그대로 받으면 됨.

function loggingIdentity<T>(arg: T[]): T[] {
   console.log(arg.length); // 배열은 .length를 가지고 있다. 따라서 오류는 없다.
   return arg; // 배열을 반환
}

function loggingIdentity2<T>(arg: Array<T>): Array<T> {
   console.log(arg.length);
   return arg;
}

loggingIdentity([1, 2, 3]);
loggingIdentity2([1, 2, 3]);

특히 제네릭은 인터페이스와 정말 많이 쓰임.
둘이 천생연분 부부라고 느낌이 들정도로 앞으로 자주 보게 됨.

// 제네릭 인터페이스
interface Mobile<T> { 
   name: string;
   price: number;
   option: T; // 제네릭 타입 - option 속성에는 다양한 데이터 자료가 들어온다고 가정
}

// 제네릭 자체에 리터럴 객테 타입도 할당 할 수 있다.
const m1: Mobile<{ color: string; coupon: boolean }> = {
   name: 's21',
   price: 1000,
   option: { color: 'read', coupon: false }, // 제네릭 타입의 의해서 option 속성이 유연하게 타입이 할당됨
};

const m2: Mobile<string> = {
   name: 's20',
   price: 900,
   option: 'good', // 제네릭 타입의 의해서 option 속성이 유연하게 타입이 할당됨
};



계속 늘어날 수 있음. 다른 타입도 만들어서 전달 가능.
이럴 때 쓰는게 제네릭
<T> : 타입 파라미터. X나 A 적어도 상관없음.


T는 어떤 타입을 전달 받아 사용할 수 있게 함.
매개변수 타입은 T[]이렇게 적어주고
사용하는 쪽에서 타입 지정.

두 번째 배열은 String
타입을 지정해주지 않아도 타입스크립트는 전달한 인자를 보고 어떤 타입인지 알 수 있음.
특정 타입으로 강제하고 싶은 경우에만 적어줘도 상관없음.

인터페이스에서 사용해봄.
이름, 가격, 옵션,
옵션에는 어떤 데이터가 올지 몰라.
String, Object, Boolean, null, undefined가 올 수도 있음.
이럴 때 제네릭 사용 가능. 방금 나열한 타입을 모두 적는 것은 비효율적.

에러 발생.

타입을 지정해주면 에러가 사라짐.

옵션 객체 모습이 정해져 있다면 이렇게 적어주면 됨.

제네릭을 사용해서 하나의 인터페이스만 선언하고, 각기 다른 모습 객체들을 많이 만들어줄 수 있음.

인터페이스 여러 개 선언 후 객체들을 만듦.
showName함수는 매개변수를 받아서 name property Return함.
book은 name이 없어서 사용하면 안됨. 지금은 매개변수 타입이 any인데
제네릭을 이용해서 Type을 넣어주도록 함.
T에는 name이 없음. 실제 전달되는 User, Car에는 name이 있겠지만,
지금 이부분만 보고 모든 매개변수에 name이 있다고 장담할 수 없음.
이럴 때는

T 타입이 올건데 그 타입은 name이 String인 객체를 확장한 형태.
라고 알려줌. 이러면 다양한 모습의 객체가 올 수 있겠지만, 항상 { name : string } 을 가지고
있게 됨. name이 없거나 string이 아니라면 book을 전달할 때처럼 Error가 남.

name이 없다는 것.
Car name을 Boolean으로 바꾸면

String이 와야해서 이 부분에서 에러가 남.
타입스크립트 제네릭에 대해 알아봄. 코드 재사용 측면에서 아주 유용.

또한 type alias 로도 잘 어울림.

type TG<T> = T[] | T;

const number_arr: TG<number> = [1, 2, 3, 4, 5];
const number_arr2: TG<number> = 12345;

const string_arr: TG<string> = ['1', '2', '3', '4', '5'];
const string_arr2: TG<string> = '12345';
type IsArray<T> = T[];

const numberArr: IsArray<number> = [1, 2, 3, 4, 5];
const stringArr: IsArray<string> = ['a', 'b', 'c', 'd', 'e'];
const mixArr: IsArray<string | number> = ['a', 2, 'c', 4, 'e'];
function getText<T>(text: T): T {
   return text;
}

getText<string>('hi'); // 'hi'
getText<number>(10); // 10
getText<boolean>(true); // true

제네릭 제약 조건(extends)

사용하는 시점에 타입을 결정해줌으로써 사실상 아무 타입이나 집어넣어도 상관 없음.

function identity<T>(p1: T): T {
   return p1;
}

identity(1);
identity('a');
identity(true);
identity([]);
identity({});

이렇게 입력값에 대한 유연성을 확보했지만 각 함수에 대해 사용처에 따라서 입력값을 제한 할 필요가 생김.

가장 대표적인 예로 forEach() 라는 메소드제네릭을 이용하여 만든다고 쳤을때, 이 forEach() 는 배열을 순회하는 고차 함수니 반드시 원본값을 배열로 받을 필요가 있음.

또한 리액트와 같은 라이브러리의 메소드를 구현할때에도 입력 가능한 값을 범위를 제한하여 만든다. 예를 들어 리액트의 속성값 전체는 객체 타입만 허용됨.

이를 위해 타입스크립트의 제네릭은 적용되는 타입의 종류를 제한할 수 있는 기능을 제공함.
다음과 같이 제네릭에 extends 키워드를 이용하면 제네릭 타입으로 입력할 수 있는 타입의 종류를 제한할 수 있음.

제네릭의 extends는 인터페이스나 클래스의 extends 와 약간 정의가 다름.
클래스의 extends는 상속의 의미로서 '확장' 의 정의를 가지지만, 제네릭의 extends는 '제한' 의 의미를 가진다는 차이점이 있다.
따라서 <T extends K> 형태의 제네릭이 있다면, T가 K에 할당 가능해야 한다 라고 정의하면 됨.

type numOrStr = number | string;

// 제네릭에 적용될 타입에 number | string 만 허용
function identity<T extends numOrStr>(p1: T): T {
   return p1;
}

identity(1);
identity('a');

identity(true); //! ERROR
identity([]); //! ERROR
identity({}); //! ERROR

속성 제약 조건

단순히 사용성을 위해 제네릭 타입을 제한 하는 것 뿐만 아니라 로직에 의해서 어쩔수 없이 제한해야 하는 경우도 있음.

예를 들어 다음 코드를 보면, T에는 .length 프로퍼티가 없다고 오류가 뜨는데, 왜냐하면 우리 입장에선 제네릭 타입이니 그럴려니 하겠지만 컴파일러 입장에선 T 타입이 대체 무엇인지 모르기 때문에 그런 것.

function loggingIdentity<T>(arg: T): T {
   console.log(arg.length);
   return arg;
}


이때는 타입 가드로 조건문을 통해 분기하는 방법도 있겠지만,

function loggingIdentity<T>(arg: T): T {
   if(typeof arg === "string" || Array.isArray(arg)) console.log(arg.length);
   return arg;
}

만일 length 같은 기본 프로퍼티가 아니라 사용가 커스텀 프로퍼티일 경우 제네릭 타입의 프로퍼티를 반드시 해당 속성을 포함하도록 지정을 해주어야 함.

interface Lengthwise {
   length: number;
}

// 제네릭 T 는 반드시 { length: number } 프로퍼티 타입을 포함해 있어야 함.
function loggingIdentity<T extends Lengthwise>(arg: T): T {
   console.log(arg.length); // 이제 .length 프로퍼티가 있는 것을 알기 때문에 더 이상 오류가 발생하지 않음.
   return arg;
}

loggingIdentity(3); // 오류, number는 .length 프로퍼티가 없음.
loggingIdentity({ length: 10, value: 3 });

매개변수 제약조건

하나의 함수에서 제네릭은 여러개 지정해서 사용할 수 있다.

이를 이용해 각 매개변수마다 다른 제네릭 타입 조건 제한을 걸수 있다

function myfunc<T extends string, K extends number>(arg1: T, arg2: K): void {
   console.log(typeof arg1); // string
   console.log(typeof arg2); // number
}
myfunc('1', 2);

이를 응용하면 다음과 같이 로직을 짤 수 있다. (이부분은 약간 어려우니 집중해서 보길 바란다)

getProperty 라는 메소드가 있고, 이 함수는 객체와 key이름을 아규먼트로 받는데, 만일 객체에 존재하지 않는 key명을 입력받을 경우 오류를 내뿜는다.

조건 분기로 해결할수도 있겠지만 제네릭 자체에서 타입을 제한하면 된다.

여기서 핵심은 K extends keyof T 제네릭 타입인데, 제네릭 T에는 x변수(객체) 가 오게되는데 이 객체의 key값만 뽑아 keyof를 통해 유니온 타입으로 'a' | 'b' | 'c' | 'd' 만들어주고 K 제네릭에 제한을 건다.

그러면 K 제네릭은 반드시 'a' | 'b' | 'c' | 'd' 상수 타입만 올수있다. 이런식으로 타입 가드 장치를 거는것도 타입스크립트 로직의 한 방법이다.

자주 응용되는 타입스크립트의 keyof / typeof 키워드 사용법은 이 글을 참고.

function getProperty<T, K extends keyof T>(obj: T, key: K) {
   return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, 'a'); // 성공
getProperty(x, 'm'); // 오류: 인수의 타입 'm' 은 'a' | 'b' | 'c' | 'd'에 해당되지 않음.

두번째 예제를 보면, 다음 swapProperty 메소드는 객체 타입을 받아서 서로의 key의 value를 스왑하는 함수.

전달받는 객체 타입의 형태는 { name: string, age: number, liveInSeoul: boolean } 이며, 이 객체 형태 대로 아규먼트로 전달해주면 문제가 없겠지만 만일 사용자가 객체 프로퍼티를 잘못 전달 했을 경우는 대비래 제네릭을 제한할 필요가 있음.

따라서 keyof Person 을 통해 name | age 유니온 타입을 만들고 이를 제네릭에 extends 해서 반드시 제네릭 T의 타입에 해당 key속성을 포함하도록 메소드 내에서 제한을 할 수 있음.

interface Person {
   name: string;
   age: number;
}
interface Korean extends Person {
   liveInSeoul: boolean;
}

// type T1 = keyof Person;
function swapProperty<T extends Person, K extends keyof Person>(p1: T, p2: T, key: K): void {
   // p1 객체에 있는 key의 value를 p2 객체에 있는 key의 value와 스왑하는 함수
   // 그런데 당연히 p1 객체에 있는 key는 p2에도 존재해야 되기 때문에 key인수의 제네릭 K를 (keyof Person) 으로 제한
   const temp = p1[key];
   p1[key] = p2[key];
   p2[key] = temp;
}

const p1: Korean = {
   name: '홍길동',
   age: 23,
   liveInSeoul: true,
};
const p2: Korean = {
   name: '김삿갓',
   age: 31,
   liveInSeoul: false,
};

swapProperty(p1, p2, 'age'); // 객체의 age 키의 값을 서로 스왑
/*
{ name: '홍길동', age: 31, liveInSeoul: true }
{ name: '김삿갓', age: 23, liveInSeoul: false }
*/

함수 제약조건

만일 일반 타입이나 인터페이스가 아닌 함수 자체를 제네릭 인자에서 받을수 있도록 제한하는 것이면 어떻게 선언할까?

매개변수에 콜백 함수를 받아들일때는 다음과 같이 제네릭 제약을 할 수 있음. (T 에는 해당 함수 자테 타입 형태로만 들어올 수 있음.)

function translate<T extends (a: string) => number, K extends string>(x: T, y: K): number {
   return x(y);
}

// 문자숫자를 넣으면 정수로 변환해주는 함수
const num = translate((a) => { return +a; }, '10');
console.log('num: ', num); // num : 10

제네릭 함수 타입

우리는 타입스크립트 함수 자체도 하나의 타입으로 지정할 수 있다고 배웠다.

예를 들어 다음과 같이 함수 타입 구조를 정해주고, 이 함수 타입 구조에 맞는 함수만 할당 가능하도록 체계적인 로직을 짤 수 있었다.

//* 인터페이스로 함수 타입을 지정
interface Add {
   (x: number, y: number): number;
}

let myFunc: Add = (x, y) => {
   return x + y;
};

제네릭 함수 역시 함주 자체 타입 구조로 만들어 할당 제한이 가능하다.

interface GenericIdentityFn {
   <T>(arg: T): T; // 제네릭 함수 타입 구조
}

function identity<T>(arg: T): T {
   return arg;
}

let myIdentity: GenericIdentityFn = identity;

myIdentity<number>(100);
myIdentity<string>('100');

다른 방식으로 아예 함수를 할당할때 제네릭을 결정하는 방식으로도 사용될 수 있다.

제네릭 <T> 가 인터페이스명 옆에 옮겨간걸 확인 할 수 있다.

interface GenericIdentityFn<T> {
   (arg: T): T;
}

function identity<T>(arg: T): T {
   return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;
let myIdentity2: GenericIdentityFn<string> = identity;

myIdentity(100);
myIdentity2('100');

혹은 인터페이스를 사용안하고 직접 리터럴로 함수 자체 타입을 타입 선언할 수 있다. (다만 가독성이 좋지 않아 많이 사용되지 않는다)

function logText<T>(text: T): T {
  return text;
}

// #1
let str: <T>(text: T) => T = logText;

// #2 : 가독성 향상을 위해 중괄호로 묶어서 표현
let str: {<T>(text: T): T} = logText;

이와 같은 방식으로 제네릭 인터페이스 뿐만 아니라 클래스도 생성할 수 있다.

다만, 이넘(enum)과 네임스페이스(namespace)는 제네릭으로 생성할 수 없다.

제네릭 클래스 타입

제네릭 클래스에서도 제네릭을 사용하여 유연하게 클래스에서 다룰 타입을 지정할 수 있다.

앞에서 살펴본 제네릭 인터페이스와 비슷하다고 보면 된다. 이처럼 제네릭 클래스도 클래스 안에 정의된 속성들이 정해진 타입으로 잘 동작하게 보장할 수 있다.

class GenericNumber<T> {
   zeroValue: T;
   add: (x: T, y: T) => T;

   constructor(v: T, cb: (x: T, y: T) => T) {
      this.zeroValue = v;
      this.add = cb;
   }
}

let myGenericNumber = new GenericNumber<number>(0, (x, y) => {
   return x + y;
});

let myGenericString = new GenericNumber<string>('0', (x, y) => {
   return x + y;
});

myGenericNumber.zeroValue; // 0
myGenericNumber.add(1, 2); // 3

myGenericString.zeroValue; // '0'
myGenericString.add('hello ', 'world'); // 'hello world'

단, 클래스를 제네릭으로 관리할때, static 정적 멤버는 제네릭으로 관리할수 없다는 점은 유의하자.

자료구조 제네릭 활용

제네릭은 데이터의 타입에 다양성을 부여해주기 때문에 자료구조에서도 많이 사용된다.

다음은 스택 과 큐 를 제네릭 및 객체지향으로 구성한 예제이다.

class MyArray<A> {
   // 자식으로 부터 제네릭 타입을 받아와 배열 타입을 적용
   constructor(protected items: A[]) {}
}

class Stack<S> extends MyArray<S> {
   // 저장
   push(item: S) {
      this.items.push(item);
   }
   // 호출
   pop() {
      return this.items.pop();
   }
}

class Queue<Q> extends MyArray<Q> {
   // 저장
   offer(item: any) {
      this.items.push(item);
   }
   // 추출
   poll() {
      return this.items.shift();
   }
}

const numberStack = new Stack<number>([]);
numberStack.push(1);
numberStack.push(2);
const data_numberStack = numberStack.pop(); // 2

const stringStack = new Stack<string>([]);
stringStack.push('a');
stringStack.push('b');
const data_stringStack = stringStack.pop(); // 'b'

const numberQueue = new Queue<number>([]);
numberQueue.offer(1)
numberQueue.offer(2)
const data_numberQueue = numberQueue.poll(); // 1

const stringQueue = new Queue<string>([]);
stringQueue.offer('a');
stringQueue.offer('b');
const data_stringQueue = stringQueue.poll(); // 'a'

생성자 매개변수

함수 인자에 생성자만 오도록 제네릭으로 제한할때 다음과 같이 쓸 수 있다.

// 매개변수 x 는 생성자 타입만 올수 있게 제네릭 제한
function add<T extends abstract new (...args: any) => any>(x: T): T {
   return x;
}

class A {}

add(A);

Generic 추가 학습

function (함수)

화살표 함수를 이용해 타입을 지정할 수 있음.
인수의 타입과 반환 값의 타입을 입력함.

let myFunc: (arg1: number, arg2: number) => number;
myFunc = function(x, y) {
  return x + y;
}
myFunc(1, 2) // 3

let noneFunc: () => void;
noneFunc = function () {
  console.log('hihi');
};


Interface처럼 함수 매개변수도 optional로 지정 가능.


Error 발생.
name이 없을 때 대비한 코드가 있지만, 타입스크립트에서는 보다 명확하게 알려줘야 함.

Error가 사라짐. name은 있어도 되고 없어도 되는 Optional parameter 선택적 매개변수라고 부름.
옵셔널이어도 타입은 항상 지켜야 함. undefined이거나 있다면 string.

자바스크립트에서 매개변수 default값을 줄 수 있음.

위에 있는 것과 모습이 같음.

이번에는 이름과 나이를 받아서, 문자열 출력.
나이는 optional parameter. 입력 해도, 안 해도 됨.

주의하실 점은 name앞에 이렇게 age가 오면 안됨.
선택적 매개변수가 필수 매개변수보다 앞에 오면 에러 발생함.
옵셔널 매개변수는 없어도 된다는 이야기니까 에러 발생함.

만약 앞에두고 사용하고 싶다면, 이런 방식으로 사용할 수 있음.
이렇게 age를 Number | undefined를 받을 수 있게 해두고 명시적으로 undefined받도록

add 함수가 있음. 숫자를 전달받아서 더해주는 함수
Rest parameter. 즉 나머지 매개변수가 사용됨.
Rest parameter는 개수가 항상 바뀔 수 있음.
Rest parameter를 사용 하면 전달 받은 매개변수를 배열로 나타낼 수 있게 함.
그래서 타입은 number[]이렇게 배열 형태로 기입.



User라는 인터페이스가 있고 name속성만 있고, Sam이라는 객체가 있음. User타입이고 name이 있음.
그리고 showName이라는 함수가 있음. this에 name을 보여줌.
여기서 this는 사용하는 방법에 따라 달라짐.

이 코드에서는 bind를 이용해서 this를 Sam객체로 강제하고 있음.
실제 실행을 해보면 Sam이라고 잘 찍힘. 마우스를 올려보면 this타입이 정해지지 않아서 빨간 줄이 뜨고 있음.
TypeScript에서 this 타입을 정할 때는 함수 첫 번째 매개변수 자리에 this를 쓰고 type을 입력해주면 됨.

매개변수가 있을 때는 어떻게 해야 할까?
age: number, gender: 'm' | 'f' 이렇게

여기 에러는 두 개 매개변수 전달하라는 것.


this는 user타입이라고 어떻게 명시하는가? 제일 앞에 적으면 됨.
a(30, ‘m’)은 this다음부터 전달됨.
전달된 매개변수가 this를 제외하고 사용.
세 개가 있다고 헷갈리면 안됨.

다음 User 인터페이스 하나 만들었고
join함수를 하나 만듦. value Data역할
나이가 숫자라면 name, age 반환
아니라면 ‘나이는 숫자로 입력해주세요.’반환
문제가 없어보이는데 실제 사용 해봄.

age가 string을 반환할 수 도 있다.라고 생각해서 에러 발생.
코드 상에는 분기처리가 되어있지만 타입만 보면 그렇지 않음.
JavaScript는 동일한 매개변수라도 다른 타입을 줄 수 있음.
이 함수는 전달받은 매개변수 타입에 따라 age타입에 따라 리턴 결과물이 달라짐. 객체이거나 문자열이 됨.
이럴 때는 오버로드를 사용. 함수 오버로드는 전달받은 매개변수 개수나 타입에 따라 다른 동작을 하게 하는 것
이 함수 위에 똑같은 모습으로 작성

이렇게 age:number일 때 User를 반환해주면 sam쪽에 에러가 사라짐.
age가 숫자라서 User를 반환한다고 판단.
동일한 방식으로 age가 string일 때 String반환.
동일한 함수지만 매개변수 타입, 개수에 따라 다르게 동작해야 한다면 오버로드를 사용할 수 있음.

Class(클래스)

ES6클래스를 다룰 수 있다는 가정. 차이점 다룸.
car Class를 하나 만듦. constructor에서 color를 받아서 this.color선언. start()메서드 만듦.

typescript에서는 에러가 발생함. color property가 Car에 없다고 나옴.
typescript에서 class작성 할 때 멤버 변수는 미리 선언해야 함.


string 적어주면 에러가 사라짐.


멤버 변수 미리 선언하지 않는 방법도 있음. 접근 제한자나 readonly키워드를 사용하면 됨.

타입 스크립트는 접근 제한자를 지원함.
public - 자식 클래스, 클래스 인스턴스에서 접근 가능함.
(아무것도 표기하지 않을 시)

지금 Car, 그리고 Car 클래스를 상속받은 Bmw가 있음.
Bmw constructor가 있고 super를 호출하고 있음.
super를 호출하지 않으면 에러가 발생함.
Bmw에는 showName이라는 메서드가 있고 멤버 변수 name을 보여줌.
지금 super의 name, 즉, Car Name이 public이기 때문에 이렇게 자식 클래스 내부에서 접근해도 문제 없이 사용할 수 있음.
명시적으로 name 앞에 public이라고 적어줘도 같음.


만약 private으로 바꾸면 사용할 수 없게됨. 자식 클래스에서 사용할 수 없게됨.
name은 car class 내부에서만 사용할 수 있음.

private을 표현하는 또다른 방법은 #을 적어주는 것
사용하는 쪽도 모두 #을 적어주면 private 키워드를 써주었을 때와 동일하게 class 내부 에러는 사라짐.
자식 클래스는 에러가 발생함. 기능상 차이는 없음.


protected하면 에러가 사라짐. protected도 자식클래스에서 접근 가능.
public으로 선언하면 에러가 안남.
protected는 자식클래스에서는 접근 가능하지만, 클래스 인스턴스에서는 접근 할 수 없음.
z4.name같은 것은 불가능 함.


지금 z4내용을 바꿀 수 있음.

readonly로 수정하면 에러 발생. 읽기 전용 property 수정 불가.

만약 name을 변경하고 싶다면 constructor 내부에서 그 작업 할 수 있게 해줘야 함.


다음은 Static property. 정적 멤버 변수 만들 수 있음.
static으로 선언한 정적 멤버 변수는 this를 써주는게 아니라 class명을 적어줌.

추상 클래스는 class 앞에 abstract씀.
추상 클래스는 new를 통해 객체를 만들 수 없음. 상속을 통해서만 사용 가능함.
이렇게 아무 작업하지 않는 추상 메서드를 만들면 자식 클래스에서 에러가남.


추상 클래스 내부에 추상 메서드는 반드시 상속 받은 쪽에서 구체적인 구현을 해줘야 함.
추상화는 프로퍼티, 메서드 이름만 선언하고 구체적인 기능은 상속 받은 쪽에서 만들어주는 것.
추상 클래스를 상속받아 만든 수많은 객체들이 동일하게 메서드를 가지고 있지만 구체적인 기능은 가지각색.

Utility Types (유틸리티 타입)


여기서 keyof 키워드 사용하면, user 인터페이스 키 값들을 유니온 형태로 받을 수 있음.

User Inferface 키 값중 하나를 입력하면 에러가 사라짐.

Partial




Partial은 프로퍼티 모두 optional로 변경해 줌.

여기 없는 프로퍼티 사용하면 에러가 남.

Required

interface Props {
  a?: number;
  b?: string;
}
 
const obj: Props = { a: 5 };
 
const obj2: Required<Props> = { a: 5 };
Property 'b' is missing in type '{ a: number; }' but required in type 'Required<Props>'.

Readonly


readonly는 읽기 전용으로 변경. 처음에 할당만 가능하고 뒤에 수정은 불가능해짐.

Record



Record<키, 타입>
이렇게 작성해줄 수 있음. 조금 복잡해 보이지만, 학년, 성적 부분을 타입으로 분리.


Grade를 키로 사용하고, Score를 타입으로 사용.


keyof User하면 User 인터페이스 id, name, age를 key로 쓴다는 것.
뒤에 오는 값들은 모두 boolean이 됨.

Pick


다음은 pick. T 타입에서, k Property만 골라서 사용.
이렇게 User인터페이스가 있을 때 admin을 만들고, Pick<User, ‘id’ | ‘name’>
이렇게 만들면, User에서 id, name만 가져와서 사용할 수 있음.

Omit


omit을 사용하면 특정 프로퍼티를 생략하고 사용할 수 있음. age와 gender는 제외되고, id와 name만 사용할 수 있음.

Exclude





비슷한 것으로 Exclude가 있음. T1에서 T2를 제외하고 사용하는 것.
omit과 다른 점은 omit은 property를 제거하는 것.
exclude는 타입으로 제거. T1타입중 T2타입 제거.

Extract

union에 할당할 수 있는 모든 union Type에서 추출해 Type구성함.

type T0 = Extract<"a" | "b" | "c", "a" | "f">;
-> type T0 = "a"

type T1 = Extract<string | number | (() => void), Function>;
-> type T1 = () => void

NonNullable



NonNullable : null을 제외한, 타입 생성
Null만 빼는 것은 아니고, undefined도 같이 제외

유틸리티 타입 추가 학습
유틸리티 타입 Docs

타입간 대입 가능한 표

어떤 타입은 하위 타입이라 아무 타입에 넣을 수 있고, 어떤 타입은 넣을수 없고 워낙 복잡해서 표로 정리해보면 다음과 같음.
초록색 체크는 strict 모드가 false일때만 허용되는 것이므로 사실상 X 라고 봐도 무방.
표를 읽는 법은 다음과 같음.
"데이터( any → ) 가 들어갈수 있는 타입은 자기자신과 unknown ~ null 타입에 대입될수 있으며, never 타입에는 대입될 수 없다" 라고 해석 하면 됨.

컴파일


C, C++, Java 프로그래밍을 해봤으면 작성한 소스 코드를 빌드(Build) 혹은 컴파일(Compile)해서 실행해봤거나 코드를 잘못 작성하여 컴파일 에러가 났던 경험이 있을 것.

컴파일은 인간이 이해할 수 있는 언어로 작성된 소스 코드(고수준 언어 : C, C++, Java 등)를 CPU가 이해할 수 있는 언어(저수준 언어 : 기계어)로 번역(변환)하는 작업(컴퓨터는 0, 1로 이루어진 기계어만 이해할 수 있기 때문)

소스 코드는 컴파일을 통해 기계어로 이루어진 실행 파일이 됨. 이 파일을 실행하면 실행 파일 내용이 운영체제의 Loader를 통해 메모리에 적재되어 프로그램이 동작.


컴파일 과정은 4가지 단계(전처리 과정 - 컴파일 과정 - 어셈블리 과정 - 링킹 과정)로 나뉨.
이 4가지 단계를 묶어서 컴파일 과정, 빌드 과정이라고 부르기도 하고 컴파일 과정과 링킹 과정을 따로 나눠서 부름.
보통 빌드 과정은 컴파일 과정보다 넓은 의미(빌드=컴파일+링킹)로 사용되는데 상황에 맞게 이해.

전처리 과정


전처리(Pre-processing) 과정은 전처리기(Preprocessor)를 통해 소스 코드 파일(.c)을 전처리된 소스 코드 파일(.i)로 변환하는 과정

이 과정에서 대표적으로 세 가지 작업을 수행

  • 주석 제거 : 소스 코드에서 주석을 전부 제거. 주석은 사람들이 알아볼 수 있게 남긴 내용이지 컴퓨터가 알 필요는 없기 때문.
  • 헤더 파일 삽입 : #include 지시문을 만나면 해당하는 헤더 파일을 찾아 헤더 파일에 있는 모든 내용을 복사해서 소스 코드에 삽입. 즉, 헤더 파일은 컴파일에 사용되지 않고 소스 코드 파일 내에 전부 복사됨. 헤더 파일에 선언된 함수 원형은 후에 링킹 과정을 통해 실제로 함수가 정의되어 있는 오브젝트 파일(컴파일된 소스 코드 파일)과 결합.
  • 매크로 치환 및 적용 : #define 지시문에 정의된 매크로를 저장하고 같은 문자열을 만나면 #define 된 내용으로 치환. 간단하게 말해 매크로 이름을 찾아서 정의한 값으로 전부 바꿔줌

컴파일 과정

컴파일(Compilation) 과정은 컴파일러(Compiler)를 통해 전처리된 소스 코드 파일(.i)을 어셈블리어 파일(.s)로 변환하는 과정.

이 과정에서 우리가 일반적으로 컴파일하면 생각하는 언어의 문법 검사가 이루어짐. 또한 Static한 영역(Data, BSS 영역)들의 메모리 할당을 수행.

  • 프론트엔드(Front-end)
    프론트엔드에서는 언어 종속적인 부분을 처리.

    소스 코드가 해당 언어로 올바르게 작성되었는지 확인(어휘/구문/의미 분석)하고 미들엔드에 넘겨주기 위한 GIMPLE 트리(소스 코드를 트리 형태로 표현한 자료 구조)를 생성.

    이 과정에서 C, C++, Java와 같은 다양한 언어들이 각 언어에 맞게 처리된 후 공통된 중간 표현(IR : Intermediate representation)인 GIMPLE 트리로 변환되므로 언어 종속적인 부분을 처리할 수 있음.

  • 미들엔드(Middle-end)
    미들엔드에서는 아키텍쳐 비종속적인 최적화를 수행.
    아키텍쳐 비종속적인 최적화란 CPU 아키텍쳐가 무엇이든(arm, x86 등) 상관없이 할 수 있는 최적화 말함.

    프론트엔드에서 넘겨받은 GIMPLE 트리를 이용해 아키텍쳐 비종속적인 최적화를 수행한 후 백엔드에서 사용하는 RTL(Register Transfer Language : 고급 언어와 어셈블리 언어의 중간 형태)를 생성함.

  • 백엔드(Back-end)
    백엔드에서는 아키텍쳐 종속적인 최적화를 수행.

    아키텍쳐 종속적인 최적화란 아키텍쳐 특성에 따라 최적화를 수행하는 것을 말함. 같은 기능을 수행하는 명령어여도 CPU 아키텍처별로 더욱 효율적인 명령어로 대체하여 성능을 높이는 작업을 예를 들 수 있음.

    미들엔드에서 넘겨받은 RTL을 이용해 아키텍쳐 종속적인 최적화를 수행하고 최적화가 완료되면 어셈블리 코드를 생성함.

    아키텍쳐 종속적인 최적화를 수행하면 해당 아키텍쳐만 이해할 수 있는 언어가 되기 때문에 아키텍쳐가 맞지 않으면 어셈블리 코드를 해석할 수 없음.

  • 어셈블리어
    기계어는 다른 말로 명령어(Machine Instruction)이라고 부르는데 명령어는 0101010과 같은 이진수로 이뤄진 숫자로 CPU 종류마다 고유한 내용을 가지고 있음.

    어셈블리어는 이런 명령어를 사람이 이해할 수 있게 부호화한 것으로 CPU 명령어(기계어)와 1대1로 매칭됨.

    많은 컴파일러가 앞서 설명한 세 단계의 구조를 따르고 있지만, 컴파일러마다 차이가 존재함.

    GNU에서 만든 C 컴파일러인 gcc는 프론트엔드/미들엔드/백엔드 단계가 깔끔하게 분리되어 있지 않고 의존성이 존재함. 그에 비해 오픈 소스 C 컴파일러인 Clang(프론트엔드) + LLVM(미들엔드, 백엔드)는 단계가 잘 분리되어 있음.

어셈블리 과정


어셈블리(Assembly) 과정은 어셈블러(Assembler)를 통해 어셈블리어 파일(.s)을 오브젝트 파일(.o)로 변환하는 과정

  • 오브젝트 파일
    어셈블리 코드는 이제 더 이상 사람이 알아볼 수 없는 기계어로 변환되는데 이를 오브젝트 코드.
    오브젝트 코드로 구성된 파일을 오브젝트 파일(Object File)이라 부르며 이 오브젝트 파일은 특정한 파일 포맷 가짐.
    ※ 오브젝트 파일 포맷의 종류는 Windows의 경우 PE(Portable Executable), Linux의 경우 ELF(Executable and Linking Format)로 나눠진


오브젝트 파일 헤더(Object File Header) : 오브젝트 파일의 기초 정보를 가지고 있는 헤더
텍스트 섹션(Text Section) : 기계어로 변환된 코드가 들어 있는 부분
데이터 섹션(Data Section) : 데이터(전역 변수, 정적 변수)가 들어 있는 부분
심볼 테이블 섹션(Symbol Table Section) : 소스 코드에서 참조되는 심볼들의 이름과 주소가 정의 되어 있는 부분.
재배치 정보 섹션(Relocation Information Section) : 링킹 전까지 심볼의 위치를 확정할 수 없으므로 심볼의 위치가 확정 나면 바꿔야 할 내용을 적어놓은 부분
디버깅 정보 섹션(Debugging Information Secion) : 디버깅에 필요한 정보가 있는 부분

여기서 중요한 부분은 심볼 테이블 섹션과 재배치 정보 섹션.

심볼(Symbol)은 함수나 변수를 식별할 때 사용하는 이름으로 심볼 테이블(Symbol Table) 안에는 오브젝트 파일에서 참조되고 있는 심볼 정보(이름과 데이터의 주소 등)를 가지고 있음.

이때 오브젝트 파일의 심볼 테이블에는 해당 오브젝트 파일의 심볼 정보만 가지고 있어야 하기 때문에 다른 파일에서 참조되고 있는 심볼 정보의 경우 심볼 테이블에 저장할 수 없음.

#include<stdio.h> 라이브러리를 이용해서 printf 함수를 사용하는 소스 코드 파일이 있다고 가정.

우린 이 소스 코드 파일을 컴파일하여 오브젝트 파일을 생성할 수 있음.

하지만 이 오브젝트 파일은 독립적으로 실행할 수 없음. 이 파일 안에는 printf 함수를 구현한 내용이 없기 때문.

전처리 과정을 통해 #include<stdio.h>로부터 printf 함수의 원형은 복사했지만 printf를 구현한 내용은 포함되어 있지 않음. 오브젝트 파일 구조에서 말한 것처럼 심볼 테이블에는 해당 오브젝트 파일의 심볼 정보만 가지고 있지 외부에서 참조하는 printf 함수에 대한 심볼 정보는 가지고 있지 않음.

즉, 이 오브젝트 파일을 실행하기 위해서는 printf 함수를 사용하는 오브젝트 파일과 printf 함수를 구현한 오브젝트 파일(libc.a 라이브러리)을 연결시키는 작업이 필요함.

이러한 연결 과정을 링킹(Linking)

링킹 과정


링킹(Linking) 과정은 링커(Linker)를 통해 오브젝트 파일(*.o)들을 묶어 실행 파일로 만드는 과정이다.

이 과정에서 오브젝트 파일들과 프로그램에서 사용하는 라이브러리 파일들을 링크하여 하나의 실행 파일을 만든다.

이때 라이브러리를 링크하는 방법에 따라 정적 링킹(Static Linking)과 동적 링킹(Dynamic Linking)으로 나눌 수 있다. 링킹 방식의 차이는 앞서 설명했던 라이브러리 포스트를 참고
https://bradbury.tistory.com/224

  • 심볼 해석(Symbol Resolution)
    심볼 해석은 각 오브젝트 파일에 있는 심볼 참조를 어떤 심볼 정의에 연관시킬지 결정하는 과정. 여러 개의 오브젝트 파일에 같은 이름의 함수 또는 변수가 정의되어 있을 때 어떤 파일의 어떤 함수를 사용할지 결정.

  • 재배치(Relocation)
    재배치는 오브젝트 파일에 있는 데이터의 주소나 코드의 메모리 참조 주소를 알맞게 배치하는 과정.

    링커가 컴파일러가 생성한 오브젝트 파일을 모아서 하나의 실행 파일을 만들 때, 각 오브젝트 파일에 있는 데이터의 주소나 코드의 메모리 참조 주소가 링커에 의해 합쳐진 실행 파일에서의 주소와 다르기 때문에 그것을 알맞게 수정해줘야 함.

    이를 위해 오브젝트 파일 안에 재배치 정보 섹션(Relocation Information Section)이 존재함.

    링킹 과정에서 같은 세션끼리 합쳐진 후 재배치가 일어남.


위 그림을 통해 알 수 있듯이 오브젝트 파일 형식은 링킹 과정에서 링커가 여러 개의 오브젝트 파일들을 하나의 실행 파일로 묶을 때 필요한 정보를 효율적으로 파악할 수 있는 구조.

링킹을 하기 전 오브젝트 파일을 재배치 가능한 오브젝트 파일(Relocatable Object File)이라 부르고 링킹을 통해 만들어지는 오브젝트 파일을 실행 가능한 오브젝트 파일(Executable Object File)이라 부름.

※ 컴파일 과정 동안 연쇄적으로 사용하는 개발 도구들(전처리기-컴파일러-어셈블리-링커)을 묶어서 툴체인(Toolchain)이라고도 부름.




Reference
https://poiemaweb.com/typescript-introduction

https://www.samsungsds.com/kr/insights/typescript.html

https://bradbury.tistory.com/226

https://www.youtube.com/watch?v=xkpcNolC270&ab_channel=%EC%BD%94%EB%94%A9%EC%95%A0%ED%94%8C

https://www.youtube.com/watch?v=5oGAkQsGWkc&list=PLZKTXPmaJk8KhKQ_BILr1JKCJbR0EGlx0&ab_channel=%EC%BD%94%EB%94%A9%EC%95%99%EB%A7%88

https://www.typescriptlang.org/

https://velog.io/@wndud0647/TIL-25-TypeScript-%ED%83%80%EC%9E%85-%EC%84%A0%EC%96%B8-%EB%B0%8F-%EC%A2%85%EB%A5%98

https://inpa.tistory.com/entry/TS-%F0%9F%93%98-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%83%80%EC%9E%85-%EC%84%A0%EC%96%B8-%EC%A2%85%EB%A5%98-%F0%9F%92%AF-%EC%B4%9D%EC%A0%95%EB%A6%AC

profile
선명한 기억보다 흐릿한 메모

0개의 댓글