'Joe' 검색 결과 1건

  1. 2008.08.18 JSON-Object-Element Mapper (15)

JSON-Object-Element Mapper

간만에 하는 포스팅입니다.

다운로드: http://jania.pe.kr/aw/moin.cgi/JsonObjectElementMapper


최근 3주 중 2주 동안 자바스크립트 노가다를 조금 했습니다. 마지막 한 주는 2주간 했던 노가다를 앞으로 어떻게 줄여볼까 하는 궁리를 하면서 보냈습니다. (버그 안잡고! ㅋ) 그래서 일명 JOE ( JSON-Object-Element mapper, "죠"라고 읽습니다 --; ) 라는 놈을 만들었습니다. 참고로, jquery 플러그인입니다.


1. 간단한 예제:
<div id="names">
   <p class="name"><span class="first">Alan</span> <span class="last">Kang</span></p>
   <p class="name"><span class="first">Cate</span> <span class="last">Kim</span></p>
</div>

서버에서 그려서 보내준 위와 같은 HTML이 있다고 칩시다. 아마도 Name이라는 클래스의 인스턴스 두 개를 담고 있는 배열을 템플릿 엔진에 전달해서 그려낸 결과겠죠.

요즘은 Ajax다 뭐다 해서, 이 HTML을 클라이언트에서 조작해야하는 경우가 왕왕 생깁니다. 이를테면, 사용자가 첫번째 사람 이름(Alan Kang)을 변경하면 동적으로 HTML이 갱신되고, 서버로 Ajax 요청을 보내는 식입니다. 이때, 클라이언트 사이드에서 이런 일이 가능하다면 좋겠죠?
$.joe.def('Name', {first: '.first', last: '.last'});

위 코드를 통해 Name이라는 클래스의 매핑정보를 정의합니다. first와 last 속성이 있고 각각 CSS Selector .first 및 .last에 대응됩니다.
var names = $('#names p.name').toObj('Name');

위 코드에서는 jquery의 CSS selector 문법을 통해 원하는 Element을 선택하고(두 개의 p.name 엘리먼트) 이를 위에서 정의한 클래스(Name)의 인스턴스와 매핑합니다. 이렇게 해서 얻어진 names는 length가 2인 배열이고, 배열의 원소는 자동으로 생성된 객체입니다.
  console.log(names[0].first()); // Alan
  names[0].first('Allen');
  console.log(names[0].first()); // Allen

getter와 setter의 이름은 동일하고 인자의 갯수에 의해 다형적으로(polymorphic) getter 혹은 setter로 실행됩니다. 이는 jquery의 컨벤션을 따른 것이예요. 위 코드를 통해 첫번째 원소의 first 값을 'Alan'에서 'Allen'으로 변경하면 HTML의 해당 부분이 알아서 갱신됩니다. 이제 화면은 바꿨으니 서버로 요청을 보내야합니다. JSON이 필요한 상황이죠.
var json = names[0].toJson();

JSON도 쉽게 얻어졌습니다. 이 JSON의 내용은 {first: 'Allen', last: 'Kang'} 입니다. 전송하는 코드는 생략하겠습니다.


2. 컴포지션, 타입 지정, 속성 매핑

참 쉽죠?

( dotty님 블로그에서 보고 너무 재미있어서 함 퍼왔습니다 --; )

예제를 조금 더 복잡하게 바꿔 보겠습니다. Person은 name, age, profile image url을 갖습니다. 여기에서 name은 위에서 정의한 Name 클래스의 인스턴스입니다.
<div class="person">
  <p class="name"><span class="first">Alan</span><span class="last">Kang</span></p>
  <p class="age">18</p>
  <p><img class="profile" src="alan.jpg" /></p>
</div>

일단 두 클래스(Name과 Person)를 정의해야겠죠:
$.joe.def('Name', {first: '.first', last: '.last'});
$.joe.def('Person', {
  name: '.name {{Name}}',
  age: '.age {{number}},
  profile: '.profile [[src]]'
});

Name을 정의하는 부분은 별 것 없으니 생략합니다. Person의 정의가 재미있는데요,
  • name 속성은 .name에 매핑되고, 타입은 Name 입니다.
  • age 속성은 .age에 매핑되고, 타입은 number 입니다.
  • profile 속성은 .profile에 매핑되고, 타입은 생략되었으므로 string입니다. 다만 [[src]] 를 통해 innerHTML이 아닌 img.src 속성에 매핑되도록 지정하였습니다.

이제 정의를 했으니 인스턴스를 만들어봅시다:
var person = $('.person').toObj('Person');

첫번째 예제에서와 달리 이번에는 CSS selector에 의해 선택된 Element가 하나 뿐이므로 배열이 아닌 단일 객체가 만들어집니다. 이제, 아래와 같이 사용할 수 있습니다:

person.name().first('Allen'); // 이름을 Alan에서 Allen으로 변경
person.age(person.age() + 1); // 나이 한 살 더 먹기
person.profile('alan.png'); // img.src 값을 alan.jpg 에서 alan.png로 변경


3. Detached Object, Methods

위 코드를 보면 불편한 점이 둘 있습니다. 첫째, 성(last)과 이름(first)을 다 바꾸려면 메서드를 두 번 호출해야 하나? 둘째, 나이를 1 증가시키는 코드가 꼭 저렇게 길어야 하나?

첫번째 문제를 해결하려면 새로운 name 객체를 구해서 이를 바로 대입하면 될겁니다:
var newName = ...?? ;
person.name(newName);

하지만 새 name 객체를 만들 방법이 마땅치 않습니다. 기존에 이미 존재하는 Element에 대해 toObj()를 호출해야 하는 방법 밖에 없죠. 꾸졌습니다. 그래서, Element와 무관하게 새 객체를 만들 수 있도록 하는 기능을 추가했습니다:
var newName = $.joe.Name('Allen', 'King');
person.name(newName);

이렇게 만들어진 객체는 Element와 관련성이 없기 때문에 detached object라고 부릅니다. 사실 detached object의 용도는 다양합니다. 우리가 만들려고 하는 애플리케이션이 주소록이고, 주소 데이터가 만 개 쯤 있다고 칩시다. 이를 객체 형태로 만들어서 메모리에 담아놓고 각종 조작(정렬, 갱신, 추가, 삭제 등)을 하는 것이야 별 문제가 없겠으나, 이러한 모든 조작이 DOM에 영향을 준다면 심각한 성능 문제가 발생합니다. 따라서, 원하는 경우에 객체를 Element와 분리시킬 필요가 있습니다. 객체와 Element를 분리시키는 방법은 두 가지가 있는데, 위와 같이 애초에 Element와 연결되지 않은 객체를 새로 생성하거나, Element와 연결된 객체를 강제로 연결 해제하는 아래와 같은 방법이 있습니다:
var person = $('.person').toObj('Person');
person.detach(); // 연결 끊기
person.name().last('Allen'); // DOM은 갱신되지 않습니다.
person.attach($('.person')); // 다시 연결하기. 이 때 DOM이 일괄 갱신됩니다.

이제 두 번째 문제를 해결해봅시다. age를 1 올리는데 person.age(person.age() + 1) 같은 지저분한 코드를 써야하는 문제였습니다. 그냥 이렇게 할 수 있으면 좋겠습니다:
person.older();

아래와 같이, 클래스를 정의할 때 원하는 메서드를 추가해주면 됩니다:
$.joe.def('Person', {
  name: '.name {{Name}}',
  age: '.age {{number}},
  profile: '.profile [[src]]',

  older: function() { this.age(this.age() + 1); }
});

이제 지저분한 코드가 안으로 숨었고, 좀 더 객체지향적인 코드가 되었습니다. 기존 코드는 왜 객체지향적이지 않을까요? Person이 갖고 있는 속성인 age의 값을 외부에서 얻어와서 뭔가 연산을 수행하고 이를 다시 age에 대입해주는 코드는 캡슐화(encapsulation) 및 정보은닉(information hiding)에 위배되기 때문이죠. 자기가 가진 속성에 대한 연산은 자기가 하는 것이 좋습니다.


4. 배열 다루기

지금까지는 고정된 HTML이 있는 상태에서 HTML의 특정 내용을 바꾸는 작업만 했습니다. 즉, Alan과 Cate가 존재하는 상황에서 이들의 이름을 바꾼 적은 있지만 제 3의 인물이 추가된 적은 없었습니다. 이게 가능하려면 객체를 통해 HTML을 뽑아낼 수 있어야 합니다.

이번엔 좀 더 복잡한 HTML을 예제로 써보겠습니다. "가족(Family)"을 소개합니다. div.person 의 내부는 기존 예시와 동일한 구조라서 생략하였습니다.:
<div class="family">
  <div class="head">
    <div class="person">... Father ...</div>
  </div>
  <div class="members">
    <div class="person">... Alan Kang ...</div>
    <div class="person">... Cate Kim...</div>
  </div>
</div>

Family에는 가장(head)이 있고, 구성원들(members)이 있습니다. 가장과 구성원들은 모두 Person 클래스의 인스턴스입니다. 물론 Person은 Name을 포함하고 있습니다. Person과 Name의 정의는 생략하고 Family를 정의하는 부분만 살펴보겠습니다:
$.joe.def('Family', {
  head: '.head .person {{Person}}',
  members: '.members {{array<Person>}}'
});

members 속성의 타입은 array<Person> 입니다. C++/Java/C# 계열 언어의 제너릭스 문법과 유사합니다. Person 객체들을 원소로 갖는 배열이라는 뜻입니다. 배열에는 아래와 같은 방식으로 접근할 수 있습니다:
var family = $('.family').toObj();
family.members()[0].name().first('Allen');

가족(family) 구성원(members) 중 첫번째(0) 사람의 이름(first)을 'Alan'에서 'Allen'으로 변경하는 코드입니다. 이 코드를 보면 자연스럽게 몇 가지 질문이 생길겁니다. 첫째, 가족 구성원을 추가하려면? 둘째, 가족 구성원을 통으로 변경하려면? 셋째, 특정 가족 구성원을 대체하려면?

둘째 문제가 쉬우니, 이걸 먼저 풀어보겠습니다. 그냥 배열을 통으로 대입하면 됩니다:
var newMembers = [
  new $.joe.Person(...1...),
  new $.joe.Person(...2...),
  new $.joe.Person(...3...)
];
family.members(newMembers);

Person 객체(detached object) 셋을 담고 있는 배열을 만들고, 얘를 통으로 members에 대입해버리는거죠. 쉽습니다.

첫째 문제와 셋째 문제는 이런식으로 풀리질 않습니다. 그래서 다른 방법을 도입했습니다. 속성의 타입이 array<T>인 경우, 자동으로 추가적인 accessor를 생성해줍니다. 예를 들어 속성 이름이 members인 경우, 아래 메서드들이 추가됩니다:
  • insertMembers(index, value);
  • deleteMembers(index);
  • appendMembers(value);
  • prependMembers(value);
  • replaceMembers(index, value);

이름이 뻔하니까 설명은 생략하겠습니다. 아마 여기까지 읽으신 분 중에 뭔가 이상하다 싶은 부분을 발견하신 분이 있을지도 모르겠습니다. 네 뭔가 이상합니다. 배열에 새 원소가 추가되면 DOM에 새 Element가 생성되어야 하는데, 우리가 정의한 클래스는 그런 일을 해낼만큼의 충분한 정보를 가지고 있지 않습니다.


4. HTML 출력

사실, 위 코드가 작동하려면 객체로부터 Element를 만들어낼 수 있게끔 뭔가 정보를 추가로 제공해야 합니다. 클래스를 정의할 때 아래와 같이 템플릿 정보를 줘야 하는 것이죠:
$.joe.def(
  'Name',
  {first: '.first', last: '.last'},
  '<p class="name"><span class="first"><@= first() @></span><span><@= last() @></span></p>'
);

보통은 <%= ... %> 를 사용하는데, 이렇게 하면 .rhtml 등 서버측 템플릿 언어와 함께 쓰는 경우 충돌이 나서(--;;) 어쩔 수 없이 <@= ... @>로 변경했습니다.

Name 클래스는 간단하니까 <@= ... @>로 해결했다고 치고, Family 클래스는? members의 갯수만큼 반복(iterate)이 필요합니다. 어떻게 할까요? 어떻게 하긴요, 걍 똑같이 합니다:
<div class="family">
  <div class="head">
    <@= head().toHtml() @>
  </div>
  <div class="members">
  <@ for(var i = 0; i < members().length; i++) { @>
  <@= members(i).toHtml() @>
  <@ } @>
  </div>
</div>

5. HTML 분리하기

이제 대충 끝났습니다. 하지만 마음에 안드는 문제가 하나 있습니다. 자바스크립트 코드 안에 HTML 템플릿이 들어있잖아요. 이걸 좀 HTML 파일 안으로 넣고 싶습니다.

2005년인가 2006년에 D모사 H모 서비스 주소록을 만들 때 사용했던 트릭이 있었는데, 템플릿 코드를 style="display: none" 으로 감춰진 엘리먼트 안에 넣어놓는거예요. 그렇게 해놓고 cloneNode를 하건, innerHTML을 하건 해서 원할 때 마다 가져다 쓰는거죠. 얼마전에 은둔고수 응주님께서도 비슷한 트릭을(특히, 이 트릭이 왜 필요한지를) 설명하신 바 있습니다.

그리고, 두둥, John Resig 사마가 최근 궁극의 솔루션을 제시하시기에 이르게되는거시었던거시었습니다. (사실 위에서 소개한 템플릿 기능도 Resig 횽아가 갈쳐줬다눈)
<script id="t_name" type="text/html">
  <p class="name">
    <span class="first"><@= first() @></span>
    <span class="last"><@= last() @></span>
  </p>
</script>

짠! style="display: none;" 할 것 없이 걍 script 엘리먼트를 만들고 그 안에 HTML 템플릿을 넣는 것이죠. 간단해보이지만 재미있는 트릭입니다. 첫째, type이 "text/html"로 되어 있는데, 브라우저는 이런 스크립트를 모르죠. 모르는 스크립트는? 자동으로 무시합니다.

한편, 검색엔진은? 위와 같은 템플릿 코드는 검색엔진이 안보는 것이 좋을텐데요, 역시나 script 태그의 내용은 검색엔진에 의해서도 무시됩니다. 몇몇 실험적인 봇은 script를 평가하려고 시도하겠지만, type이 "text/html"인 스크립트라면? 무시하는거죠.

하여간, 위 방식을 이용하면 이제 클래스 정의가 다음과 같이 깔끔해집니다:
$.joe.def(
  'Name',
  {first: '.first', last: '.last'},
  $('#t_name').html()
);

이제 표현을 위한 코드가 완전히 분리됐습니다.


6. 정말 표현을 위한 코드가 분리된 것인가?

매핑 코드에 나오는 CSS selector들을 보면서 이런 의문을 가질 법도 합니다:
CSS라면 표현(representation)을 위한 장치인데, 클래스 정의(logic)에 CSS selector를 이용하는 것이 바람직스러운가?

사실 CSS 스팩을 잘 읽어보면 CSS는 두 파트로 구분됩니다.
p.name {color: blue;}

위 코드에서 p.name 부분은 Selector 이고, color: blue; 부분은 declaration 입니다. 잘 짜여진 CSS의 경우 Selector에는 문서의 의미(semantic, logic)가 담기고, declaration에 문서의 표현(representation)이 나타납니다.

예를 들어, 아래 예시에서 첫줄의 selector는 좋고, 둘째줄은 나쁩니다. 브라우저 버그, CSS 2.1의 제약 등으로 인해 어쩔 수 없이 쓰게 되긴 합니다.
.name .first {color: blue;}
.corner_left_top {background-image: ... ;}

현실의 제약 때문에 어쩔 수 없는 부분들을 제외한다면 최대한 아래와 같이 읽을 수 있도록 selector와 declaration을 구분해줘야 합니다:
제목은 빨간색
본문은 검은색에 작은 글씨
중요한 인용은 이텔릭

굵은 글씨가 selector에 해당하는 부분입니다. 문서의 의미가 드러나 있습니다. 반면 아래와 같은 selector는 지양하는 것이 좋겠죠:
세번째 div 밑에 있는 p는 빨간색
네번째 div 밑에 있는 모든 요소는 검은색에 작은 글씨
네번째 div 밑에 있는 모든 요소 중 blockquote.italic은 이텔릭

이렇게 되면, HTML이 수정될 때 CSS를 좀 덜 바꿔도 되고, CSS를 수정할 때 HTML을 좀 덜 바꿔도 됩니다. (두 파일이 완전 독립적일 수는 없을 것 같아요. CSS 2.1이 꼬져서)

에, 그래서 요지는 문서의 의미에 해당하는 올바른 Selector만 가지고 $.joe.def 를 하면 된다는겁니다.


7. Microformats

CSS Selector가 "의미있게 잘 쓰이려면" HTML 자체도 의미있게 작성되어야 합니다. 어떤 HTML이 의미있는 것인가? 광범위한 질문이라, 외력을 빌려 간단히 요약하자면, Microformats 스타일의 HTML이 좋다고 할 수 있습니다. 지정된 Microformat을 따르건, Microformat의 컨벤션만 빌려서 쓰건 간에 상관 없습니다.

이 방식을 따르면 HTML이 좀 더 의미를 충실히 표현하게 되고, 그렇게 되면 CSS Selector가 더 의미 있어지며, 그렇게 되면 joe의 클래스 정의도 더 의미 있어집니다. 일반적으로, 한가지 장치가 여러가지 문제를 일관성 있게 해결해준다면, 그 장치는 좋은 장치라고 믿어도 되는거죠. 이 경우엔 Microformats가 그렇습니다.

반대로 말하자면, joe를 잘 쓰려고 노력하다보면 좋은 CSS, 좋은 HTML이 부수효과로 얻어질 가능성이 높습니다. :-)


8. 동적 컨텐츠와 정적 컨텐츠를 동일하게 다루기

눈치 빠른 분은 이미 아셨겠지만, joe를 사용하면 서버에서 보내준 정적 컨텐츠와 클라이언트에서 만든 동적 컨텐츠를 동일하게 취급할 수 있게 됩니다.

별도의 데이터를 자바스크립트 객체나 배열에 넣어놓고 쓰는 것이 아니라 브라우저의 DOM 자체를 데이터 소스로 사용하기 때문입니다. (물론 detached object가 있지만 이건 캐싱, 배치처리, 버퍼링, Ajax 통신 등의 용도로 한시적으로 사용하는 개념입니다)

예를 들어, 현재 로그인 한 사용자의 이름과 ID를 자바스크립트에서 알아야 하는 경우가 있습니다. 이 때 우리가 보통 쓰던 방식은 var curUser = {id: ..., name: ...}; 등과 같이 객체를 만들어서 어딘가에 쳐박아두고 필요할 때 불러쓰는 방식입니다. 하지만, 로그인 한 사용자의 이름과 ID는 이미 HTML에 담겨 있을겁니다. 중복이죠. 웹 애플리케이션에서 이상적인 역할 분담(separation of concerns)은 대략 아래와 같습니다:

  • 구조와 의미(혹은 데이터): HTML
  • 표현: CSS
  • 행위: Javascript

curUser라는 객체를 별도로 만들너 놓는 것은 중복일 뿐 아니라 부적절한 역할분담입니다. 이로 인해 발생할 수 있는 대표적인 문제는 HTML이 수정되었는데 curUser는 수정되지 않아서 로직이 꼬이는 경우죠. 페이지가 복잡해지면 복잡해질수록, 동적으로 변하는 요소가 많으면 많을수록 큰 문제가 됩니다.

반면, joe 방식에서라면 별도의 curUser를 만드는 것이 아니라,

  1. 화면 상에 나타나는 User 객체에 대한 마크업에 의미를 부여하여 최대한 구조를 통일하고
  2. 화면 상단 쯤에 표현되는 현재 로그인한 사용자 정보도 동일한 마크업을 사용하여 그려줍니다.
  3. 이 때, 표현은 CSS로 분리되어 있으므로 충분히 자유롭게 바꿀 수 있습니다.
  4. $.joe.def 를 통해 User 객체를 정의하고 curUser를 자동으로 생성합니다.

이렇게 하면 위에서 소개한 역할 분담에 좀 더 가까워지고 로직이 꼬이는 문제가 발생하지 않습니다. 왜냐하면 직접적인 DOM 조작을 통해 HTML이 변경되어도 매핑된 curUser 객체는 자동으로 갱신되기 때문입니다(getter가 호출되는 시점에서 DOM에 접근해서 데이터를 읽어오기 때문입니다).


9. 맺음말

아, 길게도 썼습니다. 코딩은 고작 300줄 하고 이빨은 이렇게 길게 까고. 원래 나대는 인생이란 그런거죠 ㅎㅎ 제가 앞으로 바라는 바는? ( 전 바라는 바하려는 바가 일치하지 않아요. 왜냐하면 땡기면 하고 안땡기면 안하거든요 --; )

  • 서버측에서도 DOM과 Javascript를 지원하는 프래임워크가 나와주고
  • joe가 이 프래임워크와 통합되면

좋겠습니다. 서버측 자바스크립트에 대해서는 예전에 글을 하나 쓴 적이 있는데, 지금은 여기에 생각이 더 보태졌어요. 간단히 말하자면, 추상화를 통해 감춰야 할 대상이 있고 오히려 드러내야할 대상이 있는데, HTML, DOM, HTTP는 감춰야할 것이 아니라 드러내야할 것이라는 생각이 강해졌습니다. 그렇게 되면, Client/Server 모두 합쳐서 DOM/HTML/CSS/Javascript/HTTP 정도만 알면 모든 웹 프로그래밍이 가능합니다.

이 얘기는 다음 기회에. (에, 비슷한 프로젝트로 http://beebole.com/pure 가 있습니다만 제 생각이랑은 초큼 달라요)


신고
< Newer     Older >

티스토리 툴바