logo
Published on

프론트엔드 개발자는 어떻게 삽질을 할까(광고 스크립트 트러블슈팅 과정)

Authors
  • avatar
    Name
    MJ
    Twitter

스크립트와 관련해서 트러블슈팅 이야기를 그냥 작성하는 게시글입니다. 재미로 읽어주세요.

외부 스크립트의 동작에 문제가 발생했다

현재 우리 서비스에서는 인도네시아에서 사용되기 때문에, 인도네시아 플랫폼의 광고 스크립트를 index.html에 추가하여 사용하고 있었습니다.

<!-- index.html -->
...
<head>
  <script async="async" data-cfasync="false" src="ads.com/[hashed-data]/invoke.js"></script>
</head>

광고는 invoke.js에서 element id가 "container-[hashed-data]"를 만나면 광고 api를 호출하여 해당 container의 자식으로 추가해주는 방식이었습니다.

하지만, 광고를 조회하고 다시 conatainer를 만날 때에는 광고 api를 호출해주지 않는 문제가 있었습니다. (단 한 번만 실행되는 방식으로 구현됐던 것 같습니다. spa에서만 생기는 문제였던 것 같습니다.)

시도한 방법

head 태그에서 삭제 후 다시 로딩해보자

다시 스크립트를 로딩하면 덮어씌우지 않을까?라는 생각에 시도한 방법이었습니다

useEffect(() => {
  const scriptTag = document.head.getElementById('script-id')
  if (scriptTag) scriptTag.parentNode.removeChild(scriptTag)

  const newScript = document.createElement('script')
  newScript.src = 'ads.com/[hashed-data]/invoke.js'
  newScript.async = true
  newScript.setAttribute('data-cfasync', 'false')

  // 새로운 스크립트를 <head>에 추가
  document.head.appendChild(newScript)
  return () => {
    if (newScript.parentNode) newScript.parentNode.removeChild(newScript)
  }
}, [])

결과는 실패였습니다. 브라우저가 이미 invoke.js를 저장하고 있으므로, 결국 다시 다운로드하지 않았습니다.

그렇다면 캐시 무효화를 써보자

스크립트 태그를 다르게 해서 다운로드하면 다르지 않을까?해서 랜덤 난수를 집어 넣었습니다.

useEffect(() => {
  const scriptTag = document.head.getElementById('script-id')
  if (scriptTag) scriptTag.parentNode.removeChild(scriptTag)

  const randomNumber = Math.floor(Math.random() * 1000000)
  const newScript = document.createElement('script')
  newScript.src = `ads.com/[hashed-data]/invoke.js?v=${randomNumber}`
  newScript.async = true
  newScript.setAttribute('data-cfasync', 'false')

  // 새로운 스크립트를 <head>에 추가
  document.head.appendChild(newScript)
  return () => {
    if (newScript.parentNode) newScript.parentNode.removeChild(newScript)
  }
}, [])

새로운 파일들이 다운로드 되기는 했지만, 역시나 동작하지 않았습니다. 아마 브라우저가 이미 다운로드한 스크립트를 사용하고 있기 때문에 새로 다운로드 받아도 동작하지 않는 것 같습니다. (스코프 문제인 것 같습니다.)

Function 객체로 직접 함수를 실행해보자

스크립트를 다운받지말고 response로 text를 응답 받은 다음 직접 핸들링할 수 있게 Function 객체에 주입을 해서 필요할 때 사용한다면 해결되지 않을까? 해서 시도해 보았었습니다.

const reloadAdScript = () => {
  const scriptUrl = `ads.com/[hashed-data]/invoke.js`

  // fetch를 사용하여 스크립트 가져오기
  fetch(scriptUrl)
    .then((response) => response.text())
    .then((scriptCode) => {
      // Function 생성자를 사용하여 코드를 실행
      const scriptFunction = new Function(scriptCode)
      scriptFunction()
    })
}

reloadAdScript()

역시나 첫 로딩 이후에는 동작하지 않았습니다. (애초에 함수 호출로 해결되는 문제가 아니었던 것 같습니다.)

다시 문제로 돌아가서

근본적인 문제는 광고 스크립트가 첫 로딩 이후 동작하지 않는다는 문제였습니다. 그래서 일단 광고 스크립트가 어떻게 생겼는지 직접 Source탭에서 열어서 확인했습니다.

function _0x3db0(_0x14c735, _0x4acdc2) {
    _0x14c735 = _0x14c735 - 0xef;
    var _0x2d860a = _0x2d86[_0x14c735];
    return _0x2d860a;
}
(function(_0x2d7c33, _0x5e9d09) {
    var _0xc45544 = _0x3db0;
    while (!![]) {
        try {
            if (_0x1312a4 === _0x5e9d09)
                break;
            else
                _0x2d7c33['push'](_0x2d7c33['shift']());
        } catch (_0x4e3ed3) {
            _0x2d7c33['push'](_0x2d7c33['shift']());
        }
    }
}(_0x2d86, 0xae266),
!function() {
    'use strict';
    var _0x1df4be = _0x3db0;
    var _0x2c6098 = _0x315f48(), _0x2dff1f, _0x34b13b = !0x1, _0x319e0c = !0x0, _0x26db36 = !0x1, _0x2654df = _0x1df4be(0x149), _0x3771fd = _0x1df4be(0x159), _0x90733b = [[/%26/g, '&'], [/%20/g, '\x20'], [/%2B/g, '+'], [/%25/g, '%'], [/%3E/g, '>'], [/%3C/g, '<'], [/%2F/g, '/'], [/%3A/g, ':'], [/%3B/g, ';']], _0x49922b = function() {

    }

빌드과정에서 난독화를 해놔서 알아볼 수 없었지만, api나 함수의 문자열은 그대로 저장돼있어서 힌트를 얻을 수 있었습니다.

이미지를 보면 reload에 대한 코드가 있었고 관련해서 광고 플랫폼 개발자에게 문의해보니, container element에 해당 함수가 할당이 되고 초기화할 수 있다는 이야기를 들었습니다. 시도해본 결과 처음 조우했을 때만 할당이 되는 것을 확인할 수 있었습니다.

그렇다면 처음 할당될 때 전역 함수에 다시 재할당을 해서 사용할 수 있지 않을까? 라는 생각으로 마지막 시도를 했습니다.

마지막으로 시도한 reload 함수를 전역에 저장하기

useEffect(() => {
  const targetDiv = document.getElementById('container-[hashed-data]')

  if (targetDiv && targetDiv.reload) {
    document.globalFunction = document.globalFunction || {}
    document.globalFunction.reload = targetDiv.reload
  }

  if (document.globalFunction.reload) {
    document.globalFunction.reload()
  }
}, [])

감격스러운 성공의 순간이다.

window 객체에 해당 함수가 저장돼서 출력됐습니다. 또한, 정상적으로 실행되어 이제 다시 container를 조우해도 광고가 제대로 나오게 됐습니다.

추가로

  • 난독화된 함수 이름이 항상 일정하다면 직접 호출해서도 사용할 수 있을 것 같았습니다. 하지만, 언젠가 새 버전의 빌드가 되고, 해싱된다면 함수명의 일치를 보장할 수 없으니 element에 직접 할당해주는 함수를 복사하는 것이 안정성면에서 더 나은 것 같습니다.

나름 재밌는 트러블 슈팅 과정(삽질) 이었습니다. 외부 script를 Source 탭에서 확인해서 전역에 등록돼있다면 직접 호출할 수 있다는 것과(어차피 스코프에 등록되기 때문에), 난독화된 소스에서 직접 데이터를 찾아 헤매는 과정도 성장의 포인트인 것 같았습니다.