웹 프로그래밍

TS: 타입스크립트

2025-12-08

TypeScript 필요성

TypeScript의 필요성은 코드 안정성 확보, 유지보수 및 협업 효율 증대, 생산성 및 개발 품질 향상이라는 세 가지 핵심 이유로 설명할 수 있으며, TypeScript 도입이 급증하는 가장 근본적인 이유입니다.

정적 타입의 유용성

  • 코드 안정성 확보: 정적 타입 시스템을 사용해서 안정적인 코드를 작성
  • 유지보수 및 협업 효율 증대: 명확한 타입 정의와 인터페이스 덕분에 빠르게 이해하고 수정
  • 생산성 및 개발 품질 향상: 타입 추론, 자동완성, 강력한 리팩토링 도구 등 IDE 지원이 강화

TypeScript의 Type

TypeScript의 Type

TypeScript의 Type 기초

  • number: 숫자 타입 \(\rightarrow\) let a: number = 10;
  • string: 문자열 타입 \(\rightarrow\) let b: string = "hello";
  • boolean: 불리언 타입 \(\rightarrow\) let c: boolean = true;
  • array: 배열 타입 \(\rightarrow\) let arr: number[] = [1, 2, 3];
  • tuple: 고정된 길이와 타입을 가지는 배열 \(\rightarrow\) let tuple: [string, number] = ["age", 30];
  • union: 여러 타입 중 하나 \(\rightarrow\) let value: string | number = "hi";
  • literal: 특정 값만 허용 \(\rightarrow\) let dir: "left" | "right" = "left";
  • object: 객체 타입 \(\rightarrow\) let obj: { name: string; age: number } = { name: "Kim", age: 25 };
  • enum: 열거형 타입 \(\rightarrow\) enum Color { Red, Green, Blue }
  • unknown: 타입을 알 수 없을 때 사용 (any보다 안전) \(\rightarrow\) let value: unknown = "hi";
  • any: 아무 타입이나 허용 (가능하면 사용 지양) \(\rightarrow\) let value: any = "hi";
  • void: 반환값이 없는 함수에서 사용 \(\rightarrow\) function log(msg: string): void { ... }
  • null / undefined: 각각 null, undefined 값 \(\rightarrow\) let a: null = null;, let b: undefined = undefined;

any, vod, null, undefined, unknown

  • any: 타입 검사 자체를 건너뜀, 타입스크립트의 타입 안전성 상실, 런타임 오류 가능성 높음
  • unknown: 타입확인 없이 사용할 수 없음, 타입 정제(narrowing) 전에는 연산 불가, any보다 타입 안전성 높음
  • null: 명시적으로 아무 값도 존재하지 않음을 나타냄
  • undefined: 값이 할당되지 않은 상태를 의미 \(\rightarrow\) 기본 초기값
  • void: 함수 반환값이 없음을 명시할 때 사용, 변수 타입으로는 거의 사용하지 않으며, void 변수에는 undefined만 할당 가능(일부 설정에선 null 허용)

type, class, interface

  • class: 실행 코드, 상태, 메소드를 실제로 구현하고 인스턴스를 만들어야 할 때(객체지향 요구시)
  • type: 다양한 타입 조합·표현이 필요하거나, 단일 객체 외의 구조(유니온, 튜플 등)까지 필요할 때
  • interface: 여러 개체가 공통 구조를 따르도록 하거나 클래스의 설계 규약(contract)이 필요할 때

class

  • 실제 동작(메소드)과 데이터를 가진 객체의 인스턴스를 생성할 때(객체지향 패턴)
  • 상태와 로직을 함께 관리할 때
  • ES6 표준의 Class 기반 객체지향 프로그래밍, 생성자(constructor)·메소드 포함, new로 인스턴스 생성
  • implements를 통해 interface 구조 보장

type

  • 복잡한 타입 조합(유니온, 교차, 튜플 등)이나 함수 타입, 객체 이외의 타입 표현이 필요할 때
  • 객체 형태, 기본 타입, 유니온 등 모든 타입에 이름을 부여할 수 있고, 유연성 높음
  • 확장성은 있지만 interface만큼 강하지 않음

interface

  • 객체의 구조(속성, 메소드)를 정의하거나, 클래스의 설계도를 만들거나, 여러 곳에서 동일한 객체 타입을 약속할 때 주로 사용
  • 객체 중심 구조에 적합, extends/implements 지원, 선언적 확장(declaration merging) 가능
  • 주로 클래스와 함께 구현(contract) 용도로 활용

간단한 정렬

정렬(Sorting)이란, 주어진 데이터(배열, 리스트 등)를 일정한 기준(오름차순, 내림차순 등)에 따라 순서대로 나열하는 알고리즘입니다.
정렬은 데이터 탐색, 중복 제거, 효율적인 데이터 관리 등 다양한 컴퓨터 과학 문제에서 매우 중요한 역할을 합니다. 대표적인 정렬 알고리즘으로는 버블 정렬, 선택 정렬, 삽입 정렬, 퀵 정렬, 병합 정렬 등이 있으며, 각각의 알고리즘은 시간 복잡도와 공간 복잡도, 구현 난이도에 차이가 있습니다.

정렬 함수 테스트 코드 작성

  • src/_tests_/sort.test.ts
import { simpleSort } from '@/sort';
import '@jest/globals';

describe('숫자 정렬 함수', () => {
    test('기본적인 정렬 동작', () => {
        expect(simpleSort([3, 1, 2])).toEqual([1, 2, 3]);
        expect(simpleSort([10, -5, 0, 2])).toEqual([-5, 0, 2, 10]);
        expect(simpleSort([1])).toEqual([1]);
        expect(simpleSort([])).toEqual([]);
        expect(simpleSort([0, -1, 1, -2, 2])).toEqual([-2, -1, 0, 1, 2]);
        expect(simpleSort([1, 2, 3, 4, 5])).toEqual([1, 2, 3, 4, 5]);
        expect(simpleSort([2, 3, 2, 1, 1])).toEqual([1, 1, 2, 2, 3]);
    });
});

간단한 정렬 함수 작성

  • src/sort.ts
export function simpleSort(arr) {
    const n = arr.length;
    const result = [...arr];
    for (let i = 0; i < n - 1; i++) {
        for (let j = 0; j < n - i - 1; j++) {
            if (result[j] > result[j + 1]) {
                const temp = result[j];
                result[j] = result[j + 1];
                result[j + 1] = temp;
            }
        }
    }
    return result;
}

간단한 정렬 함수에 타입을 추가

export function simpleSort(arr: number[]): number[] {
  const n = arr.length;
  const result = [...arr];
  for (let i = 0; i < n - 1; i++) {
    for (let j = 0; j < n - i - 1; j++) {
      if (result[j] > result[j + 1]) {
        const temp = result[j];
        result[j] = result[j + 1];
        result[j + 1] = temp;
      }
    }
  }
  return result;
}

매개변수 추가를 위한 테스트 코드 작성

import { simpleSort } from '@/sort';
import '@jest/globals';

describe('숫자 정렬 함수', () => {
    test('기본적인 정렬 동작', () => {
        expect(simpleSort([3, 1, 2])).toEqual([1, 2, 3]);
        expect(simpleSort([10, -5, 0, 2])).toEqual([-5, 0, 2, 10]);
        expect(simpleSort([1])).toEqual([1]);
        expect(simpleSort([])).toEqual([]);
        expect(simpleSort([0, -1, 1, -2, 2])).toEqual([-2, -1, 0, 1, 2]);
        expect(simpleSort([1, 2, 3, 4, 5])).toEqual([1, 2, 3, 4, 5]);
        expect(simpleSort([2, 3, 2, 1, 1])).toEqual([1, 1, 2, 2, 3]);
    });
});

describe('숫자 내림차순 정렬 함수', () => {
    test('기본적인 정렬 동작', () => {
        expect(simpleSort([3, 1, 2], 'desc')).toEqual([3, 2, 1]);
        expect(simpleSort([10, -5, 0, 2], 'desc')).toEqual([10, 2, 0, -5]);
        expect(simpleSort([1], 'desc')).toEqual([1]);
        expect(simpleSort([], 'desc')).toEqual([]);
        expect(simpleSort([0, -1, 1, -2, 2], 'desc')).toEqual([2, 1, 0, -1, -2]);
        expect(simpleSort([5, 4, 3, 2, 1], 'desc')).toEqual([5, 4, 3, 2, 1]);
        expect(simpleSort([3, 2, 2, 1, 1], 'desc')).toEqual([3, 2, 2, 1, 1]);
    });
});

매개변수 추가를 위한 정렬 코드 수정

export function simpleSort(arr: number[], ord = 'asc'): number[] {
  const n = arr.length;
  const result = [...arr];
  
  for (let i = 0; i < n - 1; i++) {
    for (let j = 0; j < n - i - 1; j++) {
      let shouldSwap = false;
      
      if (ord === 'desc') {
        // 내림차순: 앞의 요소가 뒤의 요소보다 작으면 교체
        if (result[j] < result[j + 1]) {
          shouldSwap = true;
        }
      } else {
        // 오름차순: 앞의 요소가 뒤의 요소보다 크면 교체
        if (result[j] > result[j + 1]) {
          shouldSwap = true;
        }
      }
      
      if (shouldSwap) {
        const temp = result[j];
        result[j] = result[j + 1];
        result[j + 1] = temp;
      }
    }
  }
  return result;
}

모든 테스트 통과 후 코드 개선(리팩토링)

type SortOrder = 'asc' | 'desc'; // 정렬 방향(순서)
type CompareFn = (a: number, b: number) => number; // 비교 함수

const createCompareFunction = (order: SortOrder): CompareFn => { // 비교 함수 생성
  return order === 'desc'
    ? (a: number, b: number) => b - a
    : (a: number, b: number) => a - b;
};

export function simpleSort(arr: number[], ord: SortOrder = 'asc'): number[] {
  if (arr.length <= 1) {
    return [...arr];
  }

  const result = [...arr];
  const compare = createCompareFunction(ord);
  const n = result.length;

  for (let i = 0; i < n - 1; i++) {
    let swapped = false;
    for (let j = 0; j < n - i - 1; j++) {
      if (compare(result[j], result[j + 1]) > 0) {
        [result[j], result[j + 1]] = [result[j + 1], result[j]];
        swapped = true;
      }
    }
    if (!swapped) {
      break;
    }
  }
  return result;
}

제네릭 타입을 위한 테스트 코드 작성

import { simpleSort } from '@/sort';
import '@jest/globals';

describe('숫자 정렬 함수', () => {
    test('기본적인 정렬 동작', () => {
        expect(simpleSort([3, 1, 2])).toEqual([1, 2, 3]);
        expect(simpleSort([10, -5, 0, 2])).toEqual([-5, 0, 2, 10]);
        expect(simpleSort([1])).toEqual([1]);
        expect(simpleSort([])).toEqual([]);
        expect(simpleSort([0, -1, 1, -2, 2])).toEqual([-2, -1, 0, 1, 2]);
        expect(simpleSort([1, 2, 3, 4, 5])).toEqual([1, 2, 3, 4, 5]);
        expect(simpleSort([2, 3, 2, 1, 1])).toEqual([1, 1, 2, 2, 3]);
    });
});

describe('숫자 내림차순 정렬 함수', () => {
    test('기본적인 정렬 동작', () => {
        expect(simpleSort([3, 1, 2], 'desc')).toEqual([3, 2, 1]);
        expect(simpleSort([10, -5, 0, 2], 'desc')).toEqual([10, 2, 0, -5]);
        expect(simpleSort([1], 'desc')).toEqual([1]);
        expect(simpleSort([], 'desc')).toEqual([]);
        expect(simpleSort([0, -1, 1, -2, 2], 'desc')).toEqual([2, 1, 0, -1, -2]);
        expect(simpleSort([5, 4, 3, 2, 1], 'desc')).toEqual([5, 4, 3, 2, 1]);
        expect(simpleSort([3, 2, 2, 1, 1], 'desc')).toEqual([3, 2, 2, 1, 1]);
    });
});

describe('제네릭 정렬 함수', () => {
    test('숫자 정렬', () => {
        expect(simpleSort([3, 1, 2])).toEqual([1, 2, 3]);
        expect(simpleSort([10, -5, 0, 2])).toEqual([-5, 0, 2, 10]);
        expect(simpleSort(['c', 'a', 'b'])).toEqual(['a', 'b', 'c']);
        expect(simpleSort(['banana', 'apple', 'cherry'])).toEqual(['apple', 'banana', 'cherry']);        
        expect(simpleSort([1, 2, 3], 'desc')).toEqual([3, 2, 1]);
        expect(simpleSort(['a', 'b', 'c'], 'desc')).toEqual(['c', 'b', 'a']);
        expect(simpleSort([])).toEqual([]);
    });
});

제네릭 타입을 위한 정렬 함수 작성

type SortOrder = 'asc' | 'desc';
type CompareFn<T> = (a: T, b: T) => number;

function createCompareFunction<T>(order: SortOrder): CompareFn<T> {
  if (order === 'desc') {
    return function(a: T, b: T): number {
      if (a < b) return 1;
      if (a > b) return -1;
      return 0;
    };
  } else {
    return function(a: T, b: T): number {
      if (a > b) return 1;
      if (a < b) return -1;
      return 0;
    };
  }
}

export function simpleSort<T>(arr: T[], ord: SortOrder = 'asc'): T[] {  
  if (arr.length <= 1) {
    return [...arr];
  }

  const result = [...arr];
  const compare = createCompareFunction<T>(ord);
  const n = result.length;

  for (let i = 0; i < n - 1; i++) {
    let swapped = false;
    for (let j = 0; j < n - i - 1; j++) {
      if (compare(result[j], result[j + 1]) > 0) {
        const temp = result[j];
        result[j] = result[j + 1];
        result[j + 1] = temp;
        swapped = true;
      }
    }
    if (!swapped) {
      break; // 최적화: 교체가 없으면 이미 정렬된 상태
    }
  }
  return result;
}

연결리스트

연결리스트는 각 노드가 데이터와 다음(또는 이전) 노드에 대한 참조를 가지는 자료구조로, 동적으로 크기가 변하는 선형 구조입니다.

1. Node/LinkedList 클래스 정의

  • data와 이전/다음 노드에 대한 참조 prev, next
  • head, tail, _size와 같은 내부 상태를 private으로 선언
  • 생성자에서 초기 상태를 설정
  • size(), isEmpty(), clear() 등 기본 메서드를 구현

2. 연산 메서드 및 편의 메서드 구현

  • addFirst(data), addLast(data)
  • removeFirst(), removeLast()
  • search(data), printList(), printListReverse()
  • getFirst(), getLast()

3. 테스트 및 사용 예시 작성

  • 다양한 케이스로 동작 확인

Node

  • class vs type 차이점
    • class: Class는 런타임에 존재하는 실제 객체, 인스턴스를 생성할 수 있음
    • type: Type은 런타임에 존재하지 않는 타입, 단순히 타입을 정의하고 사용할 수 있음
class Node<T> {
    public readonly data: T;
    public prev: Node<T> | null = null;
    public next: Node<T> | null = null;
    constructor(data: T) {
        this.data = data;
    }
}

LinkedList

  • private: 클래스 내부에서만 접근 가능
  • public: 클래스 외부에서 접근 가능
  • readonly: 초기화 후 변경 불가
export class LinkedList<T> {
    private head: Node<T> | null = null;
    private tail: Node<T> | null = null;
    private _size: number = 0;

    public size(): number {
        return this._size;
    }
    // ... 생략
}

const, let, var

  • const, let: block scope
  • var: function scope
    • hoisting된 var 변수는 할당이 이루어지기 전까지는 undefined(큰 의미는 없음)
    • var는 함수 스코프이지만, 전역에서 접근 가능하다는 점(알아두면 가끔 도움이 됨)
    function a() {
      try {
        var r = from();
      } catch (e) {
        var r = [];
      }
    }
public append(data: T): void {
    const newNode = new Node<T>(data);
    if (this.head === null) {
        this.head = newNode;
        this.tail = newNode;
    } else {
        if (this.tail !== null) {
            this.tail.next = newNode;
            newNode.prev = this.tail;
            this.tail = newNode;
        }
    }
    this._size++;
}
  • readonly를 적절히 활용하여 인스턴스 할당을 방지
public printListReverse(): readonly T[] {
    const result: T[] = [];
    let current: Node<T> | null = this.tail;
    while (current !== null) {
        result.push(current.data);
        current = current.prev;
    }
    return result;
}

전체 코드

class Node<T> {
    public readonly data: T;
    public prev: Node<T> | null = null;
    public next: Node<T> | null = null;
    constructor(data: T) {
        this.data = data;
    }
}

export class LinkedList<T> {
    private head: Node<T> | null = null;
    private tail: Node<T> | null = null;
    private _size: number = 0;

    public size(): number {
        return this._size;
    }

    public append(data: T): void {
        const newNode = new Node<T>(data);

        if (this.head === null) {
            this.head = newNode;
            this.tail = newNode;
        } else {
            if (this.tail !== null) {
                this.tail.next = newNode;
                newNode.prev = this.tail;
                this.tail = newNode;
            }
        }
        this._size++;
    }

    public delete(data: T): boolean {
        if (this.head === null) {
            return false;
        }
        let current: Node<T> | null = this.head;
        while (current !== null) {
            if (current.data === data) {
                this.removeNode(current);
                return true;
            }
            current = current.next;
        }
        return false;
    }

    private removeNode(nodeToRemove: Node<T>): void {
        if (nodeToRemove === this.head && nodeToRemove === this.tail) {
            this.head = null;
            this.tail = null;
        }
        else if (nodeToRemove === this.head) {
            this.head = nodeToRemove.next;
            if (this.head !== null) {
                this.head.prev = null;
            }
        }
        else if (nodeToRemove === this.tail) {
            this.tail = nodeToRemove.prev;
            if (this.tail !== null) {
                this.tail.next = null;
            }
        }
        else {
            if (nodeToRemove.prev !== null) {
                nodeToRemove.prev.next = nodeToRemove.next;
            }
            if (nodeToRemove.next !== null) {
                nodeToRemove.next.prev = nodeToRemove.prev;
            }
        }
        this._size--;
    }

    public search(data: T): T | null {
        let current: Node<T> | null = this.head;
        while (current !== null) {
            if (current.data === data) {
                return current.data;
            }
            current = current.next;
        }
        return null;
    }

    public printList(): readonly T[] {
        const result: T[] = [];
        let current: Node<T> | null = this.head;

        while (current !== null) {
            result.push(current.data);
            current = current.next;
        }
        return result;
    }

    public printListReverse(): readonly T[] {
        const result: T[] = [];
        let current: Node<T> | null = this.tail;
        while (current !== null) {
            result.push(current.data);
            current = current.prev;
        }
        return result;
    }

    public getFirst(): T | null {
        return this.head?.data ?? null;
    }

    public getLast(): T | null {
        return this.tail?.data ?? null;
    }

    public isEmpty(): boolean {
        return this._size === 0;
    }

    public clear(): void {
        this.head = null;
        this.tail = null;
        this._size = 0;
    }
}

큐(Queue)

큐(Queue)는 선입선출(FIFO: First In First Out) 방식으로 작동하는 자료구조입니다. 데이터가 들어온 순서대로 처리되며, 가장 먼저 들어온 데이터가 가장 먼저 나갑니다.

Queue 클래스 구조

  • 제네릭 타입 <T>: 다양한 타입의 데이터를 저장 가능
  • 내부 구현: 배열을 사용하여 큐를 구현
  • FIFO 방식: enqueue로 추가, dequeue로 제거
  • 스택 메서드도 제공: push, pop, top 메서드로 LIFO 방식도 지원

Queue 클래스 정의

export class Queue<T> {
    private items: T[] = [];
    
    // 큐 메서드
    public enqueue(item: T): void
    public dequeue(): T | undefined
    public front(): T | undefined
    public rear(): T | undefined
    
    // 스택 메서드
    public push(item: T): void
    public pop(): T | undefined
    public top(): T | undefined
    
    // 유틸리티 메서드
    public isEmpty(): boolean
    public get size(): number
    public clear(): void
}

enqueue / dequeue

  • enqueue: 큐의 끝에 값을 추가
  • dequeue: 큐의 앞에서 값을 제거하고 반환 (FIFO)
public enqueue(item: T): void {
    this.items.push(item);
}

public dequeue(): T | undefined {
    return this.items.shift();
}

front / rear

  • front: 큐의 앞에 있는 값을 반환 (제거하지 않음)
  • rear: 큐의 뒤에 있는 값을 반환 (제거하지 않음)
public front(): T | undefined {
    return this.items.length > 0 ? this.items[0] : undefined;
}

public rear(): T | undefined {
    const length = this.items.length;
    return length > 0 ? this.items[length - 1] : undefined;
}

스택 메서드 (LIFO)

  • push: 스택의 끝에 값을 추가 (enqueue와 동일)
  • pop: 스택의 끝에서 값을 제거하고 반환 (LIFO)
  • top: 스택의 끝에 있는 값을 반환 (rear와 동일)
public push(item: T): void {
    this.items.push(item);
}

public pop(): T | undefined {
    return this.items.pop();
}

public top(): T | undefined {
    const length = this.items.length;
    return length > 0 ? this.items[length - 1] : undefined;
}

size, isEmpty, clear

  • size: getter로 큐의 크기를 반환 (setter는 에러 발생)
  • isEmpty: 큐가 비어있는지 확인
  • clear: 큐의 모든 요소를 제거
public isEmpty(): boolean {
    return this.items.length === 0;
}

public get size(): number {
    return this.items.length;
}

public set size(_value: number) {
    throw new Error("Size cannot be modified directly");
}

public clear(): void {
    this.items = [];
}

전체 코드

export class Queue<T> {
    private items: T[] = [];

    public enqueue(item: T): void {
        this.items.push(item);
    }

    public dequeue(): T | undefined {
        return this.items.shift();
    }

    public front(): T | undefined {
        return this.items.length > 0 ? this.items[0] : undefined;
    }

    public rear(): T | undefined {
        const length = this.items.length;
        return length > 0 ? this.items[length - 1] : undefined;
    }

    public push(item: T): void {
        this.items.push(item);
    }

    public pop(): T | undefined {
        return this.items.pop();
    }

    public top(): T | undefined {
        const length = this.items.length;
        return length > 0 ? this.items[length - 1] : undefined;
    }

    public isEmpty(): boolean {
        return this.items.length === 0;
    }

    public get size(): number {
        return this.items.length;
    }

    public set size(_value: number) {
        throw new Error("Size cannot be modified directly");
    }

    public clear(): void {
        this.items = [];
    }
}

이진 탐색 트리(Binary Tree)

이진 탐색 트리(Binary Search Tree, BST)는 각 노드가 최대 두 개의 자식을 가지는 트리 자료구조이며, 왼쪽 자식은 부모보다 작고, 오른쪽 자식은 부모보다 큰 값을 가지는 특성을 가집니다.

BinaryTree 클래스 구조

  • 제네릭 타입 <T>: 다양한 타입의 데이터를 저장 가능
  • TreeNode 클래스: 각 노드는 data, left, right 속성을 가짐
  • CompareFn 타입: 커스텀 비교 함수를 통해 정렬 순서 정의
  • 기본 비교 함수: 숫자, 문자열 등 기본 타입은 자동 비교 함수 사용 가능
  • 순회 메서드: 중위, 전위, 후위, 레벨 순회 지원

TreeNode 클래스

각 노드는 데이터와 왼쪽/오른쪽 자식 노드에 대한 참조를 가집니다.

class TreeNode<T> {
    public data: T;
    public left: TreeNode<T> | null = null;
    public right: TreeNode<T> | null = null;

    constructor(data: T) {
        this.data = data;
    }
}

CompareFn 타입 및 기본 비교 함수

비교 함수는 두 값을 비교하여 음수(a < b), 0(a === b), 양수(a > b)를 반환합니다.

export type CompareFn<T> = (a: T, b: T) => number;

function defaultCompare<T extends string | number>(a: T, b: T): number {
    if (a < b) return -1;
    if (a > b) return 1;
    return 0;
}

BinaryTree 클래스 정의

생성자에서 비교 함수를 선택적으로 받으며, 제공되지 않으면 기본 타입 비교 함수를 사용합니다.

export class BinaryTree<T> {
    private root: TreeNode<T> | null = null;
    private readonly compareFn: CompareFn<T>;

    constructor(compareFn?: CompareFn<T>) {
        if (compareFn !== undefined) {
            this.compareFn = compareFn;
        } else {
            this.compareFn = defaultCompare as CompareFn<T>;
        }
    }
    
    // 주요 메서드
    public insert(value: T): void
    public search(value: T): T | null
    public remove(value: T): void
    public inOrderTraversal(): T[]
    public preOrderTraversal(): T[]
    public postOrderTraversal(): T[]
    public levelOrderTraversal(): T[]
}

insert

트리에 값을 삽입합니다. 재귀적으로 노드를 탐색하여 적절한 위치에 삽입합니다.

public insert(value: T): void {
    this.root = this.insertNode(this.root, value);
}

private insertNode(node: TreeNode<T> | null, value: T): TreeNode<T> {
    if (node === null) {
        return new TreeNode(value);
    }

    const comparison = this.compareFn(value, node.data);
    if (comparison < 0) {
        node.left = this.insertNode(node.left, value);
    } else if (comparison > 0) {
        node.right = this.insertNode(node.right, value);
    }

    return node;
}

remove

트리에서 값을 삭제합니다. 세 가지 경우를 처리합니다.

  1. 자식이 없는 경우: 노드를 삭제
  2. 자식이 하나인 경우: 자식을 부모 위치로 이동
  3. 자식이 둘인 경우: 오른쪽 서브트리의 최소값으로 대체
public remove(value: T): void {
    this.root = this.removeNode(this.root, value);
}

private removeNode(node: TreeNode<T> | null, value: T): TreeNode<T> | null {
    if (node === null) {
        return null;
    }

    const comparison = this.compareFn(value, node.data);
    if (comparison < 0) {
        node.left = this.removeNode(node.left, value);
    } else if (comparison > 0) {
        node.right = this.removeNode(node.right, value);
    } else {
        // 삭제할 노드를 찾은 경우
        if (node.left === null) {
            return node.right;
        } else if (node.right === null) {
            return node.left;
        } else {
            // 오른쪽 서브트리의 최소값을 찾아서 옮김
            const minValue = this.findMin(node.right);
            node.data = minValue;
            node.right = this.removeNode(node.right, minValue);
        }
    }

    return node;
}

private findMin(node: TreeNode<T>): T {
    while (node.left !== null) {
        node = node.left;
    }
    return node.data;
}

중위 순회 (In-order Traversal)

왼쪽 자식 → 루트 → 오른쪽 자식 순서로 방문합니다. 이진 탐색 트리에서는 오름차순으로 정렬된 결과를 얻을 수 있습니다.

public inOrderTraversal(): T[] {
    const result: T[] = [];
    this.inOrder(this.root, result);
    return result;
}

private inOrder(node: TreeNode<T> | null, result: T[]): void {
    if (node !== null) {
        this.inOrder(node.left, result);
        result.push(node.data);
        this.inOrder(node.right, result);
    }
}

전위 순회 (Pre-order Traversal)

루트 → 왼쪽 자식 → 오른쪽 자식 순서로 방문합니다.

public preOrderTraversal(): T[] {
    const result: T[] = [];
    this.preOrder(this.root, result);
    return result;
}

private preOrder(node: TreeNode<T> | null, result: T[]): void {
    if (node !== null) {
        result.push(node.data);
        this.preOrder(node.left, result);
        this.preOrder(node.right, result);
    }
}

후위 순회 (Post-order Traversal)

왼쪽 자식 → 오른쪽 자식 → 루트 순서로 방문합니다.

public postOrderTraversal(): T[] {
    const result: T[] = [];
    this.postOrder(this.root, result);
    return result;
}

private postOrder(node: TreeNode<T> | null, result: T[]): void {
    if (node !== null) {
        this.postOrder(node.left, result);
        this.postOrder(node.right, result);
        result.push(node.data);
    }
}

레벨 순회 (Level-order Traversal)

레벨별로 왼쪽에서 오른쪽으로 방문합니다. 큐를 사용하여 BFS 방식으로 구현합니다.

public levelOrderTraversal(): T[] {
    const result: T[] = [];
    const root = this.root;
    if (root === null) {
        return result;
    }

    const queue: TreeNode<T>[] = [root];
    let index = 0;
    while (index < queue.length) {
        const node = queue[index];
        index++;
        result.push(node.data);

        const left = node.left;
        const right = node.right;
        if (left !== null) {
            queue.push(left);
        }
        if (right !== null) {
            queue.push(right);
        }
    }

    return result;
}

전체 코드

class TreeNode<T> {
    public data: T;
    public left: TreeNode<T> | null = null;
    public right: TreeNode<T> | null = null;

    constructor(data: T) {
        this.data = data;
    }
}

export type CompareFn<T> = (a: T, b: T) => number;

function defaultCompare<T extends string | number>(a: T, b: T): number {
    if (a < b) return -1;
    if (a > b) return 1;
    return 0;
}

export class BinaryTree<T> {
    private root: TreeNode<T> | null = null;
    private readonly compareFn: CompareFn<T>;

    constructor(compareFn?: CompareFn<T>) {
        if (compareFn !== undefined) {
            this.compareFn = compareFn;
        } else {
            this.compareFn = defaultCompare as CompareFn<T>;
        }
    }

    public insert(value: T): void {
        this.root = this.insertNode(this.root, value);
    }

    private insertNode(node: TreeNode<T> | null, value: T): TreeNode<T> {
        if (node === null) {
            return new TreeNode(value);
        }

        const comparison = this.compareFn(value, node.data);
        if (comparison < 0) {
            node.left = this.insertNode(node.left, value);
        } else if (comparison > 0) {
            node.right = this.insertNode(node.right, value);
        }

        return node;
    }

    public search(value: T): T | null {
        return this.searchNode(this.root, value);
    }

    private searchNode(node: TreeNode<T> | null, value: T): T | null {
        if (node === null) {
            return null;
        }

        const comparison = this.compareFn(value, node.data);
        if (comparison === 0) {
            return node.data;
        } else if (comparison < 0) {
            return this.searchNode(node.left, value);
        } else {
            return this.searchNode(node.right, value);
        }
    }

    public remove(value: T): void {
        this.root = this.removeNode(this.root, value);
    }

    private removeNode(node: TreeNode<T> | null, value: T): TreeNode<T> | null {
        if (node === null) {
            return null;
        }

        const comparison = this.compareFn(value, node.data);
        if (comparison < 0) {
            node.left = this.removeNode(node.left, value);
        } else if (comparison > 0) {
            node.right = this.removeNode(node.right, value);
        } else {
            if (node.left === null) {
                return node.right;
            } else if (node.right === null) {
                return node.left;
            } else {
                const minValue = this.findMin(node.right);
                node.data = minValue;
                node.right = this.removeNode(node.right, minValue);
            }
        }

        return node;
    }

    private findMin(node: TreeNode<T>): T {
        while (node.left !== null) {
            node = node.left;
        }
        return node.data;
    }

    public inOrderTraversal(): T[] {
        const result: T[] = [];
        this.inOrder(this.root, result);
        return result;
    }

    private inOrder(node: TreeNode<T> | null, result: T[]): void {
        if (node !== null) {
            this.inOrder(node.left, result);
            result.push(node.data);
            this.inOrder(node.right, result);
        }
    }

    public preOrderTraversal(): T[] {
        const result: T[] = [];
        this.preOrder(this.root, result);
        return result;
    }

    private preOrder(node: TreeNode<T> | null, result: T[]): void {
        if (node !== null) {
            result.push(node.data);
            this.preOrder(node.left, result);
            this.preOrder(node.right, result);
        }
    }

    public postOrderTraversal(): T[] {
        const result: T[] = [];
        this.postOrder(this.root, result);
        return result;
    }

    private postOrder(node: TreeNode<T> | null, result: T[]): void {
        if (node !== null) {
            this.postOrder(node.left, result);
            this.postOrder(node.right, result);
            result.push(node.data);
        }
    }

    public levelOrderTraversal(): T[] {
        const result: T[] = [];
        const root = this.root;
        if (root === null) {
            return result;
        }

        const queue: TreeNode<T>[] = [root];
        let index = 0;
        while (index < queue.length) {
            const node = queue[index];
            index++;
            result.push(node.data);

            const left = node.left;
            const right = node.right;
            if (left !== null) {
                queue.push(left);
            }
            if (right !== null) {
                queue.push(right);
            }
        }

        return result;
    }
}

업데이트 이력

버전 변경 이력

버전 날짜 변경 내용
v20251103 2025-11-03 BinaryTree 자료구조 추가
v20251013 2025-10-13 Queue 자료구조 추가
v20250929 2025-09-29 LinkedList 추가
v20250922 2025-09-22 Type 관련 내용으로 재편집