Java/C# 프로그래머를 위한 TypeScript

Java/C# 프로그래머를 위한 TypeScript (TypeScript for Java/C# Programmers)

C#이나 Java처럼 정적 타이핑을 사용하는 언어에 익숙한 프로그래머라면 TypeScript가 인기 있는 선택지라는 걸 알 거예요. TypeScript의 타입 시스템은 더 나은 코드 완성, 더 이른 오류 감지, 프로그램 부분들 사이의 더 명확한 소통 같은 이점을 C#이나 Java와 똑같이 제공해요. 다만 JavaScript(그리고 따라서 TypeScript)가 전통적인 OOP 언어와 어떻게 다른지 한 걸음 물러서 살펴볼 가치가 있어요. 이 차이를 이해하면 더 나은 JavaScript 코드를 쓰는 데 도움이 되고, C#/Java에서 바로 TypeScript로 넘어온 프로그래머들이 빠지기 쉬운 함정을 피할 수 있어요.

출처: TypeScript 핸드북

JavaScript 함께 배우기 (Co-learning JavaScript)

이미 JavaScript에 익숙하지만 주로 Java나 C# 프로그래머라면, 이 소개 페이지가 빠지기 쉬운 흔한 오해와 함정을 설명하는 데 도움이 될 거예요. TypeScript가 타입을 모델링하는 방식 중 일부는 Java나 C#과 꽤 달라서, TypeScript를 배울 때 이 점들을 염두에 두는 게 중요해요.

Java나 C# 프로그래머인데 JavaScript 자체가 처음이라면, JavaScript의 런타임 동작을 이해하기 위해 먼저 타입 없이 JavaScript를 조금 배우는 걸 추천해요. TypeScript는 코드가 실행되는 방식을 바꾸지 않으므로, 실제로 무언가를 하는 코드를 쓰려면 여전히 JavaScript가 어떻게 동작하는지 배워야 하니까요.

TypeScript는 JavaScript와 같은 런타임 을 사용한다는 점을 기억하는 게 중요해요. 그래서 특정 런타임 동작을 구현하는 방법(문자열을 숫자로 바꾸기, 알림 표시하기, 파일을 디스크에 쓰기 등)에 대한 리소스는 언제나 TypeScript 프로그램에도 동일하게 적용돼요. TypeScript 전용 리소스로 자신을 한정하지 마세요!

클래스 다시 생각하기 (Rethinking the Class)

C#과 Java는 우리가 강제 OOP(mandatory OOP) 언어라고 부를 수 있는 것들이에요. 이 언어들에서 클래스 는 코드 구성의 기본 단위이자, 런타임에서 모든 데이터 그리고 동작의 기본 컨테이너예요. 모든 기능과 데이터를 클래스에 담아야 한다는 건 어떤 문제에는 좋은 도메인 모델일 수 있지만, 모든 도메인이 반드시 이런 방식으로 표현돼야 하는 건 아니에요.

자유 함수와 데이터 (Free Functions and Data)

JavaScript에서는 함수가 어디에나 존재할 수 있고, 데이터는 미리 정의된 classstruct 안에 넣지 않고도 자유롭게 전달할 수 있어요. 이런 유연성은 대단히 강력해요. 암묵적인 OOP 계층 없이 데이터 위에서 동작하는 "자유" 함수(클래스와 연관되지 않은 함수)는 JavaScript에서 프로그램을 작성하는 선호 모델이 되는 경향이 있어요.

정적 클래스 (Static Classes)

추가로, 싱글턴과 정적 클래스 같은 C#과 Java의 특정 구조는 TypeScript에서 불필요해요.

TypeScript에서의 OOP

그렇긴 하지만, 원한다면 여전히 클래스를 사용할 수 있어요! 어떤 문제는 전통적인 OOP 계층 구조로 푸는 게 잘 맞아요. 그리고 JavaScript 클래스에 대한 TypeScript의 지원은 그런 모델을 훨씬 더 강력하게 만들어 줘요. TypeScript는 인터페이스 구현, 상속, 정적 메서드 같은 많은 흔한 패턴을 지원해요.

클래스에 대해서는 이 가이드의 뒷부분에서 다룰게요.

타입 다시 생각하기 (Rethinking Types)

TypeScript가 타입 을 이해하는 방식은 실제로 C#이나 Java와 꽤 달라요. 몇 가지 차이를 살펴볼게요.

명목적·구체화된 타입 시스템 (Nominal Reified Type Systems)

C#이나 Java에서는 주어진 값이나 객체가 정확히 하나의 타입을 가져요 — null, 원시 타입, 또는 알려진 클래스 타입 중 하나죠. value.GetType()이나 value.getClass() 같은 메서드를 호출해서 런타임에 정확한 타입을 조회할 수 있어요. 이 타입의 정의는 어떤 이름을 가진 클래스 어딘가에 존재해요. 그리고 몇몇 이름을 가진 클래스 어딘가에 존재하죠. 명시적인 상속 관계나 공통으로 구현된 인터페이스가 없다면, 모양이 비슷한 두 클래스를 서로 대신해서 쓸 수 없어요.

이러한 측면들이 구체화된, 명목적 타입 시스템을 서술해요. 코드에 쓴 타입은 런타임에 존재하고, 타입들은 구조가 아니라 선언을 통해 관계를 맺어요.

집합으로서의 타입 (Types as Sets)

C#이나 Java에서는 런타임 타입과 그들의 컴파일 타임 선언 사이에 일대일 대응이 있다고 생각하는 게 의미가 있어요.

TypeScript에서는 타입을 '공통점을 공유하는 값들의 집합'으로 생각하는 게 더 좋아요. 타입은 그냥 집합이므로, 특정 값은 동시에 여러 집합에 속할 수 있어요.

타입을 집합으로 생각하기 시작하면 특정 연산이 아주 자연스러워져요. 예를 들어 C#에서는 string 또는 int인 값을 주고받는 게 어색해요. 이런 종류의 값을 나타내는 단일 타입이 없으니까요.

TypeScript에서는 모든 타입이 그냥 집합이라는 걸 깨달으면 이것이 아주 자연스러워져요. string 집합 또는 number 집합 중 하나에 속하는 값을 어떻게 서술할까요? 그저 그 집합들의 합집합 에 속하면 돼요: string | number처럼요.

TypeScript는 타입을 집합 이론적인 방식으로 다루는 여러 메커니즘을 제공해요. 타입을 집합으로 생각하면 그것들이 더 직관적으로 느껴질 거예요.

지워지는 구조적 타입 (Erased Structural Types)

TypeScript에서 객체는 단일 정확한 타입 이 아니에요. 예를 들어 인터페이스를 만족하는 객체를 만들었다면, 둘 사이에 선언적 관계가 없더라도 그 인터페이스가 기대되는 곳에서 그 객체를 사용할 수 있어요.

interface Pointlike {
  x: number;
  y: number;
}
interface Named {
  name: string;
}

function logPoint(point: Pointlike) {
  console.log("x = " + point.x + ", y = " + point.y);
}

function logName(x: Named) {
  console.log("Hello, " + x.name);
}

const obj = {
  x: 0,
  y: 0,
  name: "Origin",
};

logPoint(obj);
logName(obj);

TypeScript의 타입 시스템은 명목적이 아니라 구조적 이에요. objxy라는 두 프로퍼티를 숫자로 갖기 때문에 Pointlike로 사용할 수 있어요. 타입들 사이의 관계는 어떤 특별한 관계로 선언됐는지가 아니라, 그들이 포함하는 프로퍼티에 의해 결정돼요.

TypeScript의 타입 시스템은 또한 구체화되지 않아요(reified): 런타임에 objPointlike라고 알려주는 것은 아무것도 없어요. 실제로 Pointlike 타입은 런타임에 어떤 형태로도 존재하지 않아요.

집합으로서의 타입 아이디어로 돌아가 보면, objPointlike 값들의 집합과 Named 값들의 집합 양쪽의 구성원이라고 생각할 수 있어요.

구조적 타이핑의 결과 (Consequences of Structural Typing)

OOP 프로그래머들은 구조적 타이핑의 특정 두 가지 측면에 종종 놀라곤 해요.

빈 타입 (Empty Types)

첫 번째는 빈 타입(empty type) 이 기대를 어긋나는 것처럼 보인다는 점이에요:

class Empty {}

function fn(arg: Empty) {
  // do something?
}

// No error, but this isn't an 'Empty' ?
fn({ k: 10 });

TypeScript는 제공된 인자가 유효한 Empty인지 확인해서 여기서 fn 호출이 유효한지 판단해요. 그 과정은 { k: 10 }class Empty { }구조 를 살펴보는 방식으로 이루어져요. { k: 10 }Empty가 가지는 모든 프로퍼티를 가진다는 걸 알 수 있어요. Empty는 프로퍼티가 없으니까요. 그래서 이 호출은 유효해요!

이게 놀랍게 보일 수 있지만, 결국 명목적 OOP 언어에서 강제되는 관계와 아주 비슷한 관계예요. 서브클래스는 기반 클래스의 프로퍼티를 제거 할 수 없어요. 그렇게 하면 파생 클래스와 그 기반 클래스 사이의 자연스러운 서브타입 관계가 무너지니까요. 구조적 타입 시스템은 호환되는 타입의 프로퍼티를 가진다는 관점에서 서브타입을 서술함으로써 이 관계를 단순히 암시적으로 식별하는 거예요.

동일한 타입 (Identical Types)

또 다른 잦은 놀라움의 원천은 동일한 타입에서 나타나요:

class Car {
  drive() {
    // hit the gas
  }
}
class Golfer {
  drive() {
    // hit the ball far
  }
}

// No error?
let w: Car = new Golfer();

이것도 역시 에러가 아니에요. 이 클래스들의 구조 가 같기 때문이죠. 이것이 혼란의 잠재적 원천처럼 보일 수 있지만, 실제로는 서로 관련되면 안 되는 동일한 클래스는 흔하지 않아요.

클래스들이 서로 어떻게 관계를 맺는지에 대해서는 클래스 챕터에서 더 배우게 될 거예요.

리플렉션 (Reflection)

OOP 프로그래머들은 제네릭 값조차 어떤 값의 타입을 조회할 수 있는 데 익숙해요:

// C#
static void LogType<T>() {
    Console.WriteLine(typeof(T).Name);
}

TypeScript의 타입 시스템은 완전히 지워지기 때문에, 예를 들어 제네릭 타입 매개변수의 인스턴스화에 대한 정보는 런타임에서 사용할 수 없어요.

JavaScript에는 typeofinstanceof 같은 제한적인 원시 기능이 있긴 하지만, 이 연산자들도 타입이 지워진 출력 코드에 존재하는 그대로의 값에 대해 동작한다는 걸 기억하세요. 예를 들어 typeof (new Car())Car"Car"가 아니라 "object"가 돼요.

다음 단계 (Next Steps)

지금까지 일상 TypeScript에서 쓰이는 문법과 도구의 간략한 개요를 살펴봤어요. 여기서부터 다음과 같은 것들을 할 수 있어요:

더 알아보기 (Learn more)