[TIL] 2장 타입
Oct 30, 2024 | Dec 23, 2024
| Ryoon.With.Wisdomtrees
2.1 타입이란
- 자료형으로서의 타입
모든 프로그래밍 언어는 변수를 선언하는 것 부터 시작한다. 변수란 값을 저장할 수 있는 공간이자 값을 가리키는 상징적인 이름. 최신 ECMAScript 표준을 따르는 자바스크립트는 아래의 7가지 데이터 타입 자료형을 정의하고 있다.
undefined, null, Boolean, String, Symbol, Numeric(Number,BigInt), Object. 이와 같은 유형을 데이터 타입 또는 자료형이라고 한다.
- 집합으로서의 타입
개발에서의 타입은 수학의 집합과 유사하다. Type타입은 값이 가질 수 있는 유효한 범위의 집합을 말한다.
- 강타입과 약타입
타입이 결정되는 시점은 다르지만 모든 프로그램빙 언어에는 값의 타입이 존재한다. 자바스크립트는 개발자가 반드시 타입을 명시해줄 필요가 없는 언어이지만, 나름대오 타입을 이해하려고 노력하고 있다. 개발자가 의도적으로 타입을 명시하거나 바꾸지 않았는데도 컴파일러 또는 엔진 등에 의해서 런타임에 타입이 자동으로 변경되는 것을 암묵적 변환이라고 한다.
암묵적 타입 변환 여부에 따라 타입 시스템을 강타입과 약타입으로 분류할 수 있다. 강타입 특징을 가진 언어에서는 서로 다른 타입을 갖는 값끼리 연산을 시도하면 컴파일러 또는 인터프리터에서 에러가 발생한다. 이에 반해 약타입 특징을 갖는 언어에서 서로 다른 타입을 갖는 값끼리 연산할 때는 컴파일러 또는 인터프리터가 내부적으로 판단해서 특정값의 타입을 변환하여 연산을 수행한 후 값을 도출한다.
2.2 타입스크립트의 타입 시스템
- 타입 애너테이션 방식
Type Annotation이란 변수나 상수 혹은 함수의 인자와 반환 값에 타입을 명시적으로 선언해서 어떤 타입 값이 저장될 것인지를 컴파일러에 직접 알려주는 문법이다. 언어마다 타입을 명시해주는 방법은 다르다.
타입스크립트는 변수 이름 뒤에 : type 구문을 붙여 데이터 타입을 명시해준다.
위의 코드에서 type 선언부를 제거해도 코드는 정상적으로 동작한다. But, 타입을 제거하면 타입스크립트 타입 시스템이 타입 추론을 하는 과정에서 어려움을 겪을 지도?!
- 구조적 타이핑
타입을 사용하는 여러 프로그래밍 언어에서 값이나 객체는 하나의 구체적인 타입을 가지고 있다. 타입은 이름으로 구분되며 컴파일타임 이후에도 남아있다. 이것을 명목적으로 구체화한 타입 시스템( Nominal Reified Type Systems)이라고 부르기도 한다.
또한 서로 다른 클래스끼리 명확한 상속 관계나 공통으로 가지고 있는 인터페이스가 없다면 타입은 서로 호환되지 않는다.
그러나 타입스크립트에서 타입을 구분하는 방식은 조금 다르다. 이름으로 타입을 구분하는 명목적인 타입 언어의 특징과 달리 타입스크립트는 구조로 타입을 구분한다. 이것을 구조적 타이핑 - Structural type system - 이라고 한다.
- 구조적 서브타이핑
앞서 타입스크립트의 타입 시스템을 집합으로 이해할 수 있다고 언급했었다. 타입스크립트의 타입은 값의 집합 - set of values -으로 생각할 수 있다. 타입은 단지 집합에 포함되는 값이고 특정 값은 많은 집합에 포함될 수 있다. 따라서 타입스크립트에서는 특정 값이 string 또는 number타입을 동시에 가질 수 있다.
이처럼 집합으로 나타낼 수 있는 타입스크립트의 타입 시스템을 지탱하고 있는 개념이 바로 구조적 서브타이핑 - Structural Subtyping -이다.
구조적 서브타이핑이란 객체가 가지고 있는 속성(프로퍼티)을 바탕으로 타입을 구분하는 것이다. 이름이 다른 객체라도 가진 속성이 동일하다면 타입스크립트는 서로 호환이 가능한 동일한 타입으로 여긴다.
Cat은 Pet과 다른 타입으로 선언됐지만 Pet이 갖고 있는 name이라는 속성을 가지고 있다. 따라서 Cat은 타입으로 선언한 cat을 Pet타입으로 선언한 pet에 할당할 수 있다.
구조적 서브타이핑은 함수의 매개변수에도 적용된다.
greet()함수의 매개변수에 들어갈 수 있는 값은 Pet타입으로 제한돼 있다. 그러나 타입을 명시하지 않은 cat 객체를 greet()함수의 인자로 전달해도 코드는 정상적으로 실행된다. cat객체는 Pet 인터페이스가 가지고 있는 name속성을 가지고 있어 pet.name의 방식으로 name 속성에 접근할 수 있기 때문이다.
위와 같은 타이핑 방식이 구조적 타이핑이다. 이 절의 제목인 구조적 서브타이핑에서도 알 수 있듯이 타입스크립트의 서브타이핑, 즉 타입의 상속 역시 구조적 타이핑을 기반으로 하고 있다. 클래스를 사용한 다음 예시를 살펴보자.
Developer 클래스가 Person 클래스를 상속받지 않았는데도 greet(developer)는 정상적으로 동작한다. Developer는 Person이 갖고 있는 속성을 가지고 있기 때문이다.
서로 다은 두 타입 간의 호환성은 오로지 타입 내부의 구조에 의해 결정된다. 타입 A가 타입 B의 서브타입이라면 A타입의 인스턴스는 B 타입이 필요한 곳에 언제든지 언제든지 위치할 수 있다. 즉, 타입이 계층 구조로부터 자유롭다.
- 자바스크립트를 닮은 타입스크립트
타입스크립트의 타입 시스템은 구조적 서브타이핑을 사용한다고 했다. 이것은 명목적 타이핑과는 대조적인 타이핑이다. 명목적 타이핑은 타입의 구조가 아닌 타입의 이름만을 가지고 구별하는 것으로 자바등에서 사용한다.
명목적 타이핑에서 두 변수는 같은 이름의 데이터 타입으로 선언된 경우에만 서로 호환된다. 즉 안의 프로퍼티 속성이 똑같아도
Cat cat = new Person()이렇게는 어림도없다는 소리.
명목적 타이핑이 구조적 타이핑에 비해 조금 더 안전하기는 하다. 타입의 동일성(equibalance)을 확인하는 과정에서. 객체의 속성을 다른 객체의 속성과 호환되지 않도록 하여 안전성을 추구하기 때문이다.
그런데도 … 타입스크립트가 구조적타이핑을 채택한 이유는 타입스크립트가 자바스크립트를 모델링한 언어이기 때문이다. 자바스크립트는 본질적으로 덕 타이핑(duck typing)을 기반으로 한다. 덕 타이핑은 어떤 함수의 매개변숫값이 올바르게 주어진다면 그 값이 어떻게 만들어졌는지 신경 쓰지 않고 사용한다는 개념이다.
타입스크립트는 이런 동작을 그대로 모델링한다. 타입스크립트는 자바스크립트의 특징을 그대로 받아들여 명시적인 이름을 가지고 타입을 구분하는 대신 객체나 함수가 가진 구조적 특징을 기반으로 타이핑하는 방식을 택했다. 구조적 타이핑 덕분에 타입스크립트는 더욱 유연한 타이핑이 가능해졌다. 쉬운 사용성과 안전성이라는 두 가지 목표 사이의 균형을 중시하는 타입스크립트에서는 객체 간 속성이 동일하다면 서로 호환되는 구조적 타입 시스템을 제공하여 더욱 편리성을 높였다.
자바스크립트의 덕 타이핑과 타입스크립트의 구조적 타이핑은 서로 구분되는 타이핑 방식이지만, 실제 사용하는 코드를 보면 차이가 없어 보인다. 두 가지 타이핑 방식 모두 이름으로 타입을 구분하는 명목적 타이핑과는 달리 객체가 가진 속성을 기반으로 타입을 검사하기 때문이다.
덕 타이핑과 구조적 타이핑의 차이는 타입을 검사하는 시점에 있다. 덕 타이핑은 런타임에 타입을 검사한다. 자바스크립트는 덕 타이핑 언어다. 구조적 타이핑은 컴파일타입에 타입체커가 타입을 검사한다. 다시 말하지만 타입스크립트는 구조적 타이핑을 채택하고 있다.
덕 타이핑과 구조적 타이핑 모두 객체 변수, 메서드 같은 필드를 기반으로 타입을 검사한다는 점에서 동일하지만, 타입을 검사하는 시점이 다르다. 덕 타이핑은 주로 동적 타이핑에서 구조적 타이핑은 정적 타이핑에서 사용된다.
- 타입스크립트의 점진적 타입확인
타입스크립트는 점진적으로 타입을 확인하는 gradually typed 언어다. 점진적 타입 검사란 컴파일 타임에 타입을 검사하면서 필요에 따라 타입 선언 생략을 허용하는 방식이다. 타입을 지정한 변수와 표현식은 정적으로 타입을 검사하지만 타입 선언이 생략되면 동적으로 검사를 수행한다. 타입 선언을 생략하면 암시적 타입 변환이 일어난다.
add() 함수의 매개변수 x와 y에 타입을 선언하지 않았지만 타입스크립트 컴파일러는 x, y가 잘못된 것이라고 여기지 않는다. 다만 타입을 명시하지 않았기 때문에 타입스크립트 컴파일러는 add() 함수의 인자 x, y와 함수의 반환 값을 모두 any 타입으로 추론한다.
이처럼 타입스크립트에서는 필요에 따라 타입을 생략할 수도 있고 타입을 점진적으로 추가할 수도 있다. 타입스크립트에서 프로그램을 컴파일하는 데 반드시 모든 타입을 알아야 하는 것은 아니다. 그러나 타입스크립트는 컴파일타임에 프로그램의 모든 타입을 알고 있을 때 최상의 결과를 보여준다.
타입스크립트는 자바스크립트의 슈퍼셋 언어이기 때문에 모든 자바스크립트 코드는 타입스크립트 코드라고 봐도 무방하다. 따라서 .ts 파일에 자바스크립트 문법으로 소스코드를 작성하더라도 문제가 발생하지* 않는다. 특히 타입을 지정하지 않은 자바스크립트 코드를 타입스크립트로 마이그레이션할 때 타입스크립트의 점진적 타이핑이라는 특징을 유용하게 활용할 수 있다.
(* 단 .tsconfig의 설정에 따라 다를 수 있다)
그러나 이러한 특징 때문에 타입스크립트의 타입 시스템은 정적 타입의 정확성을 100% 보장해주지 않는다. 모든 변수와 표현식의 타입을 컴파일타임에 검사하지 않아도 되기 때문에 타입이 올바르게 정해지지 않으면 런타임에서 에러가 발생하기도 한다.
- 자바스크립트 슈퍼셋으로서의 타입스크립트
타입스크립트는 기존 자바스크립트 코드에 정적인 타이핑을 추가한 것으로 자바스크립트의 상위 집합이다. 타입스크립트 문법은 모든 자바스크립트 문법을 포함하고 있다. 선택적으로 타이핑을 도입할 수 있는 특징 때문에 타입스크립트는 자바스크립트가 가지고 있는 여러 문제를 그대로 가지고 있지만 이 점 덕분에 타입스크립트를 도입하는 데 진입장벽이 낮아졌다.
모든 자바스크립트 코드는 타입스크립트라고 볼 수 있지만 반대로 모든 타입스크립트 코드가 자바스크립트 코드인 것은 아니다. 다시 말하지만 타입스크립트는 타입을 명시하는 문법을 가지고 있기 때문이다.
예를 들어 다음 코드는 타입스크립트에서 유효하다.
그러나 자바스크립트 런타임에서는 “SyntaxError: Unexpected token”오류가 발생한다.
:string은 타입스크립트에서 쓰이는 타입 구문이다. 타입 구문을 사용하는 순간부터 자바스크립트는 타입스크립트의 영역으로 들어가게된다.
또한 타입스크립트 컴파일러는 타입스크립트뿐만 아니라 일반 자바스크립트 프로그램에서도 유용하게 사용할 수 있다.
- 값 vs 타입
값value은 프로그램이 처리하기 위해 메모리에 저장하는 모든 데이터이다. 다르게 말하면 프로그램에서 조작하고 다룰 수 있는 어떤 표현이며 다양한 형태의 데이터를 포함한다. 수학적 개념에서 값으로 여기는 1, 2, 3과 같은 데이터는 물론이고 1 + 2 같은 식이 반환하는 결괏값 3도 값에 해당한다. 프로그래밍 관점에서는 문자열, 숫자, 변수, 매개변수 등이 값에 해당한다.
객체 역시 값이다. 그리고 자바스크립트에서는 함수도 값이다. 모든 것이 객체인 언어답게 자바스크립트 함수는 런타임에 객체로 변화되기 때문이다.
값은 어떠한 식을 연산evaluate한 것으로 변수에 할당할 수 있다.
값 공간과 타입 공간의 이름은 서로 충돌하지 않기 때문에 타입과 변수를 같은 이름으로 정의할 수 있는데 타입스크립트가 자바스크립트의 슈퍼셋인 것과 관련이 있다. 타입스크립트 문접인 type으로 선언한 내용은 자바스크립트 런타임에서 제거되기 때문에 값 공간과 타입 공간은 서로 충돌하지 않는다.
타입스크립트 코드에서 타입과 값을 구분하는 것은 어렵지 않다. 타입은 주로 타입 선언(:)또는 단언 문(as)으로 작성하고 값은 할당 연산자인 = 으로 작성한다.
함수의 매개변수처럼 여러 개의 심볼이 함께 쓰인다면 타입과 값을 명확하게 구분해야 한다.
이 예시에서 author, date, content는 값으로, Developer, Date, string은 타입으로 사용되었다.
이처럼 타입스크립트에서는 값과 타입이 함께 사용된다. 값과 타입은 타입스크립트에서 별도의 네임스페이스에 존재한다. 타입스크립트는 개발자가 작성한 코드 문맥을 파악해서 스스로 값 또는 타입으로 해석한다. 값이 사용되는 위치와 타입이 사용되는 위치가 다르기 때문에, 코드가 어디에서 사용되었는지에 따라 타입인지 값인지를 추론할 수 있는 것이다.
타입스크립트에서 값과 타입의 구분은 맥락에 따라 달라지기 때문에 값 공간과 타입 공간을 혼동할 때도 있다.
값-타입 공간을 혼동하는 문제를 해결하기 위해 값과 타입을 구분해서 작성해야 한다.
개발을 하다 보면 때때로 프로그래밍 언어 창시자가 개발자를 일부러 곤경에 빠뜨리려고 언어의 문법을 복잡하게 설계한 게 아닐까 하는 생각이 들 때도 있다. 타입스크립트에는 앞서 언급한 대로 타입과 값이 혼용되는 것 말고도 값과 타입 공간에 동시에 존재하는 심볼도 있다. 대표적인 것이 클래스class와 enum이다.
타입스크립트에서 헷갈리는 것 중 하나가 클래스에 관한 것이다. 자바스크립트 ES6에서 등장한 클래스는 객체 인스턴스를 더욱 쉽게 생성하기 위한 문법기능Syntax sugar으로 실제 동작은 함수와 같다.
동시에 클래스는 타입으로도 사용된다. 즉 타입스크립트 코드에서 클래스는 값과 타입 공간 모두에 포함될 수 있다.
변수명 newSquare뒤에 등장하는 : Rectangle에서 Rectangle은 “타입”에 해당하지만 new 키워드 뒤의 Rectangle는 클래스의 생성자 함수인 “값”으로 동작한다.
타입스크립트에서 클래스는 타입 애너테이션으로 사용할 수 있지만 런타임에서 객체로 변환되어 자바스크립트의 값으로 사용되는 특징을 가지고 있다.
- 타입을 확인하는 방법
타입스크립트에서 typeof, instanceof 그리고 타입 단언을 사용해서 타입을 확인할 수 있다. typeof는 연산하기 전에 피연산자의 데이터 타입을 나타내는 문자열을 반환한다. typeof연산자가 반환하는 값은 자바스크립트의 7가지 기본 데이터 타입(Boolean, null, undefined, Number, BigInt, String, Symbol)과 Function(함수), 호스트 객체 그리고 object객체가 될 수 있다.
타입스크립트에는 값 공간과 타입 공간이 별도로 존재한다. 타입스크립트에서 typeof 연산자도 값에서 쓰일 때와 타입에서 쓰일 때의 역할이 다르다.
값에서 사용된 typeof는 자바스크립트런타임의 typeof 연산자가 된다.
반면 타입에서 사용된 typeof는 값을 읽고 타입스크립트 타입을 반환한다.
person 변수가 interface Person 타입으로 선언되었기 때문에 타입 공간에서의 typeof person은 Person을 반환한다. email 함수는 타입공간에서 typeof 연산자로 값을 읽을 때 함수의 매개변수 타입과 리턴 타입을 포함한 함수 시그니처 타입을 반환한다.
예시의 v1과 v2는 const 키워드로 선언된 변수로 값이 할당될 공간이다. 값 공간의 typeof는 피연산자인 person 과 email의 런타임 타입을 가리키는 문자열을 반환한다. 즉, 값에서 사용된 typeof 연산자는 자바스크립트 typeof 연산자와 동일하게 동작한다.
자바스크립트 클래스는 typeof 연산자를 쓸 때 주의해야 한다. 앞서 선언한 Rectangle 클래스를 다시 보자.
자바스크립트의 클래스는 결국 함수이기 때문에 값 공간에서 typeof Rectangle의 값은 function이 된다. 타입 공간에서 typeof Rectangle의 반환 값은 조금 특이한데 type T에 할당된 Rectangle는 인스턴스의 타입이 아니라 new키워드를 사용할 때 볼 수 있는 생성자 함수이기 때문이다.
Developer 클래스로 생성한 zig 인스턴스는 Developer가 인스턴스 타입으로 생성됐기 때문에 타입 공간에서의 typeof zig 즉, type ZigType은 Developer를 반환한다.
그러나 Developersms Developer 타입의 인스턴스를 만드는 생성자 함수이다. 따라서 typeof Developer 타입도 그 자체인 typeof Developer가 된다. typeof Developer를 풀어서 설명하면 다음과 같다.
자바스크립트에서 instanceof 연산자를 사용하면 프로토타입 체이닝 어딘가에 생성자의 프로토타입 속성이 존재하는지 판단할 수 있다. typeof 연산자처럼 instanceof 연산자의 필터링으로 타입이 보장된 상태에서 안전하게 값의 타입을 정제하여 사용할 수 있다.
타입스크립트에서는 타입 단언이라 부르는 문법을 사용해서 타입을 강제할 수도 있는데 as 키워드를 사용하면 된다. 타입 단언은 개발자가 해당 값의 타입을 더 잘 파악할 수 있을 때 사용되며 강제 형 변환과 유사한 기능을 제공한다.
뒤로