Typescript 5.2 using 키워드란 무엇인가

dante Yoon·2023년 6월 24일
6

js/ts

목록 보기
8/14
post-thumbnail

유튜브 영상으로 보기

https://www.youtube.com/watch?v=ZUAwwUiGo3c

안녕하세요, 단테입니다.

오늘은 타입스크립트 5.2 버전에서 새롭게 소개한 using 키워드에 대해 알아보겠습니다.

using

typescript의 새로운 키워드 using은 자바스크립트에 새로 도입된 TC39 제안 3단계에 실려 있으며, Symbol.dispose 또는 Symbol.asyncDispose 함수를 가진 객체를 자동으로 해제해주는 기능입니다

TC39 proposal

tc39 제안이란 ECMAScript 표준에 추가될 새로운 기능들을 제안하는 문서입니다.
tc39는 ECMAScript 표준을 관리하는 위원회의 이름이기도 합니다. 각 제안은 다음과 같은 5개의 스텝을 거쳐 최종 승인을 받습니다.

  • Stage 0: 제안자가 새로운 기능에 대한 아이디어를 공유하고 토론합니다.
  • Stage 1: 제안자가 기능의 목적, 사용법, 예상되는 문제점 등을 설명하는 문서를 작성하고 위원회에 제출합니다.
  • Stage 2: 제안자가 기능의 구체적인 문법과 의미를 정의하는 초안을 작성하고 위원회에 제출합니다. 이때 테스트 케이스도 함께 제공해야 합니다.
  • Stage 3: 제안자가 기능의 구현과 호환성을 검증하기 위해 브라우저나 트랜스파일러 등에 적용해보고, 피드백을 받습니다. 이때 기능의 사양은 거의 완성된 상태여야 합니다.
  • Stage 4: 제안자가 기능의 안정성과 완성도를 입증하기 위해 두 개 이상의 독립적인 구현체와 모든 테스트 케이스를 통과하는 것을 보여줍니다. 이때 위원회의 최종 승인을 받아 ECMAScript 표준에 포함됩니다.

ECMAScript Explicit Resource Management

앞서 말한 TC39저장소에는 명시적 리소스 관리라는 이름으로 실려있는데

FileReader 객체
Stream 객체
Database 객체
Network 객체
와 같은 메모리, IO 리소스들을 좀더 명시적인 방법으로 쉽게 해제할 수 있는 문법을 제안합니다.

왜 필요한가

리소스 선언/해제할 때

const reader = stream.getReader();
...
reader.releaseLock(); // Oops, should have been in a try/finally

stream, file reader 객체의 리소스를 release하거나 읽거나 하는 동작들은 에러 발생시 전체 프로그램을 멈출 수 있기 때문에 항상try .. catch 구문 내부에서 사용해야 합니다.

리소스 사용을 금지해야할 때

const handle = ...;
try {
  ... // ok to use `handle`
}
finally {
  handle.close();
}
// not ok to use `handle`, but still in scope

위와 같이 try catch 구문을 사용한다고 하더라도 finally 이후에 handle객체가 참조되는 것을 방지할 수 없습니다. 선언된 코드가 handle이 초기화된 곳과 같은 스코프에 있기 때문입니다.

여러 개의 리소스를 사용할 때

const a = ...;
const b = ...;
try {
  ...
}
finally {
  a.close(); // Oops, issue if `b.close()` depends on `a`.
  b.close(); // Oops, `b` never reached if `a.close()` throws.
}

b리소스는 a리소스와 항상 동일 시점에 해제되어야 하는데 a 리소스 해제가 실패하는경우 b.close()는 실행기회를 항상 잃게 됩니다.

이러한 상황들을 방지하기 위해 자바스크립트에서 리소스를 해제할 때는
리소스의 갯수에 비례하여 코드의 길이가 길어질 수 밖에 없습니다.

// sync disposal
{ // block avoids leaking `a` or `b` to outer scope
  const a = ...;
  try {
    const b = ...;
    try {
      ...
    }
    finally {
      b.close(); // ensure `b` is closed before `a` in case `b`
                 // depends on `a`
    }
  }
  finally {
    a.close(); // ensure `a` is closed even if `b.close()` throws
  }
}

using을 사용한다면

// avoids leaking `a` or `b` to outer scope
// ensures `b` is disposed before `a` in case `b` depends on `a`
// ensures `a` is disposed even if disposing `b` throws
using a = ..., b = ...;
await using a = ..., b = ...;

...

Symbol.dispose 이것만 알고 있으면 OK

함수 객체에 Symbol.dispose라고 하는 global symbol이 속성으로 할당되어 있다면 이 함수는 리소스로 판단되며 유저에 의해 직접 생명주기를 명령형으로 조절(Symbol.dispose)를 호출하거나 선언적으로 using 키워드를 사용해서 관리할 수 있습니다.

Dispose

import { open } from "fs-sync";
const getFileHandler = () => {
  const fileHandler = open("...", "r+");
  return {
    fileHandler,
    [Symbol.Dispose]: () => {
      fileHandler.close();
    },
  };
};

{
  using file = getFileHandler("dante.txt");
  // 파일 핸들러 사용 
  file.fileHandler
} // 블록을 벗어나면 리소스 해제

AsyncDispose

import { open } from "node:fs/promises";
const getFileHandler = async () => {
  const fileHandler = await open("...", "r+");
  return {
    fileHandler,
    [Symbol.asyncDispose]: async () => {
      await fileHandler.close();
    },
  };
};

{
  await using file = getFileHandler("dante.txt");
  // 파일 핸들러 사용 
  file.fileHandler
} // 블록을 벗어나면 리소스 해제

typescript, react

예를 들어, 리엑트 코드에서 파일을 읽고 쓰는 컴포넌트를 만든다고 가정해보겠습니다. using 키워드를 사용하지 않으면 다음과 같이 작성할 수 있습니다.

import React, { useEffect, useState } from "react";
import { getConnection } from "~/utils";

const NetworkComponent = ({ path }) => {
  const [networkHandle, setNetworkHandle] = useState(null);
  const [content, setContent] = useState("");

  useEffect(() => {
    // 파일 핸들을 열고 상태에 저장
    const networkHandle = getConnection()
    setNetworkHandle(networkHandle.socket);
    // 컴포넌트가 언마운트될 때 네트워크 핸들을 닫음
    return () => {
      networkHandle.socket?.close();
    };
  }, [path]);

  // 파일 내용을 읽어오는 함수
  const read = async () => {
    const buffer = await networkHandle.read();
    setContent(buffer.toString());
  };

  // 파일 내용을 수정하는 함수
  const write = async (newContent) => {
    await networkHandle.write(newContent);
    setContent(newContent);
  };

  return (
    <div>
      <h1>File: {path}</h1>
      <button onClick={readFile}>Read</button>
      <textarea value={content} onChange={(e) => write(e.target.value)} />
    </div>
  );
};

using 키워드를 사용하면 다음과 같이 간단하게 작성할 수 있습니다.


// Symbol.dispose 함수를 가진 객체를 반환하는 함수
const getConnection = (host: string, port: number) => {
  // .. 
  return {
    socket, 
    [Symbol.dispose]: () => {
      socket.end();
    },
  };
};
import React, { useEffect, useState } from "react";
import { getConnection } from "~/utils";

const NetworkComponent = ({ path }) => {
  const [networkHandle, setNetworkHandle] = useState(null);
  const [content, setContent] = useState("");

  useEffect(() => {
    using networkHandle = getConnection()
    setNetworkHandle(networkHandle.socket);
    // 컴포넌트가 언마운트될 때 자동으로 네트워크 핸들이 닫힘
  }, []);

  // 파일 내용을 읽어오는 함수
  const read = async () => {
    const buffer = await networkHandle.read();
    setContent(buffer.toString());
  };

  // 파일 내용을 수정하는 함수
  const write = async (newContent) => {
    await networkHandle.write(newContent);
    setContent(newContent);
  };

  return (
    <div>
      <h1>File: {path}</h1>
      <button onClick={readFile}>Read</button>
      <textarea value={content} onChange={(e) => write(e.target.value)} />
    </div>
  );
};
profile
성장을 향한 작은 몸부림의 흔적들

1개의 댓글

comment-user-thumbnail
2023년 8월 23일

간단한 정리 감사합니다! 빠르게 이해되었네요

답글 달기