FEInterview Prep

YKSS

자바스크립트의 명시적 리소스 관리

TC39 Explicit Resource Management 제안이 도입한 using / await using 키워드로 파일·DB·소켓 리소스를 스코프 종료 시 자동 해제하는 패턴을 정리한다.

2026-02-26·6분 읽기
JavaScriptNode.js
원문 보기 ↗

핵심 요약

Explicit Resource Management 는 TC39 Stage 3+ 제안으로, usingawait using 두 가지 선언을 추가한다. using x = resource 는 스코프를 벗어날 때 x[Symbol.dispose]() 를 자동 호출하고, await usingSymbol.asyncDisposeawait 한다. 사용자 정의 객체도 이 두 Symbol 메서드만 구현하면 프로토콜에 참여한다. 여러 리소스를 묶거나 동적으로 등록해야 할 땐 DisposableStack / AsyncDisposableStack 으로 LIFO 순 해제를 관리한다. 해제는 역순(LIFO) 으로 일어나고, finally 흐름과 호환되며 예외가 나도 반드시 실행된다.

try-finally언어 문법으로 승격한 기능이라고 읽으면 된다. 블록 스코프 = 리소스 수명이고, using 선언은 Symbol.dispose() 훅을 예약하는 문법 설탕이다. C#의 using, Python의 with, Rust의 Drop 과 같은 계열의 결정론적 해제(Deterministic Disposal) 패턴을 JS 에 이식한 것이다.

리소스 누수(파일 디스크립터, DB 커넥션, 이벤트 리스너, 타이머)는 프론트·백엔드 모두에서 실사용에서 가장 조용히 터지는 버그다. 기존 해법의 한계는 명확하다.

  • try-finally보일러플레이트가 많고, 리소스가 2개 이상이면 중첩이 기하급수적으로 늘어난다.
  • 해제 코드를 한 번이라도 누락하면 런타임/타입 시스템이 못 잡는다.
  • 비동기 리소스는 finally 안에서 await 를 또 써야 해서 에러 흐름이 뒤엉킨다.

using / await using 은 이걸 선언만으로 강제하고, 면접에서도 "리소스 관리 어떻게 하냐" 질문에 한 단계 깊은 답을 만들어 준다.

학습 포인트

면접 답변으로 연결할 학습 포인트입니다.

`using` = 스코프 기반 자동 해제

using 선언된 변수는 블록 스코프 종료 시점[Symbol.dispose]() 가 자동 호출된다. 예외가 터져도, return 으로 빠져나가도 반드시 실행되며 이는 try-finally 를 언어가 대신 깔아주는 구조다.

{
  using file = openFile("./a.txt");
  file.read();
} // 여기서 file[Symbol.dispose]() 자동 호출

블록 안에 선언이 여러 개면 역순(LIFO) 으로 해제된다.

usingSymbol.disposeblock scopeLIFO
자주 하는 오해

usinglet/const 처럼 재할당이나 호이스팅 가능한 바인딩으로 오해하는 것. using블록 스코프 전용이고 재할당 불가이며, 함수 최상위에서 해제 시점을 제어하려면 DisposableStack 을 써야 한다.

`await using` = 비동기 해제

DB 커넥션 close, 파일 flush, 스트림 end 처럼 해제 자체가 비동기인 리소스는 await using 으로 선언하고 Symbol.asyncDispose 를 구현한다. 스코프 종료 시 엔진이 await 로 해제를 기다린다.

async function query() {
  await using conn = await pool.connect();
  return await conn.query("...");
} // 여기서 await conn[Symbol.asyncDispose]()

await usingasync 함수나 최상위 await 가 허용되는 모듈 안에서만 쓸 수 있다.

await usingSymbol.asyncDisposeasync functionconnection pool
자주 하는 오해

비동기 리소스에 동기 using 을 쓰면 해제가 fire-and-forget 되어 커넥션 누수로 이어진다. 반대로 동기 리소스에 불필요하게 await using 을 쓰면 마이크로태스크를 한 번 더 만들어 성능·스택 트레이스가 지저분해진다.

사용자 정의 리소스 = 프로토콜 구현

특정 라이브러리가 지원하지 않아도 Symbol 중 하나만 구현하면 using 에 태울 수 있다.

class Timer {
  private id = setInterval(() => {}, 1000);
  [Symbol.dispose]() { clearInterval(this.id); }
}

{
  using t = new Timer(); // 블록 끝나면 clearInterval 자동
}

기존 코드베이스의 close() / destroy() 메서드를 Symbol.dispose 로 alias 하면 점진적 마이그레이션이 된다.

Symbol.disposeSymbol.asyncDisposewell-known symbolprotocol
자주 하는 오해

Symbol.dispose 안에서 예외를 던지면 이미 발생한 예외를 덮어쓰거나 SuppressedError 로 감싸진다. 해제 로직은 idempotent 하게, 실패해도 로깅/무시로 끝나게 설계해야 다른 리소스 해제를 막지 않는다.

여러 리소스는 `DisposableStack`

동적으로 개수가 변하거나 조건부로 획득하는 리소스는 DisposableStack / AsyncDisposableStackuse() 로 등록한다. 스택이 dispose 되면 등록 역순으로 전부 해제된다.

using stack = new DisposableStack();
const a = stack.use(openFile("a"));
if (needB) {
  const b = stack.use(openFile("b"));
}
// stack.dispose() 시 b → a 순으로 해제

stack.move() 로 리소스 소유권을 호출자에게 이관하는 패턴도 표준이다.

DisposableStackAsyncDisposableStackusemovedefer
자주 하는 오해

try-finally 로 여러 리소스를 중첩해서 관리하다가 중간에 실패하면 뒷 리소스는 획득되지 않아 finally 에서 null 체크가 난무한다. DisposableStack획득한 만큼만 해제해 주므로 이 문제를 구조적으로 없앤다.

`try-finally` 와의 관계

usingtry-finally대체가 아니라 더 엄격한 서브셋이다. 둘을 표로 정리하면:

항목try-finallyusing / await using
해제 누락 가능성있음 (수동)없음 (선언 강제)
비동기 해제await 수기 작성await using 이 자동
다중 리소스중첩 필요DisposableStack 로 평탄화
예외 발생 시 동작catch/finally 분기원 예외 + SuppressedError
런타임 지원전 버전ES2026 / Node 20.4+ / TS 5.2
try-finallySuppressedErrorES2026polyfill
자주 하는 오해

구형 타겟(레거시 브라우저, Node 18 이하)을 지원하는 프로젝트에서 using 을 무심코 쓰는 것. TS 5.2+ 와 @esfx/disposable 같은 폴리필 / downlevel 설정 없이는 런타임 에러로 끝난다.

읽는 순서

  1. 1이론

    TC39 explicit-resource-management 제안 문서와 MDN 의 using / Symbol.dispose 페이지를 훑고, using, await using, Symbol.dispose, Symbol.asyncDispose, DisposableStack 다섯 키워드의 역할을 한 문장씩 적어본다.

  2. 2구현

    TS 5.2+ 프로젝트에서 class Timer { [Symbol.dispose]() { ... } } 를 직접 구현하고, using 블록과 try-finally 버전을 나란히 작성해 바이트·가독성·예외 전파를 비교한다. 그다음 DisposableStack 으로 파일 2개를 동시에 관리하는 예제를 작성한다.

  3. 3실무

    현재 프로젝트에서 try { ... } finally { conn.close() } 또는 useEffect 의 cleanup 이 누락되기 쉬운 지점을 찾아, using / Symbol.asyncDispose 로 치환했을 때 런타임 요구사항(ES2026, Node 20.4+, TS 5.2+)이 맞는지 확인하고 마이그레이션 PR 초안을 만든다.

  4. 4설명

    동료에게 "usingtry-finally 의 차이, 비동기 리소스 처리, 여러 리소스 관리" 세 축으로 5분 안에 설명해본다. SuppressedError 와 LIFO 해제를 언급하지 못했다면 해당 부분을 다시 학습한다.

면접 연결 질문

medium`using` 키워드는 기존 `try-finally` 패턴 대비 어떤 문제를 구조적으로 해결하나요?
힌트

[감점 답변] "자동으로 해제해준다" 수준으로만 답하고 누락 방지 메커니즘을 언급하지 못하면 감점. [좋은 답변] (1) 블록 스코프 종료 = 결정론적 Symbol.dispose 호출, (2) 예외·return·정상 종료 모두에서 강제, (3) 다중 리소스는 DisposableStack 으로 평탄화, (4) SuppressedError 로 해제 중 예외도 관찰 가능. 트레이드오프로 런타임 타겟 제약(ES2026, Node 20.4+) 과 함수 최상위 지속 리소스는 여전히 수동 관리 필요를 언급.

medium`using` 과 `await using` 의 차이와 각각의 적용 기준은?
힌트

[감점 답변] "비동기면 await 붙이면 된다" 식으로만 답하는 것. [좋은 답변] usingSymbol.dispose(동기), await usingSymbol.asyncDispose(Promise 반환)을 호출. 기준은 해제 동작이 I/O 를 수반하는가. 예: fs.FileHandle close, pg.Pool release, ReadableStream.cancel()await using. setInterval clear, 메모리 캐시 해제는 using. await usingasync 컨텍스트에서만 사용 가능함도 언급.

hard사용자 정의 클래스를 `using` 에 호환시키려면 어떻게 구현해야 하고, 해제 중 예외는 어떻게 처리해야 하나요?
힌트

[감점 답변] [Symbol.dispose]() { ... } 만 추가한다고 답하고 끝. [좋은 답변] (1) idempotent 하게 구현해 중복 호출에도 안전, (2) 해제 중 예외를 그대로 던지면 원 예외와 함께 SuppressedError.error / .suppressed 에 래핑됨을 설명, (3) 기존 close() / destroy()Symbol.dispose 로 re-export 하는 점진 마이그레이션, (4) 비동기 해제가 필요하면 Symbol.asyncDispose 추가 구현. 보너스로 DisposableStack.adopt(value, onDispose)프로토콜 미지원 객체도 끌어들이는 패턴 언급.

자기 점검

블록 안에서 `using a`, `using b` 를 순서대로 선언했을 때 해제 순서와 그 이유를 설명하세요.
LIFO역순Symbol.dispose스코프 종료
자주 하는 오해

선언 순서대로 해제된다고 착각하는 것. 실제로는 LIFO(후입선출)ba 순으로 [Symbol.dispose]() 가 호출된다. try-finally 를 중첩한 것과 동일한 의미다.

비동기 DB 커넥션을 `using` (동기) 로 선언하면 어떤 문제가 생길지 설명하세요.
await usingSymbol.asyncDisposefire-and-forget커넥션 누수
자주 하는 오해

"어차피 dispose 가 불리니 괜찮다"고 생각하는 것. Symbol.dispose 만 호출되므로 비동기 close 가 기다려지지 않고 버려져 커넥션 풀 누수, 트랜잭션 미종료, 테스트 race condition 으로 이어진다.