4.Node.js/JavaScript&TypeScript

[TypeScript] 1편. TypeScript 핵심 개념과 기본 타입 이해하기

쿼드큐브 2025. 12. 10. 11:22
반응형
반응형

 

1편. TypeScript 핵심 개념과 기본 타입 이해하기

 

📚 목차
1. TypeScript 핵심 개념 이해하기
2. 기본 타입(Primitive Types) 이해하기
3. 타입 별칭(Type Alias) 이해하기
4. 모듈 시스템과 TypeScript 적용하기
5. Any, Unknown, Never 타입 이해하기

 

TypeScript 핵심 개념 삽화 이미지
TypeScript 핵심 개념 삽화 이미지

 

📂 [GitHub 예시 코드 보러가기] (https://github.com/cericube/nodejs-tutorials) /TypeScript

 

1. TypeScript 핵심 개념 이해하기

TypeScript(이하 TS)는 Microsoft가 개발·유지 관리하는 오픈 소스 프로그래밍 언어로, JavaScript에 정적 타입(Static Type)을 도입한 상위 확장 언어(Superset)입니다.

 

TS의 가장 큰 특징은 코드 실행 이전 단계(컴파일 시점)에 타입을 검사할 수 있다는 점입니다. 이를 통해 런타임에서 발생할 수 있는 오류를 사전에 예방하고, 보다 신뢰성 높은 코드를 작성할 수 있습니다.

 

핵심 정의: TypeScript는 “정적 타입 시스템을 갖춘 JavaScript”입니다.

 

▸ JavaScript와 TypeScript 비교

구분 JavaScript(동적) TypeScript(정적)
타입 결정 시점 코드 실행 시(런타임) 코드 작성/컴파일 시
오류 검출 시점 실행 중 오류 발생 가능 컴파일 단계에서 사전 검출
개발 경험(DX) 자동 완성·리팩토링 지원 제한적 IDE 지원 강화, 높은 개발 생산성
대규모 프로젝트 적합성 관리·유지보수 난이도 증가 명확한 타입 기반으로 유지보수 용이

 

 

🔷 TypeScript와 JavaScript의 관계

(1) TypeScript는 JavaScript의 Superset(상위 확장)입니다

TypeScript는 JavaScript 문법을 그대로 포함하면서, 여기에 타입 주석(Type Annotation)을 추가할 수 있도록 확장된 언어입니다.

따라서 모든 유효한 JavaScript 코드는 동시에 유효한 TypeScript 코드가 됩니다.

이러한 특성 때문에 TypeScript를 흔히 “JavaScript의 상위 확장(Superset)”이라고 부릅니다.

// 유효한 JavaScript → 그대로 유효한 TypeScript
const name = "Alice";
const add = (a, b) => a + b;

// TypeScript에서는 타입을 명시하여 코드의 안정성을 높일 수 있음
const age: number = 30; // age는 number 타입만 허용
const multiply = (a: number, b: number): number => {
  return a * b; // 반환 타입 역시 number로 제한됨
};

이처럼 TypeScript는 기존 JavaScript 개발 경험을 그대로 유지하면서도 정적 타입 기반의 안전성과 도구 지원을 강화할 수 있는 환경을 제공합니다.


(2) ECMAScript(ES) 표준과의 관계

TypeScript는 JavaScript의 표준 사양인 ECMAScript(ES)의 최신 문법을 적극적으로 지원합니다.

함수, 클래스, async/await, 모듈 시스템 등 ES 최신 기능을 그대로 사용할 수 있으며, 필요할 경우 TypeScript 컴파일러는 이 문법을 이전 버전의 JavaScript로 트랜스파일(Transpile) 하여 구형 Node.js 환경이나 브라우저에서도 동작할 수 있도록 변환합니다.

 

즉, TypeScript는
▸ ES 최신 기능을 활용할 수 있게 해주고
▸ 실행 환경의 호환성을 위해 자동으로 “다운그레이드”를 수행하는

두 가지 역할을 동시에 제공합니다.

 

🔷 TypeScript의 동작 원리

(1) 컴파일(Compile) 과정: 타입 검사 → JavaScript 변환

Node.js는 TypeScript 코드를 직접 실행할 수 없기 때문에, TS 코드는 반드시 순수 JavaScript로 변환되는 컴파일 과정을 거쳐야 합니다.


컴파일 단계는 아래 두 가지 과정으로 구성됩니다.

1. 타입 검사(Type Checking)

▸ TypeScript 컴파일러는 코드의 타입 정보를 분석하여 오류가 있는지 확인합니다.

▸ 이때 모든 타입 정보는 런타임에 존재하지 않으며, 컴파일러 단계에서만 사용됩니다.

2. JavaScript 변환(Transpilation)

컴파일러는 타입 주석을 제거한 뒤, 실행 가능한 JavaScript 코드로 변환합니다.

 

(2) Node.js 환경에서 TypeScript가 실행되는 방식(기존 방식)

TypeScript 코드는 아래 흐름을 통해 Node.js에서 실행됩니다.

1. 개발자가 *.ts 파일 작성
2. TypeScript 컴파일러(tsc)가 타입 검사 및 변환 수행
3. *.js 파일 출력
4. Node.js가 변환된 JavaScript 파일을 실행

# 1. TypeScript 파일 작성: src/app.ts

# 2. TypeScript 컴파일러 실행
$ tsc src/app.ts

# 3. 컴파일 결과물 생성 (예: dist/app.js)

# 4. Node.js로 실행
$ node dist/app.js

이 방식으로 Node.js 환경에서도 TypeScript 기반의 안전하고 유지보수 가능한 코드를 구현할 수 있습니다.

(3) TSX를 설치하면 실행 방식이 완전히 달라집니다
tsx는 TypeScript 런타임 실행기(runtime executor)로, 컴파일 없이 .ts, .tsx 파일을 바로 실행할 수 있게 해주는 도구입니다.

$ npx tsx src/app.ts

TypeScript 코드
    ↓ (즉시 변환: 타입 제거 + esbuild로 빠르게 JS 변환)
Node.js에서 바로 실행

 

개발환경 세팅: TypeScript 개발 환경 세팅 : tsconfig.json, tsx, Vitest

 

TypeScript 개발 환경 세팅 : tsconfig.json, tsx, Vitest

TypeScript 개발 환경 세팅 : tsconfig.json, tsx, Vitest 📚 목차1. TypeScript 설치와 tsconfig.json 핵심 설정2. TypeScript 실행 환경 구축: tsx 기반 개발3. 테스트 환경 설정(Vitest 기반)✔ 마무리 1. TypeScript 설치와 t

quadcube.tistory.com

 

2. 기본 타입(Primitive Types) 이해하기

TypeScript는 JavaScript의 모든 기본 타입(Primitive Types)을 그대로 지원하며, 여기에 타입 검사 기능을 추가합니다.

 

🔷 기본 타입 종류와 설명

타입 설명 예시
number 모든 숫자 값 (정수, 소수, NaN, Infinity 포함) let age: number = 30;
string 모든 문자열 (큰따옴표, 작은따옴표, 템플릿 리터럴 포함) let name: string = "Jane";
boolean 참(true) 또는 거짓(false)을 나타내는 논리형 let isStudent: boolean = false;
null 명시적으로 “값이 없음”을 표현 let data: null = null;
undefined 값이 아직 할당되지 않은 상태를 표현 let nothing: undefined = undefined;
symbol 고유하고 변경 불가능한 값 (ES6에서 도입) const key: symbol = Symbol("key");
bigint 매우 큰 정수를 표현할 수 있는 타입 (ES2020 이상) let bigNum: bigint = 9007199254740991n;

참고: symbol과 bigint는 일반적인 변수 선언보다는 특정 상황(예: 고유 키 정의, 매우 큰 숫자 계산 등)에서 주로 사용됩니다.

 

🔷 타입 명시 (Explicit Typing)

TypeScript에서는 변수 선언 시 타입을 명시적으로 지정할 수 있습니다.
변수 이름 뒤에 :를 붙이고 타입을 선언하면 됩니다.

let count: number = 10;
let message: string = "안녕하세요";
let isActive: boolean = true;

// 오류 발생 예시: count 변수에는 문자열을 할당할 수 없습니다.
// count = "열"; // Type '"열"' is not assignable to type 'number'.

 

🔷 타입 추론 (Type Inference)

TypeScript는 매우 강력한 타입 추론 기능을 갖추고 있습니다.

let count = 10;

위와 같이 타입을 명시하지 않아도, TypeScript는 10이 number 타입이므로 count 변수의 타입을 자동으로 number로 추론합니다.

count = 5;     // ✅ 정상
count = "다섯"; // ❌ 오류 발생

즉, 타입 추론은 코드를 간결하게 유지하면서도 타입 안정성을 확보할 수 있는 강력한 도구입니다.
하지만, 복잡한 로직에서는 명시적 타입 선언이 오히려 가독성을 높이고 유지보수에 유리할 수 있습니다.

타입 추론 오류 예시
타입 추론 오류 예시

반응형

 

3. 타입 별칭(Type Alias) 이해하기

TypeScript에서는 type 키워드를 사용하여 기존 타입에 새로운 이름(별칭)을 지정할 수 있습니다.
이러한 기능을 타입 별칭(Type Alias)이라고 합니다.

 

타입 별칭은 특히 다음과 같은 상황에서 유용합니다.
▸ 반복적으로 사용되는 타입을 간결하게 표현할 때
▸ 변수나 함수의 의미를 보다 명확하게 드러내고 싶을 때
▸ 구조가 복잡한 타입을 읽기 쉽도록 분리할 때
즉, 타입 별칭을 사용하면 코드의 표현력이 높아지고, 타입 선언을 재사용하기 쉬워져 유지보수성도 향상됩니다.

 

🔷 타입 별칭 사용법

// type 키워드를 사용하여 새로운 타입 이름 정의
type UserID = string;
type Temperature = number;
type Status = boolean;

// 정의된 타입 명을 변수 타입으로 지정
let userID: UserID = "user-12345";
let todayTemp: Temperature = 26.7;
let isPassed: Status = true;

위 코드에서
만약 let userID: string이라고 작성해도 동작은 동일하지만, UserID라는 별칭을 사용함으로써
“이 값은 단순한 문자열이 아니라 사용자 식별자”
라는 의미를 전달할 수 있습니다.

 

🔷 복잡한 타입을 별칭으로 관리하는 방법

다음은 객체 구조를 타입 별칭으로 정의한 예시입니다.

형식 예시 문법 요소
① 줄바꿈 형태 type AnimalType = { name: string; age: number; }; 속성 구분자로 세미콜론(;) 사용, 권장
② 한 줄 형태 type AnimalType = {name: string, age: number} 속성 구분자로 쉼표(,) 사용
type Point = {
  x: number;
  y: number;
};

//아래도 유효한 표현
//type Point = {x: number, y: number}; 

function logPoint(point: Point) {
  console.log(`x: ${point.x}, y: ${point.y}`);
}

logPoint({ x: 100, y: 200 });

위와 같은 형태는 다음의 장점을 제공합니다.
▸ Point 구조를 여러 함수에서 재사용 가능
▸ 동일한 객체 구조를 반복 작성하지 않아도 됨
▸ Point라는 이름만으로도 의미 전달이 가능함

 

✔️ 타입 별칭은 언제 사용하면 좋을까요?

1. 의미 있는 타입 이름이 필요한 경우

코드를 읽는 사람에게 값의 용도를 전달할 수 있습니다.

type Email = string;
type OrderID = string;
type Price = number;

 

2. 동일한 형태를 여러 곳에서 사용할 때

이를 다양한 API 함수에서 재사용하면 일관성이 매우 좋아집니다.

type ApiResponse = {
  success: boolean;
  message: string;
  data?: unknown;
};

 

3. 복잡한 타입 구조를 단일 이름으로 표현하고 싶을 때

type Coordinates = [number, number, number];
type Matrix = number[][];

 

✔️ 참고: interface와의 차이

구분 type alias interface
표현 가능 범위 모든 타입 가능 (유니언, 교차, 기본 타입 포함) 주로 객체 구조 표현
추가 확장 제한적 (intersecting 가능) extends로 확장 가능
주요 용도 타입의 “의미 부여” 및 복잡한 타입 관리 형식적 구조(Shape) 정의

 

4. 모듈 시스템과 TypeScript 적용하기

현대 JavaScript 및 TypeScript 개발에서는 파일 단위로 코드를 분리하여 관리하는 "모듈 시스템"이 필수적입니다.

이러한 모듈 시스템은 프로젝트의 구조화, 재사용성, 유지보수성을 크게 향상시켜 줍니다.


TypeScript는 JavaScript의 공식 모듈 시스템인 ESM (ECMAScript Modules)을 완벽하게 지원하며, 특히 타입 정보 또한 모듈로 분리하여 관리할 수 있다는 점이 큰 장점입니다.

 

🔷 모듈 시스템의 기본 구조

키워드 역할 예시
export 변수, 함수, 타입 등을 다른 파일에서 사용할 수 있도록 내보냄 export const A = 1;
import 다른 파일에서 export한 항목을 불러와서 사용 import { A } from './module';

모듈을 사용하면 파일 간에 명확한 경계를 유지할 수 있으며, 특히 TypeScript에서는 타입 정보까지 파일로 분리해 재사용이 가능합니다.

 

1. types.ts - 타입 정의 모듈

// types.ts
// 다른 파일에서 사용할 수 있도록 타입과 상수를 export 합니다.

export type Product = {
  id: number;
  name: string;
  price: number;
};

export const API_URL = "https://api.example.com";

▸ 여기서 Product 타입은 제품 정보를 표현하는 구조이며,

▸ API_URL은 다른 모듈에서도 사용할 수 있도록 export된 상수입니다.

 

2. app.ts - 실제 로직 구현 파일

// app.ts
// types.ts에서 정의한 타입을 가져와 사용합니다.

import { Product, API_URL } from './types';

const newProduct: Product = {
  id: 1,
  name: "노트북",
  price: 1200000,
};

// 타입 검사가 수행됩니다.
// 아래 코드는 오류를 발생시킵니다.
// const invalidProduct: Product = { id: "a", name: "PC" }; 
// ❌ Error: 'id'에 string을 넣을 수 없습니다 (number 타입이 필요함)

console.log(`새 제품: ${newProduct.name}, 가격: ${newProduct.price}`);
console.log(`API 주소: ${API_URL}`);

이 구조는 다음과 같은 실무 장점을 제공합니다:

▸ 타입 중복을 제거하고, 한 곳에서 정의하여 중앙 집중형 관리 가능

▸ 파일 간 의존 관계가 명확하게 드러나며 가독성 향상

▸ 타입 변경 시 관련 모듈에서 자동 감지 및 컴파일 오류 발생 → 안전성 확보

 

✔️ 타입 분리 전략

수많은 타입들을 각 기능 파일에 섞어서 선언하면 다음과 같은 문제가 발생할 수 있습니다:
▸ 중복 선언 → 유지보수 어려움
▸ 타입 충돌 발생 위험
▸ 하나의 파일이 지나치게 커져 코드 가독성 저하
따라서 다음과 같은 전략이 권장됩니다

전략 설명
types/ 폴더 생성 모든 타입 정의를 별도 디렉터리에서 관리
import type 문법 사용 타입 전용 임포트로 런타임 코드에 영향 주지 않음
각 도메인별 타입 파일 구분 예: user.types.ts, product.types.ts 등

 

5. Any, Unknown, Never 타입 이해하기

TypeScript의 타입 시스템을 이해하는 데 있어 any, unknown, never 타입은 매우 중요한 개념입니다.
이 세 타입은 TypeScript가 제공하는 타입 안전성 수준을 선택적으로 조절하는 수단이며, 적절히 사용하면 견고한 코드 구조를 만드는 데 도움이 됩니다.

 

🔷 any: 가장 관대하지만 가장 위험한 타입

any 타입은 “어떤 타입이든 허용”한다는 의미를 가진 타입입니다.
즉, 값의 타입이 무엇인지 TypeScript가 더 이상 확인하지 않습니다

let data: any = "이건 문자열입니다.";
data = 100; // 정상
data.toFixed(); // 컴파일 시점에는 오류 없음 → 런타임 오류 가능

any의 위험성: any를 남용하면 TypeScript를 사용하는 의미가 퇴색됩니다.

JavaScript에서 TypeScript로 전환하는 초기에 임시로 사용하는 경우 외에는 사용을 지양해야 합니다.

 

🔷 unknown: 안전하게 알 수 없는 타입

unknown 역시 '알 수 없는 타입'을 의미하지만, any보다 훨씬 안전합니다.

unknown 타입의 변수는 바로 사용할 수 없고, 반드시 타입이 무엇인지 검사한 후에만 접근이 허용됩니다.

let unsafeValue: unknown = "Hello TypeScript";

// unsafeValue.toUpperCase(); 
// Type 'unknown' is not assignable to type 'string'. -> 오류 발생!

if (typeof unsafeValue === 'string') {
  // if 문 안에서는 unsafeValue의 타입이 string으로 좁혀집니다.
  // 타입 검사 후 안전하게 접근 가능
  console.log(unsafeValue.toUpperCase()); 
}

unknown의 안전성: 외부 API 응답처럼 타입이 불분명한 데이터를 받을 때 사용하면, 개발자가 타입 가드(Type Guard)를 통해 타입을 명확히 검사하도록 강제하여 런타임 에러를 예방합니다.

항목 설명
직접 사용 불가 속성 접근, 함수 호출 불가
타입 검사 후 사용 가능 typeof, instanceof, 사용자 정의 Type Guard 등
코드를 안전하게 작성하도록 유도 런타임 오류 발생 가능성을 줄임

 

🔷 never: 절대로 발생할 수 없는 타입

never 타입은 이름 그대로 절대로 도달할 수 없는 상태 또는 결과를 표현할 때 사용합니다.


1. 항상 예외를 발생시키는 함수

이 함수는 정상적으로 종료될 수 없기 때문에 반환 타입이 never입니다.

function throwError(message: string): never {
  throw new Error(message);
}


2. 무한 루프: 함수가 끝나지 않고 무한히 실행될 때.

종료되지 않는 함수 역시 반환값이 발생하지 않으므로 never로 처리됩니다.

function loopForever(): never {
  while (true) {}
}

 

3. Exhaustiveness Checking: 모든 가능한 케이스를 처리했음을 보장할 때

이 never 패턴은 'tomato' 같은 잘못된 인자를 막기 위한 용도가 아니라,
“나중에 Fruit 타입이 변경되었을 때, switch에서 케이스 누락을 컴파일 타임에 강제로 잡기 위한 장치”입니다.

//Fruit는 문자열 리터럴 유니온 타입입니다
type Fruit = "Apple" | "Banana" | "Orange";

// 아래처럼 새로운 과일을 추가했는데 switch에서 처리 안 하면?
// type Fruit = "Apple" | "Banana" | "Orange" | "Grape";
// 에러발생: Type '"Tomato"' is not assignable to type 'never'.

/**
 * 이 함수의 핵심은 마지막 default 구문에서
 * never 타입을 이용해 "모든 케이스가 처리되었는지"를 컴파일 타임에 검사하도록 만드는 것입니다.
 */
function fruitToKorean(fruit: Fruit): string {
  switch (fruit) {
    case 'Apple':
      return '사과';
    case 'Banana':
      return '바나나';
    case 'Orange':
      return '오렌지';
    default: {
      // 이 default 블록은 논리적으로 "절대 도달하지 않아야 하는 곳"입니다.
      // 이 시점에서 fruit는 절대 존재하지 않는 타입이어야 함 (never)
      const _exhaustiveCheck: never = fruit;
      return _exhaustiveCheck;
    }
  }
}

function demoFruit() {
  console.log('=== demoFruit ===');
  console.log(fruitToKorean('Apple'));
  console.log(fruitToKorean('Banana'));
  console.log(fruitToKorean('Orange'));

  //오류 발생
  // Argument of type '"tomato"' is not assignable to parameter of type 'Fruit'.
  // console.log(fruitToKorean('tomato'));
}

1. fruitToKorean('tomato')

▸ Fruit는 "Apple" | "Banana" | "Orange" 중 하나만 허용하는 문자열 리터럴 유니온 타입 입니다.

▸ 따라서 Argument of type '"tomato"' is not assignable to parameter of type 'Fruit'. 같은 오류가 발생합니다.

▸ 정상적인 TS 환경(noImplicitAny, strict 등)이라면 이 코드는 컴파일 자체가 안 되는 게 맞습니다

 

2. type Fruit = "Apple" | "Banana" | "Orange" | "Grape"

▸ 만약 Fruit 타입이 변경된다면

▸ Type '"Tomato"' is not assignable to type 'never'. 오류가 발생합니다.

▸ switch문에서 'Grape' 케이스 누락을 강제로 잡기위한 장치 입니다.

Fruit 타입이 변경되었을 경우 오류 예시
Fruit 타입이 변경되었을 경우 오류 예시

타입 의미 특징 비고
any 무엇이든 가능 타입 검사 무효 ❌ 최소 사용 권장
unknown 알 수 없는 타입 검사 후 접근 가능 ✔ 권장
never 발생 불가능한 값 종료되지 않는 코드, 분기 체크 ✔ 상황별 사용

 


※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.

반응형

 

반응형