Change Detection 란 무엇인가?

Change Detection(이하 CD)는 뷰와 모델을 동기화 할 수 있도록 컴포넌트의 변경을 감지하고 값을 동기화 하는 프로세스다.

 

Angular JS (1.x)

뷰단의 변경을 감지하는 것 뿐만 아니라, 비동기로 돌아가는 자바스크립트(ajax, setTimeout 등..)가 데이터 변경하는 시점을 감지하여 모델의 변경의 뷰단에 반영해야 하는데 Angular 1에서는 이런 로직을 Digest Loop로 구현 했었다. 

Digest Loop로 감시할 대상을 지정하기 위해 scope마다 watcher을 등록하여 감지할 목록들을 만들어야 했는데 이런 상황 때문에, 데이터의 변경을 감지하기 위해서는 Angular 도구만을 사용해야 했다. ajax call 또는 click 같은 이벤트도 $http와 ng-click으로 구현해야만 모델 변경이 감지되었다.

이를 보완하기 위해 명시적 호출($diest, $apply, $timeout)을 지원했지만 개발자가 직접호출해야한다는 이질감이 있었다.

또한 Angular 1은 양방향 데이터 바인딩으로 구현했기 때문에 watcher들이 늘어날 수록 Digest Loop은 걷잡을 수 없이 혼란스러워졌다.

Angular

Angular2에서는 $scope기능을 없애고 Zone.js를 이용하여 CD를 구현하였는데, Zone.js 덕분에 Angular 도구 뿐만 아니라, 순수 자바스크립트로 컴포넌트를 구성할 수 있게 되었다. 또한 Angular 2는 단방향 바인딩을 구현하였기 때문에 (ngModel을 사용해서 양방향 바인딩을 구현할때도 데이터의 흐름은 단방향) CD는 어플리케이션 전체를 변경할 필요 없이 데이터가 바인딩 되어있는 곳만 변경을 감지하고 Angular2는 변경된 부분만 다시 그려주면 되었다. 이를 통해 Angular는 성능을 높일 수 있었고 강제되던 Angular 도구들을 제거할 수 있었다.


Figure: File StructureFigure: File Structure 출처 - https://angular-2-training-book.rangle.io


어떻게 변화를 감지하는가? (Zone.js - 동영상)

Angular는 Angular도구가 아닌 순수 자바스크립트로 변경한 데이터 변경 또한 감지 할 수 있다고 했다.

우리가 웹에서 ajax로 데이터로 가져오거나, Timer로 함수를 실행하거나, click 이벤트를 발생시키는것은 모두 비동기로 이루어지는데 사용자가 명시적으로 호출하지 않고도 어떻게 Angular는 어플리케이션 상태 변화를 감지할 수 있을까?

답은 Zone.js에 있었다. (Angular는 자체적인 Zone을 가지고 있고 ngZone이라고 한다.)

Zone.js는 JAVA의 ThreadLocal 개념과 비슷해서 각각의 비동기 기능에서 kick on과 kick off를 설정하지 않아도 Zone이 이를 감지하여 후킹할수 있게 해준다.

 

 

위 코드를 실행해보면 콘솔 창에 다음과 같이 나타단다.

1번에서 click event listener가 등록된것을

2번에서 setTimeout이 발생하여 Invoike 한것을 상세하게 알 수 있다.

이렇게 Zone.js는 비동기 코드들을 컨트롤하면서 사용자에게 편리한 후킹인터페이스를 제공해주는 것이고,

Angular는 ngZone을 이용해서 컴포넌트의 변화를 감지하는 것이다.

 

 

Angular에서 구현된 소스는 다음과 같다.

this.zone.onMicrotaskEmpty
.subscribe(() => {
this.zone.run(() => this.tick()
})
})

tick()
{
this.changeDetectorsRefs
.forEach((ref) => ref.detectChanges())
}

어떻게 변경하는가?

그렇다면 앞서 ngZone이 감지한 변화를 Angular는 어떻게 View에 적용할까?

Angular의 컴포넌트들은 각자 자신만의 CD를 가지고 있어서, 각자 CD 수행하고 컨트롤할 수 있다. 예를 들어 클릭 이벤트가 발생하면, Zone.js가 이벤트를 감지하여 알려주고 Angular는 CD를 실행한다. 이 때 컴포넌트는 각자 자신의 CD를 가지고 있고 컴포넌트는 트리구조로 이루어져 있기 때문에 CD의 구조도 방향이 있는 트리구조로 구성된다. 방향성이 있는 트리구조 때문에 CD는 항상 TOP에서 ROOT로 전파된다.

 

http://pascalprecht.github.io/slides/angular-2-change-detection-explained/#/59http://pascalprecht.github.io/slides/angular-2-change-detection-explained/#/59

 

Angular 컴포넌트들은 자신만의 Change Detector를 가진다?

Runtime에 Angular는 각 컴포넌트마다 자신이 가진 속성들에 대하여 Change Detector 클래스를 생성한다.

Change detector들은 컴포넌트의 각 속성에대한 Old Value를 가지고 있기때문에 독립적으로 자신의 컴포넌트에 대한 변경 여부를 확인 할 수 있고,

감지 속도를 높이기 위해 개발자가 전략을 작성하거나 속성값 등을 전달 할 수 있다.

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* @stable
*/
export declare abstract class ChangeDetectorRef {
abstract markForCheck(): void;
/**
* Detaches the change detector from the change detector tree.

*/
abstract detach(): void;
/**
* Checks the change detector and its children.
*/
abstract detectChanges(): void;
/**
* Checks the change detector and its children, and throws if any changes are detected.
*/
abstract checkNoChanges(): void;
/**
* Reattach the change detector to the change detector tree.
*/
abstract reattach(): void;
}

Change Detection을 빠르게

그렇다면 어떻게 Chage Detection을 효율적으로 실행할 수 있을까?

앞서 설명했듯이 CD는 Runtime시 각 컴포넌트마다 생성되기 때문에 다양한 형태를 가질 수 있다. 각 컴포넌트의 구조에 맞게 CD 클래스를 정의 하고 생성하기 때문에, CD가 감시해야할 대상을 줄이면 성능을 높일 수 있다. Angular는 CD가 감시해야 할 대상을 줄이기 위해 Immutable 객체와 Observable 객체를 알아?보고 감시 대상에서 제외해 할 수 있는데, 이런 최적화는 Angular의 OnPush 전략(컴포넌트에 바인딩된 Input 값의 변화가 없는 경우에는 Subtree 에 대해 Change Detection을 하지 않음)으로 설정하여 처리한다.

 

Immutable

Immutable 객체는 값의 변화가 없음을 보장한다. 새로운 값을 지정하고 싶으면 객체를 변경하는 것이 아니라 새로 생성 해야만한다.

객체를 새로 생성한다는 것은 Angular 입장에서 값(상태) 비교가 필요 없다는 것을 뜻한다. Immutable 객체를 사용하는 바인딩된 컴포넌트들은 변경된 객체의 상태를 비교해 볼 필요 없이 객체의 레퍼런스만으로 변화를 감지할 수 있다. 

(Javascript Immutable lib들을 이용하여 손쉽게 Immutable 객체를 생성해 볼 수 있는다. Immutable.js Github)

 

http://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.htmlhttp://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html

 

Observable

관찰할 수 있는, 식별할 수 있는 객체를 의미한다. 관찰할 수 있는 객체를 구독(subscribe)하면서 값을 참조하는 방법이다.

예를 들어 다음과 같은 코드가 있을 때, firstName만 변경하여도 fullName까지 함께 변경 되는 것이다.

var firstName = Observable('BK');
var lastName = Observable('KIM');

var fullName = Observable(function () {
return firstName.value + ' ' + lastName.value;
});

Angular에서는 Immutable 객체 반대 방식으로 CD를 생략한다. Immutable 객체의 레퍼런스 참조만으로 CD를 생략할 수 있었다면 반대로 Observabel 객체는 값의 변경점에서 이벤트를 발생시켜서 TOP CD까지 변경을 전파한다. 따라서 Angular는 ROOT에서 TOP으로 가는 길목에 있는 CD만 수행하면 되는 것이다. 이때 OnPush전략을 함께 사용하면 Observable 객체의 레퍼런스는 변경되지 않기 때문에, 이벤트 컴포넌트 Subtree의 CD가 생략 된다. 하위 컴포넌트들도 함께 업데이트 해야한다면 디텍터의 markForCheck()라는 API를 이용하여 Subtree까지 CD를 수행 할 수 있다.

 

http://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.htmlhttp://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html

마치며

AngularJs 1.x에서 Angular 2로 넘어오면서 가장 관심이 갔던 부분은 성능이었다.

Change Detection에 대해서 자세히 알아보기 전까진 ReactJS의 Virtual DOM과 비슷한 방식이라 생각했었는데, 깊이 들여다보니 Framewrok로서 장점을 잘 이용한 것 같다. 성능 저하와 복잡도 상승의 주 원인이던 양방향 바인딩을 없애고, Zone.js를 도입 함으로써 라이브러리 사용에 비해 자유롭지 못한 Framework의 단점을 보완하려 한 부분도 흥미로웠다. 들여다 볼수록 매력있는 Angular지만 제대로 사용하지 않으면 이러한 장점들의 존재조차 모르고 사용하게 될것이란 게 두렵기도 하다. 

참조

 

 

 

'ANGULAR' 카테고리의 다른 글

IntelliJ 에서 Angular 2 프로젝트 시작하기  (499) 2017.05.16

결론

JavaScript개발시 어떤 프레임워크 또는 라이브러리는 사용할것인가? 라는 질문에 대답하기 위해 기술조사를 했다.

사용할 기술을 선택할 때는  언제나 트레이드 오프가 발생한다. 

따라서 개발하려고 하는 서비스와 팀의 규모를 고려해서 결정 하는 것이, 트렌드와 유행에따라 결정하는것보다 타당하다 생각한다.


기술조사를 마치고 느낀점은 Angular는 개발자의 자유도를 제한하는 대신, 우리가 마주할 수 있는 이슈에 대하여 기본적인 방법을 제시해고 있었고,

React는 스스로 라이브러리임을 밝히며 몸집을 최소화하며 개발자의 선택을 존중하고 있었다. 개발자의 자유도를 제한하지 않는면에서 잘나가는? 분야에서 조예가 깊은? 개발자는 React Family를 좋아하겠다는 생각을 여러번 했다. 실제로 기술조사를 하면서 React를 추천하는 곳에서 geek함과 개발자스러운 냄새를 맡을 수 있었다.


부족한 견해일지 모르지만 서버개발 프레임워크와 비교해보았을 때 Angular를 보면서 Spring같다는 생각을, React Family를 보면서 Node.js 같다는 생각을 했다. 또한 진정으로 POJO와 닮은 Vanilla JS의 간단한 페이지를 돌아보며 이것 또한 좋은 방법 같았다. Jquery 또한 여전히 사용성에서 1등을 고수하고 있었으며, 사용 추세가 전혀 줄어들지 않고 있었다.


다만 우리가 Spring을 사용하듯이, 팀단위로 일정한 수준 이상의 JavaScript 생산성과 품질을 유지하려면 Angular의 프레임 안에서 개발해나가는게 좋을 것 같다. 개발자에게 주어지는 자유도가 높을수록 성능은 높아질 수 있지만, 유지 보수와 협업의 관점에서의 성능은 어플리케이션의 규모가 커질수록 낮아질 것이다. 


만약 우리가 Front개발에 익숙하지 않은 팀이라면 Angular를, Front개발에 익숙한 팀이라면 React Family를 사용하는 것이 좋겠다.

소규모의 팀이거나 규모가 작은 서비스는 Front개발의 숙련도를 떠나 React Family를 이용하는것이 좋을 것 같다.

Framework

프레임워크란 무엇일까? 자바스크립트 프레임워크를 비교하기 전에 프레임워크에 대한 정의부터 분명히 해야겠다.

GoF의 디자인패턴 저자 랄프존슨은 "프레임워크란 소프트웨어의 구체적인 부분에 해당하는 설계와 구현을 재사용이 가능하게끔 일련의 협업화된 형태로 클래스를 제공하는 것" 라고 말한다. 혹자는 디자인 패턴 + 라이브러리 = 프레임워크라고 주장한다.


다양한 프레임워크가 존재하는 만큼 다양한 의미를 포함하고 있지만, 자바스크립트 프레임워크는 Inversion of control을 가능케하고 Modularity, Reusability, Extensibility 를 높이기 위한 목적을 가진 라이브러리 + 구조화 된 뼈대의 조합이라고 생각하면 좋을 것 같다. 우리가 흔히 사용하는 Jquery는 뼈대를 제공하지 않고 라이브러리만 제공하기때문에 프레임워크라고 부르지 않는다. Jquery를 사용해서 MVC 패턴을 입히면 그것은 프레임워크라 부를 수 있을 것이다.


최근 JavaScript가 사용되는 분야가 많아지고 인기가 높아지면서 오픈소스로 개발되는 자바스크립트 프레임워크들 또한 다양해지고 있다.

그 중에서 Angular 와 React를 비교해보고 이러한 상황을 비꼬는 듯한 용어인 Vanilla JS에 대하여 알아보자.

Angular

Angular는 MVC Framework로 최근 4.0버전을 출시했다. AngularJs는 1.x 버전을 지칭하고 Angular는 2.0 이후 버전을 지칭하는데, 1버전과 2버전은 패러다임의 변화가 있었고, 2버전 이후부터는 하위버전 호환을 유지하고있다. AngularJs에서 성능 문제의 주요원이었던 2 way data binding을 Angular에서는 빌트인으로 제공하지 않으며 서로 호환되지도 않음으로 분리해서 보아야 한다.


Angular는 구글 자체적으로 개발하던 AtScript를 사용하려 했으나, Microsoft와 협력하여 TypeScript를 채택했다.

한편에선 Facebook의 React와 달리 구글이 Angular를 사용하는 비중이 낮다는 점을 우려하고 있다. (YouTube를 새로 개편할 때 Polymer 사용)- 링크1, 링크2

React.js

UI 컴포넌트를 만들기 위한 라이브러리로, UI 컴포넌트만을 지원함으로 지원하는 범위가 작지만 다향한 조합으로 사용 할 수 있다.

React.js로 Angular의 directives를 구현하여 프레임워크를 구성할 수 있지만, 보통 Redux를 이용하여 프레임워크를 구축한다.

JavaScript와 TypeScript를 지원하지만 JSX라는 ECMAScript 친화적인 XML 문법을 사용할 것을 추천한다.

VanillaJS

VanillaJS는 자바스크립트 라이브러리와 프레임워크 홍수를 풍자하는 단어이다. 

Java에서 POJO와 비슷한 개념이라 볼 수 있다.


순수한 JavaScript만을 이용하여 구현한?(구현해야 할) 프레임워크다. 

별도의 라이브러리 없이 순수한 JavaScript만을 사용했기 때문에 가장 빠르고, 가벼운 장정이 있다.


http://vanilla-js.com/ 에서 사용하고 싶은 컴포넌트를 선택 후 다운받아 소스를 열어보면 그 의미를 더욱 잘 알 수 있다.



Angular VS React.js ? (2016 데뷰 정리 - 영상 , 발표자료)

위에서 설명했듯이 Angular는 프레임워크고 React.js는 라이브러리다. 

둘을 1:1로 비교하는것은 무리가 있고 Angular VS React Family(React.js + Redux + React router + Babel) 정도로 비교해야할 것이다.

2016년 데뷰발표자료에 사견을 더해 둘을 비교해 봤다.

1. 성능

https://github.com/CoderK/js-framework-benchmark 로 성능 평가 해본 결과 

 

 Angular

React 

Vanilla 

 상대 시간

 2.12

2.00 

 1.00

 메모리 사용량

 2.67

1.79 

 1.00

의미있는 속도차이는 없었음. 엔터프라이즈급 서비스 개발을 위해서 생산성에 좀 더 초점을 두어야 할것 같다.

학습 비용은 비슷하다고 생각함.

2. 언어 생산성

최근 Angular CLI 까지 나오면서 개발 환경과 학습 비용은 Angular가 낮다고 생각한다. 

또한, Angular가 사용되는 곳이 많아지면서 (웹앱, 앱, 데으크탑 앱) 사용성도 증가되고 있다. 


 Angular

 React

  •  구글은 웹표준을 철저히 지키는 회사. 
  •  새로운 언어를 배운다는 개념보다 Next Es 라고 생각하자
  •  구글이 만든게 표준이 아니면 표준을 추가하려고한다...?!
  •  TypeScript는 지나친 비용을 야기한다. 
  •  타입 체커 라이브러리인 FLOW면 충분하다.
  •  언어 생산성 측면에서 JavaScript 언어의 한계가 존재 했고 TypeScript가 탄생했다. 
  •  Angular 와 React 모두 TypeScript를 사용할 수 있지만, React는 타입을 지정해 주는 체커형태 라이브러리인 FLOW를 더 많이 사용하는 편

3. 컴포넌트

 Angular

 React

  • Angular는 HTML 과 JS CSS 를 잘 분리했고 CSS 캡슐화를 내부적으로 지원해서 높은 이식성과 재사용성을 보장한다.
  • JSX를 쓰면 마크업이 JS안으로 들어가는 형태. 코드를 실행하기 전까지 마크업 미리보기가 안된다. 또한 자바스크립트를 모르는 마크업 개발자와 협업할 경우 협업 자체가 매우 어렵다.
  • Angular는 표준 HTML에 디렉티브를 확장해나가는 개념이라 기존 HTML 문법을 그대로 사용할 수 있다.(React는 className, defaultValue 같은 문법 사용)
  • React역시 Webpack를 사용하면 CSS 캡슐화를 지원할 수 있지만 네이티브로 지원하지 않은 것은 조금 아쉬운 부분인건 사실.
  • 협업이 어려운점은 동의한다. 하지만 Angular도 Html에 논리코드(디렉티브)를 끼워넣어야 함으로 구조 안에 행위를 집어 넣는 순간 순수하지 못하다.
  • 사실 구조와 기능을 분리할 수 없다. 그래서 통합하는 것을 목적으로 했고, JSX 는 기존 javascript에서 DOM을 생성하던 것의 단점을 보안하고 가독성을 높인 결과물이다.
  •  컴포넌트 = HTML + JS + CSS
  •  CSS분리에 대해서는 동의하지만 JSX의 존재에 대하여선 대립 

4. 데이터 동기화

 Angular

 React

  • Angular의 경우 양방향 바인딩으로 성능이슈가 있었지만 Angular2와 React는 상당히 닮아있고 성능부분에서도 비슷해짐.
  • Angular는 React의 Virtual DOM을 보고? 체인지 디렉터라는 것을 만들었다. 마찬가지로 모델이 바뀌면 트리구조의 DOM을 훑으면서 달라진 부분만 다시 그린다.
  • Angular는 조금 더 나아가서 Zone(실행영역)이란 것을 이용해서 DOM 변경시 setState, setValue를 개발자가 명시하지 않아도 자동으로 모델을 변경시켜준다. (React 는 DOM -> Model 은 명시해줘야함)
  • React는 Virtual DOM을 이용하여 모델이 바뀌면 새로운 Virtual DOM을 그리고 기존 Virtual DOM 과 비교해서 Diff 된 부분만 새로 그려준다.
  • 뷰와 모델의 분리 후 데이터 동기화 문제 등장. 
  • Angular의 경우 양반향 바인딩으로 성능 이슈가 있었지만, Angular2로 넘어오면서 개선된 상태

5. 비동기 처리

 Angular 

 React 

  • 비동기 처리를 위해 RxJS를 품었다. ZONE을 이용한 DOM -> MODEL 로 데이터 전송처럼 Angular 자체적으로 RxJS는 잘 품었다.
  • Javascript가 가진 다른 대안도 많은 와중에 왜 RxJS를 품은 것인지는 더 지켜봐야할 일
  •  React는 라이브러리인 만큼 Angular보다 자유롭게 비동기 라이브러리를 쓸 수 있다.
  • 페이스북은 리엑트를 라이브러리로 지원하기 때문에 RxJS를 포함하려고 하는 움직임은 없지만 커뮤니티에서 활발히 진행 중. 
  • 또한 React와 함께 쓰는 Redux와 RxJS 는 궁합이 좋고 NetFilx에서 사용한 예가 있다.
  • Angular가 RxJS를 플레임워크로 품은것은 관심가지고 볼 사항이다.




Node.js의 탄생과 JavaScript라는 언어의 범용성이 커짐에 따라 JavaScript의 테스트 환경은 점점 더 중요해졌다.

JavaScript가 서버언어로 사용되면서 JavaScript 테스트는 e2e 테스트 뿐만아니라 BDD 또는 TDD가 가능한 유닛 테스트가 필요했고,

기존 테스트 환경은 발전했고, 새로운 테스트 프레임워크들이 탄생했다.

 

여러종류의 테스트 프레임워크 중 Mocha, Jasmine, QUnit을 비교해보자.

결론

테스트는 개발을 돕기위한 도구일 뿐 목적이 아님으로 개발 환경에 맞고 사용하기 편한 툴을 선택하면 된다.
테스트 속도 차이는 미비하고, 대부분의 툴들이 비동기 테스트를 지원함으로 테스트 툴간 성능비교는 무의미 한 것 같다.

자바스크립트 프레임워크를 사용하지 않고 Jquery만 사용한다면 Qunit으로 간단하게 테스트 환경을 구축하면 되고
Angular와 같은 자바스크립트 프레임워크를 사용한다면 Mocha와 Jasmine중 선택하면 될 것이다.

TDD를 생각한다면 DOM 이 필요 없는 Jasmine을 확장성 높은 테스트 환경을 생각한다면 Mocha를 선택하면 좋겠다.
나는 테스트 기능에만 충실하고 별도의 Dependency 설정이 필요없어 사용하기 쉬운 Jasmine을 선택하고 싶다.

테스트 비교

 

 Mocha 

 Jasmine

 Qunit 

 버전

 3.4.0

 2.6.1

 2.3.2

 인기

 중간 

 높음 

 낮음 

 assertion 라이브러리

 chai 라는 외부 라이브러리 사용

 내장

 내장

 러너

 Karma 가능 

 Karma 가능 (Python, Ruby)  Karma 가능 

 난이도

 보통 (3rd party library 필요, 유연함)

 쉬움   가장 쉬움 

 커뮤니티

12.3K github Stars, 4.45K stack over flow 

 12.4K github Stars, 8.01K stack over flow

 3.63K github Stars, 1K stack over flow

 특징

 Simple, flexible, fun javascript test framework for node.js & the browser  DOM-less simple JavaScript testing framework

 A JavaScript Unit Testing framewor

테스트 문법

 Mocha 

 

var assert = require('assert');
describe('Array', function() {
describe('#indexOf()', function() {
it('should return -1 when the value is not present', function() {
assert.equal(-1, [1,2,3].indexOf(4));
});
});
});
 Jasmine
 

describe("A suite is just a function", function() {
var a;
it("and so is a spec", function() {
a = true;
expect(a).toBe(true);
});
});

 Qunit 
 


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>QUnit Example</title>
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-2.3.2.css">
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<script src="https://code.jquery.com/qunit/qunit-2.3.2.js"></script>
<script src="tests.js"></script>
</body>
</html>

 

QUnit.test( "hello test", function( assert ) {
assert.ok( 1 == "1", "Passed!" );
});

사람들이 좋아하는 이유(출처 https://stackshare.io/stackups/jasmine-vs-mocha-vs-qunit)

 Mocha

 Jasmine 

 Qunit 

 105 오픈소스

 50 TDD 로 사용할 수 있음

 5 단순함

 77 단순함

 39 오픈소스

 3 오픈소스

 62 Promise 지원

 14 RSpec 표준

 3 세팅하기 쉬움

 32 유연함  11 DOM조차 필요없는 독립성

 2 Promise 지원

 18 사용하기 쉬움

 10 훌류한 커뮤니티

 
 7 브라우저와 서버 테스트  5 세팅하기 쉬움  

 2 다른 좋은 대안이 없음

 3 단숨함  
 

 2 Pivotal-Labs에서 개발함

 

Stackshare 특징비교 (https://stackshare.io/stackups/jasmine-vs-mocha-vs-qunit)

구글 트렌드
























+ Recent posts