이벤트 추적 코드 쉽게 넣기

dev | 2011-11-09

서비스를 새롭게 개편하거나 런칭하면 사용자의 클릭 행태를 분석하기 위해서 이벤트 추적(event tracking) 코드를 넣는다. 사용자가 웹페이지에서 어떤 액션을 취했을 때, 이를 모두 저장해서 웹사이트의 어느 부분이 사용자에게 인기가 좋고 유용하며 어느 부분이 그렇지 않은지를 분석하는 기초 자료를 쌓는 것이다. 가장 쉽게 접할 수있는 이벤트 추적 기능은 아마도 구글 애널리틱스의 이벤트 트래킹 기능 일 것 같고 이런 류의 사용자 분석 도구에는 아마도 대부분 이 기능이 있을 것이다.

처음 이 이벤트 추적 기능이라는 것을 접했을 때에는 웹사이트의 '특정 부분'에 이벤트 추적 코드를 넣고 사용자들의 행동을 분석 한 후에 뭔가 개선점을 도출하는 것으로 생각했었다. 그런데 실제로 코드를 넣는 과정을 보니 기획쪽에서는 '특정 부분'이 아니라 웹페이지에서 상호작용이 일어나는 '모든 부분'에 이벤트 추적 코드를 넣기를 원했다. 모든 링크, 서식, 자바스크립트로 삽입되는 부분들까지 모든 곳에 이벤트 추적 코드를 넣는 작업은 만만하지는 않은 노가다 작업이다. 수백개의 이벤트 추적 코드를 따옴표와 이스케이프된 따옴표, HTML 태그의 늪에 넣고나면 너무 긴장하고 집중한 나머지 온몸이 뻐근해진다.

이렇게 넣어진 이벤트 추적 코드가 개선점을 도출하는데 잘 활용이 된다면 좋겠지만 실제로 보니 이벤트가 많이 발생하거나 적게 발생하는 부분을 제외한 그저 그런 클릭을 보여주는 중간 항목들은 이벤트 추적 코드 넣은 노력이 무색하게 잊혀지게 된다. 정말 모든 상호작용을 분석하는 초기화면 정도가 아닌 다음에야 이벤트 추적 코드 넣는 작업이 노가다에 비해 생산성이 너무 떨어지는 작업으로 느껴졌다. 코드를 선별적으로 넣는 것이 아니라 전체에 동일한 기준으로 넣는 것이라면 동적으로 넣었을 때와 수동으로 넣었을 때가 얼마나 차이가 날지 궁금했다.

이벤트 추적 코드를 넣을 때 중요한 부분은 수많은 이벤트들을 구분할 수 있게 이벤트마다 고유한 코드를 넣어주는 것이다. 그래서 기획자는 이 코드를 정해진 위계에 의해서 엑셀 시트에 만들어 넣고 개발자는 이 코드를 일일이 링크마다 변경해서 넣는 노가다를 하는 것이다. 이벤트가 발생한 위치를 구별하는 것이 주된 목적이기 때문에 HTML의 구조에서 이벤트가 발생한 위치를 뽑아낼 수 있으면 노가다를 할 필요가 없다.

<script type="text/javascript">
//<![CDATA[
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-******-*']);
_gaq.push(['_trackPageview']);
_gaq.push(['_trackPageLoadTime']);

(function () {
	var as = document.getElementsByTagName('a');
	for (var i = 0, cnt = as.length; i < cnt; i++) {
		addEvent(as.item(i), 'click', function () {
			var category = getStructure(this).join('-');
			var action = (
				this.href.indexOf('.pdf') > -1
				|| this.href.indexOf('.mov') > -1
				|| this.href.indexOf('.avi') > -1
				|| this.href.indexOf('.m4v') > -1
				|| this.href.indexOf('.wmv') > -1
				|| this.href.indexOf('.mp3') > -1
				|| this.href.indexOf('.rar') > -1
				|| this.href.indexOf('.zip') > -1
			) ? 'download' : (
				this.href.indexOf('.html') > -1
				|| this.href.indexOf('.txt') > -1
				|| this.href.indexOf('.js') > -1
			) ? 'example' : 'link';
			var label = this.innerHTML;
			_gaq.push(['_trackEvent', category, action, label]);
		});
	}
	addEvent(document, 'click', function (event) {
		var element = getClickableElement(event.target);
		var category, action, label;
		if (!element) {
			return;
		}
		category = getStructure(element).join('-');
		action = (
			element.href.indexOf('.pdf') > -1
			|| element.href.indexOf('.mov') > -1
			|| element.href.indexOf('.avi') > -1
			|| element.href.indexOf('.m4v') > -1
			|| element.href.indexOf('.wmv') > -1
			|| element.href.indexOf('.mp3') > -1
			|| element.href.indexOf('.rar') > -1
			|| element.href.indexOf('.zip') > -1
		) ? 'download' : (
			element.href.indexOf('.html') > -1
			|| element.href.indexOf('.txt') > -1
			|| element.href.indexOf('.js') > -1
		) ? 'example' : 'link';
		label = element.innerHTML;
		_gaq.push(['_trackEvent', category, action, label]);
	});
	function addEvent(obj, type, fn) {
		if (obj.addEventListener) {
			obj.addEventListener(type, fn, false);
		} else if (obj.attachEvent) {
			obj["e" + type + fn] = fn;
			obj[type + fn] = function() {
				obj["e" + type + fn](window.event);
			}
			obj.attachEvent("on" + type, obj[type + fn]);
		}
	}
	function getStructure(el) {
		var structure = [];
		if (el.parentNode && el.parentNode.tagName.toLowerCase() != 'body') {
			structure = getStructure(el.parentNode);
		}
		if (el.id) {
			structure.push(el.id);
		} else if (el.className) {
			structure.push(el.className);
		}
		return structure;
	}
	function getClickableElement(element) {
		if (element.tagName == undefined) {
			return false;
		}
		if (element.tagName.toLowerCase() == 'a' || element.tagName.toLowerCase() == 'area') {
			return element;
		}
		if (element.parentNode) {
			return getClickableElement(element.parentNode);
		}
		return false;
	}
})();

(function() {
	var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
	ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
	var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
//]]>
</script>

동적으로 생성된 요소도 포함할 수 있게 document요소에 델리게이션 했다.

링크의 부모요소를 재귀적으로 루트요소까지 탐색하면서 아이디(id) 값과 클래스(class) 값을 가져오고 이를 사용하면 특정 이벤트가 발생한 위치를 HTML 구조상에서 동적으로 만들어 낼 수 있다. 물론 HTML 구조가 잘 되어 있고 아이디와 클래스 값이 적절한 의미를 담고 있을 때에만 사용할 수 있기는 하지만, 요즘 이렇게 만들지 않는 사이트는 없다고 본다(진짜?).

동적으로 생성된 이벤트 추적 코드 이렇게 동적으로 생성된 이벤트 코드값은 실제로 기획자가 고생해서 만든값과 비교해 볼때 별로 문제가 없어 보인다. HTML 구조의 깊이가 너무 깊어질 경우에 이벤트 코드가 길어서 좀 보기 좋지 않은 경우를 빼고는 별 문제가 없다. 그리고 한글이 아니라는 점 정도? 이벤트 코드가 기본적으로 HTML 구조를 가지고 있기 때문에 특정 영역의 코드들을 검색한다든가 모아서 보는 데에도 탁월했다. 예를 들어서 'body-aside'를 검색하면 사이드바에서 발생한 모든 이벤트를 검색할 수 있다.

HTML을 모르는 기획자 입장에서는 좀 헷갈릴 수도 있겠다. 그래도 보기 편하라고 필터링 만드는 작업이 HTML과 JS 코드 뒤지면서 따옴표와 씨름하는 것 보다는 훨씬 수월할 것 같다.

Comments

  • 윤정근 2011-11-10

    ㅋㅋ 나두 이거 노가다가 너무 심해서 개발자랑 나눠서 넣곤 했는데. 감솨. ㅋㅋ 근데 써먹을 수 있을라나.

  • 서승호 2012-01-08

    안녕하세요. 과장님. 이거 좋겠네요. 물론 CSS에서 의미있는 클래스명을 사용한다면 금상첨화가 되겠네요. 근데, 위에 소스코드가 문서내에 있는 모든 a 엘리먼트에 대해서 재귀적으로 부모를 모두 탐색하면 성능상에 문제가 있지 않을까요? 링크 클릭 이벤트를 후킹 하는 형태로 하면 성능에 큰 무리는 없을까 하네요. 여튼 이러한 쌩 노가다 줄이는 아이디어 좋네요.

  • 신현석 2012-01-09

    페이지가 아무리 복잡해도 단계가 그리 깊지 않을 것이기 때문에 성능상 이슈가 크지는 않을 거에요.

  • iolo 2013-01-09

    노가다 코딩을 반자동화하는 부분은 완전~ 멋집니다~ 좀스럽지만... 살짝~ 태클을 걸자면~ 모든 a 태그를 트래킹하다보면... 트래킹이 필요없는 것까지 트래킹하게되서... 성능의 문제라기 보다는 "반응성"에 문제가 있을 듯...(특히 모바일에서) 코드를 조금 수정하면 나아지긴 하겠지만... 반응성 이슈를 완전히 배제할 수는 없을 듯~ 하네요. 코드를 간략히 보니... 1. getElementsByTagName("a")보다는 특정클래스를 정하고(예:ga), 트래킹할 요소들에 "ga"클래스를 추가하고,getElementsByClassName("ga")를 쓰는 방은 어떨까요? a외에 button 태그등도 가능하고... 불필요한 트래킹 안해도 되고... 2. 그래도 모든 a 태그를 다 트래킹하고 싶다면 getElementsByTagName("a")보다는 document.links 를 쓰시는 편이 좋을 듯(표준 BOM) 2. 클릭 이벤트 핸들러에서 태그의 부모를 찾아 거슬러가는 부분은 이벤트 핸들러 밖으로 빼내면 약간이지만 가벼워지겠죠? (클릭할때마다 부모가 바뀌는 경우는... -_-;;;) 마지막으로... ajax 등으로 동적으로 받아는 컨텐츠는 어떻게 될려나요? 너무 좀스럽나 -_-;;;;

  • 신현석 2013-01-09

    처음 이런 생각이 든 동기가 노가다가 하기 싫어서;;였기 때문에 되도록이면 손이 가지 않는 방법을 생각한 것입니다. 지금은 A만 하기는 했는데 인터랙션이 발생하는 요소들을 다 모으는 부분도 필요하고 말씀하신 것 처럼 임의 지정도 필요해 보이네요. 부모요소 가져오는 방법도 재귀적으로 안해도 되고, 판별하는 시점도 좀 더 고민을 해봐야 하겠네요. 동적 콘텐츠는....델리게이션 해야 할까요? 사실 이렇게 만들어진 카테고리를 그냥 쓰기는 힘들테고 나중에 결과를 후처리 하는 부분도 필요할 것 같아요.

  • 신현석 2013-01-10

    저의 회사분이 아래와 같이 의견 주셨네요. -- HTML 구조와 id 및 class 값의 의미 있는 네이밍을 활용하여 노가다성 작업을 줄이는 아이디어는 정말 좋은 것 같습니다. 하지만 제시하신 예시 코드는 성능 개선을 위한 약간의 보완이 필요할 것 같습니다. 첫째로 반복문 안에서 동일한 이벤트 핸들러를 익명 함수로 선언하는 방법은 좋지 않다고 봅니다. 이같이 하려면 getStructure나 addEvent 함수와 마찬가지로 별도의 이벤트 핸들러 함수를 반복문 밖에서 미리 선언하는 것이 좋다고 생각합니다. 둘째로 초기화 시점에 문서 내의 모든 a 태그에 대해 이벤트 핸들러를 등록하게 되면 초기 비용이 많이 든다는 점 외에도 ajax 등으로 동적으로 추가되는 DOM 요소에 대한 대응이 불가능하기 때문에, 이벤트 버블링을 활용하여 흔히 말하는 델리게이트(delegate) 방식으로 최상위 노드에만 이벤트를 등록하여 처리하는 방식이 효율적이라 생각합니다. 이렇게 처리하게 된다면 첫 번째 문제는 자동으로 해결될 것입니다. 셋째로 동일한 DOM 요소에 대해 반복적으로 클릭이 일어나는 경우도 있을 수 있는데, 이런 경우에도 매번 불필요하게 재귀적으로 추적 코드값을 생성하게 되므로, 한번 클릭이 일어난 DOM 요소의 추적 코드값들은 캐싱을 하는 방법을 생각해 볼 수 있을 것 같습니다.

  • 양성준 2013-02-05

    잘 봤어요 완벽히 이해할 수는 없지만, 어떤 말씀인지는 어렴풋이 알겠네요 ㅎ 멋지네요

  • 신현석 2013-02-06

    이벤트 카테고리 엑셀시트 작성해 보시면 느낌이 팍 오실거에요. :D

  • 김현수 2014-12-03

    잘보고 갑니다. ^^ 한번 만들어보고 싶은 기능이네요 ~ 수고하세요 ~

  • 박민우 2016-09-18

    이번에 구글 태그매니저를 좀 보고있는데, 원하시는 기능을 완벽하게 쉽게 구현할 수 있네요. 나온지 꽤 오래된 도구에요. 시간나면 한번 튜토리얼 한번 보세요- ^^ https://www.youtube.com/watch?v=3LVza7sGy7g

  • 신현석 2016-09-20

    저희 구글 태그 매니저 내부적으로 몇 번 적용해 봤는데 엄청 좋더군요. 시간나면 좀 자세히 살펴볼 생각이에요.

Post a comment

:

: 공개 되지 않습니다. Gravatar를 표시 합니다.

:

: HTML 태그를 사용할 수 없습니다.