개발하며 배우는 Polymer (1/2)
Disclaimer
이 예제는 Polymer v0.8을 사용합니다. Polymer v1.0이 정식으로 출시되며 API가 크게 바뀌었음으로 더 이상 유효하지 않습니다.
About
Polymer/Firebase를 이용하여 간단한 게시판을 만드는 과정을 설명하며 Polymer에 대한 이해를 돕기위해 쓴 글이다.
- 1번째 파트는 Polymer개발을 위한 개발환경 설정과 Polymer framework에 대해서;
- 2번째 파트는 게시판에 들어가는 모듈을 만드는 과정을 다뤄보겠다.
What is Polymer?
Polymer를 설명하는데 Web component를 빼놓고 이야기 할수는 없지만 web component에 관한 좋은 글이 이미 많이 인터넷에 올라와있는 관계로 이에 관한 설명은 대부분 해당 리소스의 링크로 대체하겠다.
간단하게 설명하자면 web component polyfill
길게 설명하자면…
‘Allo ‘Allo!
개발을 위해 먼저 환경을 선택해야 한다. 웹 서버는 빠른 프로토타이핑을 위해서 Node.js로 선택. 자연스럽게 yeoman으로 스캐폴딩 (scaffolding) 을 하기로 했다.
공식 yeoman generator인 generator-polymer는 app 개발 모드와 element 개발 모드를 지원한다. polymer element 퍼블리싱 모드도 있으나 다루지 않을 예정이라 이에 관한 자세한 정보는 generator-polymer github 페이지 에서.
아래 명령어를 실행시키면
>> npm install -g generator-polymer
>> yo polymer
기본 element을 추가할거냐는 질문후 설치를 시작한다. 일단 깔아놓고 필요없는 모듈을 지우는걸 선호해서 모두 yes.
core-element는 Polymer에서 제공하는 web application을 만드는데 기본이 되는 element들. 모든 core-element는 이곳에서 데모코드와 함께 제공된다.
paper-element는 Google의 material design을 입힌 Polymer element라고 설명할수 있겠다. paper-element는 이곳에서 확인할수 있다.
Scaffold 뜯어보기
인스톨 후 열어본 기본 프로젝트 레이아웃이 굉장히 심플하다. Polymer 외에는 그 흔한 jquery도 없고 (사실 core element에서 기본적으로 제공해 주는 기능때문에 jquery에 크게 의존할 필요는 없는게 사실) Node.js도 파일 호스팅을 위해서만 이용되기 때문에 이 자체로는 웹앱 보다는 웹페이지에 가깝다고 할수 있을듯.
generator 쓰는 이유는 여러가지가 있겠지만 기본적인 workflow를 잡아준다는게 큰 이유다. generator-polymer도 마찬가지로 lint, minify/uglify, sass compilation 등 기초적인 grunt task를 잘 배치해 두었다. 그런데 vulcanize? 이건 뭐지..? 살짝 알아보자.
web component라는 태생 자체가 template, script, style 모두 다른 타입의 소스를 하나로 묶어서 재사용 용이한 component로 만드는데 의미가 있기 때문에 1개의 web component가 여러 web component를 가질수 있고 다중의 template, script, style파일을 가질수 있는 구조다. 따라서 페이지 상에서 이들을 개별로 HTTP request를 통해 불러오는 것은 성능상 효율적이지 못하다 (1개 component가 3개의 파일로 나눠져 있고 페이지에서 20개의 component만 사용한다고 해도 60개의 request). 그래서 페이지 에서 사용하는 polymer element들을 평평하게 페이지 html에 덧입힘으로서 성능을 개선하려는 하는 노력의 일환이다. HTML import의 성능팁에 관해선 HTML5Rocks 블로그에 이미 글이 올라와 있으니.. 이에 관한 자세한 설명은 링크에서 ☺
아래 명령어를 실행시키면 사이트가 실행이 된다.
>> grunt serve
한 페이지에 두개의 element가 들어가있다. 상단의 greeting message 그리고 하단의 list section. 두 element 모두 간단하니 greeting message만 보기로 하자.
Polymer에서도 angular로 익숙한 {{ mustache }} 스타일의 바인딩이 이용되고 있음을 볼수 있다.
<script> 안의 Polymer()는 현재 scope의 polymer element를 Polymer framework에 등록시키는 일을 한다. polymer-element에 name이 설정되어 있기 때문에 위의 element는 yo-greeting이란 이름으로 Polymer에 등록되고 DOM 상에선 <yo-greeting></yo-greeting>이라고 불러주기만 하면 된다.
Polymer()안에 들어가는 객체는 yo-greeting의 Prototype 속성이 된다. 그리고 모든 속성은 template 안에서 {{ property }} 처럼 부를수 있다.
css의 :host는 web component에 종속되어 있는 속성이기 때문에 넘어가도록 하겠다. 간단하게 설명하면 yo-greeting element에게 직접 부여되는 스타일이라고 알아두면 될듯. 이 외에도 :content, :shadow, /deep/ 등등 web component에 종속된 css query는 기존의 css query와 다른점이 있으니 spec과 polymer styling guide를 참고하시길.
Polymer Element 만들기
Polymer는 web component를 위한 polyfill — 고로 web component (=element)를 만들면서 배우는게 가장 이해하기 쉽겠다. Polymer element의 성질이 angular.js의 directive와 유사하기 때문에 angular.js를 사용해온 개발자라면 큰 무리없이 만들수 있을것 같다.
하지만 Polymer도 framework인 이상 자기 나름데로 룰이 있다. Polymer element를 만들때는 항상 아래 체크리스트를 확인하는 습관을 들이자.
Polymer Element 체크리스트
- Import polymer.html
- polymer-elemnt안에선 template와 script가 순서대로 와야함
- element의 name attribute 설정, 그리고 모든 polymer element의 이름은 대쉬(-)를 포함해야함
- Polymer에 element 등록시키기. Polymer element 안에서 선언된 script 라면 굳이 이름을 제공하지 않아도 안전함
Polymer([ tag-name, ] [prototype]);
- prototype 설정이 필요하지 않은 간단한 template의 경우에는 polymer-element에 noscript 라는 attribute를 붙여주면 아래 script를 더한 것과 같은 효과를 지닌다.
<script>
Polymer();
</script>
- 외부 JS 파일을 사용하는 것도 가능하다. 이 경우에도 역시 script는 template 이후에 있어야 한다.
자 그럼 custom element 한번 만들어 볼까?
먼저 2개의 element를 만들고 이를 통해 페이지 레이아웃을 구축해보자.
- my-app은 layout과 content간의 커뮤니케이션을 돕는 정도로만 사용하려고 my-layout 상위 element를 만들고.
- my-layout에선 side-bar 메뉴를 만들고 내용을 위한 placeholder 정도만 만들어 두자. <content></content>를 넣어주면 <my-layout></my-layout> 안에 들어오는 내용이 content section을 대체한다.
Polymer에서 제공하는 core-element들을 이용하여 페이지 레이아웃을 뚝딱 하고 만들어 보았다. 먼저 위 코드에서 알아두어야 할 것은 무엇인가 한번 짚고 넘어가보자.
Element의 Life cycle
Polymer element는 아래와 같은 life cycle을 가지고 있다. 자세한 내용은 이곳에 나와있지만 한글로 간단하게 풀어보자면..
Polymer(‘tag-name’, {
created: function() { … }, // 객체가 생성 되었을 때
ready: function() { … }, // 사용준비가 되었을때 (이벤트 리스너/옵저버 설정 등등이 완료 되었을때)
attached: function () { … }, // DOM에 첨부 되었을 때
domReady: function() { … }, // DOM에 직계 child elements가 모두 첨부 된 것이 확실할 때
detached: function() { … }, // DOM에서 분리 되었을 때
attributeChanged: function(attrName, oldVal, newVal) { // 해당 attribute의 값이 변했을 때.
//var newVal = this.getAttribute(attrName);
console.log(attrName, ‘old: ‘ + oldVal, ‘new:’, newVal);
},
});
예제 코드에서 만들어 본 my-layout에 선언된 ready는 사용이 가능한 상태가 되는 순간 메뉴가 가장 처음 아이템을 선택하여 selectedChanged를 부른다. 근데 갑자기 뜬금없이 왠 selectedChanged? Life cycle에는 attributeChanged라고 쓰던데?
값이 변했을때 반응한다는 점에서 성질은 같지만 attributeChanged는 DOM에 부여된 attribute 변화에 반응하는 이벤트이고 selectedChanged는 my-layout 객체가 가지고 있는 selected라는 속성 값이 변화했을때 반응하는 이벤트이다. Polymer에선 이를 changed watcher라고 부른다.
Changed Watcher로 Polymer element 속성 변화에 반응하기
Polymer element에 객체에 속성된 property의 값이 변할때 polymer는 <속성이름>Changed라는 event handler를 불러준다. 위 코드의 예를 들면 my-layout의 selected값이 변했으니 selectedChanged가 불려지는 것이다. 그리고 page값을 안에서 설정해 주면 선택된 메뉴가 타이틀에 올라가는 거고.
별도로 pageChanged는 설정을 안해놓았기 때문에 DOM상에서 page 값이 변화해도 custom event handler는 불리지 않는다.
이정도도 충분하지 않다 싶은 경우에는 observe 블록을 사용할수도 있다. 이 블록에서 property와 change handler만 선언해 주면 설정된 attribute의 <attribute-name>Changed 대신 observe 블록안에 선언 된 change handler가 불려진다.
Element간 커뮤니케이션
Polymer element가 가지고 있는 속성의 직접적인 binding을 위해서는 polymer-element의 attributes를 사용해야 한다. 이를 통해 element의 공개된 속성을 선언하면 element 생성시 Polymer가 two way binding을 맺어 준다. two way binding이기 때문에 my-app에서 page 값을 바꾸면 my-layout에서도 똑같이 적용된다는 뜻이다.
이렇게 공개속성을 통한 접근의 장점은 쌍방향 커뮤니케이션을 설정하는데 별도의 코딩이 필요 없다는 점, 단점은 polymer element 안에서 polymer element간의 직접적인 커뮤니케이션만 지원한다는 점이다.
my-layout을 보면 알수있듯, attributes를 통한 binding은 {{ property }} 로 쓰지 말고 그냥 property 이름만 적어주어야 한다. 익숙해 지기 전엔 습관처럼 모든 binding은 {{ }} 안에 넣어주다보니 자꾸 브레이크가 걸리더라는.
이 외에도 이벤트를 통한 element간의 커뮤니케이션이 가능하다. fire function을 통해 아래 처럼 이벤트를 뿌리게 되면
var eventData = { };
this.fire('random-event', eventData);
다른 element에서 on-<event name> (위의 경우엔 on-random-event)라는 handler를 부여하고 이벤트를 구독할수 있게 되는 것이다.
자바스크립트에서 이벤트는 bubble up 형식이라 아래 element에서 위로 순차적으로 올라오며 handler를 부른다는 사실을 기억하고 다음 코드를 보고 이해해보자.
결과 확인은 이곳에서: Codepen
그런데 이벤트란게 기본적으로 위로만 향한다는게 좀 그렇다. 레벨에 상관없이 여러 element 에서 같은 이벤트 구독받고 싶은데.. 이건 안되나?
이럴땐 core-signals 라는 core-element를 써서 문제를 해결할수 있다.
core-signals를 import 한 후 아래처럼 event name은 core-signals로 설정하고 이벤트에 넘겨주는 객체에 name을 아래 코드처럼 설정하면
this.fire(‘core-signal’, {name: ‘hello’, data: null});
다른 element에서 core-signals를 import 한 후에 on-core-signals-<event name> (위의 경우엔 core-signals-hello)라는 handler를 부여함으로써 element가 어디있든 상관없이 이벤트를 구독할수 있게 된다.
Event handler 설정하기
위에선 custom 이벤트를 이용하기 위해 썼지만 on-<event-name> 은 굳이 custom 이벤트가 아니어도 기존의 DOM event에도 반응한다. 그리고 on-* handler는 굳이 Polymer element가 아니어도 상관없다. 예를 들면 이런 느낌?
<div on-click=”{{ clickHandler }}”></div>
Polymer({
clickHandler: function(event, details, sender) { ... }
});
Template binding 사용하기
TemplateBinding은 HTML Template가 template에 자신에게 부여된 컨텐츠를 만들고, 관리하고 삭제하는 것이 가능하도록 만든 Polymer에서 제공하는 익스텐션 라이브러리다.
TemplateBinding은 template에 4가지의 attributes를 통해 template의 data binding 용이함을 돕는다. 간단하지만 없어선 안될 template binding을 짧은 예제를 통해 설명하겠다.
- bind
template 안에선 binding에 선언된 값의 context 안에 있다고 이해하면 된다.
<template bind=”{{ user }}”>
사용자 정보: {{ name }}
</template>
그리고 똑같은 사용자 정보 {{ user.name }}
- repeat
repeat은 이름 그대로 반복함수와 같은 성질을 지닌다.
repeat을 사용할땐 배열의 원소에 이름을 지정해 줘도 되고 안해줘도 된다. 다만 지정해 주지 않는다면 bind와 같은 효과가 적용된다는 것만 유의 하자.
<template repeat=”{{ users }}”>
사용자 정보 {{ name }}
</template>
원소에 이름을 지정해 주는 경우에는 index 또한 받을수 있다.
<template repeat=”{{ user, index in users }}”>
{{ index }} 번째 사용자: {{ user.name }}
</template>
- if
<template if=”{{ conditionalValue }}”>
conditionalValue가 사실값(truthy)일 경우에만 출력된다.
</template>
- ref
<template id=”myTemplate”>
ref를 이용해 이 template를 언급하는 template가 사용한다.
</template><template bind ref=”myTemplate”>
인스턴스가 생성될때 이 template의 content는 무시하고 ref에 선언된 #myTemplate의 content가 대신 사용된다.
</template>
Expression 사용하기
Angular.js의 필터와 같다고 이해하면 된다. DOM 상에서 선언적으로 함수를 적용시키고 싶을때 “{{ source-data | expression }}” 같은 방식으로 사용하면 된다.
예를 들면 아래 같은 경우에는 number속성에 3이 있으면 출력되는 값은 square라는 function이 적용된 9가 된다. expression function은 해당 Polymer element의 속성이어야만 접근이 가능하다. 자세한 설명은 이곳에서.
{{ number | square }} // number = 3 일 경우에는 9가 출력된다.
Polymer({
square: function(num) { // 적용되는 값이 1번째 parameter로 들어온다.
return num * num;
}
});
Conclusion
1장은 2장을 위한 준비+Polymer이해 돕기의 시간이었다면 2장에선 본격적으로 이를 활용해서 게시판 어플리케이션을 만드는 시간을 갖도록 해보자.