NOMO.asia

Twitch, Youtube 같은 웹사이트는 사용자가 웹페이지를 최소화 했거나 다른 탭을 보고있는지를 체크한 후 재생 중인 동영상의 화질을 낮추거나, 화면은 내보내지 않고 음성만 재생하거나, 자동 재생이 되지 않도록 막아 데이터 전송량을 줄이는 동작을 한다.

이 때 사용자가 웹페이지를 보고있는지를 확인하기 위한 것이 바로 visibilitychange 이벤트 (https://developer.mozilla.org/ko/docs/Web/API/Page_Visibility_API)이다. 이 이벤트에 리스너로 등록된 함수는 웹페이지의 visibility 상태가 변하면, 즉 사용자가 탭을 다른 탭으로 변경하거나 원래 탭으로 복귀하는 등의 행동을 취하면 실행되게 된다. 따라서 웹사이트의 관리자가 앞서 설명한 경우와 같은 기능을 구현하려면 visibility 상태를 확인하고, (일정 시간 후에도 계속 visibility 상태가 false 라면) 화질을 변경하도록 하는 코드를 작성하면 된다.

그런데 이러한 동작이 실제 잘못된 결과로 나오는 경우가 가끔 있는데, 자동으로 화질이 변경될 때 영상이나 음성에 끊김이 발생하거나 아니면 아예 영상 재생이 실패하는 경우이다. 나는 이런 경우가 짜증나서 visibilitychange 이벤트를 무력화하기 위한 방법을 찾아보기 시작했다. (아예 플레이어를 대체해버리는 확장기능을 사용해도 되지만 기본 플레이어를 사용하고 싶었다.)

visibilitychange 는 웹페이지가 최소화 되거나 탭이 비활성 상태일 때 "자동으로" 무엇인가를 해주기 위한 것이라서 그냥 없애버려도 대부분 웹페이지의 동작에 아무런 문제를 발생시키지 않는다.


한 줄 요약: 만약 당신이 개발자가 아닌 사용자라면 Chrome 브라우저에서 Don't Make Me Watch 확장기능을 설치하여 쓰면 된다.

웹페이지에 visibilitychange 이벤트가 등록되었는지 확인하기

내가 방문한 웹페이지에 visibilitychange 이벤트가 등록되었는지 확인하려면 Chrome 에서 아래와 같이 하면 된다. (참고로 Firefox 는 별도의 개발자 툴을 써야한다.)

  1. F12 키를 눌러 브라우저 개발자도구를 연다.
  2. 콘솔창에 getEventListeners(document).visibilitychange 를 입력하고 엔터를 친다.

만약 결과가 undefined 로 나온다면 visibilitychange 이벤트가 등록되지 않은 것이고, 무언가 배열의 형태로 결과가 나온다면 visibilitychange 이벤트가 등록되어 있는 것이다.

유투브 메인 화면에서 콘솔창에 getEventListeners(document).visibilitychange 를 입력하니 총 6개의 visibilitychange 이벤트가 등록되어 있다고 나온다.

visibilitychange 이벤트를 무력화하기 위한 방법들

만약 이벤트 리스너, 즉 이벤트가 등록될 때 사용된 함수명을 알 수 있다면 그냥 removeEventListener("visibilitychange", 함수명) 함수를 이용해 이벤트를 지워버리면 되겠지만 보통 그런 경우는 잘 없다. window 객체로부터 쉽게 접근할 수 없는 위치에 있거나, 컴파일 과정에서 함수명이 자동으로 변경되기 때문에 함수명을 알아도 새버전의 코드가 나오면 다른 이름으로 바뀌어서 소용없게 되는등 경우가 다양하다. 물론 이런 경우에도 불가능한 것은 아니지만 원하는 결과에 비해 수고가 너무 커지게 된다.

따라서 visibilitychange 이벤트를 무력화하기 위해 써볼만한 방법이 무엇이 있는지를 찾아 정리해보았다.

콘솔창에서 이벤트를 수동으로 지우기 (Chrome)

만약 visibilitychange 이벤트를 수동으로 지우고 싶다면 F12 키를 눌러 브라우저 개발자도구를 연다음 아래의 내용을 콘솔창에 붙여넣으면 된다.

document.removeEventListener("visibilitychange", getEventListeners(document).visibilitychange[0].listener, getEventListeners(document).visibilitychange[0].useCapture)

만약 visibilitychange 이벤트가 여러개 등록되어있고, 한꺼번에 모두 지우고싶다면 아래의 내용을 콘솔창에 붙여넣으면 된다.

while(getEventListeners(document).visibilitychange !== undefined){document.removeEventListener("visibilitychange", getEventListeners(document).visibilitychange[0].listener, getEventListeners(document).visibilitychange[0].useCapture);}

위의 코드는 getEventListeners 함수를 이용하여 document 에 등록된 이벤트 리스트를 모두 가져오고, 그 중 visibilitychange 이벤트인 것을 지우도록 동작한다.

그런데 이 getEventListners 함수는 이벤트를 모니터링하기 위한 디버그용 도구 (https://developers.google.com/web/tools/chrome-devtools/console/events?hl=ko) 라서 바닐라(Vanilla) 자바스크립트 환경에서는 쓸 수 없다. 만약 개발자 도구가 아닌 일반 웹페이지에서 위 함수를 사용하려고 하면 아래와 같은 오류가 나타날 것이다.

getEventListners is not defined.

getEventListners is not a function.

event.stopImmediatePropagation() 사용하기

구글링을 통해 방법을 찾다가 이미 해당 이벤트를 비활성화 하기 위한 크롬 확장기능이 있다는 것을 알았다. 이름은 Don't Make Me Watch 이다. 아래의 링크에서 코드를 볼 수 있다.

https://github.com/NavinF/dont/blob/master/dont.js

코드를 봤더니 원리는 간단했다. 웹페이지가 열릴 때 "가장 먼저" visibilitychange 이벤트를 만든다음, 함수 내에서 event.stopImmediatePropagation(); 를 실행하도록 한다. 이게 전부다.

event.stopImmediatePropagation() 는 같은 이벤트에서 다른 리스너들이 불려지는 것을 막는 함수이다. 자세한 설명은 다음을 참고. https://developer.mozilla.org/ko/docs/Web/API/Event/stopImmediatePropagation

이벤트는 웹페이지에 추가된 순서로 불리게 되는데, Chrome extension 으로 이벤트를 웹페이지에 가장 먼저 등록하면 가장 먼저 실행되므로, 그 뒤에 등록된 다른 visibilitychange 이벤트들은 event.stopImmediatePropagation() 에 의해 불려지지 않게 된다.

addEventListener 함수 덮어쓰기

앞선 방법보다 조금 더 복잡하다. visibilitychange 이벤트가 등록되기 전 addEventListener 함수를 복사해두고 내가 작성한 함수로 덮어쓴다. 그리고 덮어쓴 함수에서는 이벤트 타입을 체크하여, visibilitychange 이면 event 를 등록하지 않고 넘기도록 한다. 따라서 이벤트를 아예 등록하지 않게 된다.

unsafeWindow.document._addEventListener = unsafeWindow.document.addEventListener;
unsafeWindow.document.addEventListener = function(a,b,c){
    if(a === "visibilitychange"){ //  || a === "blur" || a === "webkitvisibilitychange"
        console.log(a,b,c);
        return;
    }

    if(c==undefined)
        c=false;
    this._addEventListener(a,b,c);
};

위 코드는 Userscript 에서 사용하기 위한 코드라서 window 대신 unsafeWindow 를 사용했다. 유저스크립트를 사용하는 경우 visibilitychange 이벤트가 등록되기 전 Userscript 코드가 실행되어야 하기 때문에 가능한한 빨리 Usersciprt 코드가 실행될 수 있도록 metadata block 에서 run_at 을 document-start 로 해주어야 한다. 참고: https://www.tampermonkey.net/documentation.php#_run_at

참고로 위의 코드에서 함수의 입력인자 중 a 는 event type, b 는 리스너(함수), c는 capture 여부이다.

위의 코드에서 blur 와 webkitvisibilitychange 이 주석처리 되어있는데, 이 두 타입의 이벤트도 visibilitychange 와 유사하게 화면을 보고있는지를 감지하는데 쓰일 수 있다고 한다. 따라서 웹페이지에 따라 다를 수는 있지만 visibilitychange 만 무력화해서 원하는대로 자동화질변경 등의 기능이 차단되지 않는다면 저 두 타입의 이벤트까지 같이 무력화해주면 된다. 참고로 document 뿐만이 아니라 window 에도 이런 이벤트 들이 등록되어 있을 수 있다.

이 방법을 응용하면 바닐라 자바스크립트에서 지원하지 않는 getEventListeners 함수를 만들 수도 있다. 자세한 내용은 https://gist.github.com/cmbaughman/61ad5b49f10832e07b21993a20b94d8a 를 참고하면 된다.

document 의 visibilityState 관련 변수 수정하기

아래와 같은 방법으로도 가능하다고 한다.

//document object 덮어쓰기
Object.defineProperty(document, 'hidden', {
    value: false,
    writable: false
});
Object.defineProperty(document, 'visibilityState', {
    value: 'visible',
    writable: false
});
Object.defineProperty(document, 'webkitVisibilityState', {
    value: 'visible',
    writable: false
});
document.dispatchEvent(new Event('visibilitychange'));
document.hasFocus = function () {
    return true;
};

visibilitychange 이벤트 무력화를 회피하기

웹사이트를 운영하는 입장에서 visibilitychange 이벤트의 무력화는 결국 운영 비용을 기존보다 증가시킬 것이므로 좋은 것이 아니다. 실제로visibilitychange 이벤트를 무력화하는 것을 회피하는 웹사이트들이 있다. 마치 Anti-Adblock 처럼 동작하여, visibilitychange 이벤트가 무력화 된 것이 체크되면 웹사이트를 이용할 수 없게 내용을 지우고 화면을 경고 메시지로 가려버리는 방식으로 동작한다.

참고로 이러한 visibilitychange 이벤트의 무력화를 막지 않고있는 웹페이지가 있다고 하더라도 이런 무력화를 막지 못해서 못막는 것이 아니다. Adblock 의 경우와 마찬가지라고 생각하면 된다.

여하튼 따로 코드를 분석해보지는 않았지만 visibilitychange 이벤트의 무력화를 회피하기 위한 방법은 무궁무진해보인다. 간단하게 구상해본 것은 아래와 같다.

  1. global 로 a 변수를 선언하고 초기값으로 0 을 준다.
  2. visibilitychange 이벤트를 등록한다.
  3. 이벤트가 발생하면 실행될 함수에서는 a 변수의 값을 확인하여 값이 0 인 경우 값을 1로 변경하고 바로 return 한다.
    만약 값이 1일 경우 원하는 동작을 실행한다.
  4. document 가 ready 되면 visibilitychange 이벤트를 trigger 함수를 이용하여 발동시킨다.
    따라서 정상적으로 이벤트가 실행된 경우 값은 a 변수의 값은 1이어야 한다.
    만약 visibilitychange 이벤트가 무력화 되었다면 값은 a 변수의 값은 0일 것이다.
  5. setTimeout 함수를 이용하여 일정 시간이 지난 후 a 변수의 값이 1인지를 확인한다.
    만약 값이 0이라면 내용을 지우고 화면을 경고 메시지로 가려버린다.

응? 위와 같은 알고리즘이라면 event.stopImmediatePropagation() 함수가 포함된 이벤트를 이용하는 방법을 쓰고, 웹페이지가 로드된 후 일정 시간이 지난 이후부터만 event.stopImmediatePropagation() 함수가 발동되도록 하면 되는 것 아닌가? 라고 생각할 수 있겠다. 혹은 경고 메시지로 화면을 가려버리는 함수를 무력화한다던지, 단순히 a 의 값을 직접 1로 지정해주는 방법등을 생각해볼 수도 있다.

그렇지만 이것은 Adblock - Anti-Adblock - Anti-Adblock Killer 의 경우와 똑같다. 위의 방법 외에도 visibilitychange 이벤트가 잘 등록되었는지, 잘 실행될 수 있는지 확인할 수 있는 방법을 구상하는 것은 매우 쉽고, 소스 코드 분석을 어렵게 해놓는 것은 기본이며, 무엇보다 내가 작성한 무력화 코드를 공개하고 이것이 널리 알려진다면 웹사이트를 운영하는 측이 이 코드를 볼 수 있기 때문에 무력화를 쉽게 막아낼 수 있다.

결론은, 이런 코드를 작성하고 잘 동작한다면 널리 퍼트리지 말고 몰래 쓰면 된다.