타입스크립트 안전하게 활용하기

Dugi 🎈

  • 테크 인사이트

일반적인 타입 시스템자바스크립트의 타입 시스템, 타입스크립트 활용 전략에 관한 글입니다.

Introduction

타입 시스템

타입 시스템은 전산학에서 다루는 주요 주제 중 하나입니다. Low-level 프로그래밍 언어부터 high-level 프로그래밍 언어까지, 모든 프로그래밍 언어에는 연산(operator, operation)이라는 개념이 존재합니다. 언어는 특정 타입을 가지는 값 간의 연산을 정의합니다. 가장 우리에게 친숙한 연산인 사칙연산으로 예를 들어보겠습니다. C에서 +라는 operator는 intint 두 타입의 값을 input으로, int 타입의 값을 output으로 하도록 정의되어 있습니다.

Plaintext
int a = 1;  
int b = 2;  
int c = a + b; // <- int operator+(int left, int right);  

만약 언어에서 정의하지 않은 방법으로 operator를 사용하려고 하면 에러가 발생합니다.

Plaintext
struct Foo {};

int a = 1;  
struct Foo b;  
int c = a + b; // error: invalid operands to binary + (have ‘int’ and ‘struct Foo’)  

타입과 연산은 프로그래밍 언어가 정의한다는 점이 중요합니다. 언어마다 타입과 연산의 정의가 다르기 때문에 다른 언어에서는 방금의 로직이 잘 작동하기도 합니다. 동일한 코드를 자바스크립트에서 실행해 보겠습니다.

JavaScript

위 코드는 에러를 발생시키지 않고 잘 실행됩니다. 자바스크립트에서 + 연산은 C의 + 연산과 정의가 서로 다르기 때문입니다. 이 경우에는 정수 1과 object {}를 더했는데 결과가 문자열 "1[object Object]"이네요. 다소 상식과 맞진 않지만 괜찮습니다 😅 그것이 자바스크립트이니까요.

타입 시스템의 이로움

타입 시스템이 존재함으로서, 런타임에 발생할 수 있는 에러의 상당수를 예방할 수 있습니다.

C의 포인터를 예로 들겠습니다. 기본적으로 C에서 메모리 dereferencing (*) 연산은 포인터 타입에 대해서만 가능하도록 정의되어 있습니다.

Plaintext
int a = 1;  
int *addr = &a;

int b = *addr; // OK  
int c = *a;    // <- error: invalid type argument of unary ‘*’ (have ‘int’)  

프로그래머는 int와 같이 다른 타입의 값으로부터 메모리 참조가 불가능합니다. 포인터 타입이 아닌 변수들은 부적절한 메모리 주소가 아닌 값을 가지는 것이 대부분이므로, 런타임에 메모리 참조 에러를 발생시키는 것을 예방하고, 컴파일 타임에 이것을 점검하여 부적절한 메모리 참조 연산을 수행하는 프로그램이 작성되는 것을 막을 수 있습니다.

프로그래머가 코딩하는 과정에 도움을 줍니다.

변수의 타입은 컴파일러에게만 알려져 있지 않습니다. 많은 IDE들은 코드를 정적으로 검사하여, 코드의 각 부분에서 변수의 타입을 잡아내어 프로그래머에게 정보를 제공합니다. 변수의 타입 뿐만이 아니라, 타입이 소유한 property, 가능한 연산과 메소드의 인터페이스를 추천하여 실수를 줄이고 코딩을 더 빠르게 할 수 있도록 돕습니다.

과거에 쓰여진 프로그래밍 교재를 보면 다음과 같은 스타일의 코드가 많았습니다.

Java

하지만, 요즘은 위와 같은 코딩 스타일은 좋지 못한 패턴이 되었습니다. 준수한 IDE라면 대다수 언어의 타입 시스템으로부터 정보를 얻어 프로그래머에게 다양한 형태로 힌트를 제공하는 기능을 내장하고 있습니다. IDE의 지원으로부터 변수의 타입을 쉽게 알 수 있기 때문에, 변수에 타입 이름을 포함하지 않고 간결하게 작성할 수 있습니다.

Java

정적, 동적 타입 검사

타입 시스템은 두 가지 방법으로 구현할 수 있습니다.

  • 정적 타입 검사

    (static type checking): 프로그램이 실행되지 않은 상태, 즉 컴파일 타임에 타입 검사를 수행한다.

  • 동적 타입 검사

    (dynamic type checking): 프로그램이 실행 중인 상태, 즉 런타임에 타입 검사를 수행한다.

언어에 따라 둘 중 하나, 또는 둘 모두의 방법을 사용하여 타입 시스템을 구현합니다.

C++, 그 중에서도 casting을 예로 들어 둘의 차이에 대해 간단히 알아보겠습니다.

아래와 같이 AnimalDog라는 클래스를 선언하겠습니다. 당연하게도, DogAnimal을 상속하는 클래스입니다.

Plaintext
class Animal { ... };  
class Dog : public virtual Animal { ... };  

C++의 cast 연산은 정적 타입과 동적 타입을 모두 지원하는 대표적인 예시입니다. 여러 가지 cast 방법이 있는데, 여기에서는 static_castdynamic_cast의 예시를 보겠습니다.

  • static_cast: 정적 타입 검사를 수행합니다.

Plaintext
Animal *animal = static_cast<Animal *>(new Dog()); // - (1)  
Dog *dog = static_cast<Dog *>(animal);             // - (2)  
  • (1): DogAnimal을 상속받은 클래스입니다. new Dog()Dog * 타입의 값인데, 컴파일러는 Dog *Animal * 타입에 대입할 수 있다는 것을 알고 있습니다. 따라서 이 라인은 컴파일 에러를 발생시키지 않습니다.

  • (2): animalAnimal * 타입의 변수입니다. Animal *Dog *에 대입될 수 없습니다. 여기에서 실제로 animal 변수가 Dog를 가리키는 포인터라는 사실은 중요하지 않습니다. 이로 인해 컴파일 에러가 발생합니다.

dynamic_caststatic_cast와 서로 다른 behavior를 가지고 있습니다.

  • dynamic_cast: 동적 타입 검사를 수행합니다.

Plaintext
Animal *animal = new Dog();  
Dog *dog = dynamic_cast<Dog *>(animal); // - (3)  
  • (3): dynamic_cast는 런타임에 타입 검사를 수행합니다. animal은 실제로 Dog을 가리키는 포인터이므로 Dog *로의 dynamic cast는 성공합니다. 따라서 (2)와 달리 에러를 발생시키지 않습니다.

자바스크립트의 타입 시스템

이 문서에서 자바스크립트의 타입 시스템에 대한 자세한 설명을 읽을 수 있습니다.

자바스크립트에는 number, boolean, string과 같은 primitive type과 object 타입만 존재합니다. 하지만 보통 자바스크립트가 사람들에게 우스운 언어로 생각되는, 혹은 미움받는 😭 이유는 어떤 타입이 존재하느냐 보다는, 자바스크립트에서 여러 연산들이 정의된 형태 때문입니다. 글의 시작에서 언급했던 예시를 다시 볼까요?

JavaScript

또 이런 경우도 있습니다.

JavaScript

보통의 상식으로 + 연산은 숫자와 숫자 사이, 또는 문자열과 문자열 사이에만 정의되어야 하는 연산입니다. 그러나, 자바스크립트는 숫자와 문자열, 또는 숫자와 object 간의 + 연산도 정의합니다. 그리고 이러한 정의가 우리의 생각과 일치하지 않아 자바스크립트를 '이상한 언어'라고 생각하게 되는 경우가 많습니다. 포인트 충전 기능을 구현하는데, 위에서 작성한 코드에서의 실수가 발생했다고 생각해 봅시다. 10000포인트를 가지고 있던 사용자가 100포인트를 충전했는데 잔고가 1000만 포인트가 되는, 돈이 복사가 되는 💰 치명적인 버그가 발생했을 것입니다. 다른 프로그래밍 언어라면 숫자와 문자열 간의 + 연산을 정의하지 않았기 때문에 컴파일 에러 또는 런타임 에러가 발생하여 실수를 쉽게 잡아낼 수 있습니다. 하지만 자바스크립트 코드에서 발생한 실수는 실제로 결과를 확인하기 전까지 아무런 증상도 나타내지 않기 때문에 검출하기가 어렵습니다.

타입스크립트

타입스크립트는 자바스크립트에 정적 타입 검사를 할 수 있는 feature를 추가합니다. 또한, 타입스크립트의 타입 시스템은 자바스크립트의 그것과 달리, 보통의 프로그래밍 언어에 조금 더 가깝습니다. (따라서 돈이 복사가 되는 💰 버그는 발생하지 않습니다 😭) 이로 인해 다음과 같은 장점을 누릴 수 있습니다.

  • 자바스크립트의 loose한 타입 시스템에서 발생하는 비상식적인 동작을 예방할 수 있습니다. (즉, 10000 + "100"을 시도하면 컴파일 에러가 발생합니다.)

  • 코드를 실행하기 전 변수의 타입을 알 수 있습니다. Intellisense와 같은 도구는 이 정보를 바탕으로 프로그래머에게 여러 힌트를 제공할 수 있습니다.

  • 또한, 코드를 실행하기 전 null에서 property를 참조하는 코드와 같이 런타임에 에러가 발생하는 부분을 미리 찾아내고 수정할 수 있습니다.

아래는 타입스크립트의 소개 문구에서 발췌한 부분입니다.

TypeScript stands in an unusual relationship to JavaScript. TypeScript offers all of JavaScript’s features, and an additional layer on top of these: TypeScript’s type system.

For example, JavaScript provides language primitives like string and number, but it doesn’t check that you’ve consistently assigned these. TypeScript does.

This means that your existing working JavaScript code is also TypeScript code. The main benefit of TypeScript is that it can highlight unexpected behavior in your code, lowering the chance of bugs.

하지만 타입스크립트의 한계 또한 존재합니다

타입스크립트 코드는 컴파일 시 자바스크립트 코드가 됩니다. 따라서 타입스크립트로 인해 부여된 타입 정보는 컴파일 시까지만 유효합니다. 런타임에는 타입 정보가 아무런 역할을 할 수 없습니다. 이것은 런타임에만 값을 알 수 있는 값은 타입스크립트를 통해 체크가 불가능하다는 것을 의미합니다.

대표적으로

  • Fetch의 response

  • 사용자의 입력

  • JSON.parse의 결과

와 같은 값들은 타입스크립트를 통한 타입 체크가 불가능합니다.

이로 인해 타입스크립트에 의해 제공되는 단단한 타입 시스템에 쉽게 구멍이 뚫리게 됩니다. 아래 예시를 통해 설명하겠습니다.

TypeScript

위 코드를 실행하면 콘솔에 출력되는 결과가 무엇일까요? Walletamount 필드의 타입은 number라고 정의되어있으니 number일까요?

하지만 결과는 string입니다. 타입스크립트의 타입은 코드가 실행될 때 아무런 영향도 미치지 않고, 실제 값인 "10000"의 타입이 적용됩니다. 만약 다른 부분의 코드에서 wallet.amount += 100와 같은 라인이 작성되었다면, 조만간 이 사용자는 돈이 복사가 된다며 💰 좋아했겠네요.

따라서 타입스크립트의 검증만으로는 부족합니다. 런타임에 타입을 점검할 수 있기 위해서는 런타임 체크 또한 필요합니다.

TypeScript

타입스크립트 활용 전략

런타임에 타입 체크를 하세요

타입스크립트를 안전하게 활용하기 위해서는 런타임의 타입 체크 또한 함께 활용해야 합니다. 이것을 간편하게 할 수 있는 기능을 제공하는 라이브러리로 class-validator를 추천드립니다.

TypeScript

class-validator는 decorator를 사용하여, object의 각 필드가 특정 constraint를 만족하는지 여부를 쉽게 체크할 수 있습니다. 제공하는 decorator도 isInt, isString과 같은 간단한 validator부터 isEmail, isURL과 같은 high-level validator도 사용할 수 있으며, 커스텀 decorator도 정의할 수 있는 인터페이스를 제공하기 때문에 추천합니다. 타입스크립트가 정적 타입 분석을 담당하고, class-validator가 런타임 타입 분석을 담당하면 프로젝트의 타입 시스템은 한층 더 견고해질 것입니다!

any와 as를 지양하세요

any는 타입을 무력화하는 아주 강력한 도구입니다. 아무리 타입이 맞지 않아 컴파일 에러를 수십 개 출력하던 악랄한 tsc 컴파일러도, as any 한 방이면 미소를 지으며 보내주는 웃음치료사입니다. 하지만 any는 어떤 타입을 가지는 변수에도 대입될 수 있습니다. 따라서 실제로 코드의 다른 부분으로 흘러간 any 타입 변수가 런타임에 문제를 일으킬 가능성이 높습니다.

as도 변수의 타입을 변경하는 또 다른 강력한 도구입니다. 주로 어떤 값을 다른 타입의 변수에 대입하고 싶을 때 (왜 그래야 하는지는 둘째 치더라도요 😅) as any as SomeType과 같이 만능 도구로 활용하게 됩니다. 이렇게 하면 당장 타입스크립트 컴파일러는 불평불만 없이 우리의 코드를 실행할 수 있도록 비켜줍니다.

anyas로 인해 타입 시스템의 감시를 벗어난 변수와 값들은 코드의 다른 부분에서 문제를 일으키게 됩니다. 위의 돈 복사 💰 버그처럼요! 또한 이러한 실수는 코드를 실제로 실행해보기 전에 절대로 알아낼 수 없기 때문에, 코드가 자주 변경된다면 예상치 못한 사태를 일으킬 가능성이 대단히 높습니다. 또한 as로 casting이 일어난 변수는 코드의 다른 부분에서 마치 그 타입인 것처럼 보이기 때문에, 실제로 as가 적용될 수 있는 경우가 아니라면 (즉, 원래의 타입이 as로 cast하는 타입을 완전히 extend하지 않는다면) 다른 코드를 작성할 때 혼동을 일으키게 됩니다. 타입스크립트가 제공하는 견고하고 강력한 타입 시스템을 활용하기 위해서는 anyas를 지양해야 하겠습니다.

점진적으로 타입스크립트를 적용하세요

타입스크립트를 자바스크립트와 함께 사용할 수 있다는 점은 타입스크립트의 아주 큰 장점입니다. 이 때문에 원래 자바스크립트로 작성되었던 프로젝트인데, 한 번에 타입스크립트를 적용하기 부담스럽다면 일부 파일부터 차례로 적용하는 형태로 점진적 migration을 할 수 있습니다.

채널톡 프로젝트도 비교적 최근까지는 모든 코드가 자바스크립트로 작성되어 있었습니다. 약 2년 전부터 타입스크립트를 도입하기 시작하여, 최근에는 전체 중 95% 이상의 파일이 타입스크립트로 교체되었습니다. 프로젝트의 코드베이스가 아주 방대한데도 (약 3000여개의 파일!) 타입스크립트 도입 결정을 할 수 있는 배경에는 천천히 migration을 적용할 수 있다는 점이 크게 작용했습니다.'

채널톡의 웹 팀에서는 타입스크립트의 migration 진행도를 정량적으로 파악하기 위해 여러 가지 지표를 생각했습니다. 주된 지표는 다음과 같습니다.

  1. 전체 파일 중, 타입스크립트로 작성된 파일의 비율

  2. 전체 symbol 중, 타입 정보를 알 수 있는 symbol의 비율

  3. ESLint의

    typescript-eslint

    rule 중 type safety에 관한 rule을 적용했을 때, error 및 warning의 수

2번의 경우 자동화에 도움을 줄 수 있는 라이브러리들을 찾을 수 있었습니다. type-coverage 패키지는 타입스크립트 컴파일러를 조작하여 symbol의 타입 정보를 알 수 있는 인터페이스를 제공합니다. 또한, typecov라는 패키지는 GitHub Actions와 같이 CI에 붙어 타입 정보를 지속적으로 제공하는 기능 또한 제공합니다. 타입 체크 등 프로젝트의 퀄리티를 모니터링하는 기능을 제공하는 서비스 또한 존재합니다.

다양한 도구를 활용해 채널톡 프로젝트에서 타입스크립트 활용도 관련 지표를 지속적으로 측정하고 있습니다.

자바스크립트로 작성된 파일을 타입스크립트로 작성하고, 타입이 잘 정의되어 있지 않아 많은 symbol의 타입이 any로 잡히는 부분을 리팩토링하여 정확한 타입을 부여했습니다. 팀 전반적으로 이 방향으로 꾸준히 에너지를 투자한 결과, 위 차트에서 볼 수 있듯 타입스크립트 활용 관련 지표가 꾸준히 좋은 방향으로 이동했습니다.

팀 구성원들의 지속적인 migration 노력을 통해 대부분의 파일을 타입스크립트로 다시 작성했으며, 새로 추가되는 feature는 강력한 타입 시스템의 지원 아래 잠재적인 버그를 줄여가며 더 빠른 속도로 작업할 수 있게 되었습니다. 여러분도 프로젝트에 타입스크립트가 적용되어 있지 않다면, 점진적으로 도입해 보시는 것을 추천드립니다 💪

We Make a Future Classic Product

채널팀과 함께 성장하고 싶은 분을 기다립니다

사이트에 무료로 채널톡을 붙이세요.

써보면서 이해하는게 가장 빠릅니다

회사 이메일을 입력해주세요