본문 바로가기
프로그래밍/자바스크립트

프로그래밍 「 자바스크립트 편」Typescript에서 "using" 키워드는 무엇입니까?

by grapedoukan 2023. 7. 3.
728x90

TypeScript 5.2는 개체를 만든 후 "정리"의 필요성을 해결하는 것을 목표로 하는 ECMAScript의 향후 명시적 리소스 관리 기능에 대한 지원을 도입합니다. 이 기능을 통해 개발자는 네트워크 연결 닫기, 임시 파일 삭제 또는 메모리 해제와 같은 필요한 작업을 수행할 수 있습니다.

이는 최근 39단계에 도달한 TC3 제안을 기반으로 하며, 이는 JavaScript에 도입될 것임을 나타냅니다.

using 파일 핸들, 데이터베이스 연결 등과 같은 리소스를 관리하는 데 매우 유용합니다.

사용 사례

임시 파일을 생성하고, 다양한 작업을 위해 읽기 및 쓰기 작업을 수행하고, 마지막으로 파일을 닫고 삭제하는 기능이 있는 시나리오를 생각해 보십시오.

import * as fs from "fs";

export function processFile(path: string) {
    const file = fs.openSync(path, "w+");

    // use file...

    // Close the file and delete it.
    fs.closeSync(file);
    fs.unlinkSync(path);
}

이것은 괜찮지 만 조기 종료를 수행해야하는 경우 어떻게됩니까?

import * as fs from "fs";

export function processFile(path: string) {
    const file = fs.openSync(path, "w+");

    // use file...
    if (someCondition()) {
        // do some more work...

        // Close the file and delete it.
        fs.closeSync(file);
        fs.unlinkSync(path);
        return;
    }

    // Close the file and delete it.
    fs.closeSync(file);
    fs.unlinkSync(path);
}

잊어버리기 쉬운 정리의 중복이 보이기 시작했습니다. 또한 오류가 발생하는 경우 파일을 닫고 삭제한다는 보장이 없습니다. 이 모든 것을 / 블록으로 래핑하면 해결할 수 있습니다.tryfinally

export function processFile(path: string) {
    const file = fs.openSync(path, "w+");

    try {
        // use file...

        if (someCondition()) {
            // do some more work...
            return;
        }
    }
    finally {
        // Close the file and delete it.
        fs.closeSync(file);
        fs.unlinkSync(path);
    }
}

최근 변경 사항으로 인해 코드가 더 강력해졌지만 더 복잡해지기도 했습니다. 또한 "최종" 블록에 정리 논리를 계속 추가하면 다른 리소스의 처리를 방지하는 예외와 같은 추가 문제가 발생할 수 있습니다. 이러한 문제를 해결하기 위해 명시적인 자원 관리 제안이 도입되었습니다. 이 제안은 우리가 다루고 있는 정리 작업과 관련된 리소스 처리를 JavaScript의 기본 개념으로 취급하는 것을 목표로 합니다.

이것은 라는 새로운 내장 기능을 추가하는 것으로 시작되며 로 명명 된 메소드로 객체를 만들 수 있습니다. 편의를 위해 TypeScript는 이를 설명하는 새로운 전역 유형을 정의합니다.symbolSymbol.disposeSymbol.disposeDisposable

Symbol.dispose

Symbol.dispose 는 JavaScript의 새로운 전역 기호입니다. 기능이 할당된 모든 것은 '리소스' - "특정 수명을 가진 개체" - 로 간주되며 키워드와 함께 사용할 수 있습니다.Symbol.disposeusing

class TempFile implements Disposable {
    #path: string;
    #handle: number;

    constructor(path: string) {
        this.#path = path;
        this.#handle = fs.openSync(path, "w+");
    }

    // other methods

    [Symbol.dispose]() {
        // Close the file and delete it.
        fs.closeSync(this.#handle);
        fs.unlinkSync(this.#path);
    }
}

나중에 이러한 메서드를 호출할 수 있습니다.

export function processFile(path: string) {
    const file = new TempFile(path);

    try {
        // ...
    }
    finally {
        file[Symbol.dispose]();
    }
}

정리 논리를 그 자체로 옮기는 것은 우리에게 많은 것을 주지 않습니다. 우리는 기본적으로 블록의 모든 정리 작업을 메서드로 옮겼고 항상 가능했습니다. 그러나 이 메서드에 대해 잘 알려진 "이름"이 있다는 것은 JavaScript가 그 위에 다른 기능을 빌드할 수 있음을 의미합니다.TempFilefinally

그것은 우리를 기능의 첫 번째 별인 선언으로 이끕니다! 는 와 같은 새로운 고정 바인딩을 선언할 수 있는 새로운 키워드입니다. 주요 차이점은 로 선언된 변수가 범위 끝에서 호출되는 메서드를 얻는다는 것입니다!usingusingconstusingSymbol.dispose

따라서 다음과 같이 코드를 작성할 수 있습니다.

export function processFile(path: string) {
    using file = new TempFile(path);

    // use file...

    if (someCondition()) {
        // do some more work...
        return;
    }
}

그것을 확인하십시오 - 아니오 / 블록! 적어도 우리가 보는 것은 없습니다. 기능적으로, 그것이 바로 선언이 우리를 위해 할 일이지만, 우리는 그것을 다룰 필요가 없습니다.tryfinallyusing

C#의 선언, Python의 문 또는 Java의 -with-resource 선언에 익숙할 수 있습니다. 이들은 모두 JavaScript의 new 키워드와 유사하며 범위 끝에서 객체의 "해체"를 수행하는 유사한 명시적 방법을 제공합니다.usingwithtryusing

using 선언은 포함 범위의 맨 끝 또는 a 또는 n 오류와 같은 "조기 반환" 직전에 이 정리를 수행합니다. 또한 스택처럼 선입선출 순서로 삭제합니다.returnthrow

function loggy(id: string): Disposable {
    console.log(`Creating ${id}`);

    return {
        [Symbol.dispose]() {
            console.log(`Disposing ${id}`);
        }
    }
}

function func() {
    using a = loggy("a");
    using b = loggy("b");
    {
        using c = loggy("c");
        using d = loggy("d");
    }
    using e = loggy("e");
    return;

    // Unreachable.
    // Never created, never disposed.
    using f = loggy("f");
}

f();
// Creating a
// Creating b
// Creating c
// Creating d
// Disposing d
// Disposing c
// Creating e
// Disposing e
// Disposing b
// Disposing a

using 선언은 예외에 탄력적이어야 합니다. 오류가 발생하면 삭제 후 다시 throw됩니다. 반면에 함수의 본문은 예상대로 실행될 수 있지만 throw될 수 있습니다. 이 경우 해당 예외도 다시 throw됩니다.Symbol.dispose

그러나 폐기 전과 폐기 중의 논리가 모두 오류를 발생시키면 어떻게 될까요? 이러한 경우 의 새로운 하위 유형으로 도입되었습니다. 마지막으로 throw된 오류를 보유하는 속성과 가장 최근에 throw된 오류에 대한 속성을 제공합니다.SuppressedErrorErrorsuppressederror

class ErrorA extends Error {
    name = "ErrorA";
}
class ErrorB extends Error {
    name = "ErrorB";
}

function throwy(id: string) {
    return {
        [Symbol.dispose]() {
            throw new ErrorA(`Error from ${id}`);
        }
    };
}

function func() {
    using a = throwy("a");
    throw new ErrorB("oops!")
}

try {
    func();
}
catch (e: any) {
    console.log(e.name); // SuppressedError
    console.log(e.message); // An error was suppressed during disposal.

    console.log(e.error.name); // ErrorA
    console.log(e.error.message); // Error from a

    console.log(e.suppressed.name); // ErrorB
    console.log(e.suppressed.message); // oops!
}

이 예제에서 동기 메서드를 사용하고 있음을 눈치채셨을 것입니다. 그러나 많은 리소스 처리에는 비동기 작업이 포함되며 다른 코드를 계속 실행하기 전에 이러한 작업이 완료될 때까지 기다려야 합니다.

그렇기 때문에 새로운 , 그리고 그것은 우리를 쇼의 다음 스타인 선언으로 이끕니다. 이는 선언과 유사하지만 핵심은 누구의 처분을 받아야 하는지 조회한다는 것입니다. 그들은 로 명명 된 다른 방법을 사용하지만 a를 사용하여 무엇이든 작동 할 수 있습니다. 편의를 위해 TypeScript는 비동기 dispose 메서드를 사용하여 모든 개체를 설명하는 전역 유형도 도입합니다.Symbol.asyncDisposeawait usingusingawaitSymbol.asyncDisposeSymbol.disposeAsyncDisposable

async function doWork() {
    // Do fake work for half a second.
    await new Promise(resolve => setTimeout(resolve, 500));
}

function loggy(id: string): AsyncDisposable {
    console.log(`Constructing ${id}`);
    return {
        async [Symbol.asyncDispose]() {
            console.log(`Disposing (async) ${id}`);
            await doWork();
        },
    }
}

async function func() {
    await using a = loggy("a");
    await using b = loggy("b");
    {
        await using c = loggy("c");
        await using d = loggy("d");
    }
    await using e = loggy("e");
    return;

    // Unreachable.
    // Never created, never disposed.
    await using f = loggy("f");
}

f();
// Constructing a
// Constructing b
// Constructing c
// Constructing d
// Disposing (async) d
// Disposing (async) c
// Constructing e
// Disposing (async) e
// Disposing (async) b
// Disposing (async) a

와 관련하여 형식을 정의하면 다른 사용자가 해체 논리를 일관되게 수행할 것으로 예상되는 경우 코드 작업을 훨씬 쉽게 수행할 수 있습니다. 사실, 또는 메소드가 있는 많은 기존 유형이 야생에 존재합니다. 예를 들어 Visual Studio Code API는 자체 인터페이스를 정의할 수도 있습니다. Node.js, Deno 및 Bun과 같은 브라우저 및 런타임의 API는 파일 핸들, 연결 등과 같은 정리 메서드가 이미 있는 개체에 대해 and를 사용하도록 선택할 수도 있습니다.DisposableAsyncDisposabledispose()close()DisposableSymbol.disposeSymbol.asyncDispose

이제 이 모든 것이 라이브러리에는 적합하게 들릴 수 있지만 시나리오에서는 약간 무겁게 들릴 수 있습니다. 임시 정리를 많이 수행하는 경우 새 형식을 만들면 과도한 추상화와 모범 사례에 대한 질문이 많이 발생할 수 있습니다. 예를 들어, 우리의 예를 다시 보자.TempFile

class TempFile implements Disposable {
    #path: string;
    #handle: number;

    constructor(path: string) {
        this.#path = path;
        this.#handle = fs.openSync(path, "w+");
    }

    // other methods

    [Symbol.dispose]() {
        // Close the file and delete it.
        fs.closeSync(this.#handle);
        fs.unlinkSync(this.#path);
    }
}

export function doSomeWork() {
    using file = new TempFile(".some_temp_file");

    // use file...

    if (someCondition()) {
        // do some more work...
        return;
    }
}

우리가 원했던 것은 두 개의 함수를 호출하는 것을 기억하는 것이었지만 이것이 그것을 작성하는 가장 좋은 방법이었습니까? 생성자를 호출하거나, 메서드를 만들거나, 핸들을 직접 전달해야 합니까? 수행해야 하는 모든 가능한 작업에 대한 메서드를 노출해야 합니까, 아니면 속성을 공개해야 합니까?openSyncopen()

그것은 우리를 기능의 마지막 별들로 이끕니다. 이러한 개체는 임의 정리 양과 함께 일회성 정리를 모두 수행하는 데 유용합니다. A는 개체를 추적하기 위한 여러 메서드가 있는 개체이며 임의의 정리 작업을 수행하기 위한 함수를 제공할 수 있습니다. 우리는 또한 그것들을 변수에 할당 할 수 있습니다 왜냐하면 — 이것을 얻으십시오 — 그것들도 있습니다! 그래서 우리가 원래 예제를 작성할 수 있었던 방법은 다음과 같습니다.DisposableStackAsyncDisposableStackDisposableStackDisposableusingDisposable

function processFile(path: string) {
    const file = fs.openSync(path, "w+");

    using cleanup = new DisposableStack();
    cleanup.defer(() => {
        fs.closeSync(file);
        fs.unlinkSync(path);
    });

    // use file...

    if (someCondition()) {
        // do some more work...
        return;
    }

    // ...
}

여기서 메서드는 콜백을 받고 해당 콜백은 한 번 실행됩니다. 일반적으로 (및 와 같은 다른 메서드)는 리소스를 만든 직후에 호출해야 합니다. 이름에서 알 수 있듯이 스택처럼 추적하는 모든 것을 선입 선출 순서로 처리하므로 값을 만든 직후 ing하면 이상한 종속성 문제를 방지하는 데 도움이 됩니다. 유사하게 작동하지만 함수와 s를 추적 할 수 있으며 그 자체로 defer()cleanupdeferDisposableStackuseadoptDisposableStackdeferAsyncDisposableasyncAsyncDisposableAsyncDisposable.

이 방법은 여러 면에서 Go, Swift, Zig, Odin 등의 키워드와 유사하며 규칙이 유사해야 합니다.deferdefer

이 기능은 최신 기능이기 때문에 대부분의 런타임은 기본적으로 지원하지 않습니다. 이를 사용하려면 다음에 대한 런타임 폴리필이 필요합니다.

  • Symbol.dispose
  • Symbol.asyncDispose
  • DisposableStack
  • AsyncDisposableStack
  • SuppressedError

그러나 관심있는 모든 것이 및 인 경우 내장 된 s를 채우는 폴리 만 사용하여 벗어날 수 있어야합니다. 다음과 같은 간단한 것이 대부분의 경우에 작동합니다.usingawait usingsymbol

Symbol.dispose ??= Symbol("Symbol.dispose");
Symbol.asyncDispose ??= Symbol("Symbol.asyncDispose");

또한 컴파일을 로 설정하거나 그 이하로 설정하고 또는 를 포함하도록 설정을 구성해야 합니다.targetes2022lib"esnext""esnext.disposable"

{
    "compilerOptions": {
        "target": "es2022",
        "lib": ["es2022", "esnext.disposable", "dom"]
    }
}

이 기능에 대한 자세한 내용은 GitHub의 작업을 참조하세요.

728x90