[번역] 타입스크립트 5.0 베타 출시 - 1.Decorator

이관형·2023년 2월 5일
1
post-thumbnail

해당글은 https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-beta 글을 번역한 글입니다.

오늘 저희는 여러분들께 타입스크립트 5.0베타 버전 출시를 말씀드릴 수 있게 되어서 너무 기쁩니다.

이번 5.0베타 버전은 더 작고, 더 단순하고, 더 빠른 타입스크립트를 만드는 것을 목표로 하면서 많은 새로운 기능을 제공합니다. 저희는 새로운 데코레이터를 구현하였고, 노드 및 번들러에서 ESM 프로젝트를 더 손쉽게 지원하는 기능, 라이브러리를 사용하시는 분들이 손쉽게 제네릭 인터페이스를 컨트롤 할 수 있는 방법, JSDoc 기능의 보완, 구성의 단순화등 많은 기능을 제공하고자합니다.

5.0 릴리스에는 사용 빈도가 낮은 플래그에 대한 정확성 변경 및 사용 중지가 포함되어 있지만 대부분의 사용자는 이전 릴리스와 유사한 업그레이드 환경을 경험할 것으로 예상됩니다.

베타 버전을 사용하기 위해서는 여기에서 다운을 받으시거나, npm을 이용하여도 됩니다.

npm install typescript@beta

Decorators

데코레이터는 이번 ECMAScript기능이며 재사용이 가능한 방식으로 클래스와 멤버들을 커스텀화 할 수 있도록 도와주는 기능입니다.

아래 코드를 한 번 보시죠.

class Person{
  name: string
  constructor(name:string) {this.name = name}
  
  greet(){
    console.log(`Hello, my name is ${this.name}`)
  }
}

const p = new Person("Ray")
p.greet()

여기에있는 greet 함수는 정말 간단한겁니다. 그러나 해당 함수가 더 복잡한 로직을 포함하고있다고 상상해보세요. 아마도 비동기를 수행하거나, 재귀적이고 예상하지못한 버그들이 포함되어있을겁니다. 여러분들이 상상하는 어떤것과도 상관없이 디버깅을 위해서 console.log를 호출한다고 가정해봅시다.

class Person{
  name:string
  constructor(name:string) {this.name = name}
  
  greet(){
    console.log("LOG: Entering method.")
    console.log(`Hello, My name is ${this.name}`)
    console.log("LOG: Exiting method")
  }
}

다음과 흔히 볼 수 있는 패턴입니다. 이러한 패턴들이 모든 함수내에 포함되어있다면 정말 좋을것입니다.

이것이 데코레이터가 있는 이유입니다. 우리는 아래의 코드와같은 loggedMethod라는 함수를 작성할 수 있습니다.

function loggedMethod(originalMethod: any, _context:any){
  
  function replacementMethod(this:any, ...args:any[]){
	console.log("LOG: Entering method.")
    const result = originalMethod.call(this,...args)
    console.log("LOG: Exiting method")
    retrun result
  }
  
  return replacementMethod
}

"이게뭐야, TypeScript가 아니라 AnyScript인가요??"

조금만 참아보세요. 지금은 간단하게 표현하였고 여러분들은 해당 함수가 무엇을 행하는지에 대해서 집중하셔야합니다. loggedMethod함수가 originalMethod를 매개변수로받고
1. "Entering..."
2. this를 호출하고 originalMethod에 모든 변수들을 넘겨줍니다.
3. "Exiting..."

우리는 loggedMethod함수를 다음과 같이 사용해볼 수 있습니다.

class Person {
    name: string
    constructor(name: string) {
        this.name = name
    }

    @loggedMethod
    greet() {
        console.log(`Hello, my name is ${this.name}.`)
    }
}

const p = new Person("Ray")
p.greet();

// Output:
//
//   LOG: Entering method.
//   Hello, my name is Ray.
//   LOG: Exiting method.

우리는 단순히 loogedMethod 를 greet함수위에 데코레이터로 표시하였습니다. 이처럼하면 함수 대상과context 개체로 호출됩니다. 왜냐하면 loggedMethod는 새로운 함수를 리턴하게되며, 리턴된 함수는 기존의 함수를 대체하게됩니다.

아직 얘기 안했지만, loggedMethod가 두 번째 매개 변수로 정의되었습니다. 이는 "컨텍스트 객체(context object)"라고 불리우며 데코레이터가 어떻게 선언되었는지 알 수 있는 유용한 정보가 담겨있습니다. #private멤버(member)인지, static인지, 메소드의 이름은 무엇인지와같은 것들이 그 예시입니다.
자, 다시 한 번 loggedMethod를 업그레이드 시켜보고 데코레이터의 이름을 알려주는 기능을 추가해봅시다.

function loggedMethod(originalMethod: any, context:ClassMethodDecoratorContext) {
    const methodName = String(context.name)

    function replacementMethod(this: any, ...args: any[]) {
        console.log(`LOG: Entering method '${methodName}'.`)
        const result = originalMethod.call(this, ...args)
        console.log(`LOG: Exiting method '${methodName}'.`)
        return result
    }

    return replacementMethod
}

이번에는 context 파라미터를 사용해보았습니다. 타입스크립트는 ClassMethodDecoratorContext라고 불리는 타입을 제공하고있으며 이는 데코레이터에서 사용되는 컨텍스트 객체의 타입을 뜻합니다.

메타데이터는 잠시 신경을 쓰지말고, 컨텍스트 객체는 또한 addInitializer라는 유용한 함수를 가지고있습니다. 이 함수는 생성자의 시작부분에 연결하는 방법입니다.

자바스크립트에서 흔하게 사용하고있는 패턴중 하나로 예시를 들어보겠습니다.

class Person {
    name: string
    constructor(name: string) {
        this.name = name
        this.greet = this.greet.bind(this)
    }

    greet() {
        console.log(`Hello, my name is ${this.name}.`)
    }
}

이 대신에 greet 함수는 화살표 함수로 프로퍼티의 초기값으로 선언할 수 있습니다.

class Person{
  name: string
  constructor(name: string){this.name = name}
  
  greet = () => {
    console.log(`Hello, my name is ${this.name}`)
  }
}

위 코드는 greet가 독립 실행형 함수로 호출되거나 콜백으로 전달되는경우 다시 바인딩이 되지않도록 하기위해서 작성되었습니다.

const greet = new Person("Ray").greet

// We don't want this to fail!
greet();

우리는 addInitializer를 이용하여 생성자에서 bind를 호출하는 데코레이터를 작성할 수 있습니다.

function bound(originalMethod: any, context: ClassMethodDecoratorContext){
  const methodName = context.name
  
  if(context.private){
    throw Error("'bound' cannot decorator private properties like ${methodName as string}")
  }
  
  context.addInitializer(function () {
    this[methodName] = this[methodName].bind(this)
  }
}

bound는 어떠한 리턴값도 없습니다. 그래서 데코레이터를 사용할때 원본은 그대로 남아있습니다. 대신에 다른 어떠한 필드가 초기화되기 전에 로직을 추가하게됩니다.

class Person {
    name: string
    constructor(name: string) {
        this.name = name
    }

    @bound
    @loggedMethod
    greet() {
        console.log(`Hello, my name is ${this.name}.`)
    }
}

const p = new Person("Ray")
const greet = p.greet

// Works!
greet()

여러분들은 @bound와 @loggedMethod 총 두개의 데코레이터가 사용되었다는점을 주목하여야합니다. 해당 데코레이터들은 역순으로 작동됩니다. 즉, @loggedMethod는 기존 함수인 greet를 데코레이트하고 @bound는 @loggedMethod의 결과로 반환되는 함수를 데코레이트하게됩니다. 이 예시에서는 크게 중요하지않고 문제도 없습니다. 그러나 여러분들이 원하는 순서대로 진행하고싶으시다면 알고계셔야합니다.

또한 여러분들이 스타일리쉬하게 코드를 작성하는걸 선호하시면, 데코레이터를 한 줄 안에 작성하셔도됩니다.

@bound @loggedMethod greet(){
  console.log(`Hello, my name is ${this.name}.`)
}

우리는 데코레이터 기능을 돌려주는 기능까지 만들 수 있습니다. 그렇게 하면 우리는 최종 데코레이터를 조금 더 커스텀화 할 수 있습니다. 만약 우리가 원한다면, 우리는 loggedMethod 데코레이터에 커스텀화한 메시지를 추가로 작성할 수 있습니다.

function loggedMethod(headMessage = "LOG: "){
  return function actualDecorator(originalMethod: any, context: ClassMethodDecoratorContext){
    const methodName = String(context.name)
    
    function replacementMethod(this:any, ...args: any[]) {
      console.log(`${headMessage} Entering method '${methodName}'.`)
      const result = originalMethod.call(this, ...args)
      console.log(`${headMessage} Exiting method '${methodName}'.`)
      
      return result
    }
    
    return replacementMethod
  }
}

만약 위와 같이 사용하게되면 loggedMethod를 데코레이터로 사용하기 전에 호출해야합니다. 그러면 콘솔에 기록되는 메시지의 접두사로 임의의 문자열을 전달할 수 있습니다.

class Person {
    name: string
    constructor(name: string) {
        this.name = name
    }

    @loggedMethod("")
    greet() {
        console.log(`Hello, my name is ${this.name}.`)
    }
}

const p = new Person("Ray")
p.greet();

// Output:
//
//    Entering method 'greet'.
//   Hello, my name is Ray.
//    Exiting method 'greet'.

데코레이터들은 단순 함수 이상으로 사용될 수 있습니다. 데코레이터는 properties/fields, getter, setter 그리고 auto-accessor 등으로도 사용될 수 있습니다. 심지어 클래스 자체에서도 하위 클래스와같은 것들을 위해서 사용될 수 있습니다.

실험적으로 사용되던 레거시 코드와의 차이점

만약 여러분들이 타입스크립트를 오랜기간동안 사용해왔다면, 아마 수년간 "실험적인(experimental)" 데코레이터가 지원되어왔다는것을 알것입니다. 이 실험적으로 사용되어온 데코레이터들은 믿을 수 없을 정도로 유용했지만, 그들은 훨씬 오래된 버전의 데코레이터를 대상으로 모델링 되었었습니다. 그리고 항상 --experimentalDecorators라고 불리는 컴파일러 옵션 플래그를 필요로해왔습니다. 이 플래그를 사용하지않고 오류 메시지를 표시하지않는 방법은 없습니다.

--experimentalDecorators는 한동안은 계속 존재할 것입니다. 그러나 플래그없이도 데코레이터들은 이제 오류를 표시하지 않을 것입니다. --experimentalDecorator 외부에서도 타입체크가 이루어질것이며 방출(emit)될 것입니다.

새로운 데코레이터는 --emitDecoratorMetadata와 호환되지않으며 데코레이터 매개변수를 허용하지 않습니다. 향후 ECMA스크립트 기능이 이러한 격차를 해소하는데 도움이 될 것입니다.

마지막으로, 현재 클래스 데코레이터가 존재하는 경우 export 키워드 뒤에 오도록 요구되어집니다.

export @register class Foo {
    // ...
}

export
@Component({
    // ...
})
class Bar {
    // ...
}

타입스크립트는 자바스크립트 파일 내에서 이러한 요구를 적용합니다. 그러나 타입스크립트에서는 이렇게 작성하지않아도 됩니다. 저희들은 기존의 "실험적인" 데코레이터와 표준화된 데코레이터 사이에서 더 쉬운 마이그레이션 경로를 제공하기를 희망합니다. 게다가 저희는 많은 사용자들로부터 원래 스타일에 대한 선호도를 들었습니다. 그리고 저희는 앞으로의 표준 논의에서 그 문제를 선의로 논의할 수 있기를 바랍니다.

잘 작성된 데코레이터를 작성해봅시다.

위에서 작성한 예시인 loggedMethod와 bound 데코레이터는 일부러 간단하게 작성하고 타입에 관한 많은 세부사항을 생략하였었습니다.

데코레이터를 작성하는것은 상당히 복잡할 수 있습니다. 예를들어서, 잘 작성된 loggedMethod 데코레이터는 다음과 같을것입니다.

function loggedMethod<This,Args extends any[],Return>(target: (this:This, ...args: Args) => Return, context: ClassMethodDecoratorContext<This, (this:This, ...args:Args) => Return> ){
  const methodName = String(context.name)
  
  function replacementMethod(this: This, ...args:Args):Return {
    console.log(`LOG: Entering method '${methodName}'.`)
    const result = target.call(this, ...args)
	console.log(`LOG: Exiting method '${methodName}'.`)
    return result
  }
}

우리는 기존 함수의 리턴타입, 파라미터, this의 타입을 따로 This, Args, Return을 이용하여서 모델링을 하였습니다. 얼마나 복잡한 데코레이터 기능이 정의되는지는 보증하려는 내용에 따라 다릅니다. 명심하세요. 여러분들이 작성한 데코레이터는 많이 사용될것이기때문에, 잘 작성하는것이 중요합니다. 하지만 가독성에 있어서는 복잡해지기때문에, 절충점을 찾는것이 중요합니다.

profile
백엔드개발자🖥

0개의 댓글