
- #typescript
타입스크립트 - 기본 타입 정리하기
Prolip
2024-11-16
정적 타입 언어와 동적 타입언어인 자바스크립트와 타입스크립트의 기본 개념 정리하기
프로그램?
프로그램은 크게 3가지 동작으로 구성된다.
- 입력
- 처리
- 출력
이 3가지 동작이 반복됨으로써 프로그램이 동작하게 된다.
프로그래밍의 시작점으로 볼 수 있는 입력엔 값이 필요하다.
이 값은 변수에 존재하는데, 변수란 값을 저장할 수 있는 공간이며 값을 가리키는 상징적인 존재로 볼 수 있다.
컴퓨터의 메모리 공간은 한정적이다. 어떤 공간에 값을 할당할 때 미리 어떤 값인지 특정할 수 있다면 보다 효율적으로 적재할 수 있을 것이다.
또, 명시적으로 타입이 지정되어있다면 어떤 일을 수행할지 보다 쉽게 알아볼 수 있을 것이다.
만약 처리 과정 혹은 변수에 타입이 없다면 어떻게 될까?
처리(입력) { 출력 무언가 }
위 과정의 입력, 처리, 출력을 봤을 때 우린 이게 어떻게 동작하는지 알 수 없다.
처리(입력: 거북이) { 출력 무언가: 장난감 }
하지만 위의 과정을 본다면 입력으로 거북이를 받아 출력으로 장난감을 반환하는 것을 한 눈에 보고 알 수 있다.
이처럼 이 3가지 동작에 타입이 명확하게 선언되어있다면 프로그램을 쉽게 이해할 수 있으며, 보다 안정적으로 동작하도록 도울 수 있다.
정적 타입 언어과 동적 타입 언어
정적 타입 언어와 동적 타입 언어가 무엇일까?
우리는 자바스크립트를 사용하며 직접 타입을 지정해본 기억이 없다.
const name = "jimin" let age = 26
이렇게 단순히 값을 선언하는 방식에서 사용하는 let, const를 사용할 뿐이다.
하지만 모든 프로그래밍 언어에는 값의 타입은 분명히 존재한다.
function addNum(numA, numB) { if(typeof numA === "number" && typeof numB === "number") { return numA + numB } throw "숫자만 입력 가능합니다." } addNum(15, 13) // 28 addNum(15, "13") // Uncaught 숫자만 입력 가능합니다.
우리가 단순히 명시하지 않을뿐, 분명히 타입은 존재한다.
만약 if 문이 없다면 이 함수는 어떻게 동작할까?
function subNum(numA, numB) { return numA - numB } subNum(15, 13) // 2 subNum(15, "5") // 10
자바스크립트의 특징을 확인하기 쉽게 ‘-’ 연산을 수행하는 함수로 구현했다.
어라? 그런데 이상하다. 분명 number 타입에 string 타입을 연산했는데도 값이 나왔다.
이는 자바스크립트 엔진이 문자열 값을 숫자 형태로 변환해 빼기 연산을 수행했기 때문이다. 이를 암묵적 타입 변환이라고 한다.
만약 이 함수를 타입스크립트에서 똑같이 구현한다면 개발자가 코드를 작성하는 컴파일 타임에 에러를 확인할 수 있었을 것이다.
이렇게 컴파일러 혹은 런타임 환경에서 암묵적 타입 변환이 일어나는 언어를 약타입 언어라고 하며, 타입스크립트와 같이 타입 에러가 발생하는 언어는 강타입 언어라고 한다.
이렇게 작성된 코드는 동작은 하지만 실제 런타임 환경인 사용자가 실행하는 환경에서 예상치 못한 에러를 발생시킬 수 있으니 위험하다.
그럼 이제 다시 정리해보자면.
정적 타입 언어
- 정적 타입 언어란 컴파일 타임에 타입이 결정되는 언어이다. 즉, 변수를 선언할 때 각 데이터의 자료형을 선언해줘야한다.
- 컴파일 타임이란 인간이 작성한 코드가 기계가 알아들을 수 있게 변환되는 과정으로 작성하는 즉시, 에러를 확인할 수 있다. 타입스크립트, 자바 등은 정적 타입 언어이다.
동적 타입 언어
- 동적 타입 언어란 런타임 환경에서 타입이 결정되는 언어이다. 즉, 데이터 타입을 개발자가 명시하지 않고, 자유롭게 개발할 수 있다.
- 파이썬, 자바스크립트 등은 동적 타입 언어이다.
let foo = 1; foo = "bar"
- 이렇게 처음 변수를 선언할 때 number 타입인 1을 할당했지만 이후에 string 타입을 할당해도 자바스크립트에선 이를 문제로 판단하지 않는다.
- 유연하게 동작하지만 이는 런타임 환경, 즉 사용자에게 배포된 환경에서 에러가 발생할 수 있는 위험성을 가지고 있다.
자바스크립트에서의 타입
- 자바스크립트에서 우리가 직접 지정하지 않았을 뿐. 자바스크립트의 모든 값은 데이터 타입을 갖는다. 일반적으로 ES6 기준 7개의 데이터 타입을 제공한다. 크게 2가지로 나눈다.
- 원시타입(값 자체가 변경되지 않는 불변 데이터다.): Numeric, string, boolean, undefined, null, Symbol.
- 객체타입(참조를 통해 데이터가 관리된다.): array, func, object가 모두 포함된다. 함수도 일급 객체로 객체 타입이다.
타입스크립트에서의 타입
타입스크립트를 사용하면 콜론을 사용해 명시적으로 타입을 선언할 수 있다. 이를 타입 애너테이션이라한다. 그리고 타입스크립트는 기존 자바스크립트 코드에 점진적으로 타입을 적용할 수 있는 특징을 가진다.
그렇기에 변수 이름 뒤에 : type을 제거해도 코드는 정상적으로 동작한다. 다만, 타입 추론 과정이 어려워질 뿐이다.
const age: number = 26 // number const name: string = "jimin" // string const jiminIsFree: boolean = false // boolean let jimin: string | undefined; // 아직 값이 없다. 하지만 후에 할당될 수 있다. const [user, setUser] = useState<string | null>(null); // 명시적으로 값이 없는 상태.. function doSomething(): never { while(true) {} } // 내부적으로 while 문이 끝나지 않는다. never.. 즉 절대 반환될 수 없다. function print(x): void { console.log(x); } // 반환되는 값이 없다.. // 떵냄새가 나는 any let anything: any = 1; anything = "hi", anything = false // 나 아무거나 다 돼요. // unknown let yet: unknown; // any 보다는 낫다. 여기에 any는 할당 못 함. // 외부 라이브러리 등에서 어떤 값이 들어올지 특정할 수 없을 때 사용. // 떵냄새가 나는 object let anythingObject: object; object = [1, 2, 3]; object = { name: "jimin", age: 26 } object = () => {} // 모두 할당 가능하니 구체적으로 사용하자.
구조적 타이핑
일반적으로 타입을 사용하는 프로그래밍 언어에서 값, 객체는 하나의 구체적인 타입을 가진다.
이는 이름으로 구분되고, 컴파일 타임 이후에도 남는데, 서로 다른 클래스가 명확한 상속 관계나 공통으로 가지는 인터페이스가 없다면 타입은 서로 호환되지 않는다.
interface Developer { faceValue: number; } interface Person { faceValue: number; } let developer: Developer = { faceValue: 62 }, let jimin: Person = { faceValue: 26 } developer = jimin // OK jimin = developer // OK
하지만 타입스크립트는 타입을 구조로 구분하며, 이를 구조적 타이핑이라고 한다.
Developer와 Person은 모두 동일한 구조를 가진다. faceValue라는 속성을 공통으로 가지기 때문에 타입스크립트는 이를 정상적으로 생각한다.
구조적 서브타이핑
구조적 서브타이핑이란 객체가 가지고 있는 속성을 바탕으로 타입을 구분하는 것이다. 위에서 살펴봤듯 다른 객체라도 가진 속성이 동일하면 서로 호환이 가능한 동일 타입으로 여긴다.
class Person { name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } } class Developer { name: string; age: number; sleepTime: number; constructor(name: string, age: number, sleepTime: number) { this.name = name; this.age = age; this.sleepTime = sleepTime; } } function greet(p: Person) { console.log("Hello I'm ", p.name); } const jimin = new Developer("Jimin", 26, 4); greet(jimin); // Hello, I'm Jimin
타입스크립트는 타입을 구조로 구분한다.
greet 함수는 매개변수로 Person 타입을 받는다. class는 값과 타입으로 모두 쓰일 수 있다. 타입에 의해 쓰인 class는 컴파일 이후 타입으로 사용된다면 사라지고, 값으로 쓰였다면 남는다.
어쨌든 Developer 클래스는 Person 클래스를 상속 받지 않았는데, greet 함수는 에러를 발생시키지 않는다.
이유는 Developer 클래스는 Person 클래스가 가진 속성을 모두 포함하고 있기 때문이다. 이런 동작은 타입스크립트가 덕 타이핑을 기반으로한 자바스크립트를 모델링한 언어이기 때문이다.
명목적 타이핑은 타입을 이름으로 구분해 더 안전할 수 있지만 구조적 타이핑은 타입을 구조로 구분하기 때문에 조금 더 유연하게 동작할 수 있다. 하지만 아래와 같은 경우 이 구조적 타이핑은 문제가 될 수 있다.
interface RightAngledTriangle { hypotenuse: number; adjacent: number; opposite: number; } function printPerimeter(triangle: RightAngledTriangle) { let total = 0; for (const side of Object.keys(triangle)) { const length = triangle[side]; // Error total += side; } console.log(total); }
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'RightAngledTriangle'. No index signature with a parameter of type 'string' was found on type 'RightAngledTriangle'
왜 이렇게 에러가 발생할까? 매개변수의 타입은 RightAngledTriangle로 모든 속성이 number 타입일 것이라고 당연히 생각할 수 있다.
const myTriangle = { hypotenuse: 5; adjacent: 4; opposite: 3; author: "jimin" } printPerimeter(myTriangle);
이렇게 뜬금없이 저자에 대한 속성이 들어간 삼각형이 매개변수로 주어진다고 가정해보자. 그럼 구조적으로 author를 제외한 모든 속성이 포함되어 이는 RightAngledTriangle로 인식되기 때문이다.
값 공간과 타입 공간
type Developer = { isWorking: true }; const Developer = { isTyping: true }; console.log(Developer); // { isTyping: true }
위에서 이미 선언된 Developer라는 이름으로 const 변수를 생성했다.
값에 대한 공간과 타입에 대한 공간의 이름이 서로 충돌하지 않아 타입과 변수를 같은 이름으로 정의해도 문제가 되지 않는다.
타입스크립트 문법으로 선언한 Developer는 자바스크립트로 변환될 때 제거된다.
class Person { name: string; age: number; constructor(name, age) { this.name = name; this.age = age; } } const person: Person = new Person("jimin", 26);
하지만 class는 타입으로 사용될 수도 있으며, 값으로도 동시에 존재할 수 있다.
person 변수를 선언할 때 앞에서 타입 애너테이션 방식으로 타입을 선언하는 데 사용했다.
하지만 뒤의 Person은 Person 객체를 생성하는 생성자 함수로 동작했다.
enum WeekDays { MON = "Mon", TUES = "Tues", WEDNES = "Wednes", THURS = "Thurs", FRI = "Fri", } type WeekDaysKeys = keyof typeof WeekDays; function printDay(key: WeekDaysKeys, message: string) { const day = WeekDays[key]; if (day <= WeekDays.WEDNES) { console.log(`It's still ${day}day, ${message}`); } } printDay("TUES", "wanna go home"); enum FontSize { sm = 8, md = 16, lg = 24, } type FontSizeKeys = keyof typeof FontSize; function fontPicker(sizes: { sm: number; md: number; lg: number; }, size: Partial<FontSizeKeys>) { return sizes[size]; } fontPicker(FontSize, "md");
복잡하긴 하지만, 이 코드에선 enum이 타입으로 동작하기도 하며, 객체로 동작하기도 한다.
이처럼 타입스크립트에서 어떠한 심볼이 값으로 사용되면 컴파일러로 변환된 자바스크립트 코드에도 정보가 남게 된다. 하지만 타입으로만 사용된 요소는 컴파일 이후 자바스크립트 파일에서 사라지게 된다.
이렇게 사라지는 것을 트리 쉐이킹이라고도 하며, enum은 트리 쉐이킹이 되지 않는다.. const enum을 쓰면 뭐 사라진다고는 하는데, 사용을 지양하는 편이라고 하는 것 같다.

S3 + Route53 + CloudFront로 프로젝트 배포하기.
