[우아한타입리액트] 2장. 타입(1)

Lina Hongbi Ko·2024년 11월 18일
0
post-thumbnail

2. 타입(1)

💡 타입이란

📍 자료형으로서의 타입

  • 모든 프로그래밍 언어는 변수를 선언하는 것부터 시작함
  • 프로그래밍 언어에서 변수란 값을 저장할 수 있는 공간(컴퓨터의 메모리)이자 값을 가리키는 상징적인 이름
  • 개발자는 변수를 선언하고, 그 변수에 특정한 값인 데이터를 할당함.
var name = 'zig';
var year = 2022;

위 예시에서는 name과 year라는 이름으로 선언한 변수에 각각 'zig'와 2022 라는 값을 할당하고 있다. 컴퓨터의 메모리 공간은 한정적이어서 특정 메모리에 값을 효율적으로 저장하기 위해서는 먼저 해당 메모리 공간을 차지할 값의 크기를 알아야 한다. 값의 크기를 명시한다면 컴퓨터가 값을 참조할 때 한번에 읽을 메모리의 크기를 알 수 있어 값을 훼손하지 않고 가져올 수 있다.

예를 들어 메모리에 숫자 타입 값이 할당되어 있다면 자바스크립트 엔진은 이 값을 숫자로 인식해 8바이트 단위로 메모리 공간에 저장된 값을 읽어올 것이다.

변수에 저장할 수 있는 값의 종류는 프로그래밍 언어마다 다르다. 최신 ECMAScript 표준을 따르는 자바스크립트는 7가지 데이터 타입(자료형)을 정의한다.

  1. undefined
  2. null
  3. Boolean (불리언)
  4. String (문자열)
  5. Symbol (심볼)
  6. Numeric (Number와 BigInt)
  7. Object
  • 데이터 타입은 여러 종류의 데이터를 식별하는 분류 체계 -> 컴파일러에 값의 형태를 알려줌
  • 메모리에 저장된 값을 데이터 타입으로 설명할 수 있고, 모든 데이터를 해석할 때 데이터 타입 체계가 사용됨

메모리의 관점에서의 데이터 타입은 프로그래밍 언어에서 일반적으로 타입으로 부르는 개념과 같다. 개발자는 타입을 사용해서 값의 종류를 명시할 수 있고, 메모리를 더욱 효율적으로 사용할 수 있다.

📍 집합으로서의 타입

  • 프로그래밍에서의 타입은 수학의 집합과 유사함
  • 타입은 값이 가질 수 있는 유효한 범위의 집합임
const num: number = 123;
const str: string = "abc";

function func(n: number) {
	// ...
}

func(num);
func(str); 
// Argument of type 'string' is 
not assignable to parameter of type 'number'
  • 어떤 값이 T타입이라면 컴파일러(또는 개발자)는 이 값으로 어떤 일을 할 수 있고, 어떤 일을 할 수 없는지 사전에 알 수 있음

  • 타입 시스템은 코드에서 사용되는 유효한 값의 범위를 제한해서 런타임에서 발생할 수 있는 유효하지 않은 값에 대한 에러를 방지해줌

위 예시에서는 func() 함수의 인자로 number 타입 값만 할당할 수 있도록 제한되어 있다. 따라서 number의 집합에 속하지 않은 string 타입의 str을 func() 함수의 인자로 사용하면 에러가 발생한다. 마치 집합의 경계처럼 func() 함수의 인자로 들어갈 수 있는 값을 number 타입의 집합으로 제한하는 것이다.

함수 인자에 들어갈 값의 타입을 정의하지 않은 경우를 보자. 아래와 같이 인자로 받은 값에 2를 곱해 반환하는 함수가 있으면

function dobule(n) {
	return n * 2;
}

double(2); // 4
double("z"); // NaN

dobule()의 내부 동작을 살펴보면 숫자를 인자로 받을 거라고 기대한다는 것을 알 수 있다. 만약 인자로 숫자가 아닌 다른 타입의 값을 전달하면 의도치 않는 작업을 수행해서 원하는 값을 얻지 못한다. 하지만 함수의 매개변수 타입을 명시한다면 올바르지 않은 타입의 값으로 함수를 호출했을때 타입스크립트 컴파일러는 곧바로 에러를 발생시킨다.

function double(n: number) {
	return n * 2;
}

double(2); // 4
double("z"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'

일단 타입을 제한하면 타입스크립트 컴파일러는 함수를 호출할 때 호환되는 인자로 호출했는지를 판단한다.
*헷갈리지말자 : 인수 -> argument / 인자(값) -> parameter

2의 타입은 number이므로 double() 의 매개변수 타입인 number에 할당 할 수 있고, 타입스크립트 컴파일러는 코드를 문제 없이 컴파일 한다. 하지만 string 타입인 'z'는 number에 할당할 수 없기 때문에 에러가 발생한다.

📍 정적 타입과 동적 타입

  • 자바스크립트만 사용했다면 변수와 값을 다룰때 타입은 고려하지 않고, 코드를 작성했을 수 있음 하지만 자바스크립트에도 분명히 타입이 존재함

  • 다만 개발자가 컴파일 이전에 타입을 직접 정의해줄 필요가 없었을 뿐!

  • 타입을 결정하는 시점에 따라 타입을 정적 타입(static type)과 동적 타입(dynamic type)으로 분류할 수 있음

  • 정적 타입 시스템 : 모든 변수의 타입이 컴파일 타임에 결정됨

  • 코드 수준에서 개발자가 타입을 명시해줘야하는 C, 자바, 타입스크립트 등이 정적 타입 언어

  • 번거롭게 느껴질 수 있지만 컴파일타임에 타입 에러를 발견할 수 있어서 프로그램의 안정성을 보장할 수 있음

  • 동적 타입 시스템 : 변수 타입이 런타임에서 결정됨

  • 파이썬, 자바스크립트가 대표적인 동적 타입 언어로 개발자는 직접 타입을 정의해줄 필요 없음

  • 프로그램을 실행할 때, 타입 에러가 발견되기 때문에 개발 과정에서 에러 없이 마음껏 코드를 작성할 수 있지만 언제 프로그램에 오류가 생길지 모르는 불안감이 있음

런타임에서 타입을 예측할 수 없다면 매우 위험할 수 있다. 예를 들면, multiplyByThree() 함수 인자로 들어올 값의 타입을 number라고 가정하고 연산을 수행할 때 실제로 런타임에 입력되는 변수가 string 타입 값을 갖고 있다면 예상치 못한 결과를 반환해 오류가 발생할 수 있다.

function multiplyByThree(number) {
	return number * 3;
}

multiplyByThree(10); // 30
multiplyByThree("f"); // NaN

*컴파일타임과 런타임 : 기계(컴퓨터, 엔진)가 소스코드를 이해할 수 있도록 기계어로 변환되는 시점 -> 컴파일타임 / 이후 변환된 파일이 메모리에 적재되어 실행되는 시점 -> 런타임

📍 강타입과 약타입

  • 타입이 결정되는 시점은 다르지만 모든 프로그래밍 언어에는 값의 타입이 존재함

  • 자바스크립트는 개발자가 반드시 타입을 명시해줄 필요가 없는 언어이지만, 나름대로 타입을 이해하려고 노력하고 있음 -> 개발자가 의도적으로 타입을 명시하거나 바꾸지 않았는데도 컴파일러 또는 엔진 등에 의해 런타임에 자동으로 변경함(암묵적 타입 변환)

  • 암묵적 타입 변환 여부에 따라 타입 시스템을 강타입과 약타입으로 분류할 수 있음

  • 강타입 특징을 가진 언어 : 서로 다른 타입을 갖는 값끼리 연산을 시도하면 컴파일러 또는 인터프리터에서 에러 발생

  • 약타입 특징을 가진 언어 : 서로 다른 타입을 갖는 값끼리 연산할 때는 컴파일러 또는 인터프리터가 내부적으로 판단해서 특정 값의 타입을 변환하여 연산을 수행하고 값을 도출

빼기(-) 연산자가 여러 언어에서 어떻게 동작하는지 살펴보면, (더하기 연산자는 자바스크립트의 더하기 연산자가 문자열, 숫자 타입 모두 사용할 수 있고, 타입스크립트 역시 문자열과 숫자 타입 간의 더하기 연산을 하는 점진적 타입을 사용하기 때문에 분류예시로 적절하지 않다고함)

  • C++
#include <iostream>
int main() {
	std::count << '2' - 1; // '2'는 아스키 값으로 50
}
> make -s
>
/main 49
  • 자바
class Main {
	public static void main(String[] args) {
    	System.out.printIn('2', - 1);
    }
}
> sh - c javac -classpath .:target/dependency/* -d . $(find . -type f -name '*.java')
>		 java -classpath .:target/dependency/*
Main 49
  • 파이썬
print('2' - 1);
Traceback (most recent call last): File
	"main.py", line 1, in <module>
    	print('2' - 1)
    TypeError : unsupported operand type(s) for -: 'str' and 'int'
  • 루비
puts "2" - 1
>		bundle exe ruby main.rb
Traceback (most recent call last):
main.rb:1:in `<main>': undefined method `-' for "2":String (NoMethodError)
Did you mean? -@
exit status 1
  • 자바스크립트
console.log("2" - 1);
// 1
  • 타입스크립트
console.log("2" - 1); // '2' error
// type error
THhe left-hand side of an arithmetic operation mus tbe of type 'any', 'number', 'bigint' or an enum type.

C++, 자바, 자바스크립트에서는 서로 다른 타입을 갖는 값(문자열/숫자)으로 빼기 연산을 수행하면 정상적으로 동작한다. C++와 자바에서는 내부적으로 문자열 타입 값을 숫자 타입인 아스키 값으로 변경해 연산한다.

자바스크립트에서는 문자열로 표기된 숫자를 실제 숫자값으로 변환해 빼기 연산한다.

파이썬, 루비, 타입스크립트에서는 컴파일러 혹은 인터프리터에 타입 에러가 발생한다.

결론적으로,

  • C++, 자바, 자바스크립트는 약타입 언어이고,
  • 파이썬, 루비, 타입스크립트는 강타입 언어로 분류 할 수 있다.

암묵적 변환은 개발자가 명시적으로 타입을 변환하지 않아도 다른 데이터 타입끼리 연산을 진행할 수 있는 편리함 제공하지만, 작성자의 의도와 다르게 동작할 수 있기 때문에 예기치 못한 오류가 발생할 가능성도 높아짐

예를 들어, 자바스크립트는 타입이 명백하게 잘못 작성된 코드도 암묵적 타입 변환을 통해 어떻게든 결과를 도출한다.

const a = 3 + []; // "3"
const b = null + 12; // 12

let obj= {};
obj.foo; // undefined

function foo(num) {
	return num / 2;
}
foo("bar"); // NaN

자바스크립트 코드가 오류를 내뱉지 않고 어떤 값이든 반환하는 것이 항상 바람직하진 않다. 예를 들어, '2' -1 의 연산 결과가 1인 것은 어느 정도 납득할 수 있지만 3 + [] 예시에서 숫자 타입 값과 배열 간의 연산 결과가 문자열 타입의 값임을 확인할 수 있는데 이것은 쉽게 납득할 수 있는 연산 결과가 아니다. 오히려 이럴 때는 컴파일 타임 혹은 런타임에서 타입 에러가 발생하는게 안전할 수 있다.

  • 자바스크립트는 약타입 언어임 -> 런타임에 발생할 수 있는 에러를 예측하고 방지하는 코드를 작성하는 것이 프로그램을 안전하게 만드는데 도움이 됨(타입 안정성)

  • 타입을 사용해 프로그램이 유효하지 않은 작업을 수행하지 않도록 방지

  • 타입을 명시해서 코드를 작성한 후에는 프로그램 내에 기술된 개발자의 의도가 논리적으로 합당하지 검사하는 기준이 필요함

  • 타입 시스템 : 타입 검사가 프로그램에 타입을 할당하는 데 사용하는 규칙 집합

  • 타입 시스템은 크게 2가지로 구분함

    • 어떤 타입을 사용하는지를 컴파일러에 명시적으로 알려줘야 하는 타입 시스템
    • 자동으로 타입을 추론하는 타입 시스템
  • 타입스크립트는 두 가지 타입 시스템의 영향을 모두 받았음

  • 개발자는 직접 타입을 명시하거나, 타입스크립트가 타입을 추론하도록 하는 방식 중에서 선택할 수 있음

📍 컴파일 방식

  • 컴파일의 일반적인 의미 : 사람이 이해할 수 있는 방식으로 작성한 코드를 컴퓨터가 이해할 수 있는 기계어로 바꿔주는 과정

  • 개발자가 자바, C# 등의 고수준 언어로 소스코드를 작성하면, 컴파일러는 컴퓨터가 이해할 수 있게 바이너리 코드로 변환함

  • 바이너리는 0과 1로 이루어진 이진 코드

  • 언어마다 컴파일 과정과 단계가 조금씩 차이가 있지만 기본적으로 컴파일은 서로 다른 수준(고수준->저수준) 간의 코드 변환을 의미

  • 타입스크립트의 컴파일 결과물은 여전히 사람이 이해할 수 있는 방식인 자바스크립트 파일임

  • 타입스크립트가 탄생한 이유는 사람이 이해하기 쉬운 방식으로 코드를 작성하기 위해서가 아니라 자바스크립트의 컴파일타임에 런타임 에러를 사전에 잡아내기 위한것!

  • 타입스크립트를 컴파일하면 타입이 모두 제거된 자바스크립트 소스코드만이 남게 됨

  • 타입스크립트는 다른 타입의 개념을 사용하는 언어와는 구별되는 특징이 있음

  • 자바는 그 자체로 언어지만, 타입스크립트를 자바스크립트에 타입이라는 레이어를 끼얹은 일종의 템플릿 언어 또는 확장언어로 해석하는 의견도 있음

💡 타입스크립트의 타입 시스템

📍 타입 애너테이션 방식

  • 타입 애너테이션(type annotation)이란 변수나 상수 혹은 함수의 인자와 반환 값에 타입을 명시적으로 선언해서 어떤 타입 값이 저장될 것인지를 컴파일러에게 직접 알려주는 문법

  • 언어마다 타입을 명시해주는 방법은 다름

  • 자바와 C#에서는 변수 앞에 데이터 타입을 작성

int woowahanNum = 2010; // Integer (whole number)
float woowahanFloatNum = 2.91f; // Floating point number
char woowahanLetter = 'B'; // Character
boolean woowahanBool = true; // Boolean
String woowahanText = 'WoowaBros'; // String

자바에서는 변수에 데이터 타입을 명시하지 않는다면 에러가 발생함

woowahanText = "WoowaBros";
// error : cannot find symbol woowahanText

cannot find symbol 이라는 식별자를 찾지 못했다는 에러 메시지를 볼 수 있다. 자바에서는 항상 변수 이름보다 데이터 타입을 우선 명시해줘야 한다.
개발자는 woowahanText를 변수 이름으로 사용했지만 자바 컴파일러는 이것을 타입 구문으로 여겨서 에러가 발생한다. 타입스크립트의 타입 선언 방식은 조금 다르다.

변수 이름 뒤에 : type 구문을 붙여 데이터 타입을 명시해준다.

let isDone: boolean = false;
let decimal: number = 6;
let color: string = "blue";
let list: number[] = [1, 2, 3];
let x: [string, number]; // tuple
  • 타입스크립트는 기존 자바스크립트 코드에 점진적으로 타입을 적용할 수 있는 특징을 가짐
  • 위 예시의 : type 선언부를 제거해도 코드가 정상적으로 동작함 but 타입을 제거하면 타입스크립트 타입 시스템이 타입 추론을 하는 과정에서 어려움을 겪음

📍 구조적 타이핑

  • 타입을 사용하는 여러 프로그래밍 언어에서 값이나 객체는 하나의 구체적인 타입을 가지고 있음
  • 타입은 이름으로 구분되며 컴파일타임 이후에도 남아 있음
  • 이것을 명목적으로 구체화한 타입 시스템(Nominal Reified Type Systems)이라고 부르기도 함
class Animal {
	String name;
    int age;
}
  • 서로 다른 클래스끼리 명확한 상속 관계나 공통으로 가지고 있는 인터페이스가 없다면 타입은 서로 호환되지 않음
interface Developer {
	faceValue: number;
}

interface BankNote {
	faceValue: number;
}

let developer: Developer = { faceValue: 52 };
let bankNote: BankNote = { faceValue: 10000 };

developer = bankNote; // ok
bankNote = developer; // ok
  • 타입스크립트에서 타입을 구분하는 방식은 조금 다름 -> 이름으로 타입을 구분하는 명목적 타입 언어의 특징과 달리 타입스크립트는 구조로 타입을 구분(구조적 타이핑 Structural type system)

📍 구조적 서브타이핑

  • 타입스크립트의 타입 시스템을 집합으로 이해할 수 있다고 언급했음
  • 타입스크립트의 타입은 값의 집합으로 생각할 수 있음
  • 타입은 단지 집합에 포함되는 값이고, 특정 값은 많은 집합에 포함될 수 있음
  • 따라서 타입스크립트에서는 특정 값이 string 또는 number 타입을 동시에 가질 수 있음
type stringOrNumber = string | number;
  • 집합으로 나타낼 수 있는 타입스크립트의 타입 시스템을 지탱하고 있는 개념이 바로 구조적 서브타이핑임
  • 구조적 서브타이핑 : 객체가 가지고 있는 속성(프로퍼티)을 바탕으로 타입을 구분하는 것, 이름이 다른 객체라도 가진 속성이 동일하다면 타입스크립트는 서로 호환이 가능한 동일한 타입으로 여김
interface Pet {
	name: string;
}

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

let pet: Pet;
let cat: Cat = { name:"Zag", age: 2};

pet = cat; // ok

Cat은 Pet과 다른 타입으로 선언되었지만 Pet이 갖고 있는 name이라는 속성을 가지고 있다. 따라서 Cat 타입으로 선언한 cat을 Pet 타입으로 선언한 pet에 할당할 수 있다.

구조적 서브 타이핑은 함수의 매개변수에도 적용된다.

interface Pet {
	name: string;
}
let cat = { name: "Zag", age: 2 };

function greet(pet: Pet) {
	console.log("Hello, " + pet.name);
}

greet(cat); // ok

greet() 함수의 매개변수에 들어갈 수 있는 값은 Pet 타입으로 제한되어 있다. 그러나 타입을 명시하지 않은 cat 객체를 greet() 함수의 인자로 전달해도 코드는 정상적으로 실행된다. cat 객체에는 Pet 인터페이스가 가지고 있는 name 속성을 가지고 있어 pet.name의 방식으로 name 속성에 접근할 수 있기 때문이다.

위와 같은 타이핑 방식이 구조적 타이핑 방식이다. 이 절의 제목인 구조적 서브타이핑에서도 알 수 있듯 타입스크립트의 서브타이핑, 즉 타입의 상속 역시 구조적 타이핑을 기반으로 하고 있다.

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 developer = new Developer("zig," 20, 7);

greet(developer); // Hello, I'm zig

Developer 클래스가 Person 클래스를 상속받지 않았는데도 greet(developer)는 정상적으로 동작한다. Developer는 Person이 갖고 있는 속성을 갖고 있기 때문이다.

  • 서로 다른 두 타입 간의 호환성은 오로지 타입 내부의 구조에 의해 결정됨
  • 타입 A가 타입 B의 서브타입이라면 A 타입의 인스턴스는 B 타입이 필요한 곳에 언제든지 위치할 수 있음 -> 타입이 계층 구조로부터 자유로움!

📍 자바스크립트를 닮은 타입스크립트

  • 타입스크립트의 타입 시스템은 구조적 서브타이핑을 사용
  • 명목적 타이핑과는 대조적인 파이핑 방식임
  • 명목적 타이핑 : 타입의 구조가 아닌 타입의 이름만을 가지고 구별 (C++, 자바 등에서 사용)
  • 명목적 타이핑에서 두 변수는 같은 이름의 데이터타입으로 선언된 경우에만 서로 호환됨
class Cat {
	String name;
    	public void hit() {}
}

class Arrow {
	String name;
    public void hit() {}
}

public class Main {
	public static void main(String[] args) {
    	// error : incompatible types: Cat cannot be converted to Arrow
        Arrow cat = new Cat();
        // error : ncompatible types: Arrow cannot be converted to Cat
        Cat arrow = new Arrow();
    }
}

Cat과 Arrow 클래스는 String 타입의 name 변수와 hit() 메서드를 가지고 있다는 점에서 구조적으로 동일하지만, 각 클래스로 생성한 인스턴스는 서로 호환되지 않는다.

  • 명목적 타이핑을 채택한 언어에서는 이름으로 타입을 구분하기 때문에 구조가 같더라도 이름이 다르다면 다른 타입으로 취급

  • 명목적 타이핑은 타입의 동일성을 확인하는 과정에서 구조적 타이핑에 비해 조금 더 안전 -> 개발자가 의도한 타입이 아니라면 변수에 타입을 명시하는 과정에서 에러를 내뱉음 -> 객체의 속성을 다른 객체의 속성과 호환되지 않도록 해 안전성을 추구함

  • 타입스크립트가 구조적 타이핑을 채택한 이유는 타입스크립트가 자바스크립트를 모델링한 언어이기 때문

  • 자바스크립트는 본질적으로 덕 타이핑(duck typing)을 기반으로 함

  • 덕 타이핑 : 어떤 함수의 매개변숫값이 올바르게 주어진다면 그 값이 어떻게 만들어졌는지 신경 쓰지 않고 사용한다는 개념
    *덕타이핑: 어떤 타입에 부합하는 변수와 메서드를 가질 경우 해당 타입에 속하는 것으로 간주하는 방식. 만약 어떤 새가 오리처럼 걷고, 헤엄치며 꽥꽥 거리는 소리를 낸다면 그 새를 오리라고 부르는 것

  • 타입스크립트는 이런 동작을 그대로 모델링 -> 자바스크립트의 특징을 그대로 받아들여 명시적인 이름을 가지고 타입을 구분하는 대신, 객체나 함수가 가진 구조적 특징을 기반으로 타이핑 하는 방식을 택함

  • 구조적 타이핑 덕분에 타입스크립트는 더욱 유연한 타이핑 가능해짐 -> 쉬운 사용성과 안전성이라는 두 가지 목표 사이의 균형을 중시하는 타입스크립트에서는 객체 간 속성이 동일하다면 서로 호환되는 구조적 타입 시스템을 제공해 더욱 편리성을 높임

  • 자바스크립트의 덕 타이핑과 타입스크립트의 구조적 타이핑은 서로 구분되는 타이핑 방식이지만, 실제 사용하는 코드를 보면 차이가 없어 보임

  • 두 가지 타이핑 방식 모두 이름으로 타입을 구분하는 명목적 타이핑과는 달리 객체가 가진 속성을 기반으로 타입을 검사

  • 덕 타이핑과 구조적 타이핑의 차이 : 타입을 검사하는 시점에 있음

  • 덕 타이핑은 런타임에 타입을 검사(동적 타이핑에서 사용). 자바스크립트는 덕 타이핑 언어

  • 구조적 타이핑은 컴파일타임에 타입체커가 타입을 검사(정적 타이핑에서 사용). 타입스크립트는 구조적 타이핑 언어

  • 덕타이핑과 구조적 타이핑 모두 객체 변수, 메서드 같은 필드를 기반으로 타입을 검사한다는 점에서 동일하지만, 타입을 검사하는 시점 다름

📍 구조적 타이핑의 결과

  • 타입스크립트 구조적 타이핑의 특징 때문에 예기치 못한 결과가 나올때도 있음
interface Cube {
	width: number;
    height: number;
    depth: number;
}

function addLines(c: Cube) {
	let total = 0;
    
    for(const axis of Object.keys(c)) {
    	// Element implicitly has an 'any' type
        // because expression of type 'string' can't be used to index type 'Cube'
        // No index signatrue with a parameter of type 'string'
        // was found on type 'Cube'
        const length = c[aixs];
        
        total += length;
    }
}

addLines() 함수의 매개변수인 c는 Cube 타입으로 선언되었고, Cube 인터페이스의 모든 필드는 number 타입을 가지기 때문에 c[axis]는 당연히 number 타입일 것이라고 예측할 수 있다. 그러나 c에 들어올 객체는 Cube의 width, height, depth 외에도 어떤 속성이든 가질 수 있기 때문에 c[axis]의 타입이 string일 수도 있어 에러가 발생한다.

아래와 같은 상황이다.

const namedCube = {
	width: 6,
    height: 5,
    depth: 4,
    name: "SweetCube", // string 타입의 추가 속성이 정의되었음
};

addLines(namedCube); // ok

타입스크립트는 c[axis]가 어떤 속성을 지닐지 알 수 없고, c[axis] 타입을 number라고 확정할 수 없어서 에러를 발생시킨다. 타입스크립트 구조적 타이핑의 특징으로 Cube 타입 값이 들어갈 곳에 name 같은 추가 속성을 가진 객체도 할당할 수 있기 때문에 발생하는 문제다.

이러한 한계를 극복하고자 타입스크립트에 명목적 타이핑 언어의 특징을 가미한 식별할 수 있는 유니온(Discriminated Unions) 같은 방법이 생겼다.

📍 타입스크립트의 점진적 타입 확인

  • 타입스크립트는 점진적으로 타입을 확인하는 언어
  • 점진적 타입 검사란 컴파일 타임에 타입을 검사하면서 필요에 따라 타입 선언 생략을 허용하는 방식임
  • 타입을 지정한 변수와 표현식은 정적으로 타입을 검사하지만 타입 선언이 생략되면 동적으로 검사를 수행, 타인 선언을 생략하면 암시적 타입 변환이 일어남
function add(x, y) {
	return x + y;
}

// 위 코드는 아래와 같이 암시적 타입 변환이 일어남
function add(x: any, y: any): any;

add() 함수의 매개변수 x와 y에 타입을 선언하지 않았지만 타입스크립트 컴파일러는 x, y가 잘못된 것이라고 여기지 않는다. 다만 타입을 명시하지 않았기 때문에 타입스크립트 컴파일러는 add() 함수의 인자 x, y와 함수의 반호나 값을 모두 any 타입으로 추론한다.

  • 타입스크립트에서는 필요에 따라 타입을 생략할 수도 있고, 타입을 점진적으로 추가할 수도 있음

  • 타입스크립트에서 프로그램을 컴파일하는 데 반드시 모든 타입을 알아야 하는 것은 아님 but 타입스크립트는 컴파일타임에 프로그램의 모든 타입을 알고 있을 때 최상의 결과를 보여줌

  • 타입스크립트는 자바스크립트의 슈퍼셋 언어이기 때문에 모든 자바스크립트 코드는 타입스크립트 코드라고 봐도 무방

  • .ts 파일에 자바스크립트 문법으로 소스코드를 작성하더라도 문제 발생하지 않음

  • 특히 타입을 지정하지 않은 자바스크립트 코드를 타입스크립트로 마이그레이션할 때 타입스크립트의 점진적 타이핑이라는 특징을 유용하게 활용할 수 있음

  • 타입스크립트의 타입 시스템은 정적 타입의 정확성을 100% 보장해주진 않음

  • 모든 변수와 표현식의 타입을 컴파일타임에 검사하지 않아도 되기 때문에 타입이 올바르게 정해지지 않으면 런타임에서 에러가 발생하기도 함

const names = ["zig", "colin"];
console.log(names[2].toUpperCase());
// TypeError: Cannot read property 'toUpperCase' of undefined

*any 타입: 타입스크립트에서 any 타입은 타입스크립트 내 모든 타입의 종류를 포함하는 가장 상위 타입으로 어떤 타입 값이든 할당할 수 있다. 단, 타입스크립트 컴파일 옵션인 noImplicitAny 값이 true일 때는 에러가 발생한다. noImplicitAny는 타입 애너테이션이 없을 때 변수가 any 타입으로 추론되는 것을 허락하지 않는다. 타입스크립트로 코드를 작성할 때는 정확한 타이핑을 위해 tsconfig의 noImplicitAny 옵션을 true로 설정하는 게 좋다. noImplicitAny 옵션이 켜져 있다면 앞의 예시에서는 다음과 같은 에러를 발생시킴 -> Parameter 'x' implicitly has an 'any' type. Parameter 'y' implicitly has an 'any' type.

📍 자바스크립트 슈퍼셋으로서의 타입스크립트

  • 타입스크립트는 기존 자바스크립트 코드에 정적인 타이핑을 추가한 것으로 자바스크립트의 상위 집합
  • 타입스크립트 문법은 모든 자바스크립트 문법을 포함하고 있음
  • 선택적으로 타이핑을 도입할 수 있는 특징 때문에 타입스크립트는 자바스크립트가 가지고 있는 여러 문제를 그대로 가지고 있지만 진입장벽 낮아짐
  • 모든 자바스크립트 코드는 타입스크립트라고 볼 수 있지만 반대로 모든 타입스크립트 코드가 자바스크립트 코드인 것은 아님(타입스크립트는 타입을 명시하는 문법을 가지고 있기 때문)

다음의 코드는 타입스크립트에서 유효함

function greet(name: string) {
	console.log("Hello", name);
}

그러나 자바스크립트 런타임에서는 오류 발생

// SyntaxError : Unexpected token

: stirng은 타입스크립트에서 쓰이는 타입 구문이다. 타입 구문을 사용하는 순간부터 자바스크립트는 타입스크립트의 영역으로 들어가게 된다.

또한 타입스크립트 컴파일러는 타입스크립트 뿐만 아니라 일반 자바스크립트 프로그램에서도 유용하게 사용할 수 있다.

let developer = "Colin";
conosle.log(developer.toUppercase());

이 코드를 타입스크립트 컴파일러로 실행하면 아래와 같은 오류가 발생한다

// Property 'toUpperCase' does not exist on type 'string'.
// Did you mean 'toUpperCase'?

developer 변수가 문자열 이라는 것을 알려주지 않아도 타입스크립트는 초깃값(여기서는 Colin)으로 타입을 추론해서 toUppercase 대신 toUpperCase 메서드로 대체할 것을 제안한다.

만약, 같은 코드를 자바스크립트 런타임에서만 실행하면 다음과 같은 에러를 던져줄 뿐이다.

// developer.toUppercase is not a function```
코드를 입력하세요

📍 값 vs 타입

  • 값은 프로그램이 처리하기 위해 메모리에 저장하는 모든 데이터
  • 값은 프로그램에서 조작하고 다룰 수 있는 어떤 표현이며, 다양한 형태의 데이터를 포함함
  • 수학적 개념에서의 값으로 여기는 1, 2, 3과 같은 데이터는 물론이고 1 + 2 같은 식이 반환하는 결괏값 3도 값에 해당
  • 프로그래밍 관점에서는 문자열, 숫자, 변수, 매개변수 등이 값에 해당
11; // 숫자 값
"hello typescript"; // 문자열 값
let foo = "bar"; // 변숫값
  • 객체 역시 값, 자바스크립트에서는 함수도 값임
  • 모든 것이 객체인 언어답게 자바스크립트 함수는 런타임에 객체로 변환되기 때문
// 함수
function goWork(developer) {
	console.log(`tired ${developer}`);
}

*일반적으로 evaluate를 '평가한다'는 뜻으로 해석하지만 프로그래밍에서는 값을 구하다 또는 문제를 푼다는 의미로 이해하기도 함

값은 어떠한 식을 연산(evaluate)한 것으로 변수에 할당할 수 있다. 아래 예시에서는 developer 라는 변수에 'zig'라는 문자열 값을 할당했음

const developer = 'zig';

앞서 언급한 대로 함수 역시 값으로 볼 수 있음. 위 예시에서 정의된 함수 goWork를 다른 방식으로 정의하면,

// 함수
const goWork = function (developer) {
	console.log(`tried ${developer}`);
};

함수 역시 변수에 할당할 수 있는 값임을 알 수 있다.

자바스크립트 대신 타입스크립트를 사용하게 되면서 타입이라는 개념이 등장한다. 타입스크립트는 변수, 매개변수, 객체 속성 등에 : type 형태로 타입을 명시한다.

const a: string = "hello";
const b: number = 2022;
const c: boolean = true;
const d: number[] = [1, 2, 3];

또는 type이나 interface 키워드로 커스텀 타입을 정의할 수도 있다.

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

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

값 공간과 타입 공간의 이름은 서로 충돌하지 않기 때문에 타입과 변수를 같은 이름으로 정의 할 수 있는데, 타입스크립트가 자바스크립트의 슈퍼셋인 것과 관련이 있다. 타입스크립트 문법인 type으로 선언한 내용은 자바스크립트 런타임에서 제거되기 때문에 값 공간과 타입 공간은 서로 충돌하지 않는다.

type Developer = { isWorking: true };
const Developer = { isTyping: true }; // ok

type Cat = { name: string; age: number };
const Cat = { slideStuffOffTheTable: true }; // ok
  • 타입스크립트 코드에서 타입과 값을 구분하는 것은 어렵지 않음
  • 타입은 주로 타입 선언(:) 또는 단언문(as)으로 작성하고 값은 할당 연산자인 =으로 작성함

다음 예시 코드에서 변수 developer의 타입은 Developer이며 developer에 할당된 값은 { name: "Zig", isWorking: ture } 라는 사실을 쉽게 알 수 있음

interface Developer {
	name: string;
    isWorking: boolean;
}

const developer: Developer = { name: "Zig", isWorking: true };

함수의 매개변수처럼 여러 개의 심볼이 함께 쓰인다면 타입과 값을 명확하게 구분해야 한다.

function postTIL(author: Developer, date: Date, content: string): Response {
	// ...
}

이 예시에서 author, date, content는 값으로 Developer, Date, string 은 타입으로 사용 되었다.

  • 타입스크립트에서는 값과 타입이 함께사용됨
  • 값과 타입은 타입스크립트에서 별도의 네임스페이스에 존재함
  • 타입스크립트는 개발자가 작성한 코드 문맥을 파악해서 스스로 값 또는 타입으로 해석함
  • 값이 사용되는 위치와 타입이 사용되는 위치가 다르기 때문에, 코드가 어디에서 사용되었는지에 따라 타입인지 값인지를 추론할 수 있다는 뜻
  • 타입스크립트에서 값과 타입의 구분은 맥락에 따라 달라지기 때문에 값 공간과 타입 공간을 혼동할 때도 있음
function email(options: {person: Person; subject: string; body: string}) {
	// ...
}

자바스크립트의 구조 분해 할당을 사용하면 email 함수의 매개변수로 넘기는 options 객체를 아래와 같이 풀어 쓸 수 있음

function email({ person, subject, body }) {
	// ...
}

그러나 같은 코드를 타입스크립트에서 구조 분해 할당하면 오류 발생함

function email({ person:
	Person, // subject:
    string, // body:
    string,
}) {
	// ...
}

*구조 분해 할당(Destructuring assignment) : ES6부터 도입된 자바스크립트의 기능으로 자바스크립트 또는 타입스크립트에서 배열이나 객체의 속성을 개별 변수로 분해하여 그 값을 변수에 할당하는 것을 말한다.

값의 관점에서 Person과 string이 해석되었기 때문에 오류가 발생. 개발자의 의도는 매개변수 객체의 속성인 person을 Person 타입으로, subject와 body를 string 타입으로 설정하여 함수 매개변수에 들어올 수 있는 객체 타입을 제한하는 것이었다. 그러나 위에서는 Person, string이 값 공간에 있는 것으로 해석되고, person과 Person은 각 함수의 매개변수 객체 내부 속성의 키-값 쌍에 해당하는 것으로 해석된다.

이처럼 값-타입 공간을 혼동하는 문제를 해결하기 위해 값과 타입을 구분해서 작성해야 한다.

function email({
person, subject, body
}: {
person: Person;
subject: string;
body: string;
}) {
	// ...
}

타입스크립트에는 앞서 언급한 대로 타입과 값이 혼용되는 것 말고도 값과 타입 공간에 동시에 존재하는 심볼도 있다.

대표적인 것이 클래스와 enum이다.

타입스크립트에서 헷갈리는 것 중 하나가 클래스에 관한 것이다. 자바스크립트 ES6에서 등장한 클래스는 객체 인스턴스를 더욱 쉽게 생성하기 위한 문법 기능으로, 실제 동작은 함수와 같다.

그리고 클래스는 동시에 타입으로도 사용된다. 즉 타입스크립트 코드에서 클래스는 값과 타입 공간 모두에 포함될 수 있다.

class Rectaangle {
	constructor(height, width) {
    	this.height = height;
        this.width = width;
    }
}

const rect1 = new Rectangle(5, 4);
class Developer {
	name: string;
    domain: string;
    
    constructor(name: string, domain: string) {
    	this.name = name;
        this.domain = domain;
    }
}

const me: Developer = new Developer("zig", "frontend");

변수명 me 뒤에 등장하는 : Developer에서 Developer는 타입에 해당하지만 new 키워드 뒤의 Developer는 클래스의 생성자 함수인 값으로 동작한다.

타입스크립트에서 클래스는 타입 애너테이션으로 사용할 수 있지만 런타임에서 객체로 변환되어 자바스크립트의 값으로 사용되는 특징을 가지고 있다.

enum 역시 런타임에 객체로 변환되는 값이다. enum은 런타임에 실제 객체로 존재하며, 함수로 표현할 수도 있다.

enum Direction {
	Up, // 0
    Down, // 1
    Left, // 2
    Right, // 3
}

enum 예시를 자바스크립트 코드로 컴파일하면 아래와 같다.

"use strict"
var Direction;
(function (direction) { 
  Direction[(Direction["Up"])] = 0)] = "Up";
  Direction[(Direction["Down"] = 1)] = "Down";
  Direction[(Direction["Left"] = 2)] = "Left";
  Direction[(Direction["Right"] = 3)] = "Right";
})(Direction || (Direction = {}));
enum WeekDdays {
	MON = "Mon",
    TUES = "Tues",
    WEDNES = "Wednes",
    THURS = "Thurs",
    FRI = "Fri",
}

// 'MON' | 'TUES' | 'WEDNES' | 'THURS' | 'FRI'
type WeekDaysKey = keyof typeof WeekDays;
function printDay(key: WeekDaysKey, message: sring) {
	const day = Weekdays[key];
    if(day <= WeekDays.WEDNES) {
    	console.log(`It's still ${day}day, ${message}`);
    }
}
printDay("TUES", "wanna go home");

여기서는 enum이 타입으로 사용됐다. WeekDdays enum에 keyof typeof 연산자를 사용해서 type WeekDdaysKey를 만들어, printDay() 함수의 key 인자에 넘겨줄 수 있는 값의 타입을 제한한다.

enum도 클래스처럼 타입 공간에서 타입을 제한하는 역할을 하지만 자바스크립트 런타임에서 실제 값으로도 사용될 수 있다.

enum이 값 공간에서 사용되면,

// enum이 값 공간에서 사용된 경우
enum MyColors {
	BLUE = "#0000FF",
    YELLOW = "#FFFF00",
    MINT = "2AC1BC"
}

function whatMintColor(palette: {MINT: string}) {
	return palette.MINT;
}

whatMintColor(MyColors); // ok

위 예시에서 MyCcolors enum은 일반적인 객체처럼 동작한다. whatMintColor() 함수의 인자인 palette는 MINT 라는 속성을 갖는 객체고, MyColors는 string 타입의 MINT 속성을 가지고 있기 때문에 코드가 정상적으로 실행된다.

타입스크립트에서 어떠한 심볼이 값으로 사용된다는 것은 컴파일러를 사용해서 타입스크립트 파일을 자바스크립트 파일로 변환해도 여전히 자바스크립트 파일에 해당 정보가 남아 있음을 의미.

반면 타입으로만 사용되는 요소는 컴파일 이후에 자바스크립트 파일에서 해당 정보가 사라진다.

자바스크립트 문법의 여러 키워드가 타입스크립트에서 값 또는 타입으로 해석된다.

profile
프론트엔드개발자가 되고 싶어서 열심히 땅굴 파는 자

0개의 댓글