본문 바로가기

프로그래밍/자바스크립트(javascript)

[자바스크립트/javascript] 클라이언트 탐지, 기능탐지, 퀵스 탐지

브라우저 제조사들이 공통 인터페이스를 구현하기 위해 노력하긴 하지만 브라우저마다 기능과 단점이 있을 수 밖에 없습니다.

여러 플랫폼에서 동작하는 브라우저는 종종 플랫폼별로, 심지어는 기술적으로 같은 버전인데도 서로 다른 문제를 갖고 있습니다.

이러한 차이 때문에 웹 개발자들은 모든 브라우저에서 공통인 최소한의 기능만 이용하여 디자인하거나 다양한 클라이언트 탐지 방법을 통해 제한점을 우회하여 개발합니다.

 

클라이언트 탐지는 웹 개발에서 가장 고민이 많은 주제입니다. 모든 브라우저가 공통 기능을 지원한다면 이 주제는 논란거리가 대부분 사라질 겁니다.

브라우저 사이의 차이와 혼란스러운 점이 너무 많아서 클라이언트 탐지는 보험처럼 만약을 대비한 수단일 수도 있지만 개발 전략에서 빼놓을 수 없는 중요 파트입니다.

 

브라우저 마다 장단점이 있습니다. 클라이언트 탐지는 반드시 마지막 수단으로 미뤄야 합니다. 더 일반적인 솔루션이 가능한다면 그걸 써야합니다.

먼저 일반적인 솔루션을 사용하고 브라우저 전용 솔루션은 빠진 부분을 메꾸는 용도로만 쓰입니다.

 

 

기능 탐지

 

 

클라이언트 탐지 중에서 가장 널리 쓰이는 방법은 '기능 탐지'라는 방법입니다.

기능 탐지는 어떤 브라우저를 사용 중인지에는 관심이 없고 어떤 기능이 지원되는지에 주목합니다.

브라우저 자체에 대한 지식은 불필요하며 필요한 기능의 존재 여부에 따라 해결책을 찾을 수 있습니다.

기능 탐지의 기본 패턴은 다음과 같습니다.

 

if( object.propertyInQuestion ) {
	
}

 

예를 들어 DOM 메서드 document.getElementById()는 인터넷 익스플로러 5 이전 버전에서는 지원되지 않습니다. 하지만 같은 기능 비표준 document.all 프로퍼티로 구현 가능합니다.

 

<!DOCTYPE html>
<html>
    <head>
        <title>Example HTML Page</title>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <script>
            window.onload = function() {

                function getElement(id) {
                    if(document.getElementById) {
                        return document.getElementById(id);
                    } else if (document.all) {
                        return document.all[id];
                    } else {
                        throw new Error("No way to retrieve element!");
                    }
                }

                console.log(getElement("example"));
            }
        </script>
    </head>
    <body>
        <div id="example"></div>
    </body>
</html>

 

 

 

getElement() 함수의 용도는 주어진 ID로 요소를 찾아 반환하는 겁니다.

document.getElementById()가 표준방식이므로 먼저 이 메서드를 테스트합니다.

메서드가 존재하면 이 메서드를 사용합니다. 그렇지 않으면 document.al을 체크하고 존재한다면 사용합니다.

두 메서드 중 아무것도 존재하지 않으면 함수를 실행할 수 없음을 알리는 에러를 반환합니다.

 

기능 탐지에서 매우 중요한 두 가지 개념이 있습니다.

같은 결과를 얻을 수 있는 가장 일반적인 방법을 제일 먼저 테스트 해야 합니다.

document.getElementById() 를 document.all보다 먼저 테스트하는 것이 이에 해당합니다.

일반적인 먼저 테스트하면 테스트할 조건 수가 줄어들어 코드 실행이 능률적입니다.

 

두 번째로 중요한 개념은 사용하려는 기능을 정확히 테스트해야 합니다.

A라는 기능이 어떤 브라우제 있다해서, 다른 브라우저에 그를 대신할 B 기능이 반드시 존재하는건 아닙니다.

 

function getWindowWidth() {
    if(document.all) {
    	return document.documentElement.clientWidth;   // 잘못된 사용!
    } else {
    	return window.innerWidth;
    }
}

console.log(getWindowWidth());

 

크롬에서의 결과

 

부정확한 기능 탐지의 예입니다.

getWindowWidth() 함수는 먼저 document.all 이 존재하는지 체크합니다.

존재한다면 함수는 document.documentElement.clientWidth를 반환합니다.

인터넷 익스플로러 8 및 이전 버전은 window.innerWidth 프로퍼티를 지원하지 않습니다. 이 코드의 문제는 document.all 이 지원된다고 해서 브라우저가 인터넷 익스플로러 라는 보장이 없다는 것입니다.

오페라 초기버전은 document.all 과 window.innerWidth를 모두 지원합니다.

 

 

안전한 기능 탐지

 

 

기능 탐지는 단순히 원하는 기능이 존재하는지만이 아니라 그 기능이 정확히 동작함을 확인할 수 있을 때 가장 효과적입니다.

테스트하는 객체멤버가 존재하기만 한다면 메서드라는 확신, 일종의 타입 강제에 의존합니다.

 

function isSortable(object) {
    return !!object.sort;
}

 

이 함수는 sort() 메서드가 있는지 체크해서 객체가 정렬 가능한지 확인하려고 합니다.

문제는 객체에 sort라는 프로퍼티가 있어도 true를 반환하다는 겁니다.

 

var result = isSortable({ sort : true });

function isSortable(object) {
    return typeof object.sort == "function";
}

console.log(result);

 

 

 

 

단순히 프로퍼티의 존재 여부만 테스트해서는 객체가 정렬 가능한지 확신할 수 없습니다.

더 나은 방법은 sort가 정말 함수인지 체크하는 겁니다.

 

typeof 연산자를 써서 sort가 실제 함수인지 확인하므로 이를 호출해서 데이터를 정렬할 수 있는지 알 수 있는 더 믿음직한 방법이다.

가능한 한 기능 탐지에 typeof 를 써야 하지만 이 역시 만능은 아닙니다.

특정 상황에서는 호스트 객체가 typeof의 값을 정확히 반환한다고 확신하기 어려울 때도 있습니다.

 

가장 어처구니 없는 사례는 인터넷 익스플로러에서 발생합니다. 다음코드는 document.createElement() 가 존재하는 대부분의 브라우저에서 true를 반환합니다.

 

function hasCreateElement() {
    return typeof document.createElement == "function";
}

console.log(hasCreateElement());

 

 

 

IE 8 및 이전 버전에서는 함수는 false를 반환하는데 typeof document.createElement 가 "function" 이 아니라 "object" 를 반환하기 때문입니다.

DOM 객체는 호스트 객체이며 IE 8 및 이 전 버전에서는 JScript 대신 COM를 써서 호스트 객체를 구현했습니다.

IE 9는 DOM 메서드에서 정확히 "function"을 반환합니다.

 

인터넷 익스플로러에서 typeof가 예상대로 동작하지 않는 예는 또 있습니다.

인터넷 익스플로러만 지원하는 ActiveX 객체는 다른 객체와 매우 다르게 동작합니다.

 

// 인터넷 익스플로러에서 에러 발생
var xhr = new ActiveXObject("Microsoft.XMLHttp");
if (xhr.open) {	 // 여기서 에러가 발생합니다.

}

 

함수에 프로퍼티처럼 접근하면 자바스크립트 에러가 발생합니다.

typeof를 쓰면 좀 더 안전하지만 인터넷 익스플로러는 typeof xhr.open 에서 "unknown"을 반환합니다.

결국 브라우저와 객체를 가리지 않고 함수의 존재 여부를 테스트하려면 다음 함수를 써야 합니다.

 

// 피터 마이콕스가 개발
function isHostMethod(object, property) {
    var t = typeof object[property];
    return t  == 'function' ||
         (!!( t == 'object' && object[property])) ||
         t == 'unknown';
}

var xhr = new XMLHttpRequest();

result = isHostMethod(xhr, "open");	// true
console.log(result);

result = isHostMethod(xhr, "foo");  // false
console.log(result);

 

 

isHostMethod() 함수는 아직까지 가장 안전한 방법이며 브라우저들 사이의 혼란을 이해하고 만들어 졌습니다.

하지만 호스트 객체가 현재의 구현 내용을 유지하거나 하위 호환성을 유지하도록 개발될 거란 보장은 어디에도 없습니다.

구현하려는 기능이 이후 바뀔 수 있음을 인식하고 대비해야 합니다.

 

 

기능 탐지는 브라우저 탐지가 아니다.

 

 

특정 기능이나 기능 집합을 탐지할 때 어떤 브라우저에서 실행 중인지 알 필요는 없습니다.

"브라우저 탐지" 코드는 수많은 웹사이트에서 쓰이지만 기능 탐지를 잘못 사용한 사례입니다.

 

var isFirefox = !!(navigator.vendor && navigator.vendorSub);

var isIE = !!(document.all && document.uniqueID);

console.log(isFirefox);
console.log(isIE);

 

파이어폭스로 검사해도 false 가 나옵니다.

 

 

이 코드는 잘못된 기능 탐지의 고전적 사례입니다.

과거에는 navigator.vendor와 navigator.vendorSub만 체크하면 브라우저가 파이어폭스임을 알 수 있었지만 곧 사파리에서 같은 프로퍼티를 구현하면서 부정확한 결과를 얻게 되었습니다

 

또한, 인터넷 익스플로러를 체크할 때 document.all 과 document.uniqueID가 모두 있으면 인터넷 익스플로러라고 간주합니다.

이는 두 프로퍼티가 IE의 미래 버전에도 존재할 것이며 다른 브라우저는 두 프로퍼티를 결코 구현하지 않을 거라고 가정하는 것입니다. 이코드에서는 좀 더 효율적인 코드를 위해 NOT연산자를 두 번 연달아 써서 결과를 불리언으로 바꿨습니다.

 

하지만 몇 가지 기능을 묶어서 브라우저 그룹을 만드는 방법을 적절합니다.

애플리케이션에서 특정 브라우저 기능이 필요하다면 기능 탐지를 반복하기보다는 한 번에 하는 편이 훨씬 낫기 떄문입니다.

 

// 브라우저에서 넷스케이프 스타일 플로그인을 지원하는지 체크합니다.
var hasNSPlugins = !!(navigator.plugins && navigator.plugins.length);

// 브라우저에서 기본적인 DOM Level 1 기능을 지원하는지 체크합니다.
var hasDOM1 = !!( document.getElementById && document.createElement &&
                  document.getElementsByTagName);


console.log(hasNSPlugins);
console.log(hasDOM1);

 

파이어 폭스 결과

 

 

크롬 결과

 

 

 

첫 번째는 브라우저에서 넷스케이프 스타일 플러그인을 지원하는 것인지 이고, 두 번째는 DOM 레벨 1 기능을 지원하는 가입니다.

테스트에서 반환하는 불리언 값은 나중에 다시 사용할 수 있으므로 기능을 또 탐지하는 것보다 효율적입니다.

 

 

쿽스 탐지

 

 

기능 탐지와 비슷한 개념으로 '쿽스 탐지'가 있습니다. 이는 브라우저의 특정 동작방식을 찾아내려는 겁니다.

퀵스 탐지는 지원되는 것을 대신 뭔가 정확히 동작하지 않는 것을 찾아내려 합니다.

퀵스 탐지는 보통 짧은 코드를 실행하고 기능이 정확히 동작하는지 확인합니다.

 

예를 들어 인터넷 익스플로러 8 및 이전 버전에는 [[Enumerable]]  속성이 false로 지정된 인스턴스 프로퍼티가 있다면, 같은 이름의 프로토타입 프로퍼티를 for-in 루프에서 표시하지않는 버그가 있습니다. 이 버그를 이용해 테스트합니다.

 

 

var hasDontEnumQuirk = function() {
    var o = { toString : function() {} };
    for ( var prop in o ) {
       if ( prop == "toString" ) {
          return false;
       }
    }
    
    return true;
}();

console.log(hasDontEnumQuirk);

 

크롬 실행 결과 ( 크롬에는 버그가 없습니다. ) 

 

 

이 코드는 익명 함수를 이용해 퀵스 테스트를 합니다. toString() 메서드를 정의한 객체를 생성합니다.

올바른 ECMAScript 구현에서는 for-in 루프에서 toStirng을 프로퍼티로 반환합니다.

 

그 외에 자주 테스트하는 버그는 사파리 3 미만 버전에서 가려진 프로퍼티를 나열하는 버그입니다.

 

var hasEnumShadowQuirk = function() {
	var o = { toString : function() {} };
    var count = 0;
    for ( var prop in o ) {
       if ( prop == "toString" ) {
          count++;
       }
    }
    
    return ( count > 1 );
}();

console.log(hasEnumShadowQuirk);

 

크롬 실행 결과 ( 크롬에는 버그가 없습니다. ) 

 

 

브라우저에 해당 버그가 있다면 toString() 메서드를 따로 정의한 객체는 for-in 루프에 toStrin을 두 번 표시합니다.

 

이러한 버그는 보통 한 브라우저에 국한되며 다음 버전에서 수정될 수 도 있고 아닐 수도 있습니다.

퀵스 탐지는 코드를 실행해야 하므로 직접적인 영향이 있는 버그만 테스트 하고, 가능한 스크립트 첫 부분에서 이를 배제하는 편이 좋습니다.