[js] Function의 toString 이용하기

자바스크립트에서 함수객체는 매우 특별한 toString을 제공합니다. 다른 객체들과 달리 본인을 투영(reflaction)할 수 있습니다. 이 특징을 이용한 두 가지 패턴을 살펴보면서 함수객체의 재정의를 할 수 있는 하이브리드형 템플릿을 살펴 봅니다.

 
 

함수객체의 toString

자바스크립트의 함수객체는 toString 메서드를 통해 함수의 정의 내용 전체를 문자열로 반환하는 기능을 갖고 있습니다.
예를 들어 다음과 같은 결과를 얻을 수 있습니다.

function test(){
	return 1;
}

console.log( test.toString() ); // "function test(){return 1;}"

이 기능은 거의 모든 브라우저에서 호환됩니다. 단지 구형 파폭4.x 에서는 ”가 나오기 때문에 문제를 일으킵니다(..만 파폭 구형지원이란건 요즘 세상에 하지 않으니까요 ^^)

 
 

여러 줄이 있는 문자열 생성에 응용하기

이러한 특성은 여러 줄로 된 문자열을 js로 생성하기 위한 편법에 이용됩니다.

var str = function(){/*
여러줄로 된
문자열을 만들기가
매우 편리합니다
*/}

console.log(
	str.toString().replace( /^function.*(){/*s|s*/}$/g, '' )
); //몸체의 문자열만 나온다

정규식을 약간 보기 쉽게 해석해보면 | 앞부분은

  1. ^ – 처음부터
  2. function – 이 나오고
  3. .* – 크롬같은 넘들은 분명히 function()라고 해도 function () 이렇게 괄호 앞에 공백을 자동으로 추가하곤 합니다. 그 외에 기명함수도 있으니까..
  4. (){/* – 이스케이프 치우면 (){/* 입니다. 즉 괄호와 주석시작까지
  5. s – 공백문자 하나가 나온다. 이 경우는 줄 바꿈 문자입니다

뒷부분은 간단히 줄바꿈문자 하나, */} 에 대응해서 앞부분과 뒷부분 모두 지워버립니다.

근데 이건 뭐 그렇다고 치고 진짜 중요한건 이게 아닙니다.

 
 

함수 생성 템플릿으로 쓰기

일반적으로 함수를 작성할 때 함수 내부의 로직을 곰곰히 보면 그 함수만의 기능과 다른 함수의 기능을 불러오는 부분이 있습니다. 예를 들어 다음의 두 함수를 보죠.

function A( val ){
	val = val * val;
	return val;
}

function B( val ){
	val = val + 10;
	return val;
}

A, B함수의 경우 거의 내용이 전부 같고 오직 val = val… 이 부분만 다릅니다. 따라서 정석으로는 다른 부분만 함수로 만드는게 맞습니다. 하지만 그게 쉽지 않은 경우도 많습니다.

  1. 다른 부분이 작은 코드 파편이고 한 번에 몰려있지 않고 함수 내부의 코드 위 아래에 분산 되어있다면 공통 부분을 뽑아내 함수를 만들기 곤란하거나 너무 많은 함수를 만들게 된다.
  2. 성능이 대단이 중요한 함수의 경우 함수 내부에서 작은 코드 조각의 독립성을 위해 다른 함수를 부르는 부하를 갖기 곤란한 경우 그냥 함수를 여러 판 만들게 된다.

하지만 위와 같은 이유로 같은 코드를 갖고 있는 함수를 2개 이상 만들면 수정시 두 군데를 수정해야하기 때문에 반드시 버그로 연결됩니다. 실무 세계에서 저런 대상의 함수는 처음 예처럼 단순하지 않습니다.

function animation( option ){

	//애니메이션 일반 준비작업 100줄

	option.target에 따라 해야할 준비작업

	//애니메이션 일반 실행작업 100줄

	option.target에 따라 해당 결과를 반영하는 작업

}

이런 예에서 언틋 생각하기엔 성능을 위해서 if, switch를 통해 option.target에 따른 분기처리를 해주면 될것 같지만, 이렇게 되면 새로운 target 타입이 나왔을 때 대응할 수 없게 됩니다. 확장을 외부로 열어둘 방법이 필요한데, 성능을 위해 option.target.ready(), option.target.result() 를 부르는게 부담스러운 경우에 해당됩니다.

여튼 코드 조각에 따라 함수를 여러판 만들되 코드는 단일하게 유지하면서 코드의 검사를 인터프리터에게 맡길 수 있는 중간 타협점이 바로 함수 생성 템플릿입니다.
일단 코드로 보면서 생각해보죠.

var template = function( option ){
	//option ready
	'@targetReady'
	//option run
	'@targetResult'
};

우선 템플릿 대상함수를 완전히 함수로 정의하되 코드 조각이 삽입될 부분을 문자열로 지정합니다. 위의 코드에서 ‘@targetReady’ 와 ‘@targetResult’ 가 이에 해당됩니다.
이러면 js는 함수를 인터프리팅할 때 나머지 모든 로직을 잘 검증해주고 템플릿이 삽입될 곳은 식문이 들어가 있으므로 에러없이 잘 통과시키게 됩니다.
이제 이 템플릿을 이용해 코드를 주입합니다.

var str = template.toString()
	.replace( "'@targetReady'", 'option.target.style.display = "block";' )
	.replace( "'@targetResult'", 'option.target.style.left = result + "px";' );

이렇게 str을 생성하면 함수의 템플릿 문자열 부분만 변경된 완전한 함수구문이 생성됩니다. 이걸로 새로운 함수를 만들어내면 되겠죠.

var domAnimation = ( new Function( '', 'return ' + str ) )();

이제 domAnimation은 target이 dom객체일 때의 애니메이션 함수가 되었습니다.

 

new Function 의 보강

new Function 을 통해 함수를 생성하는 로직은 깔끔하고 좋지만 여기에는 큰 문제가 하나 있습니다. new Function은 스코프를 인식하지 않는다는 점입니다. 따라서 이를 보강하려면 new Function을 호출하는 시점에 인자로 보내야만 합니다. 위의 domAnimation 이 공용 객체인 animation이라는 객체를 참조하는 코드라면 아래와 같이 정리할 수 있습니다.

var domAnimation = ( new Function( 'animation', 'return ' + str ) )( animation );

인자로 지역변수화 시켜 스코프를 세탁하면 되죠.

 
 

결론

개인적으로 이 발견은 일이년 정도 전에 하고 회사 프레임웍 여기저기에 응용하는데, 장점이라면 템플릿함수 태반은 인터프리터가 검증해준다는 점이고, 단점은 템플릿 코드가 문자열로 치환되니까 안정성에서 보호되지 않는다는 점입니다.
반대로 안정성을 보호하지 않아도 될 정도의 작은 코드 조각이 아니라면 함수 템플릿의 대상으로 쓰지 않는게 좋다는 의견이기도 합니다 ^^;