JavaScript의 using을 사용해 보자 - Part 2

김준기 · 원프레딕트 프론트엔드 리드
September 27, 2023

글 목록

  1. JavaScript의 using을 사용해 보자 - Part 1
  2. JavaScript의 using을 사용해 보자 - Part 2

Intro

지난 글에서 예고한 대로 TypeScript를 이용해, using을 명시적으로 사용해 보겠습니다. 리소스를 관리하는 방식을 using을 이용하여 작성해 보겠습니다.

동기에 대한 명시적 선언

다음은 명시적으로 Symbol.dispose를 이용하여, 동기 코드에 대한 종료 함수를 선언하는 방법입니다. using이 선언된 블록이 종료되면, 명시된 Symbol.dispose에 할당된 함수를 실행합니다.

{
  const getResource = () => {
    return {
      [Symbol.dispose]: () => {
        console.log("using 종료.")
      },
    }
  }

  using fileHandler = getResource()
}
// 종료 후, [Symbol.dispose] 실행.
// using 종료. 출력

비동기에 대한 명시적 선언

다음은 명시적으로 Symbol.asyncDispose를 이용하여, 비동기 코드에 대한 종료 함수를 선언하는 방법입니다. top-level에서 await를 사용하는 모습이 약간은 어색해 보입니다.

const getResource = () => ({
  [Symbol.asyncDispose]: async () => {
    await asyncSomethingFunc()
  },
})
{
  await using fileHandler = getResource()
}

1편으로 돌아가보기

1편에서 우리는 데이터베이스 연결에 대한 이야기를 나누어 보았습니다. 지금까지 using에 대해서 이야기한 것을 바탕으로 using을 이용하여 데이터베이스 연결을 관리해 보겠습니다.

getPosts 함수 내부 들여다 보기

using을 쓰지 않는 경우

let db

try {
  db = await connectToDatabase()
  let collection = await db.collection("posts")
  let posts = await collection.find({}).limit(50).toArray()

  return posts
} catch (error) {
  console.error(error)
} finally {
  await db.close()
}

using을 쓰는 경우

const getConnection = async () => {
  const connection = await connectToDatabase()
  return {
    connection,
    [Symbol.asyncDispose]: async () => {
      await connection.close()
    },
  }
}

{
  await using db = await getConnection()
  let collection = await db.collection("posts")
  let posts = await collection.find({}).limit(50).toArray()

  return posts
}

위 코드를 보면 try...catch...finally로 연결되는 블록을 using을 활용해서 명시적으로 컨트롤할 수 있게 변경된 것을 볼 수 있습니다.

실제 폴리필은 어떻게 이루어질까?

폴리필

단계를 분리하여 함수별로 확인해 보겠습니다.

크게 3가지의 단계로 진행됩니다.

  1. 이미 정의된 Symbol객체
  2. disposableResource의 관리
  3. 명시된 disposeResource의 사용 및 해제

Symbol 정의

현재 dispose, asyncDispose로 정의된 Symbolcore-js에 있는 정의를 활용합니다. 해당 파일은 WellKnownSymbol로 dispose, asyncDispose를 선언합니다.

import "core-js/modules/esnext.symbol.dispose.js"
import "core-js/modules/esnext.symbol.async-dispose.js"

// WellKnownSymbol로 `dispose`, 'asyncDispose`를 선언합니다.

// core-js/moduekls/esnext.symbol.dispose.js
defineWellKnownSymbol("dispose")

// core-js/moduekls/esnext.symbol.async-dispose.js
defineWellKnownSymbol("asyncDispose")

disposable-stack을 활용하기

이러한 명시적 관리는 disposable-stack으로 명명된 stack을 통해 내부에서 관리하고 있습니다. 실제 내부 코드는 제외하고 사용하는 함수를 표기하겠습니다.

import "core-js/modules/esnext.disposable-stack.constructor.js"

defineBuiltIns(DisposableStackPrototype, {
  // Dispose Resource 객체를 생성합니다.
  dispose: function dispose() {},
  // Stack에 담겨 있는 동기 Dispose Resource 객체를 꺼내와 dispose method를 실행합니다.
  use: function use(value) {},
  // Stack에 담겨 있는 비동기 Dispose Resource 객체를 꺼내와 dispose method를 실행합니다.
  adopt: function adopt(value, onDispose) {},
  // Stack에 담겨 있는 비동기 Dispose Resource 객체를 골라 지연합니다.
  defer: function defer(onDispose) {},
  // 해당 Stack의 instance가 다 비워지면 다음 Stack을 찾아 이동합니다. Stack은 블록 단위로 구성됩니다.
  move: function move() {},
})

실제 TypeScript에서 폴리필을 구성해 주는 코드입니다.

var __addDisposableResource =
  (this && this.__addDisposableResource) ||
  function (env, value, async) {
    ...생략...
     // built-in으로 되어있는 DisposableResource를 등록합니다.
    return value
  }

var __disposeResources =
  (this && this.__disposeResources) ||
  (function (SuppressedError) {
    return function (env) {
      // built-in으로 되어있는 disposeResource 함수를 실행합니다.
    }
  })(
    // Error 전달
    SuppressedError
  )

Outro

TypeScript5.2에서 폴리필을 구성해서 실제 core-js에 구현된 부분까지 훑어보았습니다. using을 아직 한 번도 사용해 본 적이 없거나, using을 막 도입하여 패턴을 변경하고 싶은 분들에게 필요한 내용들이 되었으면 합니다.

내일은 마침 추석 연휴가 시작되는 날입니다. 모두 즐거운 한가위 되셨으면 합니다.

원프레딕트는 더 나은 제품을 고민하며 기술적인 문제를 함께 풀어낼 동료를 찾고 있습니다.
자세한 내용은 채용 사이트를 참고해 주세요.