자바스크립트 템플릿 엔진 비교

어떤 템플릿 엔진을 써야할까?

평소에는 레식 사마가 만들었던 micro-template 을 변형해 만든 JSTools의 pre-compile 기능을 주로 써왔었는데,.. 아무래도 일반적이지 않다보니 (물론 난 pre-compile이 좀더 익숙하지만) 대중적으로 어떤 템플릿 엔진들이 있는지 좀 알아봤다. 역시나 나와 같은 고민을 하는 사람이 이미 전세계에 수없이 많고, 그중에서 LinkedIn에서 공유한 자료가 꾀나 괜찮았다. 정리해보자. 템플릿 엔진은 크게 두 가지로 분류할수있다.

1. 자바스크립트 로직을 품는 레식 사마의 micro-template 계열

존레식이 처음 제안했던 마이크로 템플릿은 jQuery-Template 에 포함되면서 발전해봤고,  이제는 다른 엔진에 밀려 deprecated 된다는 얘기가 있어서 최종적으로는 underscore.js 로 귀결되는 듯싶다. 마이크로템플릿의 장점은 learning-curve가 거의 없다. Syntax가 간단하고 직관적이어서 딱 3가지만 알면된다.  다음은 underscroe.js의 문법이다.

  • < % 자바스크립트 코드 %> 
  • < %= 치환될 데이터 변수 %>
  • < %- escaped 데이터 변수 %>

기본적으로 자바스크립트 로직을 템플릿에서 쓸수 있기 때문에 루프라든가 조건문을 자유롭게 쓸수 있다.  단, 템플릿에 자바스크립트 로직이 들어가기 때문에 다소 보기가 어려울수도 있지만 확실히 배우기 쉽다.  

2. 자바스크립트 로직이 없는  logic-less의 mustache 계열

템플릿 문법에 자바스크립트 로직이 없기 때문에 반복문이나 조건문 같은 로직을 템플릿 자체 문법을 가지고 해결한다.  다음은 간단한 dust.js 템플릿 코드다.

{?tags}
  <ul>
    {#tags}
      <li>{.}</li>
    {/tags}
  </ul>
{:else}
  No Tags!
{/tags}

템플릿 엔진을 선택할때 고려해야할 요소

다음은 LinkedIn에서 고민했던 요소들이다.

  1. DRY: how DRY is the templating technology? is there support for code-reuse and partials?
  2. i18n: is there support for translations and multiple languages?
  3. Hot reload: are changes visible immediately or is there a compile/deploy cycle?
  4. Performance: how long does it take to render in the browser and server?
  5. Ramp-up time: how is the learning curve?
  6. Ramped-up productivity: once you’ve ramped-up, how fast can you build things?
  7. Server/client support: can the same template be rendered both client-side and server-side?
  8. Community: is there an active community using this project? Can you google issues?
  9. Library agnostic: are there dependencies on other JS libraries, such as jQuery or Mootools?
  10. Testable: how hard is it to write unit and integration tests?
  11. Debuggable: is it possible to step through the code while it’s running to track down errors?
  12. Editor support: is there an editor with auto-complete, syntax highlighting, error checking, etc?
  13. Maturity: is this a relatively stable project or still experimenting and churning?
  14. Documentation: how is the documentation?
  15. Code documentation: do the templates encourage/require documentation/comments?

위와 같은 요소들을 다 고려하기에 내 프로젝트가 너무나 초라하다? 라고 생각된다면 고민할 필요도 없다. 그냥 underscore.js 를 쓰면되겠다.  위와 같은 많은 고민을 해야하는 경우는 프로젝트의 사이즈가 큰 경우라고 보면 된다. 프로젝트 사이즈가 크지 않을 경우에는 배우기 쉽고 쓰기 쉬운 underscore.js 를 강력 추천한다. 하지만 내가 진행하는 프로젝트가 어느정도 규모가 있고 템플릿을 적극적으로 쓸 의지가 확고하다면, Logic-Less 계열의 템플릿 엔진도 고민해봄직하다. 기본적으로 템플릿 자체에 로직이 없기 때문에 훨씬 더 간결한 템플릿을 만들어 낼 수 있다. 즉, 더 적고 더 효율적인 코드를 생산해 낼수있다는 얘기다. 특히 모바일 플랫폼이라면 사이즈도 적이다!

템플릿 엔진 최적화

엔진마다 최적화 방법이 서로 다르고, 이미 각 엔진마다 Best Practice가 있으므로 문서를 보고 잘 따라하면 된다. 가령, 최적화의 끝판이라고 불리는 precompile 기능도 요즘엔 기본으로 문서에 추가되는 분위기다. 물론 템플릿 자체 최적화도 중요하지만 얼마나 효율적으로 템플릿을 나누고 구성하느냐는 전적으로 개발자 몫이다. 아무리 좋은 도구를 가지고 있다한들 제대로 활용을 못하면 무용지물인것 처럼 템플릿을 어떻게 구성해야하는지는 경험에 기인하는 경우가 많다. 따라서 지금이라도 당장 템플릿 사용 경험을 쌓는것이 좋다!  

References
[1] http://engineering.linkedin.com/frontend/client-side-templating-throwdown-mustache-handlebars-dustjs-and-more [2] http://engineering.linkedin.com/frontend/leaving-jsps-dust-moving-linkedin-dustjs-client-side-templates [3] http://coenraets.org/tutorials/mustache/
[4] http://akdubya.github.io/dustjs/  

유사배열 객체만들기 (making array-like object in JS)

평소에는 jQuery를 잘 쓰지 않는다. 이미 jindo를 이용해 원하는 형태로 얼마든지 개발할수있기 때문이다. 하나의 라이브러리를 딥하게 쓰다보면, 그 라이브러리에 매우 익숙하게 되고 무엇이 문제인지 무엇이 좋은지 잘 모를때가 있다. 그래서 종종 개인 프로젝트에는 jQuery를 일부러라도 찾아서 쓴다.

몇일전 부터 jQuery를 조금 딥하게 보고 있다가 특이한 점을 발견했다.
바로 jQuery() 객체로 인스턴스를 만들면 다음과 같이 배열로 반환한다는 점이다.

> jQuery()
[]

> jQuery('#test')
[<div id=​"test" class=​"this is a class">​OK​</div>​]

하지만 겉모습만 배열이지 실제로는 jQuery의 인스턴스다.

jQuery() instanceof Array  // false
jQuery() instanceof jQuery // true

오~! 신기한데 어떻게 저럴수 있지? 겉모습은 배열이지만 배열 아닌 유사배열!! 자바스크립트에서 내가 아는 유사배열은 두가지다!. 하나는 arguments 객체고 다른 하나는 셀렉터가 반환하는 NodeList다. 유사배열은 배열이 가지고 있는 배열 특유의 메소드들이 없다. 그래서 어떻게 보면 불편할수도 있다.

그런데 jQuery와 같이 인스턴스가 유사배열 형태로 반환이 되면 장점이 있다. 다음과 같이 DOM을 수정하면 바로 디버거 콘솔창에서 변화를 확인해볼수있다는 사실이다!!

> jQuery('<div>').addClass('test')
[<div class=​"test">​</div>​]

확인해본 결과 웹킷계열의 디버거에서만 위와같이 찍히고, 다른 브라우저들은 그냥 객체로 나온다. 그래도 개발은 크롬에서 주로 하기 때문에 위와같이 콘솔로그만 찍혀도 엄청 도움이 된다.

그래서 만들어봤다. 어떻게 저렇게 하면 나올수있을까?
혹시나 싶어 배열을 프로토타입 상속으로 만들어봤다.

var fine = function(){ };
fine.prototype = Array.prototype;
var x = new fine();

x instanceof fine;  // true
x instanceof Array; // true

그런데 배열을 프로토타입 상속으로 받으면 x는 배열의 인스턴스도 된다. 뭐 누구의 인스턴스가 되냐 안되냐가 그렇게 중요한 문제는 아니지만, 여기서 해당 인스턴스가 되면 필요없는 배열의 메소드들이 fine 객체에 들어갈수가 있다는 것이 문제가 된다.

그럼 어떻게 해야하나? 결론부터 말하면 만들고 싶은 객체에 length와 splice 프로퍼티를 넣으면된다.

> var foo = {length:0, splice:function(){}, x:1, y:1};
> console.log(foo);
[splice: function, x: 1, y: 1]

이런 트릭이 있었다니..대박!! 물론 웹킷계열의 디버거에서만 저렇게 나온다. 요거때메 몇일을 궁리하다가 검색으로 찾아냈는데… 사실은 jQuery 코드에도 위와 같은 코드가 숨겨져 있었다!!

// For internal use only.
// Behaves like an Array's method, not like a jQuery method.
push : core_push,
sort : [].sort,
splice : [].splice

꼼꼼한 레식사마~ ㅋ

내가 몰랐던 재밌는 자바스크립트 이야기

몇달전 자바스크립트 가든이라는 문서의 한글 번역본을 읽다가 포기하고 다시 원문을 보면서 재번역을 했다. 처음엔 그냥 오역이 몇가지 보여서 오역만 바로 잡아볼까하고 원번역자에게 메일을 보냈는데.. 결국엔 전체를 모두 다시 번역했다. 현재는 원저자에게 Pull Request를 보낸상태고 머지는 언제 될찌 모르겠다. 현재 상황에서 기억 남는 몇가지를 남겨보면,

1. 호이스팅(Hoisting)

이게 말이지 참 재밌다.  JSLint에서 이렇게 해라~ 라고 추천했던 가이드들이 몇가지 있었는데.. 예를 들면,

“var문은 맨위로 올려라” 하는 가이드가 있다. 왜 위로 올리라는 거지? 성능 문젠가? 스코핑 때문에 그런가? 그래.. 올리라면 올리지모.. 라고 하면서 var로 선언한 코드를 전부 위로 올렸던 기억이 있는데.. 여기엔 이유가 있었다. 바로 호이스팅!!! 호이스팅은 자바스크립트가 실행되기 전에 선언문을 스코프 위로 올리는 걸 얘기한다. 따라서 다음과 같이 for 문안에 선언한 i 변수는

for(var i=0; i<1000; ++i){
    // 어쩌구 저쩌구
}

  다음과 같은 스코프 상단으로 옮겨진다는 사실!!

var i=0;
for(i=0; i<1000; ++i){
    // 어쩌구 저쩌구
}

이렇게 자동으로 옮겨지는 것을 호이스팅이라고 한다. 이런 특징을 모르면 전역변수를 의도하고 쓰더라도 실제 지역변수에 있는 값을  쓰게 될지도 모른다!! 자세한 내용은 자바스크립트 가든의

함수 스코프를 참고하자!

2. 왜 new Array()는 안되는가?

이것도 역시 JSLint에서 가이드하는 내용중에 하나다. new Array()로 쓰지말고 [] 각괄호를 이용해 배열을 선언하라고 하는데, 이유는 new Array(3) 으로 넣을때 우리가 생각했던대로 동작하지 않기 때문이다. 보통은 배열에 3이라는 값을 하나 추가한 결과를 반환해줄꺼라 기대하지만, 요건 배열의 크기가 3인 빈배열을 반환한다. 심지어 인덱스는 초기화도 안한다는 사실!! 역시 자세한 내용은 자바스크립트 가든의

배열 생성자를 참고하자.

3. 계륵 같은 new 키워드의 비밀

함수를 만들때 new 키워드를 붙여서 생성하지만, new 키워드가 없어도 생성할수있다.

function Foo(){
    this.value = 1;
}
new Foo();
Foo();

위와 같이 호출하면 new 키워드의 유무에 따라 반환 값이 달라진다. new 키워드를 붙이면 Foo 함수가 생성자로써 동작하게 되면서 특별한 반환값이 없으면 this가 가리키는 값을 반환해준다. 즉, Foo 객체의 인스턴스를 반환해주는데,. Foo()는 그냥 함수를 실행한다. 그래서 this 객체는 Foo를 가리키는게 아니라 전역을 가리키게 되고, 반환하는 값이 없으므로 결과값은

undefined 가 된다. 뭐 여기까지는 누구나 아는 사실인데.. Foo 객체가 생성자로 동작할때 즉, new 키워드가 붙을때 , 이 생성자에 명시적인 return 값이 있다면 또 얘기가 달라진다. 명시적인 객체를 리턴하면 그값이 결과값으로 반환되지만, return 값이 없거나 참조타입이 아닌 프리미티브 타입을 반환하면 this가 가리키는 값을 반환한다. 반면에 new 키워드가 없는 경우, Foo 객체는 생성자가 아닌 일반 함수로 동작하면서 return 문의 값을 반환한다. 그래서 결론은 명시적인 반환값을 객체로 주면, new 키워드가 있든 말든 동일하게 동작한다는 사실!! 그래서 팩토리 패턴으로 만들 경우 자연히 new 키워드가 필요없게 된다! 자세한 내용은 역시 자바스크립트 가든의 함수 생성자를 참고하자!

4. 타이머는 스택킹 된다!

자바스크립트에서 타이머는 setTimeout과 setInterval 두가지가 있다. 어찌 생각해보면 setIntveral을 한번만 실행하고 없애면 setTimeout 은 필요없는거 아닌가? 하는 생각을 종종했는데.. 역시나 setTimeout은 ECMA 표준 스펙이 아니었다! 헉! 그랬꾸나.. 그래서 setTimeout의 첫번째 인자에 대한 해석도 엔진마다 다르다는 사실!… 여튼 이건 뭐 그렇게 중요한 얘기는 아니고.. 진짜 재밌는 얘기는 바로 setInterval 함수가 스택킹된다는 사실이다! setInterval 함수는 일정 주기마다 함수가 호출되는데.. 엄밀히 말하면 일정주기마다 함수를 실행하는 것은 아니고 일정 주기마다 실행할 함수를 쌓는다. 그래서 자바스크립트 쓰레드가 그 함수를 실행할때가 되면 실행하게 될테고, 뒤이어 실행되어질 함수는 앞에 함수가 다 끝날때까지 기다리게 된다. 이거다 싱글 쓰레드라서 발생하는 문젠데.. 그럼 한번 생각해보자!. 주기가 엄청 짧은데.. 실행되는 함수는 그 주기보다 길경우 어떻게 될까? 그렇다! 무조건 쌓이게 된다!! 이렇게 쌓인 함수는 시간이란 변수가 더이상 필요없게 된다.  즉, 내가 원하는 주기가 깨질수도 있다는 사실!! 자세한 내용은

타이머를 참고하자!

 5. 삼중 등호의 비밀

이중 등호(==)와 삼중 등호(===) 지금까지는 삼중 등호는 인스턴스를 비교해서 보다 정확하고, 이중 등호는 대충 (?) 비교해서 부정확하다라고 생각하고 있었다.. 하지만 이 둘의 차이는 그런게 아니었다.!!  바로 자동 타입 변환을 하느냐 마느냐의 차이!! 이중 등호는 자동 타입 변환을 해서 끝까지 비교하지만, 삼중 등호는 타입 변환 없이 바로 비교한다. 그래서 훨씬더 빠르다! 자세한 내용은

타입 비교를 봐라! 이 외에도 재밌는 내용이 참 많은데.. 여튼 난 위 5가지가 가장 새로웠다!

나도 맹글었다. PathUI SVG 버전

라파엘이 좀 손에 익으니, 금방 맹글게 되네..
오랜만에 삼각함수 검색했다. ㅇㅎㅎ

코드 좀 정리해서 모듈로 배포해야지.
샘플 페이지는 >>> 여기에

몇가지 코드 정리할 필요가 있다.
1) 옵션을 뺄수있는 설정들 정리
2) 주석 정리
3) 모듈 테스트

주석빼면 100줄도 안되네.. ㅇㅎㅎ

자바스크립트 정규식에 대한 고찰..

오늘 미투머니 테스트 코드를 작성하다가,
그동안 안개속에 쌓였던, 정규식의 맘을 헤아리게 되어 몇가지 공유합니다.
제가
말하고자 하는 내용은 사실 이 포스트에 다 있어요.

정규식 RegExp Vs String


정규식은
기본적으로 문자열 패턴을 응용한 놈입니다.
그러니, 당연히 String 객체와는 뗄레야 뗄수가 없는 놈입죠~


동안 크게 신경안쓰고 있다가, 아~ 그렇치 하는걸 오늘 알았어요. 저도..ㅋㅋㅋ
너무나 당연한 사실을 저만 늦게 깨달은
기분.. 아하하하

정리하면,

정규식은 2가지 형태로 사용할수있습니다.
1. 정규표현식
객체(RegExp)를 사용하는 방법
과,
2. 문자열 객체(String)의 정규식 메소드를 이용하는 방법

메소드만 정리하면, 이렇쵸!!

RegExp.test() – Boolean 값을 리턴
RegExp.exec()
– 매칭된 값을 Array로 리턴
String.split() –
String.match() –
String.replace()

String.search() –


RegExp.test() Vs RegExp.exec() 의 성능 차이


반적으로 두 메소드 중에서, test() 메소드의 성능이 더 좋다고 얘기합니다.
왜 그럴까요?.. 그냥 성능이 좋다고
하니까 그려려니 하나요? 이젠 제대로 이해해봅시다.
여기엔 이유가 있습니다.

바로 캡쳐링이라는 기능 때문입니다.
캡쳐링이 뭐냐하면, 간단히 말하면, 패턴으로 찾은 놈을 따로 저장하는걸 얘기합니다.

앞의 두 메소드의 리턴값이 하나는
Boolean 이고 하나는 Array인 점이 바로 여기에 있습니다.
당연히, test 메소드가 리턴값이 Boolean
이므로, 성능이 더 좋겠죠?
네,.. 맞습니다. 하지만 단순히 리턴값의 사이즈가 작다고 성능이 더 좋다고 얘기할수는 없습니다.

왜냐면, 결국 RegExp 객체가 찾은 패턴을 모두 가지고 있기 때문이죠.
따라서, test() 메소드만을 사용한다고 성능이
좋아지는 것은 아니랍니다.
정확히 얘기하면, 어떻게 패턴을 정의하냐에 따라 달라지겠죠!!

예를 들어보겠습니다.

var
str = “테스트 테스트1 테스트2 테스트3 테스트4”;
var regx = /테스트\d/;

regx.test(str);
  // true
regx.exec(str);  // [“테스트1”]

위와 같이 실행하면,
test()는 true, exec()는 [“테스트1”] 배열을 반환합니다.
그리고 여기서 하나더!! 위에서도 잠깐
언급했지만, 두 메소드 모두 결국 RegExp 전역 객체를 사용하게 됩니다.

그러니까, 각 메소드를 실행하고,
아래와 같이 RegExp 객체의 $_ 와 $1 값을 확인해보면,
모두 같음을 확인할수있습니다.
RegExp.$_ 
// 테스트 테스트1 테스트2 테스트3 테스트4″  – 테스트할 문자열
RegExp.$1  // “”  – 캡쳐링된 문자열


얘기는 결국, test() 메소드와 exec() 메소드의 내부 구현은 같되, 리턴값만 다름을 의미합니다.
즉, 현재 까진
리턴 사이즈 말고는 성능이 같다는 얘기죠~!!

이제 좀 변형해봅시다. RegExp 객체에 패턴을 저장하기 위한 캡쳐링
옵션을 줘보도록 하지요.
regx = /(테스트)/ 로 바꿔서 해봅시다.

var str =
“테스트 테스트1 테스트2 테스트3”;
var regx = /(테스트\d)/;

regx.test(str);  
// true
regx.exec(str);  // [“테스트1”, “테스트1”]
RegExp.$1;       // 
“테스트1”    – test(), exex() 모두 같음.

뭐가 다른지 감이 오나요?..
exec()가 뱉어내는 리턴값을 유심히 보세요. 아직 모르시겠나요?
캡쳐링은 매핑된 결과를 RegExp 객체가 내부에 저장한다고 앞서 말씀 드렸습니다.
바로 그 캡쳐링된 결과를 RegExp.$1, RegExp.$2 등으로 읽어올수있습니다.
물론 캡쳐링은 하나의 패턴안에서 괄호를 여러번 사용함으로써 저장할수 있습니다.

가령, 이렇게 쓸수도 있다는거죠!!

var str = “테스트 테스트1 테스트2 테스트3”;
var regx = /(테스트)\s(테스트\d)/; 

regx.exec(str);   // [“테스트 테스트1”, “테스트”, “테스트1”]

자 이제 exec()의 리턴값의 구성을 이해하시겠죠?


[매칭된 문자열, 캡쳐링된 첫번째값, 캡쳐링된 2번째값, 캡쳐링된 3번째 값, … , 캡쳐링된 N번째 값]

이젠 RegExp에 저장하지 않도록 비캡쳐링(?:xxx) 옵션을 주고 하나 더 해보죠.

var
str = “테스트 테스트1 테스트2 테스트3”;
var regx = /(?:테스트\d)/;

regx.test(str);   // true
regx.exec(str);  // [“테스트1”]
RegExp.$1;  
    //  “”    – test(), exex() 모두 같음.

자~~ 이젠 어떤 차이가 있지는
아시나요? 여전히 모르시겠다구요?
비캡쳐링(?:) 옵션을 사용해서 RegExp 에 찾은 패턴값을 저장하지 않았습니다.
그러니까, exec() 리턴값에도 찾은 패턴값이 넘어오지 않게 되죠?

자 그럼, exec() 리턴값의 구성을  한번더 정리해보죠~


[매칭된 문자열, RegExp.$1, RegExp.$2, RegExp.$3, … , RegExp.$N]

마지막으로 그럼,

도대체 성능과는 무슨 관계가 있는거냐?

이 질문에 답할 시간입니다.
이미 눈치채고 계신분이라면, 조용히 닫기버튼을 누르셔도 됩니다. ㅋ

test()와 exec() 메소드의
성능차이는 간단합니다. 이렇게 정리하도록 하죠!

“간단한
패턴 문자가 있는지 없는지를 확인할땐, 비캡쳐링과 test() 메소드 조합을 사용해라!

되셨
나요?

이렇게 얘기 할수도 있습니다.

“test() 메소드를 사용할때는 반드시 비캡쳐링 패턴으로 정의해라!”

그 이유는 앞서 주구장창 설명한 캡쳐링되어 저장된 값 때문입니다.


패턴 플래그 g 에 대한 고찰

이제 오늘
깨달은 것의 하일라이트!! 바로 g 플래그 옵션입니다.
패턴 플래그는 총 3가지가 있죠. i, g, m 뭐 다 아실꺼라
생각하고 각각의 설명은 생략합니다.
모르면 검색해보아요~

이제 g에 일반적으로 알려진 사실을 흔히 쓰는 예제로
보면, 아래와 같습니다.

var str1 = “No pain, No gain!”;
var str2
= str1.replace(/ain/g, “XXX”);
// str2 = “No pXXX, No gXXX!”

즉,
“g 옵션을 쓰면, 모든 패턴을 찾게 된다.”
Global 의 G 가 바로 그 g 옵션인거죠..

그러면, 얼핏 이런 사실을 알고 있을때, 지금껏 제가 착각해왔던
것은 아래와 같은 겁니다.
앞에서 해왔던 예제를 이어봅니다.

var str = “테스트 테스트1
테스트2 테스트3”;
var regx = /(테스트\d)/g;

regx.exec(str);  // [“테스트1”, “테스트2”, “테스트3”] 일까요???????

g
는 글로벌 옵션이니까,.. 저렇게 나와야 하는게 아닐까?.. 하는거죠!!
결론부터 얘기하면 아닙니다…
왜인지는 아시겠죠? 아직도 그 이율 모르신다면, 앞에서 설명한 exec() 메소드의 리턴값 구성을 다시한번 보세요.

그러면 이렇게 해보죠..

var str = “테스트 테스트1
테스트2 테스트3”;
var regx = /(테스트\d)/g;

regx.exec(str);  // 1번 실행, [“테스트1”, “테스트1”]
regx.exec(str);  //
연속해서 두번 실행, [“테스트2”, “테스트2”]
regx.exec(str);  // 연속해서 세번 실행, [“테스트3”,
“테스트3”]
RegExp.$1;  // “테스트3”

어랏?.. 저렇게 나올껄 예상하셨나요?
올~~ 예상했다면, 당신은 규식이를 잘 하는 분입니다.
아직도 모르시겠다구요? 글로벌 옵션이긴 하지만, 적어도 제가 기대했던
것과는 사뭇 다릅니다.

자 그럼 g 옵션의 비밀을 정리해보죠..
정리하면,

패턴 플래그 g 옵션은
연속해서 패턴을 찾을때,

다음 패턴 검색을
위해 RegExp객체에 패턴 검색의 시작 위치를 저장해 둔다.


이해되셨나요? 앞에 예제를 곱씹어보세요~ ^^

test() 메소드와 exec()
메소드는 리턴값이 다른 만큼 분명 그 쓰임도 다릅니다.
따라서, 패턴을 어떻게 정의하느냐에 따라서,
RegExp
객체에 얼마나 많은 정보가 캡춰링 되어 저장되는지, 그 비효율성도 고민하셔야합니다.

여기까집니다.
도움이
되셨나요? 도움이 되셨다면,.. 리플이라도..ㅋㅋ
아놔.. 이거 다 아는거잖아.. 썅~ 뭐냐? 이러면,.. 걍 쌩까세요~
ㅋㅋㅋ