클라이언트 탐지

모든 브라우저가 공통 기능을 지원하지 않습니다.
브라우저 사이의 차이로 인해 발생되는 혼란스러운 점이 너무 많아서 개발 전략에 빼놓을 수 없는 중요 파트입니다.

클라이언트 탐지보다는 일반적인 솔루션이 있다면 그걸 사용해야 하며, 지원되지 않는 부분을 메꾸기 위한 용도로 사용하시기 바랍니다.

기능 탐지

클라이언트 탐지중 가장 널리 쓰이는 방법입니다.
브라우저 자체에 대한 지식은 불 필요하며, 기능의 존재 여부에 따라 해결책을 찾을 수 있습니다.

function getElement(id){
    if(document.getElementById){
        return document.getElementById(id);
    }else if (document.all){
        return document.all[id];
    }else{
        throw enw Error("get element error");
    }
}

위 예제를 보시죠
IE5 이전버전에는 document.getElementById()메서드를 지원하지 않습니다.
하지만 document.all프로퍼티로 구현이 가능합니다.
위 예제에서는 기능별 탐지를 해서 브라우저가 달라도 사용 할 수 있게 함수를 만들었습니다.

주의할점은 일반적으로 getElementBtId()를 사용하므로 이부분을 먼저 체크하게 IF문이 위와같은 순서로 작성되었습니다.

사용하려는 기능을 정확히 테스트 해야 합니다.

안전한 기능 탐지

예를들면 sort()메서드의 기능탐지를 할 때, sort()메서드가 있는지만 체크가 가능하지 해당 기능이 정렬 가능한지 확신 할 수는 없습니다.
typeof를 사용하여 아래와 같이 함수인지는 확인 할 수 있습니다.

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

위 예제는 함수인지 확인해서 기능을 더 안전하게 확인할 수 있는 방법입니다.

하지만 이 역시 IE등에서 문제가 발생하기 때문에 완벽한 방법은 아닙니다.

브라우저와 객체를 가리지 않고 함수의 존재 여부를 테스트 하기 위해서는 아래와 같이 사용해줍시다.

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

지금 까지 가장 안전한 방법이며 브라우저 사이의 혼란을 이해하고 만들어진 메소드입니다.
하지만! 미래에 정확히 동작할 거란 보장이 없기 때문에 이점은 인식하고 있어야 합니다.

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

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

//잘못된 기능 탐지의 고전적 사례들
var isFirefox  =!!(navigator.vendor && navigator.vendorSub);
var isIE = !!(document.all && document.uniqueID);

브라우저 별로 같은 프로퍼티를 사용하거나 하는일이 생겨서 부정확한 결과를 얻게됩니다.
위 예제를 사용하지 맙시다!

쿽스 탐지

쿽스 탐지는 지원되는 것을 찾는게 아니고 정확히 동작하지 않는 것을 찾아내려 합니다.
쿽스란 버그입니다.
예를 들면 IE8이전 버전에는 [[Enumerable]]속성이 false로 지정된 인스턴스 프로퍼티가 있다면, 같은 이름의 프로토타입 프로퍼티를 for-in 루프에서 표시하지 않는 버그가 있습니다.

브라우저 탐지

가장 논란이 많은 탐지 방법입니다.
브라우저의 사용자 에이전트 문자열로 실행중인 브라우저를 확인 합니다.
에이전트 문자열은 HTTP요청을 보낼 때마다 받는 응답 헤더에 포함되어 있습니다.
자바스크립트에서는 navigator.userAgent로 접근합니다.

초기 브라우저는 에이전트 문자열이 단순했습니다.
하지만 세월이 흐르니 운영체제 플랫폼 등의 문제와 규격의 변경으로 인해서 문자열 형식이 매우 길어졌습니다.
자주 그리고 많이 바뀌었는데 그 이유는 과거에 사용하던 브라우저 탐지 스크립트와의 호환성을 유지하면서 새 스크립트에는 다른 정보를 제공하려 했기 때문입니다.

아래 사이트에서 브라우저 에이전트의 역사를 보실 수 있습니다.
Browser Agent History

브라우저 탐지 사용

사용자 에이전트 문자열을 가지고 특정 브라우저 판단하기는 대단히 복잡한 문제입니다.
렌더링 엔진과 최소 버전이 무엇인지 안다면 아래와 같이 구현 할 수 있습니다.

// 이렇게 하지맙시다.
if(isIE6 || isIE7){ //ie8 ie9등 새로운 버전에 대한 지원이 안됩니다.
    //코드
}

//이렇게 사용합시다.
if (ieVer >= 6){ //추후 추가되는 버전에 대해서도 적용이 됩니다.
    //코드
}

위같은 코드는 브라우저 버전에 의존하므로 취약합니다.
아래와 같이 사용해 줍시다.
브라우저 탐지 스크립트는 브라우저를 판단할 때 위와같이 사용합니다.

식별

렌더링엔진, 브라우저, 플랫폼, 윈도 운영체제, 모바일 장치, 게임시스템 식별하는 소스는 아래와 같습니다.
만약 전체가 필요하지 않을경우 필요한 부분을 찾아서 사용하시면 됩니다.


var client = function(){

    //렌더링 엔진
    var engine = {
        ie: 0,
        gecko: 0,
        webkit: 0,
        khtml: 0,
        opera: 0,

        //complete version
        ver: null
    };

    //브라우저
    var browser = {
        ie: 0,
        firefox: 0,
        safari: 0,
        konq: 0,
        opera: 0,
        chrome: 0,

        //specific version
        ver: null
    };

    //플랫폼, 장치, 운영체제
    var system = {
        win: false,
        mac: false,
        x11: false,

        //모바일
        iphone: false,
        ipod: false,
        ipad: false,
        ios: false,
        android: false,
        nokiaN: false,
        winMobile: false,

        //게임
        wii: false,
        ps: false
    };

    //렌더링 엔진 및 브라우저 탐지
    var ua = navigator.userAgent;
    if (window.opera){
        engine.ver = browser.ver = window.opera.version();
        engine.opera = browser.opera = parseFloat(engine.ver);
    } else if (/AppleWebKit\/(\S+)/.test(ua)){
        engine.ver = RegExp["$1"];
        engine.webkit = parseFloat(engine.ver);

        //크롬인지 사파리인지 판단
        if (/Chrome\/(\S+)/.test(ua)){
            browser.ver = RegExp["$1"];
            browser.chrome = parseFloat(browser.ver);
        } else if (/Version\/(\S+)/.test(ua)){
            browser.ver = RegExp["$1"];
            browser.safari = parseFloat(browser.ver);
        } else {
            //비슷한 버전
            var safariVersion = 1;
            if (engine.webkit < 100){
                safariVersion = 1;
            } else if (engine.webkit < 312){
                safariVersion = 1.2;
            } else if (engine.webkit < 412){
                safariVersion = 1.3;
            } else {
                safariVersion = 2;
            }

            browser.safari = browser.ver = safariVersion;
        }
    } else if (/KHTML\/(\S+)/.test(ua) || /Konqueror\/([^;]+)/.test(ua)){
        engine.ver = browser.ver = RegExp["$1"];
        engine.khtml = browser.konq = parseFloat(engine.ver);
    } else if (/rv:([^\)]+)\) Gecko\/\d{8}/.test(ua)){
        engine.ver = RegExp["$1"];
        engine.gecko = parseFloat(engine.ver);

        //파이어폭스인지 판단
        if (/Firefox\/(\S+)/.test(ua)){
            browser.ver = RegExp["$1"];
            browser.firefox = parseFloat(browser.ver);
        }
    } else if (/MSIE ([^;]+)/.test(ua)){
        engine.ver = browser.ver = RegExp["$1"];
        engine.ie = browser.ie = parseFloat(engine.ver);
    }

    //브라우저 탐지
    browser.ie = engine.ie;
    browser.opera = engine.opera;

    //플랫폼 탐지
    var p = navigator.platform;
    system.win = p.indexOf("Win") == 0;
    system.mac = p.indexOf("Mac") == 0;
    system.x11 = (p == "X11") || (p.indexOf("Linux") == 0);

    //윈도 운영체제 탐지
    if (system.win){
        if (/Win(?:dows )?([^do]{2})\s?(\d+\.\d+)?/.test(ua)){
            if (RegExp["$1"] == "NT"){
                switch(RegExp["$2"]){
                    case "5.0":
                        system.win = "2000";
                        break;
                    case "5.1":
                        system.win = "XP";
                        break;
                    case "6.0":
                        system.win = "Vista";
                        break;
                    case "6.1":
                        system.win = "7";
                        break;
                    default:
                        system.win = "NT";
                        break;
                }
            } else if (RegExp["$1"] == "9x"){
                system.win = "ME";
            } else {
                system.win = RegExp["$1"];
            }
        }
    }

    //모바일 장치
    system.iphone = ua.indexOf("iPhone") > -1;
    system.ipod = ua.indexOf("iPod") > -1;
    system.ipad = ua.indexOf("iPad") > -1;
    system.nokiaN = ua.indexOf("NokiaN") > -1;

    //윈도우 모바일
    if (system.win == "CE"){
        system.winMobile = system.win;
    } else if (system.win == "Ph"){
        if(/Windows Phone OS (\d+.\d+)/.test(ua)){;
            system.win = "Phone";
            system.winMobile = parseFloat(RegExp["$1"]);
        }
    }

    //iOS 버전 판단
    if (system.mac && ua.indexOf("Mobile") > -1){
        if (/CPU (?:iPhone )?OS (\d+_\d+)/.test(ua)){
            system.ios = parseFloat(RegExp.$1.replace("_", "."));
        } else {
            system.ios = 2;  //can't really detect - so guess
        }
    }

    //안드로이드 버전 판단
    if (/Android (\d+\.\d+)/.test(ua)){
        system.android = parseFloat(RegExp.$1);
    }

    //게임 시스템
    system.wii = ua.indexOf("Wii") > -1;
    system.ps = /playstation/i.test(ua);

    //결과 반환
    return {
        engine:     engine,
        browser:    browser,
        system:     system
    };

}();

아래와 같을 때 사용해주세요

  • 기능이나 쿽스를 직접 정확히 탐지할 수 없을 떄
  • 같은 브라우저의 기능이 플랫폼 별로 다를 때
  • 정보 수집 목적으로 정확히 알아야 할 때

클라이언트 탐지가 필요하다면 가장 먼저 기능 탐지를 시도합시다.
쿽스 탐지는 코드를 어떻게 진행할지 정할 때, 브라우저 탐지는 사용자 에이전트 문자열에 완전히 의존하므로 마지막 선택으로 미뤄둡시다.

마치며

지금까지 클라이언트 탐지에 대해서 알아 보았습니다.
다음 포스트에서는 DOM에 대해서 알아보겠습니다.
좋은 하루 되세요 :)