TypeScript

TypeScript 핵심 가이드: 입문부터 활용까지

Chrysans 2025. 2. 21. 14:44
728x90
반응형

TypeScript로 견고한 코드 작성하기: 타입의 힘을 활용한 모던 개발 가이드

1. TypeScript란 무엇인가?

1.1 TypeScript의 탄생 배경과 역사

JavaScript는 웹 개발의 핵심 언어로 자리잡았지만, 대규모 애플리케이션 개발에서 여러 한계점을 보였습니다. 이러한 문제들을 해결하기 위해 Microsoft의 Anders Hejlsberg(C# 설계자)가 주도하여 2012년 TypeScript를 개발했습니다.

이러한 문제를 해결하기 위해 Microsoft는 2012년 TypeScript를 발표했습니다.

  • 타입 시스템의 부재
    • JavaScript는 동적 타입 언어이기 때문에 런타임에서만 타입 오류를 발견할 수 있었습니다
    • 이는 버그 발견이 늦어지고, 코드 품질 관리가 어려워지는 원인이 되었습니다
  • 대규모 개발의 어려움
    • 코드베이스가 커질수록 유지보수가 어려워졌습니다
    • 팀 단위 협업에서 코드 이해도와 생산성이 저하되었습니다
  • 객체지향 프로그래밍 지원 부족
    • JavaScript의 프로토타입 기반 상속은 복잡하고 직관적이지 않았습니다
    • 클래스, 인터페이스 등 전통적인 OOP 개념의 부재로 구조적인 설계가 어려웠습니다
  • IDE 지원의 한계
    • 코드 자동완성, 리팩토링 등 개발 도구의 지원이 제한적이었습니다
    • 정적 분석을 통한 개발자 생산성 향상이 어려웠습니다.

1.2 TypeScript vs JavaScript: 무엇이 다른가?

기본적인 차이점

// JavaScript
function calculateTotal(items) {
  return items.reduce((total, item) => total + item.price, 0);
}

// 문제점: 런타임에서만 오류 발견
calculateTotal([
  {name: 'item1'}, // price 속성이 없지만 코드 작성 시점에서는 오류를 발견할 수 없음
  {price: 20}
]);
// TypeScript
interface Item {
  name: string;
  price: number;
}

function calculateTotal(items: Item[]): number {
  return items.reduce((total, item) => total + item.price, 0);
}

// 컴파일 시점에서 오류 발견
calculateTotal([
  {name: 'item1'}, // ❌ Error: price 속성이 없습니다
  {price: 20}     // ❌ Error: name 속성이 없습니다
]);

주요 차이점 설명

  1. 타입 시스템
    • JavaScript: 동적 타입 - 런타임에 타입 확인
    • TypeScript: 정적 타입 - 컴파일 시점에 타입 확인
  2. 개발 도구 지원
    • JavaScript: 제한된 IDE 지원
    • TypeScript: 풍부한 IDE 지원 (자동 완성, 리팩토링 등)
  3. 오류 감지
    • JavaScript: 런타임에서만 오류 발견
    • TypeScript: 컴파일 시점에서 대부분의 오류 발견

1.3 TypeScript의 동작 원리

TypeScript는 다음과 같은 과정을 거쳐 작동합니다:

  1. 컴파일 과정

타입스크립트 컴파일 과정 - 코드 -> 구분 분석 -> 타입검사 -> 변환 -> 자바스트립트 순으로 컴파일 한다.

  1. 시작: TypeScript 파일
    • .ts 또는 .tsx 파일로 작성된 TypeScript 코드
  2. 구문 분석 (Parsing)
    • TypeScript 코드를 파싱하여 AST(Abstract Syntax Tree) 생성
    • 기본적인 구문 오류 검사
  3. 타입 검사 (Type Checking)
    • 정적 타입 검사 수행
    • 타입 오류 감지
  4. 변환 (Transformation)
    • TypeScript 특정 문법을 JavaScript로 변환
    • 타입 정보 제거
    • 선택된 JavaScript 버전에 맞게 코드 변환
  5. 결과: JavaScript 파일
    • 순수 JavaScript 코드 생성
    • .js 파일 출력

tsconfig.json은 이 전체 컴파일 과정에 영향을 미치는 설정 파일입니다. 이 파일을 통해:

- 컴파일러 옵션 설정
- 출력 JavaScript 버전 지정
- 모듈 시스템 설정
- 타입 체크 엄격도 설정등을 제어할 수 있습니다.

  1. 타입 검사 시스템
// 타입 추론 예시
let message = "Hello"; // TypeScript가 자동으로 string 타입으로 추론

// 명시적 타입 지정
let message: string = "Hello"; // 개발자가 직접 타입을 지정

// 유니온 타입 예시
let id: string | number; // string 또는 number 타입 허용
id = "abc123"; // OK
id = 123;      // OK
id = true;     // ❌ Error: boolean 타입은 허용되지 않음

1.4 개발 환경 설정

TypeScript를 시작하기 위한 기본 환경 설정 방법을 알아보겠습니다:

 

Node.js 설치

# Node.js 설치 확인 node --version


TypeScript 설치

# 전역 설치 npm install -g typescript

프로젝트별 설치

# npm install typescript --save-dev
**TypeScript 설정 파일 (tsconfig.json)**

{
  "compilerOptions": {
    // JavaScript 출력 버전 설정
    "target": "es5",         // ES5 버전의 JavaScript로 컴파일

    // 모듈 시스템 설정
    "module": "commonjs",    // Node.js에서 주로 사용하는 CommonJS 모듈 시스템 사용

    // 타입 체크 관련 설정
    "strict": true,          // 모든 엄격한 타입-체킹 옵션 활성화
    
    // 모듈 호환성 설정
    "esModuleInterop": true, // CommonJS/AMD/UMD 모듈을 ES6 모듈처럼 사용 가능

    // 성능 최적화 설정
    "skipLibCheck": true,    // 선언 파일(*.d.ts)의 타입 체크 스킵

    // 파일 이름 일관성 설정
    "forceConsistentCasingInFileNames": true  // 파일 이름의 대소문자 일관성 강제
  }
}

 

 

2. TypeScript 기본 타입과 문법

2.1 기본 타입 시스템

TypeScript의 기본 타입들을 실제 사용 사례와 함께 살펴보겠습니다.

 

2.1.1 원시 타입 (Primitive Types)

// 기본적인 타입 선언
const userName: string = "김철수";
const userAge: number = 30;
const isActive: boolean = true;

// undefined와 null
const notInitialized: undefined = undefined;
const empty: null = null;

// any와 unknown의 차이점
let anyValue: any = 4;         // 어떤 타입이든 허용 (타입 검사를 하지 않음)
let unknownValue: unknown = 4;  // 어떤 타입이든 허용 (타입 검사를 함)

// any 사용 예시 (권장하지 않음)
anyValue = "문자열";  // OK
anyValue = true;     // OK
anyValue.toFixed();  // OK (컴파일 시점에서 오류를 발견하지 못함)

// unknown 사용 예시 (더 안전함)
unknownValue = "문자열";  // OK
unknownValue = true;     // OK
// unknownValue.toFixed();  // ❌ Error: 타입 검사가 필요함

// 타입 검사 후 사용
if (typeof unknownValue === 'number') {
    unknownValue.toFixed();  // OK
}

 

2.1.2 배열과 튜플

// 배열 타입 선언
const numbers: number[] = [1, 2, 3, 4, 5];
const names: Array<string> = ["철수", "영희", "민수"];

// 여러 타입을 가진 배열
const mixed: (string | number)[] = ["철수", 1, "영희", 2];

// 튜플 - 고정된 길이와 타입을 가진 배열
type UserInfo = [string, number, boolean];
const user: UserInfo = ["김철수", 30, true];  // 순서와 타입이 모두 일치해야 함

// 실제 사용 예시: React useState와 비슷한 구조
type State<T> = [T, (newValue: T) => void];

function useState<T>(initialValue: T): State<T> {
    let value: T = initialValue;
    const setValue = (newValue: T) => {
        value = newValue;
    };

    return [value, setValue];
}

 

2.1.3 객체 타입

// 객체 타입 정의
interface User {
    name: string;
    age: number;
    email?: string;  // 선택적 속성 (optional property)
    readonly id: number;  // 읽기 전용 속성
}

// 객체 생성
const newUser: User = {
    name: "김철수",
    age: 30,
    id: 1
};

// 인터페이스 확장
interface Employee extends User {
    department: string;
    role: string;
}

// 실제 사용 예시
const employee: Employee = {
    name: "김영희",
    age: 28,
    id: 2,
    department: "개발팀",
    role: "프론트엔드 개발자"
};

// 타입 별칭(Type Alias)을 사용한 객체 타입
type Point = {
    x: number;
    y: number;
    describe(): string;
};

const point: Point = {
    x: 10,
    y: 20,
    describe() {
        return `(${this.x}, ${this.y})`;
    }
};

 

2.1.4 함수 타입

// 기본적인 함수 타입
function add(a: number, b: number): number {
    return a + b;
}

// 화살표 함수와 타입
const multiply = (a: number, b: number): number => a * b;

// 함수 타입 정의
type MathOperation = (a: number, b: number) => number;

// 함수 타입 활용
const calculate = (
    operation: MathOperation, 
    a: number, 
    b: number
): number => {
    return operation(a, b);
};

// 오버로드된 함수 타입
function processValue(value: number): number;
function processValue(value: string): string;
function processValue(value: number | string): number | string {
    if (typeof value === "number") {
        return value * 2;
    }
    return value.toUpperCase();
}

2.2 타입 추론

TypeScript는 많은 경우 타입을 자동으로 추론할 수 있습니다:

// 변수 타입 추론
let message = "안녕하세요";  // 자동으로 string 타입으로 추론
let number = 42;           // 자동으로 number 타입으로 추론

// 배열 타입 추론
let items = [1, 2, 3];     // number[] 타입으로 추론
let mixed = [1, "hello"];  // (string | number)[] 타입으로 추론

// 객체 타입 추론
const user = {
    name: "김철수",
    age: 30,
    isAdmin: false
};  // { name: string; age: number; isAdmin: boolean; } 타입으로 추론

// 실제 사용 예시
const userList = [
    { id: 1, name: "김철수" },
    { id: 2, name: "이영희" }
];  // { id: number; name: string; }[] 타입으로 추론

// 함수 반환 타입 추론
function getFirstItem<T>(items: T[]) {
    return items[0];  // 반환 타입이 T 또는 undefined로 추론됨
}

 

3. TypeScript 중급 문법과 고급 기능

3.1 제네릭(Generics)

제네릭이란?

제네릭은 타입을 마치 함수의 매개변수처럼 사용할 수 있게 해주는 기능입니다. 여러 타입에서 동작하는 컴포넌트를 만들 수 있으며, 타입 정보를 유지하면서 타입을 유연하게 처리할 수 있습니다.

any와의 차이점

// any를 사용할 경우
function getFirstAny(arr: any[]): any {
    return arr[0];
}

const numberAny = getFirstAny([1, 2, 3]); // 타입: any
numberAny.toFixed(); // 컴파일러가 타입 체크를 못함

// 제네릭을 사용할 경우
function getFirst<T>(arr: T[]): T {
    return arr[0];
}

const number = getFirst([1, 2, 3]); // 타입: number
number.toFixed(); // 컴파일러가 타입 체크 가능
  • 타입 안정성
    • any: 모든 타입 검사를 비활성화
    • 제네릭: 타입 안정성을 유지하면서 유연성 제공
  • 타입 정보
    • any: 타입 정보가 완전히 소실됨
    • 제네릭: 타입 정보가 보존됨
  • 컴파일러 지원
    • any: IDE의 자동완성, 타입 체크 지원 불가
    • 제네릭: IDE의 모든 타입 관련 기능 지원

 

제네릭이 필요한 이유

  1. 타입 안정성: 컴파일 시점에서 타입 체크가 가능
  2. 코드 재사용성: 다양한 타입에 대해 동일한 로직 적용 가능
  3. 타입 추론: 컴파일러가 타입을 자동으로 추론

 

실제 사용 사례

// 제네릭 타입 정의
interface Repository<T> {
    findById: (id: number) => T;
    save: (item: T) => void;
}

// 제네릭 함수
const createRepository = <T>(): Repository<T> => {
    const items: T[] = [];

    return {
        findById: (id: number): T | undefined => {
            return items.find(item => item.id === id);
        },
        save: (item: T): void => {
            items.push(item);
        }
    };
};


//실제 사용
interface User {
    id: number;
    name: string;
}

// 유저 저장소 생성
const userRepository = createRepository<User>();

// 사용
userRepository.save({ id: 1, name: "Kim" });
const user = userRepository.findById(0); // { id: 1, name: "Kim" }

3.2 고급 타입

유니온 타입(Union Types)

두 개 이상의 타입을 하나의 타입으로 결합하는 방식입니다.

type StringOrNumber = string | number;

// 유니온 타입이 필요한 이유
function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        return " ".repeat(padding) + value;
    }
    return padding + value;
}

 

인터섹션 타입(Intersection Types)

여러 타입을 하나로 결합하여 모든 타입의 기능을 가진 단일 타입을 만드는 방식입니다.

interface ErrorHandling {
    success: boolean;
    error?: { message: string };
}

interface ArticleData {
    title: string;
    content: string;
}

type Article = ErrorHandling & ArticleData;

// 사용 예시
const article: Article = {
    success: true,
    title: "TypeScript 완벽 가이드",
    content: "TypeScript는..."
};

 

리터럴 타입(Literal Types)

특정 문자열이나 숫자 값 자체를 타입으로 지정하는 방식입니다.

// 옷 사이즈는 "S", "M", "L" 중에서만 선택할 수 있음
type Size = "S" | "M" | "L";

// 신호등은 "빨간색", "노란색", "초록색" 중에서만 가능
type TrafficLight = "Red" | "Yellow" | "Green";

// 주사위는 1부터 6까지의 숫자만 가능
type Dice = 1 | 2 | 3 | 4 | 5 | 6;


// 셔츠 주문 함수
function orderShirt(size: Size) {
    console.log(`주문하신 ${size} 사이즈 셔츠를 준비하겠습니다.`);
}

orderShirt("M");       // 성공: "M"은 허용된 값
orderShirt("XL");      // 에러: "XL"은 Size 타입에 없음

 

  • 유니온 타입: 여러 "타입들" 중 선택
  • 리터럴 타입: 특정 "값들" 중 선택

 

타입 가드(Type Guards)

런타임에서 타입 검사를 수행하여 타입을 보장하는 방식입니다.

interface Bird {
    fly(): void;
    layEggs(): void;
}

interface Fish {
    swim(): void;
    layEggs(): void;
}

// 타입 가드 함수
function isFish(pet: Fish | Bird): pet is Fish {
    if ((pet as Fish).swim !== undefined) {
        return true;
    } else {
        // pet is Bird
        return false;
    }
}

// 사용 예시
function moveAnimal(pet: Fish | Bird) {
    if (isFish(pet)) {
        pet.swim(); // 이 스코프에서는 pet이 Fish 타입임을 보장
    } else {
        pet.fly(); // 이 스코프에서는 pet이 Bird 타입임을 보장
    }
}

 

  • isFish 의 반환 타입인 pet is Fish 는 술어 타입으로 타입 가드 역할을 하는 특별한 종류의 리턴 타입입니다.
컴파일러에게 함수의 반환값에 따라 pet의 타입을 좁힐 수 있다는 힌트를 제공하는 역할을 합니다.
이를 통해 타입 가드(Type Guard)를 구현할 수 있게 되는 거죠. 따라서 isFish 함수의 내부 로직에 의해 false가 반환될 수도 있습니다. 이 경우에는 pet이 Fish 타입이 아니라는 것을 의미하게 됩니다.

 

 

keyof 연산자

객체 타입을 받아서 그 객체의 모든 프로퍼티 키를 나열한 것과 같은 문자열 리터럴 유니온 타입을 생성합니다.

interface User {
    id: number;
    name: string;
    email: string;
}

// keyof 사용
type UserKeys = keyof User; // "id" | "name" | "email"

// 실제 활용 예시
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const user: User = {
    id: 1,
    name: "John",
    email: "john@example.com"
};

const userName = getProperty(user, "name"); // 타입 안전하게 접근 가능

 

3.3 고급 타입 시스템의 활용

 

조건부 타입 (Conditional Types)

TypeScript의 강력한 기능 중 하나인 조건부 타입을 살펴보겠습니다:

// 기본 조건부 타입 문법
type IsString<T> = T extends string ? true : false;

// 실제 사용 예시
type A = IsString<string>;  // type A = true
type B = IsString<number>;  // type B = false

// 실용적인 예제
type NonNullable<T> = T extends null | undefined ? never : T;

// 유틸리티 조건부 타입
type ExtractTypeFromArray<T> = T extends Array<infer U> ? U : never;
type NumberType = ExtractTypeFromArray<number[]>;  // type NumberType = number

매핑된 타입 (Mapped Types)

기존 타입을 기반으로 새로운 타입을 생성하는 방법입니다:

// 기본 매핑된 타입
interface User {
    name: string;
    age: number;
    email: string;
}

// 모든 필드를 선택적으로 만들기
type PartialUser = {
    [K in keyof User]?: User[K];
};

// 모든 필드를 읽기 전용으로 만들기
type ReadonlyUser = {
    readonly [K in keyof User]: User[K];
};

// 실제 활용 예시
type Nullable<T> = {
    [K in keyof T]: T[K] | null;
};

const nullableUser: Nullable<User> = {
    name: "김철수",
    age: null,
    email: "example@email.com"
};

4.실전 TypeScript 활용

4.1 React와 TypeScript

컴포넌트 타입 정의

// Props 인터페이스 정의
interface ButtonProps {
    text: string;
    onClick: () => void;
    variant?: 'primary' | 'secondary';
    disabled?: boolean;
}

// 함수형 컴포넌트
const Button: React.FC<ButtonProps> = ({
    text,
    onClick,
    variant = 'primary',
    disabled = false
}) => {
    return (
        <button
            className={`btn btn-${variant}`}
            onClick={onClick}
            disabled={disabled}
        >
            {text}
        </button>
    );
};

// 제네릭 컴포넌트
interface ListProps<T> {
    items: T[];
    renderItem: (item: T) => React.ReactNode;
}

function List<T>({ items, renderItem }: ListProps<T>) {
    return (
        <ul>
            {items.map((item, index) => (
                <li key={index}>{renderItem(item)}</li>
            ))}
        </ul>
    );
}

4.2 상태 관리와 TypeScript

Redux와 TypeScript

// 액션 타입 정의
enum ActionTypes {
    ADD_TODO = 'ADD_TODO',
    TOGGLE_TODO = 'TOGGLE_TODO'
}

// 액션 인터페이스
interface AddTodoAction {
    type: ActionTypes.ADD_TODO;
    payload: {
        text: string;
    };
}

interface ToggleTodoAction {
    type: ActionTypes.TOGGLE_TODO;
    payload: {
        id: number;
    };
}

type TodoActionTypes = AddTodoAction | ToggleTodoAction;

// 상태 인터페이스
interface TodoState {
    todos: {
        id: number;
        text: string;
        completed: boolean;
    }[];
}

// 리듀서
const todoReducer = (
    state: TodoState = initialState,
    action: TodoActionTypes
): TodoState => {
    switch (action.type) {
        case ActionTypes.ADD_TODO:
            return {
                ...state,
                todos: [...state.todos, {
                    id: state.todos.length + 1,
                    text: action.payload.text,
                    completed: false
                }]
            };
        case ActionTypes.TOGGLE_TODO:
            return {
                ...state,
                todos: state.todos.map(todo =>
                    todo.id === action.payload.id
                        ? { ...todo, completed: !todo.completed }
                        : todo
                )
            };
        default:
            return state;
    }
};

5.성능 최적화와 모범 사례

5.1 타입 시스템 최적화

// ❌ 안티패턴: 과도한 any 사용
function processData(data: any) {
    return data.someProperty; // 타입 안정성 없음
}

// ✅ 좋은 패턴: 제네릭 사용
function processData<T extends { someProperty: any }>(data: T) {
    return data.someProperty;
}

// ❌ 안티패턴: 중복된 타입 정의
interface UserData {
    name: string;
    age: number;
}

interface AdminData {
    name: string;
    age: number;
    role: string;
}

// ✅ 좋은 패턴: 타입 확장 사용
interface BaseUser {
    name: string;
    age: number;
}

interface Admin extends BaseUser {
    role: string;
}

 

5.2 컴파일 최적화

{
    "compilerOptions": {
        "target": "es2020", // 컴파일된 코드의 대상 ECMAScript 버전을 지정합니다. 여기서는 ES2020을 대상으로 합니다.
        "module": "esnext", // 컴파일된 코드에서 사용할 모듈 시스템을 지정합니다. 여기서는 ES 모듈을 사용합니다.
        "strict": true, // 엄격한 타입 검사를 활성화합니다. 이는 더 나은 코드 품질과 안정성을 위해 권장됩니다.
        "skipLibCheck": true, // 선언 파일의 유효성 검사를 건너뜁니다. 이는 컴파일 속도를 향상시킬 수 있습니다.
        "forceConsistentCasingInFileNames": true, // 파일 이름의 대소문자 일관성을 강제합니다.
        "esModuleInterop": true, // CommonJS 모듈을 ES 모듈처럼 가져올 수 있도록 설정합니다. 이는 모듈 간 상호 운용성을 향상시킵니다.
        "moduleResolution": "node", // 모듈 해석 전략을 지정합니다. 여기서는 Node.js 스타일의 모듈 해석을 사용합니다.
        "resolveJsonModule": true, // JSON 파일을 모듈로 가져올 수 있도록 설정합니다.
        "isolatedModules": true, // 각 파일을 별도의 모듈로 컴파일하도록 설정합니다. 이는 더 나은 성능과 증분 컴파일을 가능하게 합니다.
        "noEmit": true, // 컴파일 결과를 파일로 내보내지 않도록 설정합니다. 이는 타입 검사만 수행하고 실제 코드 생성은 하지 않습니다.
        "jsx": "react-jsx" // JSX 코드 변환 방식을 지정합니다. 여기서는 React의 새로운 JSX 변환을 사용합니다.
    }
}

 

6.디버깅과 테스팅

6.1 TypeScript 디버깅

// 타입 단언 사용
function assertIsString(val: any): asserts val is string {
    if (typeof val !== "string") {
        throw new Error("Not a string!");
    }
}

// asserts는 TypeScript에서 함수가 어떤 조건을 만족하지 않으면 예외를 던질 것이라는 것을 나타내는 키워드입니다. 
// 이를 "어서션 함수(Assertion Function)"라고 부릅니다.

// 디버깅을 위한 타입 가드
function isError(error: unknown): error is Error {
    return error instanceof Error;
}

try {
    // 어떤 작업 수행
} catch (error: unknown) {
    if (isError(error)) {
        console.error(error.message);  // 타입 안전하게 에러 메시지 접근
    }
}

//isError 함수는 error 매개변수가 Error 타입의 인스턴스인지 확인하는 타입 가드 함수입니다.
//try 블록에서 어떤 작업을 수행하고, 에러가 발생하면 catch 블록에서 처리합니다.
//catch 블록에서는 error 매개변수를 unknown 타입으로 받아, 어떤 타입의 에러든 처리할 수 있도록 합니다.
//isError 함수를 사용하여 error가 Error 타입인지 확인하고, 맞다면 error.message를 안전하게 사용할 수 있습니다.

 

7.마무리

TypeScript는 현대 웹 개발에서 필수적인 도구가 되었습니다. 이 가이드를 통해 기본 개념부터 고급 기능까지 다뤄보았습니다. 실제 프로젝트에서 TypeScript를 활용하면서 다음 사항들을 항상 고려하세요:

  • 타입 안정성과 코드 품질 향상
  • 개발자 경험 개선
  • 생산성 향상
  • 버그 조기 발견

TypeScript를 효과적으로 활용하면 더 안정적이고 유지보수가 용이한 애플리케이션을 개발할 수 있습니다.

이 글이 TypeScript를 시작하거나 깊이 있게 이해하는 데 도움이 되었기를 바랍니다. 추가 질문이나 의견이 있다면 댓글로 남겨주세요.

728x90
반응형