트위터 트윗 글속에 삽입하기


데모

이렇게만 보면 그냥 공식 홈페이지에서 제공하는 위젯과 동일하다. 이게 무슨 대단한 일이라고?

근데 위 위젯을 렌더링하는 과정에서 그 어떤 외부 통신도 이뤄지지 않았다. 이미지 스크린샷이 아니기 때문에 모든 HTML 태그가 정상 작동한다 (모든 버튼과 링크가 동일하게 작동함). 즉,

  • 트위터가 망해도 이 위젯은 계속 동일하게 그려진다.
  • 어떤 추적 정보도 외부 사이트에 전달되지 않는다. (뭔가 클릭해서 외부 사이트로 직접 이동하지 않는 한)

목적

트위터에서는 트윗 삽입을 위해 공식 embed 기능을 제공하고 있지만, 여기서 트위터측 js 코드를 제거하면 볼썽사나운 blockquote 하나만 떨렁 남게된다. 즉, 트위터 측에서 제공하는 js를 삽입해야만 제대로 된 트윗을 그릴 수 있다는 것인데, 극단적으로 말해서 내 블로그에 바이러스를 까는 것 같은…(엥? 여기서 이렇게까지 과민반응을 한다고?)…것 같아서 어쨌든 다른 방법을 찾아보는게 이 글의 목적이다. (그냥 블로그 글 하나 더 쓰려고 안해도 되는 삽질을 하는거라고 하자)

그렇지만 외부 JS 코드를 삽입하는 것에는 조심해야 하는게 맞다. XSS은 아주 손쉽게 외부 공격자에게 노출될 수 있는 취약점이기 때문이다. 이번 사례에서는:

  • 공격자가 https://platform.twitter.com/widgets.js 를 탈취해서 공격 코드를 집어넣으면 블로그 방문객들의 개인정보가 빠져나갈 수 있다.
  • 지금이야 말도 안되는 소리지만, 나중에 일론이 트위터 말아먹고 해킹이라도 해서 돈 벌자고 생각하면 트위터에서 직접 공격 코드를 집어넣을 수도 있다.

여튼 외부 위젯에서, dynamic한 부분은 포기하고 딱 그려진 부분만 HTML 형태로 퍼나르는 것이 오늘의 목표라고 하겠다.

요구사항

  1. <Bakje twt="123123" /> 같은 태그를 삽입하면 트윗 embed가 블로그에 삽입되는 것을 원했다.
    • <Image> 태그가 빌드 과정에서 원본 이미지를 변환하는 것처럼 작동하는걸 기대했다.
  2. 블로그를 표시하는 동안에는 트위터 쪽으로 어떤 통신도 이뤄져선 안된다.
  3. 그러려면 트위터 글 snapshot을 로컬에 저장해야 한다.
  4. 저장해야 할 snapshot에는 글 본문, 작성자 이름, 작성사의 프로필 이미지, 리트윗, 좋아요 숫자, 트윗으로 가는 링크 등을 보관해야 한다.
  5. 트위터 API를 호출해서 이런 정보를 받아올 수 있을까? 인터넷을 조금 조사해보니 API를 호출하려면 인증이 필요하다. 왠지 싫은데… 어쨌든 할 순 있다.
    • 아니면 웹페이지를 직접 긁어서 데이터를 만들면 되니까 데이터 수집은 어떻게든 가능할 것 같다.
  6. 수집한 데이터를 로컬에는 어떻게 저장하나? 글 삭제 등 변경이 있을 경우엔?
  7. 데이터 수집은 언제 하나? 처음 1. 에서 기대했을 때에는 블로그 빌드 시점이었는데, 다른 요구조건을 검토해보니 글 작성 시점에 수동으로 데이터를 수집하는 편이 나아보인다.
    • 빌드 시점에 데이터를 수집하면 빌드 과정에서 외부 통신이 필요해지는데 그렇게 되면 빌드가 불안정해진다는 문제가 생긴다.
    • 타인이 빌드 시점을 추론할 수 있게 되는 문제도 생길 수 있다. 이를테면 블로그 포스트에 좋아요가 299개로 박제되어 있다면, 빌드 시점에 좋아요가 299개였다는 셈이니까…

디자인

구현

디자인은 대충 그렸는데, 어쨌든 웹을 긁어다가 다른 웹사이트에 그대로 재구성해야 하는 상황이다.

원본 상태 생성

데이터를 퍼가려면 원본을 정의해야 한다. 여기선 박제할 원본을 이렇게 정의했다:

트위터 공식 위젯 작성 사이트에서 위젯을 생성하였을 때 결과물이 생성된 페이지의 HTML 부분

그런데 사이트에서는 client side에서 런타임에 위젯을 그린다. 즉, 서버 데이터만 슬쩍 퍼나르는 정도로는 <div id="bad"></div> 정도의 태그만 나온다는 것이다. 그러니까 브라우저를 써서 모든 JS 렌더링 과정을 거치고, 그 결과물을 박제해야 한다. 여기서는 브라우저 역할을 해줄 라이브러리로 playwright을 이용했다.

HTML 박제

HTML은 그냥 document.querySelector(...).innerHTML, 혹은 playwright에서 제공하는 locator.inner_html()을 써서 가져오면 된다.

CSS 박제

위와같이 HTML DOM의 경우엔 현재 상태를 반환하는 함수가 있지만, CSS의 경우 그런게 없다. 웹사이트에서 CSS를 단순하게 제공하는 방식(<link rel="stylesheet" href="...">)이라면 그냥 파일 전체를 가져오는 것도 고려해볼만 한데, 여기서는 DOM 생성 시점에 CSS 또한 생성하기 때문에 browser inspector로 보는 HTML 파일에선 CSS에 해당하는 부분이 없었다.

그렇지만 각 HTML element를 inspector로 보면 CSS가 적용되어 있고 어떤 룰을 적용했는지도 표시되고 있는 상태. 일단 여기서는 더이상 생각하는 것을 멈추고 적용된 CSS를 모두 긁어모으는 방법을 선택하기로 했다. 어차피 브라우저까지 쓰는 마당에 무엇을 못하랴, 브라우저 inspector를 그대로 써서 CSS를 긁어보자.

그렇지만 위젯 하나 그리는 데도 100개 가까운 HTML element를 사용하고 각 element마다 여러개의 CSS 룰이 적용되고 있으니 손으로 일일이 복사 붙여넣기는 번거롭다. 그러니 수작업을 하지 말고 브라우저 inspector가 쓰는 Chrome Devtools Protocol을 사용하자. 이번에는 DOM과 CSS를 긁어야 하니 DOM과 CSS 도메인을 이용하면 된다.

이미지 박제

이미지는 <img src="...">로 로드하는 경우도 있고, CSS에서 url(...) 속성을 이용해 로드하는 경우도 있다. 이미지를 로컬에 저장한 다음, <img src="..."> 태그와 url(...) 속성을 모두 로컬 경로로 바꿔줘서 땜빵…아니 해결했다.

테스트

여기까지 한 다음에 HTML 파일과 이미지 파일을 떨궈서 브라우저에서 열었을 때 정상적으로 표시되고 외부 통신이 없으니 성공! 만약 외부 통신이 발생하면 원인을 찾아 더 제거해주면 될 것 같다.

결론

URL 넣으면 뽑아낼 수 있게 자동화는 했는데, 내가 앞으로 계속 쓸지는 잘 모르겠다. 아예 서비스 형태로 가공해서 장사를 해먹으면 모를까.