[js] 커스텀데이터 사용하기

이 기능은 모든 태그에 자유롭게 값을 추가할 수 있는 표준으로 더 이상 태그마다 허용된 attribute만 선언해야하는 한계를 제거해줍니다.

커스텀데이터는 html5에 추가된 내용으로 공식문서는 아래서 볼 수 있습니다.

http://www.w3.org/TR/2011/WD-html5-20110525/elements.html#custom-data-attribute

이를 이용해보기로 하죠.

 
 

html과 css로 사용하기top

우선 html을 보면 다음과 같이 data-XXX 라는 형태로 속성과 값을 추가하게 됩니다.

<div data-test="hello"></div>

이 때 속성의 이름은 단지 data-XXX뿐아니라 data-XXX-XXX 처럼 몇번이라도 빼기를 이어갈 수 있습니다.

<div data-test="hello" data-test-name="world"></div>

사실 이게 html5에서 표준이 된 것만으로도 오류없는 태그가 된 셈이므로 맘 놓고 css의 속성 셀렉터를 매우 광범위하게 이용할 수 있습니다.
예를 들어 2차원 배열요소라면 다음과 같은 2단계 데이터 지정을 생각해볼 수 있습니다.

<ul data-record="1">
	<li data-field="name" data-value="hika">hika</li>
	<li data-field="gender" data-value="male">male</li>
	<li data-field="job" data-value="player">player</li>
</ul>
<ul data-record="2">
	<li data-field="name" data-value="bobo">bobo</li>
	<li data-field="gender" data-value="female">female</li>
	<li data-field="job" data-value="worker">worker</li>
</ul>

이러면 css에서도 이를 활용한 질의가 가능해집니다.

[data-record] {display:block;clear:both}

[data-field] {float:left;width:100px}

[data-field=name] {color:#00f}
[data-field=gender] {color:#f00}
[data-field=job] {color:#0f0}

실제 아래와 같은 화면을 보게 됩니다.

Screenshot_6

이건 뭐 거의 sql이죠. css의 attribute관련 선택자는 일치뿐만 아니라 포함이나 불일치(=, ~=, |=, ^=, $=, *=) 등을 포함하는 복잡한 조건을 허용합니다.

즉 아래와 같은 셀렉터가 등장할 수 있습니다.

[data-field=name][data-value^=h]{color:#f0f}

최초 예제에서 위의 셀렉터를 추가하면 hika에게만 h로 시작하는 조건이 일치하게 되어 아래와 같이 표시될 것입니다.

Screenshot_7

근데 진짜 파워는 처음 말씀드린대로 자바스크립트에서 일어납니다.

 
 

자바스크립트로 사용하기top

예제는 앞에 구축한 html을 계속 이용하기로 합니다. 모든 브라우저에서 문제없는 레트로 api를 활용하면 다음과 같이 시용할 수 있습니다.

//우선 갖고 놀 dom객체 획득
var dom = document.getElementsByTagName('li')[0];

//속성값 얻기
console.log( dom.getAttribute( 'data-value' ) ); //hika

//속성값 쓰기
dom.setAttribute('data-value', 'hika00' );

console.log( dom.outerHTML ); //변경된 결과를 보자!

속성값을 쓰고 실제 콘솔창을 보시면 아래와 같은 내용이 보입니다.

Screenshot_9

당연하게도 data-value부분의 값이 hika00으로 변경되어 있겠죠. 특이한 것은 없으나 이 모든게 더 이상 불법(?)이 아니라 합법(?)적이라는 것입니다 ^^

이제 HTML5스펙에 정의되어있는 custom data 스펙의 API를 이용해보죠.

var dom = document.getElementsByTagName('li')[0];

//속성값 얻기
console.log( dom.dataset.value ); //hika

//속성값 쓰기
dom.dataset.value =  'hika00';

console.log( dom.outerHTML ); //변경된 결과를 보자!

이 경우도 위와 완전히 동일한 스샷이 나옵니다. 즉 HTML5의 커스텀데이터 스펙은

  • data-* 형태를 파싱하여
  • dom의 dataset 이라는 속성에 키밸류객체로 잡아주며
  • 이쪽에 get, set을 하면 DOM의 attribute로서도 자동으로 갱신해주는 것

입니다.

새로운 키를 추가하거나 기존의 키를 삭제하는 것도 물론 다음과 같이 간단히 됩니다.

//기존의 data-value를 삭제하고
delete dom.dataset.value;

//새로운 data-name을 추가한다.
dom.dataset.name = 'hika';

console.log( dom.outerHTML ); //변경된 결과를 보자!

Screenshot_10

이제 그 결과 data-value는 제거되고 data-name이 새롭게 추가된 걸 볼 수 있습니다.

 
 

간접 객체 할당top

근데 자비스크립트로 커스텀데이터를 쓰면 문자열고 싶은게 아니라 객체도 넣고 싶습니다. 만약 함수를 할당하면 어떻게 될까요?

dom.dataset.plus = function( a, b ){
	return a+ b;
}; //응?

console.log( dom.outerHTML ); //변경된 결과를 보자!

Screenshot_11

객체가 들어가지 않고 자동으로 toString()을 호출하여 문자열이 들어가 있음을 확인할 수 있습니다.
따라서 dataset에 직접 객체를 넣는 것은 불가능하고 문자열만 넣을 수 있으니 uuid를 생성하여 넣어주고 그걸 조회할 자바스크립트측의 해시맵을 만들어주는 방법을 생각해야 합니다.
우선 자바스크립트측의 데이터 저장소와 uuid를 생성합니다.

var uuid = 1;
var dataset = {};

그럼 이제 특정 엘레멘트를 넘겨받아 그 안에 data-uuid가 있는지 확인하고 없다면 자동으로 고유한 uuid를 부여해주고 그 값을 반환하는 함수를 만들어 보죠.

var getUUID = function ( el ){
	//uuid가 설정되지 않았다면 설정한다.
	if( !el.dataset.uuid ) el.dataset.uuid = uuid++;

	return el.dataset.uuid;
}

위와 같은 방법을 쓰면 uuid가 없는 엘레멘트는 uuid를 부여해주면서 uuid값을 증가시키므로 계속 고유한 값을 부여받게 됩니다. 이제 이 고유키를 이용해 객체를 저장하면 됩니다.

//데이터 저장하기
var setData = function( el, data ){
	var id = getUUID( el ); //id를 얻자
	dataset[id] = data; //객체든 뭐든 저장하자!
};

//데이터 가져오기
var getData = function( el ){
	var id = getUUID( el );
	return dataset[id];
}

이렇게 하면 고유키를 이용해서 자바스크립트측 객체인 dataset에 원하는 갑이든 객체든 저장하고 가져올 수 있게 됩니다. 위에서 하려고 했던 함수할당을 실제로 할 수 있게 되었습니다.

setData( dom, function( a, b ){
	return a+ b;
} );

var plus = getData( dom );
console.log( plus( 2, 3 ) ); //5출력

위에서는 uuid에 맞춰 dataset에 하나의 객체만 넣게 했지만 여러 개의 객체를 관리하고 싶다면

  • 각 id에 맞춰 다시 dataset[id] = {} 와 같이 오브젝트를 만들고
  • setData와 getData에서 부가적인 key를 받는 구조로 확장

하시면 됩니다.

 
 

호환성레이어 구축top

우리를 괴롭히는 구형 브라우저나 단편화된 수많은 모바일, 스마트TV..심지어 냉장고나 자동차의 브라우저들이 있기 때문에 신형API에만 의존할 수는 없는 노릇입니다.
호환성레이어를 구축하기 전에 먼저 해야하는 것은 우선 이 브라우저가 dataset 스펙을 지원하는가 입니다. 이를 디텍팅해보죠.

// 1. div를 하나 만들자!
var div = document.createElement('div');

// 2. 그안에 자식을 만들고,
div.innerHTML = '<div data-hello-world="hello"></div>';

// 3. 첫번째 자식을 얻어온다.
var dom = div.childNodes[0];

// 4. 이를 이용해 테스트를 한다.
var isCustomdataSupported = dom.dataset && dom.dataset.helloWorld == "hello";

위의 과정을 통해 이 브라우저가 커스텀데이터API를 준수하는지 확인할 수 있습니다. 이제 이를 이용하여 getUUID 함수를 두벌로 나눠서 만들어보죠.

if( isCustomdataSupported ){
	getUUID = function ( el ){
		if( !el.dataset.uuid ) el.dataset.uuid = uuid++;
		return el.dataset.uuid;
	};
}else{
	getUUID = function ( el ){
		if( !el.getAttribute('data-uuid' ) )el.setAttribute( 'data-uuid',  uuid++ );
		return el.getAttribute('data-uuid' );
	};
}

removeAttribute를 이용하면 setData나 getData도 어렵지 않게 정리하실 수 있을 겁니다(실습거리로  ^^;)

 
 

결론top

문득 이 스펙의 이 왜 만들어졌을까 생각해봤습니다. 이건 마치 엘레멘트를 적극적으로 데이터 컨테이너로 쓰라는 거나 마찬가지입니다.

기존의 전반적인 기조가 DOM을 오염시키지 말라는거였는데 그 기조를 완전히 벗어나고 있습니다. 사실 aria스펙도 그런 느낌을 받았는데 커스텀데이터는 대놓고 태그별 시멘텍을 깔아뭉갤 수 있는 수단이라 보여집니다.

현실과의 타협점일까요? 그나마 문자열만 받아들이기 때문에 GC에 영향을 줄 수 있는 객체 참조는 일어나지 않습니다만 그래도 제겐 매우 신선하게 다가왔습니다.