본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 4장: using 선언과 명시적 리소스 관리
2026년 1월 28일·프로그래밍·

4장: using 선언과 명시적 리소스 관리

TypeScript 5.2에서 도입된 using 선언과 Symbol.dispose를 활용한 명시적 리소스 관리 패턴을 실전 예제와 함께 심층 분석합니다.

12분700자8개 섹션
typescriptperformancedevtools
공유
typescript-deepdive4 / 12
123456789101112
이전3장: TC39 표준 데코레이터 완벽 이해다음5장: 조건부 타입 심층 분석

3장에서 TC39 표준 데코레이터를 살펴봤습니다. 이 장에서는 TypeScript 5.2에서 도입된 또 다른 TC39 표준 기능인 명시적 리소스 관리(Explicit Resource Management) 를 다룹니다. using 선언은 파일 핸들, 데이터베이스 연결, 락(lock) 등 생명주기를 가진 리소스를 안전하게 관리하는 새로운 패러다임입니다.

리소스 관리의 문제

프로그래밍에서 가장 흔한 버그 유형 중 하나는 리소스 누수(Resource Leak) 입니다. 파일을 열고 닫지 않거나, 데이터베이스 연결을 반환하지 않거나, 이벤트 리스너를 해제하지 않는 경우입니다.

전통적인 리소스 관리의 문제
typescript
async function processData() {
  const connection = await db.connect();
  const file = await fs.open("output.txt", "w");
 
  try {
    const data = await connection.query("SELECT * FROM users");
    await file.write(JSON.stringify(data));
  } finally {
    // 문제 1: 하나가 실패하면 다른 것도 정리되지 않을 수 있음
    await file.close();
    await connection.release();
  }
}

try/finally 패턴은 동작하지만, 리소스가 늘어날수록 중첩이 깊어지고, 정리 순서를 실수하기 쉽습니다. C#의 using, Python의 with, Java의 try-with-resources가 해결한 문제를 JavaScript/TypeScript에서도 해결하려는 것이 이 제안의 목적입니다.

using 선언의 기본

Symbol.dispose와 Disposable 인터페이스

using 선언을 사용하려면, 리소스 객체가 Symbol.dispose 메서드를 구현해야 합니다.

Disposable 인터페이스
typescript
interface Disposable {
  [Symbol.dispose](): void;
}
 
interface AsyncDisposable {
  [Symbol.asyncDispose](): Promise<void>;
}

using 키워드로 선언된 변수는 현재 스코프가 끝날 때 자동으로 [Symbol.dispose]()가 호출됩니다.

using 기본 사용법
typescript
class FileHandle implements Disposable {
  private closed = false;
 
  constructor(private path: string) {
    console.log(`Opening ${path}`);
  }
 
  write(data: string) {
    if (this.closed) throw new Error("File already closed");
    console.log(`Writing to ${this.path}: ${data}`);
  }
 
  [Symbol.dispose]() {
    if (!this.closed) {
      this.closed = true;
      console.log(`Closing ${this.path}`);
    }
  }
}
 
function processFile() {
  using file = new FileHandle("data.txt");
  file.write("Hello, World!");
  // 스코프 종료 시 자동으로 file[Symbol.dispose]() 호출
  // 출력:
  // Opening data.txt
  // Writing to data.txt: Hello, World!
  // Closing data.txt
}

await using: 비동기 리소스 관리

비동기 정리가 필요한 리소스는 Symbol.asyncDispose를 구현하고, await using으로 선언합니다.

await using
typescript
class DatabaseConnection implements AsyncDisposable {
  private released = false;
 
  static async connect(url: string): Promise<DatabaseConnection> {
    console.log(`Connecting to ${url}`);
    // 실제 연결 로직...
    return new DatabaseConnection();
  }
 
  async query(sql: string) {
    if (this.released) throw new Error("Connection released");
    console.log(`Executing: ${sql}`);
    return [{ id: 1, name: "Alice" }];
  }
 
  async [Symbol.asyncDispose]() {
    if (!this.released) {
      this.released = true;
      console.log("Releasing connection to pool");
      // 실제 연결 반환 로직...
    }
  }
}
 
async function getUsers() {
  await using conn = await DatabaseConnection.connect("postgres://localhost/mydb");
  const users = await conn.query("SELECT * FROM users");
  return users;
  // 스코프 종료 시 await conn[Symbol.asyncDispose]() 자동 호출
}

여러 리소스의 관리

여러 using 선언이 있을 때, 리소스는 선언의 역순으로 정리됩니다. 이는 스택(LIFO) 방식으로, 나중에 열린 리소스가 먼저 닫힙니다.

여러 리소스 정리 순서
typescript
function multiResource() {
  using a = createResource("A");
  using b = createResource("B");
  using c = createResource("C");
 
  // 작업 수행...
 
  // 스코프 종료 시 정리 순서: C → B → A
}

이 동작은 리소스 간 의존성이 있을 때 중요합니다. 예를 들어, 데이터베이스 트랜잭션은 커넥션보다 먼저 종료되어야 합니다.

의존적 리소스 관리
typescript
async function transferMoney(fromId: string, toId: string, amount: number) {
  await using conn = await pool.getConnection();
  await using tx = await conn.beginTransaction();
 
  try {
    await tx.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", [amount, fromId]);
    await tx.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", [amount, toId]);
    await tx.commit();
  } catch (error) {
    await tx.rollback();
    throw error;
  }
 
  // 정리 순서: tx[Symbol.asyncDispose]() → conn[Symbol.asyncDispose]()
}

DisposableStack과 AsyncDisposableStack

개별 using 선언 외에도, 여러 리소스를 그룹으로 관리할 수 있는 DisposableStack이 제공됩니다.

DisposableStack
typescript
function setupServerResources() {
  using stack = new DisposableStack();
 
  const logger = stack.use(new FileLogger("server.log"));
  const cache = stack.use(new RedisConnection("localhost:6379"));
  const db = stack.use(new PostgresPool("postgres://localhost/app"));
 
  // 세 리소스를 모두 사용하여 작업...
 
  return { logger, cache, db };
  // 스코프 종료 시 stack이 모든 리소스를 역순으로 정리
}

DisposableStack은 다음 메서드를 제공합니다.

DisposableStack API
typescript
class DisposableStack implements Disposable {
  // 리소스 추가 (Disposable 구현체)
  use<T extends Disposable>(resource: T): T;
 
  // 정리 콜백 추가
  adopt<T>(value: T, onDispose: (value: T) => void): T;
 
  // 일반 콜백 추가
  defer(fn: () => void): void;
 
  // 소유권 이전 — 이 스택은 더 이상 리소스를 정리하지 않음
  move(): DisposableStack;
 
  // 수동 정리
  [Symbol.dispose](): void;
}

adopt와 defer

adopt는 Disposable을 구현하지 않는 기존 리소스를 관리할 때 유용합니다.

adopt로 기존 API 통합
typescript
function processImage() {
  using stack = new DisposableStack();
 
  // Disposable을 구현하지 않는 기존 API
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d")!;
 
  // adopt로 정리 로직 연결
  stack.adopt(canvas, (c) => c.remove());
 
  // defer로 콜백만 등록
  stack.defer(() => console.log("Image processing complete"));
 
  // 작업 수행...
}

move: 소유권 이전

move()를 사용하면 리소스 소유권을 다른 스코프로 이전할 수 있습니다.

리소스 소유권 이전
typescript
function createManagedResources() {
  using stack = new DisposableStack();
 
  const db = stack.use(new DatabasePool());
  const cache = stack.use(new CacheClient());
 
  // 소유권을 새 스택으로 이전
  // 기존 stack은 이 리소스들을 정리하지 않음
  return stack.move();
}
 
function main() {
  // 반환된 스택이 리소스 소유권을 가짐
  using resources = createManagedResources();
  // main 스코프 종료 시 resources가 모든 리소스 정리
}

실전 패턴

락(Lock) 관리

비동기 락
typescript
class AsyncLock implements AsyncDisposable {
  private locked = false;
  private waitQueue: (() => void)[] = [];
 
  async acquire(): Promise<AsyncDisposable> {
    while (this.locked) {
      await new Promise<void>((resolve) => this.waitQueue.push(resolve));
    }
    this.locked = true;
 
    return {
      [Symbol.asyncDispose]: async () => {
        this.locked = false;
        const next = this.waitQueue.shift();
        if (next) next();
      },
    };
  }
}
 
const lock = new AsyncLock();
 
async function criticalSection() {
  await using guard = await lock.acquire();
  // 이 영역은 동시에 하나의 실행만 허용
  await doSomethingExclusive();
  // 스코프 종료 시 자동으로 락 해제
}

타이머와 인터벌 관리

타이머 리소스 관리
typescript
function createInterval(callback: () => void, ms: number): Disposable {
  const id = setInterval(callback, ms);
  return {
    [Symbol.dispose]() {
      clearInterval(id);
    },
  };
}
 
function createTimeout(callback: () => void, ms: number): Disposable {
  const id = setTimeout(callback, ms);
  return {
    [Symbol.dispose]() {
      clearTimeout(id);
    },
  };
}
 
function monitorSystem() {
  using heartbeat = createInterval(() => sendPing(), 5000);
  using timeout = createTimeout(() => shutdown(), 60000);
 
  // 모니터링 로직...
 
  // 스코프 종료 시 인터벌과 타임아웃 자동 정리
}

이벤트 리스너 관리

이벤트 리스너 자동 해제
typescript
function createEventBinding(
  target: EventTarget,
  event: string,
  handler: EventListener,
  options?: AddEventListenerOptions
): Disposable {
  target.addEventListener(event, handler, options);
  return {
    [Symbol.dispose]() {
      target.removeEventListener(event, handler, options);
    },
  };
}
 
function setupUI() {
  using stack = new DisposableStack();
 
  stack.use(createEventBinding(window, "resize", handleResize));
  stack.use(createEventBinding(document, "keydown", handleKeydown));
  stack.use(createEventBinding(button, "click", handleClick));
 
  // UI 활성화 기간 동안 이벤트 처리...
 
  // 스코프 종료 시 모든 이벤트 리스너 자동 해제
}

에러 처리

using 선언에서 리소스 사용 중 에러가 발생하더라도, [Symbol.dispose]는 반드시 호출됩니다. 이는 try/finally와 동일한 보장입니다.

에러 발생 시 정리 보장
typescript
function riskyOperation() {
  using resource = createResource();
 
  // 에러가 발생해도 resource[Symbol.dispose]()는 호출됨
  throw new Error("Something went wrong");
}

정리 과정에서도 에러가 발생할 수 있습니다. 본문과 정리 모두에서 에러가 발생하면, SuppressedError로 감싸져 두 에러 모두 보존됩니다.

SuppressedError
typescript
function doubleError() {
  try {
    using resource = {
      [Symbol.dispose]() {
        throw new Error("Cleanup error");
      },
    };
    throw new Error("Operation error");
  } catch (e) {
    // e는 SuppressedError
    if (e instanceof SuppressedError) {
      console.log(e.error);       // Error: Cleanup error
      console.log(e.suppressed);  // Error: Operation error
    }
  }
}

tsconfig 설정

using 선언을 사용하려면 다음 설정이 필요합니다.

tsconfig.json
json
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "ESNext.Disposable"]
  }
}
Info

Symbol.dispose와 Symbol.asyncDispose는 아직 모든 JavaScript 런타임에서 네이티브로 지원되지 않을 수 있습니다. TypeScript는 폴리필 없이도 다운레벨 컴파일을 지원하지만, 런타임 환경에 따라 disposablestack 폴리필이 필요할 수 있습니다.

정리

using 선언은 JavaScript/TypeScript에서 리소스 관리를 혁신적으로 개선합니다. Symbol.dispose를 구현하는 것만으로 스코프 기반 자동 정리가 가능해지며, DisposableStack으로 여러 리소스를 그룹으로 관리할 수 있습니다. 파일 핸들, 데이터베이스 연결, 이벤트 리스너, 타이머 등 생명주기를 가진 모든 리소스에 적용할 수 있는 범용적인 패턴입니다.

다음 장에서는 TypeScript 타입 시스템의 핵심인 조건부 타입을 심층 분석합니다. extends 키워드를 활용한 타입 분기, 분배적 조건부 타입, 그리고 infer와의 조합 패턴을 다룹니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#typescript#performance#devtools

관련 글

프로그래밍

5장: 조건부 타입 심층 분석

TypeScript 조건부 타입의 원리, 분배적 조건부 타입, infer 키워드와의 조합, 그리고 실전 활용 패턴을 깊이 있게 다룹니다.

2026년 1월 30일·13분
프로그래밍

3장: TC39 표준 데코레이터 완벽 이해

TypeScript 5.0에서 도입된 TC39 Stage 3 데코레이터의 원리, API 구조, 실전 패턴을 다루고, 기존 실험적 데코레이터와의 차이를 분석합니다.

2026년 1월 26일·14분
프로그래밍

6장: 매핑 타입과 키 재매핑 고급 패턴

TypeScript 매핑 타입의 원리, 수정자 조작, 키 재매핑(as 절), 그리고 조건부 타입과의 결합 패턴을 실전 예제와 함께 다룹니다.

2026년 2월 1일·12분
이전 글3장: TC39 표준 데코레이터 완벽 이해
다음 글5장: 조건부 타입 심층 분석

댓글

목차

약 12분 남음
  • 리소스 관리의 문제
  • using 선언의 기본
    • Symbol.dispose와 Disposable 인터페이스
    • await using: 비동기 리소스 관리
  • 여러 리소스의 관리
  • DisposableStack과 AsyncDisposableStack
    • adopt와 defer
    • move: 소유권 이전
  • 실전 패턴
    • 락(Lock) 관리
    • 타이머와 인터벌 관리
    • 이벤트 리스너 관리
  • 에러 처리
  • tsconfig 설정
  • 정리