애플리케이션을 개발하다보면 필연적으로 맞닥뜨릴 수 밖에 없는 상황이 몇가지가 있다. 그 중 하나가 바로 의존성 관리(Dependency Control)이다. 백엔드 애플리케이션에서는 다양한 제어 역전(IoC; Inversion of Control) 컨테이너를 이용해서 적용이 가능하다. 최신 프론트엔트 프레임워크 역시도 의존성 주입(DI; Dependency Injection)과 관련한 기능들을 포함하고 있다. 하지만 자바스크립트 언어의 특성인지는 몰라도 백엔드쪽의 IoC 컨테이너와는 다른 형식으로 DI를 구현한다. 이 포스트에서는 타입스크립트를 이용해서 Vue.js 애플리케이션을 개발할 때 적용할 수 있는 IoC 컨테이너중 하나인 InversifyJS를 이용해서 백엔드 애플리케이션 개발과 거의 비슷한 개발 경험을 적용시켜 보도록 한다.

이 포스트에 쓰인 샘플 코드는 이곳에서 확인할 수 있다.

VueJs 자체 제공 DI

공식 문서에 보면 버전 2.2.0+ 부터 DI를 지원한다고 되어 있다. 아래는 공식 문서에서 제공하는 방식으로, 우선 부모 컴포넌트에서 디펜던시를 정의한다.

View the code on Gist.

그리고 부모 컴포넌트에서 주입한 디펜던시를 자식 콤포넌트에서 받아 활용할 수 있다.

View the code on Gist.

위 코드를 보면 손쉽게 이해할 수 있다. 부모 컴포넌트에서 정의한 MyDependency 객체를 자식 컴포넌트에서 받아서 곧바로 접근이 가능한 셈이다. 하지만, 이 방식에서는 중대한 문제가 하나 있다. 백엔드 개발 경험이 있는 개발자라면 이런 식의 DI는 굉장히 제한적일 수 밖에 없다. 오직 부모 컴포넌트가 지정한 디펜던시 인스턴스만 자식 컴포넌트에서 활용할 수 있고 그 이외에는 접근이 불가능하다. 게다가 개별 디펜던시간의 의존성 역시 해결하기 쉽지 않다.

VueJs + 타입스크립트 적용시 DI

VueJs의 주 메인테이너인 Evan You코멘트를 보면 오히려 VueJs의 프레임워크 디자인 철학에 가까운 것인데, 기본적으로 타입스크립트와 같은 클라스 기반 API 보다는 자바스크립트의 본래 모습에 가까운 오브젝트 기반 API를 제공하는 것을 우선으로 한다. 따라서 위의 코드는 타입스크립트 기반에서는 주입된 MyDependency를 활성화시킬 수 없어 정상적으로 작동하지 않는다.

이 부분은 [email protected] 버전을 기준으로 언급하는 것이다. 최근 버전에서는 달라졌을 수 있다.

따라서 VueJs에 타입스크립트를 적용시킬 때 DI를 원활하게 지원하기 위해서는 객체간 상호 의존성과 기존 오브젝트 기반 DI 이 두가지를 해결해야 한다. 이 두가지는 InversifyJS를 이용해서 서비스 로케이터 패턴을 적용하면 손쉽게 해결이 가능하다.

서비스 로케이터 패턴은 안티 패턴으로써 그다지 환영받을만한 접근법은 아니지만 이 경우에는 다른 방법이 없으니 어쩔 수 없다. 물론 다른 더 좋은 방법이 분명히 있을텐데 찾지 못했다. 아무래도 VueJs에 좀 더 전문적인 식견을 가진 누군가가 이부분을 도와주면 더욱 좋겠다.

InversifyJS로 IoC 컨테이너 구성하기

InversifyJS는 타입스크립트에서 IoC 컨테이너를 구현하기 위한 라이브러리로 Ninject의 문법을 상당부분 차용했다. 따라서 C#으로 백엔드를 구현하면서 Ninject를 써 봤다면 쉽게 이해할 수 있다. 아래와 같이 같단한 인터페이스 타입을 정의해 보자. Ninject와 InversifyJS 개발자들이 닌자 덕후라서 아래와 같은 타입이 나온 걸 감안하고 보자.

인터페이스 정의

View the code on Gist.

위와 같이 기본적인 WeaponWarrior 타입 인터페이스를 정의했다. 아래는 실제 이 인터페이스를 구현한 모델이다.

모델 구현

우선 InversifyJS는 Symbol을 이용해서 DI에 필요한 타입을 정의한다. 아래와 같이 WarriorWeapon 인터페이스 타입을 Symbol로 정의한다.

View the code on Gist.

InversifyJS@injectable@inject 데코레이터를 제공한다. 아래는 @injectable 데코레이터를 이용해서 클라스 타입을 정의한 코드이다.

View the code on Gist.

@inject 데코레이터는 컨스트럭터 파라미터에 적용시킨다. 이 때 앞서 정의한 SERVICE_IDENTIFIER.WEAPON와 같은 Symbol 객체를 사용한다. 대신 단순히 Symbol("Weapon")을 사용해도 상관없다.

View the code on Gist.

IoC 컨테이너 생성

앞에서 구현한 모델과 인터페이스를 통해 실제로 IoC 컨테이너를 생성할 차례이다. 아래 코드를 보자.

View the code on Gist.

위 코드 마지막의 container.bind<T>(...).to(...) 부분을 보면 C#에서 IoC 컨테이너를 구성하는 방법과 상당히 유사한 것을 볼 수 있다. 이렇게 최종적으로 만들어진 container를 개별 Vue 컴포넌트에서 사용하기만 하면 된다.

Vue 컴포넌트에 DI 적용하기

이전 포스트와는 달리 Hello.vueNinja.vue라는 이름의 자식 컴포넌트를 하나 추가해서 DI를 확인해 보도록 하자.

View the code on Gist.

위 코드에서 볼 수 있다시피 Hello.vue는 자식 컴포넌트로 Ninja.vue를 추가한다. 이제 Ninja.vue를 뜯어보자.

View the code on Gist.

DependencyConfigs.ts에서 생성한 container 인스턴스를 직접 실행시켜 Ninja 인스턴스를 받아왔다. 실제로 앱을 실행시켜 보면 아래와 같은 결과를 확인할 수 있다.

지금까지 VueJs 앱을 개발할 때 타입스크립트를 사용하게 되면 어떤 식으로 IoC 컨테이너를 활용해서 의존성 관리를 하는지에 대해 간략하게 알아보았다. 아직까지는 VueJs 프레임워크 수준에서 DI 관련 타입스크립트 지원이 미진한 감이 있지만 조만간 vue-property-decorator 또는 vue-class-component 라이브러리에서 좀 더 간결한 DI 접근 방식에 대한 업데이트가 있길 기대한다.