JavaScript Module Injector 만들기

실용성은 없을것이다...개념이해 정도로..

Posted by javarouka(이항희) on 2016-11-04

과거 개인 블로그 글을 가져온 포스트입니다

JavaScript에서 DI를…

최근 AngularJS 에 관심이 많아서 여러모로 살펴보는 중인데, 그 중에서도 재미있게 본 것은 Dependecy Injection 을 JavaScript 레벨에서 지원해준다는 것이었다.

Java 등에서 쓰이는 Spring Framework에서는 ApplicationContext 에 빈을 등록해두면 특정 애노테이션을 확인하여 DI 해주는 방식으로 진행되지만 JavaScript에서는 Annotation 같은 것이 없고 (비슷하게 구현해볼 수는 있지만 낭비…) 다른 방법으로 구현해야 한다.

비결은 Function.prototype.toString 에 있었다.

Function.toString

JavaScript의 함수는 toString을 할 경우 함수의 소스코드를 문자열로 반환한다.

1
2
3
4
5
function imFunction(you, say, ho) {
console.log(you, say, ho);
}
document.getElementById('result').innerHTML = imFunction.toString();

jsFiddle

여기서 중요한 것은 함수의 인자 목록도 문자열에 포함되어 있다는 것이다.

이걸 활용하면, DI를 흉내내볼 수 있다.

구현시작…

먼저 정규식이 필요하다

함수의 toString 결과를 함수의 이름, 인자, 몸체.이 셋으로 나눠볼 정규식을 만들어보자.

(함수 몸체와 이름은 일단 쓸일이 없지만 후 확장을 위해 한번에 구해봤다..)

1
var FN_PARSE = /^function\s*(\S+)[^\(]*\(\s*([^\)]*)\)\s*\{([\W\w]+)\}$/m

위 정규식으로 match 할 경우 [toString 결과, 함수 이름, 함수 인자, 함수 몸체] 의 배열을 얻을 수 있다.

주의할 점이, 자바스크립트는 함수 인자 목록에도 주석을 사용할 수 있기에 자칫 주석으로 인자 이름을 잘못 가져올 수 있다.

주석을 제거하는 정규표현식도 준비한다.

1
var STRIP_COMMENT = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg

그렇다면 적당한 함수를 하나 준비해본다

1
2
3
function hello(man, to, women) {
console.log(man + to + women);
}

파싱해보자.

1
2
3
4
5
6
7
8
9
10
11
var FN_PARSE = /^function\s*(\S+)[^\(]*\(\s*([^\)]*)\)\s*\{([\W\w]+)\}$/m,
STRIP_COMMENT = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
function hello(man, to, women) {
console.log(man + to + women);
}
var parsed = hello.toString().match(FN_PARSE),
fnName = parsed[1],
fnArgs = parsed[2].replace(STRIP_COMMENT, '').split(',')
fnBody = parsed[3];

잘 된다!

jsFiddle

모듈 레지스트리 및 인젝터 구현

그럼 남은일은 모듈을 등록할 레지스트리를 구현하는 일이다.

여기서는 간단히 이름 기반의 DI만 지원하는 것으로 하고, 키-값 객체로 관리하게 해보자.

일단 AMD 모듈이 아닌 일반적인 모듈로 구현해봤다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
(function(ctx) {
var FN_PARSE = /^function\s*(\S+)[^\(]*\(\s*([^\)]*)\)\s*\{([\W\w]+)\}$/m,
STRIP_COMMENT = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,
M = {};
ctx.Injector = {
// 의존성 모듈을 새로 등록한다.
register: function(name, mo) {
M[name] = mo;
},
// 함수를 받아 의존성을 주입한 뒤 즉시 실행한다.
execute: function(fn, ctx) {
return this.di(fn, ctx || this)();
},
// 함수를 받아 의존성을 주입한다.
di: function(fn, ctx){
// 함수를 정규식으로 분해한다.
var parsed = fn.toString().match(FN_PARSE),
fnName = parsed[1],
args = parsed[2].replace(STRIP_COMMENT, '').split(','),
body = parsed[3],
i = 0, j,
injected = [];
// 인자의 이름으로 레지스트리에서 찾아 순서대로 적재
for(j = args.length; i < j; i++) {
injected[i] = M[args[i].trim()] || undefined;
}
// 래핑 함수를 반환한다.
return function() {
return fn.apply(ctx || null, injected);
}
}
};
})(this);

di 함수에서 대해 조금 설명하면, 함수를 먼저 분석기로 쪼개서 배열을 얻은 뒤, 인자 배열을 돌면서 등록된 모듈과 매치하는 배열을 생성한 뒤 wrap 하여 반환하는 방식이다.

어디 잘 돌아가나 테스트.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var Coffee = {
pour: function(some) {
return "커피를 " + some + "에 따르고 ";
}
}
var Milk = {
pour: function(some) {
return "우유를 " + some + "에 따르고 "
}
}
Injector.register('coffee', Coffee);
Injector.register('milk', Milk);
function Cup(coffee, milk) {
var me = "머그컵";
return coffee.pour(me) + milk.pour(me) + " 섞어 마신다";
}
var drink = Injector.di(Cup);
var coffeeMilk = drink(),
directDrink = Injector.execute(Cup);
console.log(coffeeMilk); // 커피를 머그컵에 따르고 우유를 머그컵에 따르고 섞어 마신다
console.log(coffeeMilk == directDrink); // true

맛있는 커피우유가 만들어진 것 같다.

jsFiddle

생각해볼 것들.

현재 구현이 포스팅하며 날림한거라 미비하거나 주의할 점이 몇가지 있다

  • uglify 등 minify 할 경우 인자 이름이 보존이 안된다. mangle 옵션 등으로 인자이름을 보전해야 올바른 동작이 가능하다.
  • 래핑 함수를 반환하는 관계로 스코프가 꼬일 수 있다.

댓글: