UI Laboratory

UI 개발을 위한 레퍼런스

INDEX

로딩과 실행


스크립트의 위치

아래 코드는 <head> 태그에서 자바스크립트 파일을 3개나 불러와 심각한 성능 문제를 발생한다. <script> 태그마다 자바스크립트 코드를 내려받고 실행하는 동안 페이지 표시가 멈추기 때문에 체감 속도가 떨어진다. 브라우저는 <body> 태그를 만날 때까지 아무것도 표시하지 않기 때문이다.
			<html xmlns="http://www.w3.org/1999/xhtml">
			<head>
				<title>
				
				<script type="text/javascript" src="/jquerylab/scripts/shCore.js">
				<script type="text/javascript" src="/jquerylab/scripts/shBrushBash.js">
				<script type="text/javascript" src="/jquerylab/scripts/shBrushCpp.js">
				<link rel="stylesheet" type="text/css" href="/jquerylab/inc/css/common.css" />
			</head>
			<body>
				....
			</body>
			</html>>
		
그러므로 아래 코드와 같이 모든 <script> 태그를 가능한 한 <body> 태그의 마지막에 배치해서 페이지 전체를 내려받는 것에 영향을 주지 않게 하길 권한다.
			<html xmlns="http://www.w3.org/1999/xhtml">
			<head>
				<title>
				<link rel="stylesheet" type="text/css" href="/jquerylab/inc/css/common.css" />
			</head>
			<body>
				....
				
				
				<script type="text/javascript" src="/jquerylab/scripts/shCore.js">
				<script type="text/javascript" src="/jquerylab/scripts/shBrushBash.js">
				<script type="text/javascript" src="/jquerylab/scripts/shBrushCpp.js">
			</body>
			</html>>
		

비차단 스크립트

비차단 스크립트를 만드는 비결은 페이지를 완전히 불러온 다음 자바스크립트 소스코드를 불러오는 것이다.

| 동적 <script> 태그

			var script = document.createElement("script");
			script.type = "text/javascript";
			script.src = "file1.js";
			document.getElementsByTagName("head")[0].appendChild(script);
		

새로 만든 <script> 태그에서 file1.js 파일을 불러온다. 새로 만든 <script> 태그를 페이지에 추가하는 순간부터 file1.js 파일을 내려받기 시작한다. 이 테크닉의 요점은 파일을 내려받는 위치에 관계없이 페이지의 다른 작업을 차단하지 않으면서 내려받고 실행한다. 심지어 이 코드를 <head> 태그 안에 두어도 페이지의 다른 부분에 영향을 주지 않는다.

동적으로 만든 <script> 태그에서 내려받은 파일에 있는 코드는 보통 바로 실행된다. 하지만 독립실행코드가 아닌 다른 스크립트에서 사용할 인터페이스만 있는 코드라면 문제가 생길 수 있다. 다시 말해 이 코드를 완전히 내려받은 시점과 사용할 수 있는 시점을 파악 할수 있어야 한다. 동적으로 추가한 <script> 태그에서 발생한 이벤트를 이용해서 내려받은 시점과 사용가능 시점을 파악할 수 있다.

사파리3 이상과 파이어폭스, 오페라, 크롬은 모두 <script> 태그의 src 에 지정된 파일을 가져온 시점에 이벤트를 발생시킨다. 따라서 이 이벤트 리스너를 이용하면 동적으로 내려받은 코드의 사용가능 시점을 알 수 있다.

반면 IE는 구현방식이 달라서 readystatechange 이벤트를 사용한다. 외부 스크립트 파일을 내려받은 상태에 따라 <script> 태그의 readyState 속성이 달라진다.

다음 함수는 표준 방식과 IE방식을 하나로 묶은 크로스브라우징 소스이다.
			
		
이 함수는 두개의 매개변수를 받는다. 하나는 불러올 자바스크립트 파일의 URL이고 다른 하나는 자바스크립트 파일을 완전히 불러왔을 때 실행할 콜백 함수이다. loadScript()함수의 사용법은 다음과 같다.
			
		
동적 스크립트 로딩은 브라우저에 관계없이 동작하고 쓰기도 편하므로 자바스크립 파일을 비차단적으로 내려받아야 할 때 널리 쓰일수 있다.

DOM 스크립팅


DOM 접근과 수정

DOM은 XML문서와 HTML문서에서 동작하는 언어 독립적인 API이다. DOM은 언어 독립적인 API지만, 브라우저의 DOM 인터페이스는 자바스크립트로 구현된다. 브라우저 대부분이 DOM과 자바스크립트를 별도로 구현한다. 별도의 기능 두가지를 구현하고 서로 통신하게 하는 만큼 사용자 응답이 느려질수 밖에 없고 DOM에 자주 접근할수록 코드는 느리게 실행된다.

요소에 접근하거나 수정하는 작업중에서도 가장 나쁜 케이스는 루프 안에서 작업하는 것이고, HTML 컬렉션에 대한 루프는 최악이다.

			function innerHTMLLoop(){
				for(var count=0;count<15000;count++){
					document.getElementById('here').innerHTML += 'a';
				}
			}
		

이 코드의 문제는 반복할때마다 요소에 두번씩 접근한다는 것이다. 한번은 innerHTML 속성의 값을 읽기 위해, 다른 한번은 그 값을 수정하기 위해서이다.

아래와 같이 바뀐 내용을 지역변수에 저장하고 페이지 내용은 루프의 마지막에서 한 번만 수정하도록 고치면 더 효율적이다.

			function innerHTMLLoop2(){
				var content='';
				for(var count=0;count<15000;count++){
					content+='a';
				}
				document.getElementById('here').innerHTML += cotent;
			}
		

모든 브라우저에서 고친 함수가 더 빠르다. 즉, DOM에 자주 접근할 수록 코드는 느리게 실행된다.

| innerHTML vs DOM 메서드

페이지 일부를 수정할 때 비표준이지만 널리 지원되는 innerHTML을 쓰는 게 좋을까? 아니면 document.createElement() 같은 순수한 DOM 메서드만 써야 할까?

innerHTML을 쓰면 HTML 페이지를 많이 바꿔야 하는, 성능에 민감한 작업을 브라우저 대부분에서 빠르게 실행할 수 있다. 하지만 평범한 작업에서는 차이가 크지 않다.
오래된 브라우저일수록 innerHTML이 확실히 더 빠르지만 최근 브라우저에서는 차이가 줄었다. 크롬, 사파리와 같은 웹킷에 기반한 최신 브라우저에서는 반대로 DOM 메서드가 조금 더 빠르다. 따라서 어떤 방법을 선택할지는 사용자가 주로 쓰는 브라우저가 무엇인지에 따라 정하면 된다.

| HTML 컬렉션

HTML 컬렉션은 DOM노드에 대한 참조를 담고 있는 배열과 비슷한 객체이며 다음 메서드가 반환하는 값이 컬렉션이다.

다음 속성도 HTML 컬렉션을 반환한다.

이 메서드와 속성은 배열과 비슷한 목록인 HTMLCollection 객체를 반환하며 배열속성인 length 속성이 있으며, 목록에 있는 요소의 색인을 만든다.

비효율적인 컬렉션
			//예기치 못한 무한 루프
			var alldivs = document.getElementsByTagName('div');
			for(var i=0;i<alldivs.length;i++){
				document.body.appendChild(document.createElement('div'));
			}
		

이 코드는 이미 존재하는 div를 만날 때마다 새 div를 만들어서 body태그에 붙이며 종료조건인 alldivs.length가 반복할 때마다 문서의 현재상태를 반영해서 1씩 증가하므로 이 루프는 사실 무한 루프이다. 이렇게 HTML 컬렉션에서 루프를 실행하면 논리적으로 실수를 할 수도 있고 반복할때마다 쿼리를 다시 실행해야 하므로 느리다.

반복하거나 컬렉션의 length에 접근할 때마다 쿼리를 재실행해야 하므로 모든 브라우저에서 성능이 심각하게 낮아진다. 컬렉션의 length를 변수에 복사하고 루프의 종료 조건에서 그 변수와 비교하면 이문제를 간단히 해결할 수 있다.

			function loopCacheLengthCollection(){
				var coll = document.getElementsByTagName('div'),
					len = coll.length;
				for(var count=0;count<len;count++){
					...
				}
			}
		

하지만 상황에 따라 배열에서 실행하는 루프가 컬렉션에서 실행하는 루프보다 빠르므로 컬렉션 요소를 먼저 배열에 복사하면 배열로 더 빠르게 접근할 수 있다.
아래 코드는 컬렉션을 배열에 복사하는 범용함수와 배열에서 실행하는 루프이다.

HTML 컬렉션을 일반적인 배열에 복사하는 함수
			function toArray(coll){
				for(var i=0,a=[],len=coll.length;i<len;i++){
					a[i] = coll[i];
				}
				return a;
			}
		
컬렉션을 만들고 배열에 복사
			var coll = document.getElementsByTagName('div');
			var arr = toArray(coll);
		
복사한 배열에서 실행하는 루프
			function loopCopiedArray(){
				for(var count=0;count<arr.length;count++){
					...
				}
			}
		
컬렉션 요소에 접근할 때의 지역 변수

루프 안에서 컬렉션 요소에 접근할 때는 어떻게 해야 할까??

일반적으로 DOM 속성이나 메서드에 두번이상 접근할 때는 지역변수를 이용하는 것이 최상이다. 컬렉션에서 루프를 실행할때 첫번째 최적화는 컬렉션을 지역변수에 저장하고 루프 바깥에서 length를 캐시한 후 루프안에서 두번이상 접근하는 요소는 지역변수를 통해 접근하는 것이다.

다음 예제는 루프 안에서 세 가지 속성에 접근한다. 가장 느린방법은 전역인 document에 매번 접근하는 것이고, 최적화한 방법은 컬렉션에 대한 참조를 캐시하는 방법이며, 가장 빠른 방법은 컬렉션의 현재 요소도 변수에 캐시하는 방법이다.

			// 느린 방법
			function collectionGlobal(){
				var coll = document.getElementsByTagName('div'),
					len = coll.length,
					name ='';

				for(var count=0;count<len;count++){
					name = document.getElementsByTagName('div')[count].nodeName;
					name = document.getElementsByTagName('div')[count].nodeType;
					name = document.getElementsByTagName('div')[count].tagName;
				}
				return name;
			}
		
			// 더 빠른 방법
			function collectionGlobal(){
				var coll = document.getElementsByTagName('div'),
					len = coll.length,
					name ='';

				for(var count=0;count<len;count++){
					name = coll[count].nodeName;
					name = coll[count].nodeType;
					name = coll[count].tagName;
				}
				return name;
			}
		
			// 가장 빠른 방법
			function collectionGlobal(){
				var coll = document.getElementsByTagName('div'),
					len = coll.length,
					name ='',
					el = null;

				for(var count=0;count<len;count++){
					el =  coll[count];
					name = el.nodeName;
					name = el.nodeType;
					name = el.tagName;
				}
				return name;
			}
		

| DOM 이동

느린 DOM 이동

childNodes 컬렉션을 이용하거나 nextSibling으로 형제요소를 선택해서 주변요소를 조작해야 할 때가 종종 있다.

			function testChildNodes(){
				var el = document.getElementById('sample_page'),
					ch = el.childNodes,
					len = ch.length,
					name = '';

				for (var count=0;count<len;count++ ){
					name = ch[count].nodeName;
					alert(name);
				}
				return;
			}
		

IE에서는 nextSibling이 childNodes보다 훨씬 빨리 실행된다. 구형버전의 IE를 지원해야 하고 성능이 중요할 때는 아래와 같이 nextSibling을 써야 한다.

			function testNextSibling(){
				var el = document.getElementById('sample_page'),
					ch = el.firstChild,
					name = '';

				do{
					name = ch.nodeName;
					alert(name);
				}while(ch=ch.nextSibling);

				return;
			}
		
선택자 API

querySelectorAll() 내장메서드는 CSS선택자를 이용해서 노드를 선택하며 자바스크립트와 DOM을 사용해서 필요한 목록을 얻을 때까지 반복해서 좁혀나가는 것보다는 이러한 메서드를 사용하는 것이 확실히 더 빠르다. 이 메서드는 IE8을 비롯한 대부분의 최신브라우저에서 지원한다.

			var elements = document.querySelectorAll('#menu a');
		

querySelectorAll() 메서드는 CSS 선택자 문자열을 매개변수로 받아서 매개변수에 일치하는 노드를 포함하는 배열과 비슷한 객체인 nodeList를 반환한다. 이 메서드는 HTML 컬렉션이 아니므로 반환된 노드는 문서구조를 동적으로 반영하지 않는다. 이 메서드를 쓰면 앞에서 설명했던 성능 문제를 예방할 수 있다.

몇가지 쿼리 결과를 합쳐서 작업해야 할 때 더 편리하다. 다음과 같이 querySelectorAll()을 써서 요소 전체의 목록을 얻을 수 있다.

			var errs = document.querySelectorAll('div.warning, div.notice');
		

쿼리 결과의 첫번째 노드만 반환하는 메서드인 querySelector()도 있다.

			var div = document.querySelector('#test');
		

리페인트와 리플로우

요소의 테두리 두께를 바꾸거나 문단에 텍스트를 추가해서 줄이 늘어나는 등 요소의 기하학적 구조(너비와 높이)에 영향을 미치도록 DOM이 변경되면 브라우저는 변경된 요소의 기하학적 구조를 다시 계산해야 하고, 이에 영향을 받는 다른 요소의 기하학적 구조와 위치도 다시 계산해야 한다. 이러한 과정을 리플로우라고 부른다. 리플로우가 끝나면 브라우저는 영향을 받은 부분을 다시 그리는데, 이러한 과정을 리페인트라고 부른다. 리플로우와 리페인트는 웹 애플리케이션 UI의 반응이 떨어질수 있으며 가능한 이런 일이 적게 일어나도록 하는 것이 최선이다.

| 리플로우가 일어날 때

다음과 같은 경우에 리플로우가 일어난다.

| 리플로우와 리페인트 최소화하기

리플로우와 리페인트를 최소화하려면 DOM과 스타일 변경을 하나로 묶어 적용해야 한다.

스타일 변경
			var el = document.getElementById('mydiv');
			el.style.borderLeft = '1px';
			el.style.borderRight = '2px';
			el.style.padding = '5px';
		

세가지 속성을 변경했는데 이들 모두가 요소의 기하학적 구조에 영향을 미친다. 최악의 경우 리플로우를 세번 수행한다.

세가지 변경을 하나로 묶어서 DOM을 한번만 수정하도록 한다. 아래와 같이 cssText 속성을 쓰면 된다.

			var el = document.getElementById('mydiv');
			el.style.cssText = 'border-left:1px; border-right:2px; padding:5px;';
		

css 인라인 스타일을 수정하지 않고 클래스명을 바꿔서 스타일을 한번에 바꿀 수 있다. 하지만 클래스명을 바꿀 때마다 CSS 캐스케이딩을 다시 계산해야 하므로 성능은 조금 떨어질 수 있다.

			var el = document.getElementById('mydiv');
			el.className = 'active';
		
DOM 변경 한번에 처리하기

DOM 요소에서 여러가지를 바꿔야 할 때는 다음 방법을 써서 리플로우와 리페인트의 숫자를 줄일 수 있다.

  1. 1. 요소를 문서 흐름에서 분리한다.
  2. 2. 여러가지 변경을 적용한다.
  3. 3. 요소를 문서 흐름에 다시 삽입한다.

이렇게 하면 리플로우는 1단계와 3단계에서 두번만 일어난다.

요소를 문서 흐름에서 분리시키는 데는 세가지 방법이 있다.