저는 NestJS와 같은 대형 프레임워크를 별로 좋아하지 않습니다. 나는 가벼운 방식으로 내가 결정한 구조로 원하는 방식으로 소프트웨어를 구축할 수 있는 자유를 항상 좋아했습니다. 하지만 NestJS를 테스트할 때 마음에 들었던 점은 종속성 주입이었습니다.
종속성 주입(DI)은 클래스에서 종속성을 생성하고 관리하는 책임을 제거하여 느슨하게 결합된 코드를 개발할 수 있는 디자인 패턴입니다. 이 패턴은 유지 관리, 테스트 및 확장 가능한 애플리케이션을 작성하는 데 중요합니다. TypeScript 생태계에서 TSyringe는 이 프로세스를 단순화하는 강력하고 가벼운 종속성 주입 컨테이너로 돋보입니다.
TSyringe는 TypeScript/JavaScript 애플리케이션을 위한 경량 종속성 주입 컨테이너입니다. Microsoft가 GitHub(https://github.com/microsoft/tsyringe)에서 유지관리하며 데코레이터를 사용하여 생성자 주입을 수행합니다. 그런 다음 Inversion of Control 컨테이너를 사용하여 인스턴스 또는 값으로 교환할 수 있는 토큰을 기반으로 종속성을 저장합니다.
TSyringe에 대해 자세히 알아보기 전에 종속성 주입이 무엇인지, 왜 중요한지 간략하게 살펴보겠습니다.
종속성 주입은 개체가 개체를 직접 생성하는 대신 외부 소스로부터 종속성을 받는 기술입니다. 이 접근 방식은 다음과 같은 여러 가지 이점을 제공합니다.
먼저 TypeScript 프로젝트에서 Tsyringe를 설정해 보겠습니다.
npm install tsyringe reflect-metadata
tsconfig.json에 다음 옵션이 있는지 확인하세요.
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true } }
애플리케이션 진입점에서 반영 메타데이터 가져오기:
import "reflect-metadata";
예를 들어 애플리케이션의 진입점은 Next.js 13+의 루트 레이아웃이거나 작은 Express 애플리케이션의 기본 파일일 수 있습니다.
소개의 예를 들어 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) ...
각 클래스가 다른 클래스에만 의존하므로 이 예는 매우 간단합니다. 그러나 우리 서비스는 많은 클래스에 의존할 수 있으며 종속성 주입은 모든 것을 깔끔하게 유지하는 데 정말 도움이 됩니다.
하지만 잠깐만요! 나를 그렇게 두지 마세요! 테스트는 어떻습니까?
또한 주입은 모의 개체를 종속성으로 직접 보내 코드를 테스트하는 데 도움이 될 수 있습니다. 코드 예시를 살펴보겠습니다:
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 토큰에는 종속 클래스에 주입될 모의 객체가 포함되어 있습니다.
이름 지정에 토큰 사용: 주입 토큰에 문자열 리터럴을 사용하는 대신 상수 토큰을 만듭니다.
export const USER_REPOSITORY_TOKEN = Symbol("UserRepository");
Scoped containers: Use scoped containers for request-scoped dependencies in web applications.
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 중국어 웹사이트의 기타 관련 기사를 참조하세요!