이번에는 얼마전에 GitHub에 올려놓았던 Discord 이미지 서버 주소를 media.discordapp.net에서 cdn.discordapp.com으로 변경하는 스크립트의 개발 로그를 써볼 예정입니다.


완성품은 Seia-Soto/switch-discord-desktop-image-source에서 볼 수 있습니다.

Seia-Soto/switch-discord-desktop-image-source
Change the `src` property of image elements in DOM `media.discordapp.net` to `cdn.discordapp.com` automatically via JavaScript mutation observer. - Seia-Soto/switch-discord-desktop-image-source

HTTP 503: Service Unavailable

네, media.discordapp.net의 자기소개입니다. 사실 약간 어이가 없었습니다. 옛날에는 속도가 느리더라도 결과가 나오기는 했거든요. 물론 거의 1분을 기다려야 했습니다. 그리고 샬며시 봐달라는 듯 나오는 이미지입니다.

Discord에서 이미지를 제공해주는 도메인은 2가지 종류가 있습니다.

  • media.discordapp.net: 이미지의 썸네일과 orientation을 설정
  • cdn.discordapp.com: 이미지 자체를 전송

이 때 Discord의 썸네일은 media.discordapp.net에서 로드되는데 썸네일을 처리하는 로직이 병목 현상에 발이 묶인 건지 그 당시에 cdn.discordapp.com 쪽에서 오는 데이터는 빠르게 잘 넘어오고 있더라고요. 그래서 media.discordapp.net으로 가는 요청을 cdn.discordapp.com으로 프록시 등을 사용해서 대신 데이터를 넘겨주면 어떨까 싶었습니다. 사실 스크립트로 결과물이 나오게 된 이유는 제가 안드로이드나 iOS 등 네이티브 스택이 아닌 웹 스택만 줄줄이 쌓고 있었기 때문입니다. 아쉽게도 지금은 어떻게 만들 줄을 모르기 때문에 일단은 스크립트로 작성해보기로 했습니다.

코딩하기

가장 먼저 할 것은 현재 이미지를 모두 가져오는 것입니다. (그 전에 jQuery를 import하지 않도록 하세요, 반드시)

const images = document.querySelector('img')

다음 할 일은 재사용 가능한 함수를 만들어주는 것입니다. 그 이후에 업로드되는 이미지의 주소도 변경해야 하기 때문이죠. DOM 노드 리스트를 받고 src 속성 값을 변경해줍니다.

const updateImageSource = images => {
  for (let i = 0, l = images.length; i < l; i++) {
    const image = images[i]

    image.src = image.src.replace(/media\.discordapp\.net/i, 'cdn.discordapp.com')
    image.style['image-orientation'] = 'from-image'
  }
}

마지막으로 추후에 올라오는 이미지를 가져오는데에 MutationObserver API를 사용할 수 있습니다. Mutation Observer는 실시간으로 변경(추가 그리고 제거를 포함함)되는 DOM 노드의 리스트를 반환합니다. 길이가 굳이 길 필요는 없기 때문에 Mutation Observer의 Constructor와 함께 반환된 객체를 바로 사용하면 됩니다. 여기에서 추가된 노드들은 i번째 mutation list의 addedNodes DOM 객체 리스트에 포함되어 있습니다. 그러므로 나머지는 간단히 tagName 속성을 사용하여 이미지 태그만 가져온 다음 마지막 observe 메서드를 통해 document.body에 대해 subtree 변경 사항을 확인합니다.

new MutationObserver(mutations => {
  const newImages = []

  for (let i = 0, l = mutations.length; i < l; i++) {
    const addedNodes = mutations[i].addedNodes

    for (let k = 0, s = addedNodes.length; k < s; k++) {
      const addedNode = addedNodes[k]

      if (addedNode.tagName === 'IMG') {
        newImages.push(addedNode)
      }
    }
  }

  updateImageSource(newImages)
})
  .observe(document.body, {
    childList: true,
    subtree: true
  })

테스트하기

아래는 완성된 코드를 Minify한 것입니다. 숏코딩 자체는 평소에 하지 않아 어떤 요소를 중점적으로 해야 하는지는 모르겠지만 최대한 그 당시에는 최선이라고 생각하고 있습니다...

d=document;g='image';p='discordapp';a=c=>c.forEach(e=>{e.src=e.src.replace('media.'+p+'.net','cdn.'+p+'.com');e.style[g+'-orientation']='from-'+g});a(d.querySelectorAll('img'));new MutationObserver(m=>{x=[];m.forEach(z=>{n=z.addedNodes;n.forEach(k=>{if(k.tagName=='IMG'){x.push(k)}})});a(x)}).observe(d.body,{childList:true,subtree:true})

위 코드를 복사해서 Discord 클라이언트의 개발자도구 콘솔에 붙여넣어줍니다. (Ctrl Shift I를 통해 Chrome 개발자 도구를 열 수 있습니다) 아래 서버는 제가 개인적으로 Discord 봇 개발 중 사용하는 테스트 서버입니다. img 태그의 src 값이 cdn.discordapp.com으로 대체되는 것을 확인할 수 있습니다.

감사합니다. 끝이예요.