TypeScript의 TSyringe 및 종속성 주입

Patricia Arquette
풀어 주다: 2024-09-25 19:21:42
원래의
787명이 탐색했습니다.

TSyringe and Dependency Injection in TypeScript

저는 NestJS와 같은 대형 프레임워크를 별로 좋아하지 않습니다. 나는 가벼운 방식으로 내가 결정한 구조로 원하는 방식으로 소프트웨어를 구축할 수 있는 자유를 항상 좋아했습니다. 하지만 NestJS를 테스트할 때 마음에 들었던 점은 종속성 주입이었습니다.

종속성 주입(DI)은 클래스에서 종속성을 생성하고 관리하는 책임을 제거하여 느슨하게 결합된 코드를 개발할 수 있는 디자인 패턴입니다. 이 패턴은 유지 관리, 테스트 및 확장 가능한 애플리케이션을 작성하는 데 중요합니다. TypeScript 생태계에서 TSyringe는 이 프로세스를 단순화하는 강력하고 가벼운 종속성 주입 컨테이너로 돋보입니다.

TSyringe는 TypeScript/JavaScript 애플리케이션을 위한 경량 종속성 주입 컨테이너입니다. Microsoft가 GitHub(https://github.com/microsoft/tsyringe)에서 유지관리하며 데코레이터를 사용하여 생성자 주입을 수행합니다. 그런 다음 Inversion of Control 컨테이너를 사용하여 인스턴스 또는 값으로 교환할 수 있는 토큰을 기반으로 종속성을 저장합니다.

종속성 주입 이해

TSyringe에 대해 자세히 알아보기 전에 종속성 주입이 무엇인지, 왜 중요한지 간략하게 살펴보겠습니다.

종속성 주입은 개체가 개체를 직접 생성하는 대신 외부 소스로부터 종속성을 받는 기술입니다. 이 접근 방식은 다음과 같은 여러 가지 이점을 제공합니다.

  1. 향상된 테스트 가능성: 단위 테스트에서 종속성을 쉽게 조롱하거나 스텁할 수 있습니다.
  2. 향상된 모듈성: 구성 요소가 더욱 독립적이며 쉽게 교체하거나 업데이트할 수 있습니다.
  3. 더 나은 코드 재사용성: 애플리케이션의 여러 부분에서 종속성을 공유할 수 있습니다.
  4. 향상된 유지 관리성: 종속성에 대한 변경 사항은 종속 코드에 최소한의 영향을 미칩니다.

T주사기 설정

먼저 TypeScript 프로젝트에서 Tsyringe를 설정해 보겠습니다.

npm install tsyringe reflect-metadata

로그인 후 복사

tsconfig.json에 다음 옵션이 있는지 확인하세요.

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

로그인 후 복사

애플리케이션 진입점에서 반영 메타데이터 가져오기:

import "reflect-metadata";

로그인 후 복사

예를 들어 애플리케이션의 진입점은 Next.js 13+의 루트 레이아웃이거나 작은 Express 애플리케이션의 기본 파일일 수 있습니다.

TSyringe를 사용하여 종속성 주입 구현

소개의 예를 들어 T주린지 설탕을 추가해 보겠습니다.

어댑터부터 시작해 보겠습니다.

// @/adapters/userAdapter.ts
import { injectable } from "tsyringe"

@injectable()
class UserAdapter {
    constructor(...) {...}

    async fetchByUUID(uuid) {...}
}

로그인 후 복사

@injectable() 데코레이터가 보이시나요? 이 클래스가 런타임에 주입될 수 있음을 TSyringe에 알리는 것입니다.

그래서 내 서비스는 방금 만든 어댑터를 사용하고 있습니다. 해당 어댑터를 내 서비스에 삽입해 보겠습니다.

// @/core/user/user.service.ts
import { injectable, inject } from "tsyringe"
...

@injectable()
class UserService {
    constructor(@inject('UserAdapter') private readonly userAdapter: UserAdapter) {}

    async fetchByUUID(uuid: string) {
    ...
        const { data, error } = await this.userAdapter.fetchByUUID(uuid);
    ...
    }
}

로그인 후 복사

여기에서는 서비스가 내 명령 클래스에 주입될 것이기 때문에 @injectable 데코레이터도 사용했지만 생성자 매개변수에도 @inject 데코레이터를 추가했습니다. 이 데코레이터는 TSyringe에게 런타임에 userAdapter 속성에 대한 UserAdapter 토큰에 대한 인스턴스 또는 값을 제공하도록 지시합니다.

그리고 마지막으로 내 핵심의 루트인 명령 클래스(종종 유스케이스라고 잘못 불림)입니다.

// @/core/user/user.commands.ts
import { inject } from "tsyringe"
...

@injectable()
class UserCommands {
    constructor(@inject('UserService') private readonly userService: UserService) {}

    async fetchByUUID(uuid) {
    ...
        const { data, error } = this.userService.fetchByUUID(uuid);
    ...
    }
}

로그인 후 복사

이 시점에서 우리는 TSyringe에게 무엇을 주입할 것인지, 생성자에 무엇을 주입할 것인지를 알려 주었습니다. 하지만 우리는 아직 종속성을 저장할 컨테이너를 만들지 않았습니다. 두 가지 방법으로 이를 수행할 수 있습니다.

종속성 주입 레지스트리를 사용하여 파일을 생성할 수 있습니다.

// @/core/user/user.dependencies.ts
import { container } from "tsyringe"
...

container.register("UserService", {useClass: UserService}) // associate the UserService with the token "UserService"
container.register("UserAdapter", {useClass: UserAdapter}) // associate the UserAdapter with the token "UserAdapter"

export { container }

로그인 후 복사

그러나 @registry 데코레이터를 사용할 수도 있습니다.

// @/core/user/user.commands.ts
import { inject, registry, injectable } from "tsyringe"
...

@injectable()
@registry([
    {
        token: 'UserService',
        useClass: UserService
    },
    {
        token: 'UserAdapter',
        useClass: UserAdapter
    },
])
export class UserCommands {
    constructor(@inject('UserService') private readonly userService: UserService) {}

    async fetchByUUID(uuid) {
    ...
        const { data, error } = this.userService.fetchByUUID(uuid);
    ...
    }
}

container.register("UserCommands", { useClass: UserCommands})

export { container }

로그인 후 복사

두 방법 모두 장단점이 있지만 결국 취향의 문제입니다.

이제 컨테이너가 종속성으로 채워졌으므로 컨테이너의 해결 방법을 사용하여 필요에 따라 컨테이너에서 종속성을 가져올 수 있습니다.

import { container, UserCommands } from "@/core/user/user.commands"

...
const userCommands = container.resolve<UserCommands>("UserCommands")
await userCommands.fetchByUUID(uuid)
...

로그인 후 복사

각 클래스가 다른 클래스에만 의존하므로 이 예는 매우 간단합니다. 그러나 우리 서비스는 많은 클래스에 의존할 수 있으며 종속성 주입은 모든 것을 깔끔하게 유지하는 데 정말 도움이 됩니다.

하지만 잠깐만요! 나를 그렇게 두지 마세요! 테스트는 어떻습니까?

TSyringe를 사용한 테스트

또한 주입은 모의 개체를 종속성으로 직접 보내 코드를 테스트하는 데 도움이 될 수 있습니다. 코드 예시를 살펴보겠습니다:

import { container, UserCommands } from "@/core/user/user.commands"

describe("test ftw", () => {
    let userAdapterMock: UserAdapterMock
    let userCommands: UserCommands

    beforeEach(() => {
        userAdapterMock = new UserAdapter()
        container.registerInstance<UserAdapter>("UserAdapter", userAdapter)
        userCommands = container.resolve<UserCommands>("UserCommands")
    });

    ...
});

로그인 후 복사

이제 UserAdapter 토큰에는 종속 클래스에 주입될 모의 객체가 포함되어 있습니다.

모범 사례 및 팁

  1. 인터페이스 사용: 종속성을 쉽게 교체하고 테스트할 수 있도록 인터페이스를 정의하세요. 이 글에서는 단순화를 위해 인터페이스를 사용하지 않았지만, 인터페이스는 생명입니다.
  2. 순환 종속성 방지: TSyringe에 문제를 일으킬 수 있는 순환 종속성을 방지하도록 코드를 구성하세요.
  3. 이름 지정에 토큰 사용: 주입 토큰에 문자열 리터럴을 사용하는 대신 상수 토큰을 만듭니다.

    export const USER_REPOSITORY_TOKEN = Symbol("UserRepository");
    
    
    로그인 후 복사
  4. Scoped containers: Use scoped containers for request-scoped dependencies in web applications.

  5. Don't overuse DI: Not everything needs to be injected. Use DI for cross-cutting concerns and configurable dependencies.

If you've come this far, I want to say thank you for reading. I hope you found this article instructive. Remember to always consider the specific needs of your project when implementing dependency injection and architectural patterns.

Likes and comment feedback are the best ways to improve.

Happy coding!

위 내용은 TypeScript의 TSyringe 및 종속성 주입의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

원천:dev.to
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
저자별 최신 기사
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿