Skip to content

About

트위터(X) 타임라인을 일정 주기별로 자동갱신하며 여러 탭을 한 화면에 보여줌

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

X Deck

x.com 의 추천 · 팔로잉 · 알림(전체) · 멘션 타임라인을 한 화면에 나란히 놓고 실시간으로 받아보는 크롬 확장입니다.

API 요금이 들지 않습니다. 사용자의 브라우저 세션으로 x.com 을 띄워둔 뒤, x.com 이 스스로 주고받는 타임라인 응답을 곁에서 읽어 덱 화면에 다시 그리는 방식입니다. 새로운 요청을 만들어 보내지 않으므로 x.com 이 화면에 뿌리는 내용과 덱이 받는 내용이 항상 같습니다.

Note

이 프로젝트는 x.com 의 화면 구조(DOM)와 응답 형식에 기대어 동작합니다. x.com 이 UI 를 개편하면 수집이 멈출 수 있으며, 그때 고쳐야 할 곳은 src/content/selectors.ts 한 파일로 모아두었습니다.


목차


주요 기능

여러 타임라인을 동시에

컬럼 수집처
추천 x.com/home 의 추천 탭
팔로잉 x.com/home 의 팔로잉 탭
알림(전체) x.com/notifications
멘션 x.com/notifications/mentions

알림에 (전체) 를 붙여 둔 것은 멘션과 나란히 놓이는 자리가 많기 때문입니다. 알림은 멘션까지 담은 전체 목록이고 멘션은 그중 한 갈래라, 이름만으로 갈리지 않으면 어느 쪽에 무엇이 쌓이는지 알기 어렵습니다.

각 타임라인을 컬럼으로 띄울지, 종으로만 지켜볼지, 아예 끌지는 설정에서 고르며, 컬럼 머리글을 끌어다 놓아 순서를 바꿀 수 있습니다. 머리글을 누르면 그 컬럼이 맨 위로 올라갑니다. 알림(전체) 컬럼에는 게시물과 알림(좋아요 · 팔로우 · 리포스트)이 섞여 시간순으로 쌓입니다.

폭을 먹지 않는 지켜보기

창이 좁아 컬럼을 늘릴 수 없을 때, 남은 타임라인을 종으로 지켜볼 수 있습니다. 컬럼과 똑같이 수집해두고 화면에는 자리를 내주지 않으며, 새 글이 오면 상단 바의 종에 안 본 수가 붙습니다. 종을 누르면 덱 위에 겹쳐 펼쳐지고, 그 안에서 답글 · 하트 · 리포스트까지 컬럼에서 하던 그대로 처리한 뒤 닫으면 원래 배치가 그대로 남습니다.

안 본 수는 덱을 연 뒤에 들어온 글만 셉니다. 보관해둔 과거 글은 세지 않습니다. 같은 멘션이 알림(전체)과 멘션 양쪽에 실려 와도 종에는 한 건으로 셉니다.

컬럼 수는 배치를 정하는 기준이기도 합니다. 넷을 모두 컬럼으로 켜 두면 좌우로 나란히 놓는 데 1,420px 이 필요하지만, 멘션 · 알림을 종으로 돌리면 716px 로 줄어 같은 창에서 추천 · 팔로잉이 나란히 섭니다.

화면에 맞춰 눕는 배치

  • 좌우로 나란히 — 컬럼을 가로로 늘어놓습니다
  • 위아래로 쌓기 — 세로로 긴 창에 적합합니다
  • 탭으로 하나씩 — 좁은 화면에서 상단 탭으로 갈아 끼웁니다

창이 좁아 고른 배치를 그릴 수 없으면 자동으로 한 단계 눕고, 창을 다시 키우면 고른 배치로 돌아옵니다. 저장된 설정 자체는 바뀌지 않습니다.

덱을 벗어나지 않는 상호작용

  • 하트 · 리포스트 · 인용 — 덱 안에서 처리하며, 결과는 x.com 계정에 그대로 반영됩니다
  • 답글 · 새 글 작성 — x.com 의 공식 작성 화면을 덱 안 대화상자로 띄웁니다
  • 게시물 상세 · 프로필 — 답글 트리와 프로필을 덱 안 창에서 확인합니다
  • 미디어 원본 — 사진은 눌러 라이트박스로 확대하고, 동영상 · GIF 는 그 자리에서 재생합니다. 상세 창 안의 사진도 같은 라이트박스로 열립니다
  • 번역 — 한국어가 아닌 글에만 번역하기 가 붙고, 언제나 한국어로 옮깁니다. 번역문은 원문 아래에 덧붙습니다. 인용된 글에도 따로 붙으며, 원문이 여섯 줄에서 잘려 있어도 번역문은 글 전체를 보여줍니다
  • 사진 번역 — 사진 속 일본어 · 영어를 한국어로 옮깁니다 (선택 기능, 준비물)

사진 속 글자 번역 (선택)

라이트박스에서 사진 하나를 골라 그 안의 글자를 한국어로 옮깁니다. 세로쓰기 일본어도 대상입니다. 게시물 상세 창에서 연 사진도 같은 라이트박스로 뜨므로, 원글은 물론 답글에 달린 사진에도 그대로 씁니다.

이 기능만은 브라우저 안에서 끝나지 않습니다. 이 PC 에 깔린 codex · claude 명령과 그 구독 계정 을 그대로 빌려 쓰며, 둘 사이를 잇는 작은 프로그램(bridge/)을 한 번 등록해야 합니다. 별도 API 키나 추가 요금은 필요하지 않습니다. 무엇을 미리 갖춰야 하는지는 사진 번역 준비 에 정리해 두었습니다.

등록한 뒤로는 띄워둘 것이 없습니다 — 번역이 필요할 때 브라우저가 알아서 켜고 끝나면 함께 내립니다.

고른 명령 결과
Codex 글자를 한국어로 바꿔 다시 그린 사진, 또는 읽어낸 원문과 번역문 중에서 고릅니다
Claude 읽어낸 원문과 번역문 을 사진 아래에 나란히 깝니다

Claude 는 이미지를 만들지 못해 글자로만 답합니다. Codex 가 내주는 사진은 원본에서 글자만 바꾼 것이 아니라 새로 그린 것이라, 잔글씨나 로고가 뭉개질 수 있습니다.

기본값은 꺼짐 입니다. 설정에서 켜고 브리지에 로그인이 확인되어야 라이트박스에 단추가 붙습니다.

읽기 편하게 다듬는 표시 설정

카드 밀도(기본 · 조밀), 컬럼 테두리와 카드 구분선, 미디어 표시 방식(바로 표시 · 라벨만 · 숨김)과 크기(작게 ~ 원본 비율), 동영상 자동 재생, 다크 · 라이트 테마, 그리고 PC 에 설치된 글꼴을 불러와 지정하는 기능을 제공합니다.

놓치지 않는 수집

  • 목록을 내려 읽는 동안에는 새 글을 끼워넣지 않고 상단 배지에 모아둡니다
  • 받은 게시물은 IndexedDB 에 보관해 브라우저를 껐다 켜도 남아 있습니다 (보관 기간 · 컬럼당 상한 조절 가능)
  • 스크롤을 내리면 보관된 과거 글을 이어서 읽습니다

설치와 빌드

요구 사항

  • Node.js 20.19 이상 (또는 22.12 이상) — Vite 7 의 요구 사항입니다
  • Chrome 120 이상 또는 동등한 크로미움 계열 브라우저

사진 번역(선택 기능)만은 준비물이 더 있습니다 — 사진 번역 준비 를 보세요.

빌드

npm install
npm run build      # dist/ 에 확장 번들을 생성합니다

브라우저에 로드

  1. 주소창에 chrome://extensions 를 입력합니다
  2. 우측 상단의 개발자 모드 를 켭니다
  3. 압축해제된 확장 프로그램을 로드합니다 를 누른 뒤 dist/ 폴더를 선택합니다

동기화 폴더(OneDrive · Dropbox 등) 안에서 작업한다면 결과만 밖으로 빼서 그쪽을 로드하세요. 브라우저는 시작할 때 압축해제 확장의 폴더를 다시 읽는데, 동기화 도구가 파일을 아직 내려놓지 않았으면 확장을 목록에서 버립니다 — 재시작할 때마다 확장이 사라지는 증상이 여기서 옵니다.

npm run build -- --out C:\ext\x-deck   # 이번 빌드만 그리로
setx XDECK_OUT C:\ext\x-deck           # 정해두면 build·dev 가 계속 따라갑니다

윈도우에서는 build.bat 을 두 번 눌러 같은 일을 할 수 있습니다. 인자를 주지 않으면 C:\ext\x-deck 에 굽고(XDECK_OUT 이 있으면 그쪽), 다른 자리는 build.bat D:\ext\x-deck 처럼 절대 경로로 적습니다. 감시 모드는 dev.bat 이며 자리를 고르는 방법이 같습니다.

빌드가 동기화 폴더 안에 결과를 놓으면 그때마다 경고를 한 줄 적습니다. 지정한 폴더가 비어 있지 않고 지난 빌드 결과도 아니면, 지우지 않고 멈춥니다.

같은 폴더를 두 번 로드해 두지 마세요. 항목이 둘 남으면 브라우저가 시작할 때 같은 폴더를 두 번 읽어 확장이 사라질 수 있습니다. chrome://extensions 에서 그 폴더를 가리키는 항목을 오류 항목까지 모두 지운 뒤 한 번만 다시 로드하면 정리됩니다.

사진 번역 준비 (선택)

사진 속 글자를 옮기는 일은 이 PC 에 깔린 codex · claude 명령과 그 구독 계정 이 대신합니다. 그래서 이 기능만 브라우저 밖의 준비가 필요하며, 브리지를 한 번 등록해야 합니다. 쓰지 않을 기능이면 이 절을 통째로 건너뛰어도 나머지 동작에는 아무 영향이 없습니다.

미리 갖춰야 하는 것

준비물 설명
Windows 등록 스크립트가 지금은 윈도우 레지스트리(HKEY_CURRENT_USER)만 다룹니다
Node.js 브리지가 Node 로 도는 프로그램입니다. 빌드에 쓴 설치본을 그대로 씁니다
codex 또는 claude CLI npm i -g @openai/codex · npm i -g @anthropic-ai/claude-code. 하나만 깔아도 되고 둘 다 깔아도 됩니다. 등록 스크립트가 깔린 쪽을 최신 판으로 받아둡니다
각 CLI 의 구독 로그인 codex login · claude /login 으로 마칩니다. 이미 내고 있는 요금제를 그대로 빌려 쓰므로 별도 API 키는 필요하지 않습니다
dist/ 폴더로 로드한 확장 브리지는 확장 ID 하나만 받아들입니다. 그 ID 는 manifest.json 의 key 가 정하므로 빌드 결과를 그대로 로드해야 값이 맞습니다

등록은 크로미움 계열 다섯(Chrome · Edge · Chromium · Brave · Whale)에 한꺼번에 씁니다. 깔려 있지 않은 브라우저에 등록해도 해가 없으며, 나중에 깔면 그대로 동작합니다.

등록과 확인

  1. bridge/install-bridge.bat 을 더블클릭합니다 (npm run bridge:install 도 같은 일을 합니다). 등록과 함께 codex · claude 를 최신 판으로 받습니다 — 받지 않으려면 --skip-update 를 붙입니다
  2. 브라우저를 완전히 껐다 켭니다. 창만 닫는 것으로는 부족한 경우가 있습니다
  3. 덱의 설정 › 번역 에서 사진 번역 사용 을 켭니다 (기본값은 꺼짐)
  4. 로그인 칸에서 codex · claude 의 상태를 봅니다. 되어 있지 않으면 로그인 단추를 눌러 뜨는 콘솔 창에서 절차를 마친 뒤 상태 다시 확인 을 누릅니다
  5. 둘 다 로그인되었다면 주로 쓸 명령 과 Codex 결과(다시 그린 이미지 · 읽은 글) 를 고릅니다. 하나만 쓸 수 있으면 그쪽으로 자동으로 갑니다
  6. 라이트박스에서 사진을 열면 사진 번역 단추가 붙습니다

해제는 bridge/uninstall-bridge.bat(npm run bridge:uninstall)입니다. 등록이 건드리는 자리는 HKEY_CURRENT_USER 와 bridge/ 폴더뿐이라 관리자 권한이 필요 없고, 해제하면 흔적이 남지 않습니다. 여기에 더해 최신 판 받기는 npm 전역 패키지(codex · claude)를 갱신합니다.

필요 없는 것 — API 키, 띄워둘 터미널, 포트 · 열쇠 설정. 등록한 뒤로는 번역이 필요할 때 브라우저가 브리지를 알아서 켜고 끝나면 함께 내립니다.

알아둘 것

  • 글자를 바꿔 다시 그리는 데는 한 장에 30초 ~ 1분 남짓 걸립니다. 읽은 글과 번역만 받으면 훨씬 빠릅니다
  • 번역은 한 번에 한 장씩 처리합니다. 결과는 원본 주소를 열쇠로 보관해 같은 사진을 두 번 청하지 않습니다
  • 각 구독 요금제의 사용량을 씁니다. 같은 계정으로 다른 작업을 하고 있다면 한도를 나눠 쓰게 됩니다
  • 단추를 누른 그때에 한해 사진 한 장 이 각 회사 서버로 나갑니다 (개인정보와 보안)

~/.codex/config.toml 이 지금 codex 판과 맞지 않아 codex 가 뜨지 못하면, 브리지가 문제가 된 줄을 꺼두고 다시 시도합니다 (고치기 전 파일은 config.toml.bak 에 남습니다). 그 밖에 잘 되지 않을 때의 세부(브리지 미등록, 로그인 풀림)는 bridge/README.md 에 있습니다.

npm 스크립트

명령 설명
npm run build 확장 번들을 dist/ 에 생성합니다. -- --out <폴더> 또는 환경 변수 XDECK_OUT 으로 자리를 옮길 수 있습니다
npm run dev 감시(watch) 모드로 빌드합니다. 코드를 고치면 다시 굽고, 확장 페이지에서 새로고침하면 반영됩니다. 출력 자리는 build 와 같은 방법으로 옮깁니다
npm run typecheck 타입 검사만 수행합니다 (tsc --noEmit)
npm test 테스트를 한 번 실행합니다
npm run test:watch 감시 모드로 테스트를 실행합니다
npm run check 타입 검사와 테스트를 차례로 실행합니다
npm run bridge:install 사진 번역 브리지를 등록하고 codex · claude 를 최신 판으로 받습니다 (선택 기능을 쓸 때만, 한 번)
npm run bridge:uninstall 브리지 등록을 해제합니다
npm run icons icons/ 의 아이콘 세트를 다시 생성합니다

빌드는 scripts/build.mjs 가 지휘합니다. 덱 UI 는 Vite 가 단일 IIFE 로 굽고(그림자 DOM 에 넣어야 하므로 CSS 까지 인라인), 인터셉터 · 브리지 · 백그라운드는 esbuild 가 맡습니다. manifest.json 의 버전은 package.json 의 값을 빌드 시점에 주입합니다. 결과를 놓을 자리는 scripts/out-dir.mjs 가 고릅니다 — 명령줄이 환경 변수를 이기고, 상대 경로는 명령을 친 자리 기준으로 풉니다.

테스트

브라우저를 띄우지 않고 Vitest + happy-dom 위에서 돕니다. x.com 로그인이 필요 없으므로 아무 때나 반복해서 돌릴 수 있습니다.

npm run check      # 타입 검사 + 테스트

재는 대상은 일곱 갈래입니다.

  • 응답 파싱 — src/core/parser.ts 가 GraphQL 응답을 제대로 옮기는지. 리포스트 · 인용 · 알림 · 폴백 판정처럼 조용히 깨지는 자리를 봅니다
  • DOM 판단 규칙 — src/content/selectors.ts 의 탭 찾기, 알약 판별, 주인공 게시물 고르기, 사진 클릭 가로채기 등을 손으로 만든 최소 DOM 으로 확인합니다
  • 순회 비용 — 매 초 도는 판정(로그인 여부 · 알약 찾기)이 타임라인 길이를 타지 않는지. 결과가 아니라 게시물 안을 몇 번 만졌는지 를 세므로, 답만 맞고 문서 전체를 훑는 구현은 걸러집니다
  • 설정 저장 — 확장을 다시 설치했을 때 지정해둔 값이 살아남는지, 예전 저장값이 지금 형태로 옮겨지는지
  • 브리지 문구 — 사진 번역이 실패했을 때 그 사정이 사용자에게까지 닿는지. codex 를 한 번도 띄우지 않고 잽니다
  • 번역 도착 언어 — 한국어로 못박혀 있는지, 언어 코드가 틀린 한국어 글을 글자로 가려내는지
  • 빌드 출력 자리 — --out · XDECK_OUT 을 어떻게 읽는지, 빌드 결과가 아닌 폴더를 지우지 않는지

셀렉터가 실제 x.com 에 지금도 걸리는지 는 손으로 만든 DOM 으로는 알 수 없습니다. 그건 진짜 화면을 떠 넣는 tests/fixtures/ 쪽이 맡습니다. 픽스처에는 계정 정보가 들어 있어 저장소에 올리지 않으며, 없으면 해당 테스트만 건너뛰고 나머지는 그대로 돕니다. 뜨는 방법은 tests/fixtures/README.md 에 있습니다.

happy-dom 에는 레이아웃 엔진이 없어 화면상의 위치와 크기를 알 수 없습니다. tests/setup/dom.ts 가 그 값을 테스트가 정할 수 있게 바꿔 끼웁니다.


사용법

덱 열기

툴바의 확장 아이콘을 누르면 x.com 탭이 열리고 그 위에 덱이 얹힙니다. 이미 열려 있는 덱 탭이 있으면 그 탭으로 이동합니다.

설정의 x.com 열면 덱으로 가 켜져 있으면(기본값) 평소처럼 x.com/home 에 들어가도 덱이 뜹니다. 덱의 네 컬럼이 그대로 대신하는 자리 — 홈(/home)과 알림(/notifications) — 이 대상입니다. 게시물 · 프로필 등 나머지 주소는 덮지 않으므로 원본 화면을 그대로 쓸 수 있습니다.

화면을 옮겨 다니는 동안에도 계속 지켜봅니다. x.com 은 화면을 바꿀 때 문서를 다시 띄우지 않아서, 처음 한 번만 판단하면 로그인 화면에서 시작한 탭은 로그인을 마쳐도 새로고침하기 전까지 덱이 뜨지 않습니다.

x.com 을 앱(PWA)으로 설치해 창으로 띄운 경우에도 덱이 자동으로 올라옵니다.

로그인이 되어 있지 않으면 덱이 스스로 비켜나며 x.com 의 공식 로그인 화면이 그대로 보입니다. 로그인을 마치면 수집이 이어서 시작됩니다.

상단 바

요소 하는 일
배치 버튼 좌우 · 위아래 · 탭 중에서 컬럼 배치를 고릅니다
종 아이콘 지켜보는 타임라인을 덱 위에 겹쳐 펼칩니다. 안 본 글이 있으면 그 수가 배지로 붙으며, 지켜보는 타임라인이 없으면 나타나지 않습니다
프로필 사진 내 프로필을 덱 안 창으로 엽니다
글쓰기 새 게시물 작성창을 엽니다
보관함 아이콘 지금까지 보관한 게시물 수를 보여줍니다
번개 아이콘 전체 절전. 모든 컬럼의 새 글 받아오기를 멈춥니다. 켜져 있으면 단추에 색이 들어오고 컬럼 머리글에 절전 배지가 붙습니다
눈 아이콘 덱을 잠시 비켜 아래의 x.com 원본을 그대로 사용합니다
해 · 달 아이콘 다크 · 라이트 테마를 전환합니다
톱니 아이콘 설정 패널을 엽니다

설정 항목

수집

항목 설명
새 게시물 자동 반영 x.com 상단의 '새 게시물 보기' 알림을 자동으로 눌러 다음 타임라인을 받아옵니다
유휴 강제 갱신 지정한 시간 동안 알림이 없으면 타임라인을 직접 다시 불러옵니다
스크롤 중 대기 목록을 내려 읽는 동안 새 글을 끼워넣지 않고 상단 배지에 모아둡니다. 영상 · GIF 가 도는 동안 과 번역을 기다리는 동안에도 같은 방식으로 모아두며, 이때는 이 설정과 무관합니다. 영상이 끝나거나 멈추면 그때부터 다시 끼워넣습니다 — 소리를 켜 둔 영상이 남아 있다는 이유만으로 붙들지는 않습니다

표시 — 타임라인, 자동 진입 여부, 테마, 글꼴, 카드 밀도, 컬럼 테두리, 카드 구분선, 미디어 표시 방식 · 자동 재생 · 크기

타임라인 은 네 종류마다 끔 · 컬럼 · 종 중 하나를 고릅니다. 컬럼은 화면에 자리를 차지하고, 종은 자리를 쓰지 않고 상단 바의 종에 안 본 수만 띄웁니다. 창이 좁아 컬럼을 늘릴 수 없을 때 종으로 돌리면 배치를 지킨 채 확인할 수 있습니다. 컬럼은 최소 하나 남겨야 하므로 마지막 컬럼은 다른 쪽으로 옮길 수 없습니다.

번역

항목 설명
사진 번역 사용 기본값은 꺼짐입니다. 켜면 아래 항목이 나타납니다
브리지 install-bridge.bat 을 한 번 실행하면 끝입니다. 이후 브라우저가 알아서 켜므로 띄워둘 것도, 맞출 값도 없습니다
로그인 codex · claude 각각의 상태를 보여주고, 로그인 단추를 누르면 콘솔 창이 떠 절차를 시작합니다
주로 쓸 명령 둘 다 로그인된 경우에만 나타납니다. 하나만 쓸 수 있으면 그쪽으로 자동으로 갑니다
Codex 결과 글자를 바꿔 다시 그린 이미지(한 장에 80초쯤) 또는 읽은 글과 번역(훨씬 빠름) 중에서 고릅니다. Claude 는 그림을 만들지 못해 늘 글입니다
빠른 등급으로 Codex 로 글을 옮길 때만 나타납니다. 약 14% 빨라지는 대신 구독 사용량을 더 씁니다

보관 — 보관 기간(1 ~ 30일), 컬럼당 최대 건수(500 ~ 5,000건), 보관 데이터 비우기

설정은 chrome.storage.sync 에 저장되어 같은 브라우저 계정을 쓰는 다른 기기에서도 이어집니다. 확장을 지웠다 다시 설치하는 경우를 대비해 x.com 페이지 쪽에 사본을 하나 더 남겨둡니다.

창 크기와 배치

컬럼 하나당 최소 폭 340px(높이 300px)을 기준으로, 지금 띄운 컬럼 수만큼 자리가 있는지 계산합니다. 예를 들어 좌우로 나란히 놓으려면 컬럼이 둘일 때 716px, 넷일 때 1,420px 이 필요합니다. 자리가 모자라면 위아래로 쌓기 또는 탭으로 자동 전환됩니다.


동작 방식

왜 확장 페이지가 아니라 x.com 페이지 위인가

덱은 별도의 확장 페이지가 아니라 x.com 탭 위에 얹히는 오버레이 입니다.

x.com 은 frame-ancestors 'self' 로 임베드를 막는데, 이는 곧 x.com 이 x.com 을 임베드하는 것은 허용 한다는 뜻입니다. 부모 문서를 x.com 으로 두면 다음과 같은 이점이 있습니다.

  • 쿠키가 same-site 로 그대로 실려 로그인 상태를 다시 만들 필요가 없습니다
  • 최상위 탭이므로 브라우저의 타이머 스로틀링을 받지 않습니다
  • 로그인은 x.com 자신이 처리하므로 자격 증명이 확장을 거치지 않습니다
  • 탭이 하나만 생깁니다

덱은 x.com 의 DOM 을 지우지 않고 살려둔 채 덮습니다. 그 아래에서 x.com 이 계속 폴링해야 '새 게시물 보기' 알림이 뜨고, 그 알림이 수집의 출발점이기 때문입니다. 덱의 스타일은 그림자 DOM 과 구성된 스타일시트(Constructable Stylesheet)로 넣어 x.com 의 CSS 및 CSP 양쪽에서 격리됩니다.

살려두되 그리지는 않습니다. 수집기가 탭을 오갈 때마다 x.com 은 타임라인을 통째로 다시 그리는데, 덱에 가려 아무도 볼 수 없는 화면에 대해 레이아웃부터 페인트 · 사진 디코딩까지 전부 치릅니다. 그래서 덱이 덮고 있는 동안에는 <body> 를 visibility: hidden 으로 둡니다. 덱은 <body> 가 아니라 <html> 바로 아래에 붙으므로 함께 감춰지지 않습니다. 자리는 그대로 잡아두는 속성이라 화면상의 위치를 읽는 셀렉터(탭 찾기 · 알림 찾기)도 그대로 동작하고, document.hidden 과 무관하므로 폴링도 이어집니다. 통과 모드로 넘어가면 덮개를 걷습니다.

영상은 떼어냅니다. 그리지 않는 것만으로는 영상이 멈추지 않습니다 — 화면에 안 보여도 재생과 디코딩은 계속됩니다. 추천 타임라인은 영상이 많아 이 몫이 작지 않습니다. 세우기만 하면 x.com 이 곧바로 다시 틀고, 재생 요청만 삼키면 플레이어가 재생 중이라고 믿은 채 남아 계속 돕니다. 그래서 수집 문서에서는 영상의 원본을 떼어 플레이어가 붙잡을 것을 없앱니다. 덱이 화면을 덮는 순간에는 이미 돌고 있던 영상도 한 번 훑어 떼어냅니다. 계속 되살아나면 정해진 횟수에서 손을 뗍니다. 덱 자신의 영상은 그림자 DOM 안에 있고 확장은 페이지와 다른 실행 환경을 쓰므로 영향받지 않으며, 덱에서 마우스를 올려 보는 미리보기는 그대로입니다. 통과 모드로 원본을 볼 때와 덱 안 창으로 띄운 게시물에서도 영상은 정상 재생됩니다.

수집 경로

x.com/home?xdeck_role=foryou&xdeck=1        ← 확장 아이콘이 여는 탭 (홈 화면에 자동으로 얹힐 때는 이 표시가 없습니다)
├─ interceptor.js  (MAIN world)     fetch/XHR 응답을 복제해 넘기고, 문서를 항상 '보임' 으로 유지
├─ bridge.js       (ISOLATED)       자식 프레임 전용 진입점
├─ deck.js         (ISOLATED)       그림자 DOM 에 덱 UI 를 얹고, 이 문서가 '추천' 을 직접 수집
└─ 숨은 iframe
   ├─ x.com/home?xdeck_role=following            ← 팔로잉 담당
   ├─ x.com/notifications/mentions?...=mentions  ← 멘션 담당
   └─ x.com/notifications?...=notifications      ← 알림 담당

숨은 프레임은 opacity: 0 으로 감춥니다. display: none 이나 화면 밖 배치는 렌더링이 멈춰 타임라인이 갱신되지 않기 때문입니다. 다만 opacity: 0 은 투명하게 그릴 뿐 그리기 자체를 건너뛰지는 않으므로, 프레임 안쪽 <body> 에는 최상위 문서와 같은 visibility: hidden 을 걸어 둡니다. 자리는 그대로 잡아두는 속성이라 셀렉터는 그대로 동작합니다.

프레임은 띄우는 컬럼과 종으로 지켜보는 타임라인 모두에 세웁니다. 컬럼을 끄면 화면에서만 사라지는 것이 아니라 그 타임라인을 한 건도 받지 않게 되므로, 지켜보기는 화면 자리를 내주지 않을 뿐 수집 비용은 컬럼과 같습니다.

프레임 주소에는 일회용 값(xdeck_t)이 하나 더 붙습니다. 늘 같은 주소로 띄우면 캐시에 남은 응답이 그대로 쓰이는데, 그 응답에는 확장이 걷어내야 할 X-Frame-Options 가 아직 붙어 있어 프레임이 막힙니다.

가로챈 GraphQL 응답은 src/core/parser.ts 에서 Tweet 형태로 정규화한 뒤 IndexedDB 에 저장하고 화면에 그립니다. 정석 경로가 실패하면 응답 전체를 훑는 폴백으로 넘어가며, 이때는 컬럼 머리글에 폴백 파싱 배지가 붙습니다.

늘어놓는 차례도 컬럼마다 다릅니다. 홈 컬럼(추천 · 팔로잉)은 받아온 차례가 자리를 정합니다 — 알고리즘 타임라인이라 글 자체의 시각은 뒤죽박죽이고, 방금 받아온 것이 위에 오는 것이 스트림의 뜻입니다.

추천은 한 응답 안에서도 x.com 이 보내준 차례를 그대로 지킵니다. 무엇을 위에 놓을지를 저쪽이 정해서 보내주므로, 그 안에서 글 시각순으로 다시 세우면 x.com 이 맨 위에 올린 글이 한참 아래로 내려갑니다. 팔로잉은 시간순이 곧 그 목록의 차례라 다시 세워도 결과가 같습니다.

알림 컬럼(알림 · 멘션)은 글 자체의 시각이 정합니다. x.com 의 알림 화면이 시간 순서라 그렇게 읽히기 때문입니다 — 여기서 받아온 차례를 앞세우면 사흘 전 알림이 다시 받아왔다는 이유만으로 어제 온 답글 위에 앉습니다.

같은 것을 두 번 쌓지 않는 기준은 게시물과 알림이 다릅니다. 게시물은 x.com 의 id 로 충분하지만, 알림은 그렇지 않습니다 — 모아 보여주는 알림('N 개를 마음에 들어 합니다')은 다시 받아올 때마다 id 가 갈려서, 같은 문구 · 같은 대상 글의 알림이 여러 줄로 쌓였습니다. 그래서 알림의 신원은 내용에서 만듭니다: 누가(사람) · 무엇을(아이콘) · 어느 글에. 문구는 넣지 않으므로 건수가 늘어도 한 줄로 남고, 그 줄의 문구만 최신으로 갈아 끼웁니다. 사람도 대상 글도 없는 안내성 알림('X 가입 기념일입니다!')은 문구가 유일한 근거라 문구로 가리며, 이때도 건수는 지우고 봅니다. 문구마저 없을 때만 x.com 의 id 를 그대로 씁니다.

절전 — 게임 등 다른 일을 하는 동안 멈춰 둡니다

새 글 한 뭉치를 받아오는 값의 대부분은 덱이 아니라 x.com 쪽에서 듭니다. 우리가 두드리면 x.com 은 응답을 주면서 자기 타임라인도 함께 다시 그립니다. 덱에 가려 보이지도 않는 화면인데 React 재실행부터 스타일 재계산 · 레이아웃까지 전부 치릅니다.

상단 바의 번개 단추를 켜면 모든 컬럼에서 두드리기를 멈춥니다 — 알림 클릭도, 자동 갱신도, 탭 이동도, 대타 방문도 하지 않습니다. 그동안 컬럼은 멈춰 있습니다. 머리글에 절전 배지가 붙어 멈춘 것과 고장난 것을 구별할 수 있습니다.

컬럼 하나만 재울 수도 있습니다. 각 컬럼 머리글의 새로고침 단추 왼쪽에 같은 번개가 있고, 그 컬럼만 멈춥니다. 값이 가장 비싼 추천은 재워두고 멘션은 살려두는 식으로 쓸 수 있습니다 — 추천은 영상이 많아 x.com 이 다시 그리는 값이 크지만 늘 새로 볼 필요는 없고, 멘션은 값이 싼데 놓치면 안 되기 때문입니다.

두 스위치는 따로 놉니다. 상단 바 쪽이 '지금은 아무 것도 받지 마라' 는 순간 스위치라면 컬럼 쪽은 '이 컬럼은 원래 급하지 않다' 는 상시 지정이라, 상단 바의 번개를 껐다 켜도 컬럼별 지정은 그대로 남습니다. 전체 절전이 켜져 있는 동안에는 컬럼 단추가 눌리지 않습니다 — 어차피 결과가 달라지지 않기 때문입니다.

멈추는 것만으로는 멈춰 있지 않습니다. 팔로잉 수집기가 자기 목록을 되찾으려고 홈 링크를 다시 누르거나 탭을 튕기면 홈의 기본 탭인 추천 타임라인이 딸려 옵니다. 청하지 않은 응답이지만 귀속은 정확해서 그대로 추천 컬럼에 쌓입니다 — 전체 절전에서는 옆 컬럼도 함께 잠들어 있어 드러나지 않던 자리입니다. 그래서 멈춰둔 컬럼은 누가 끌고 온 목록이든 들이지 않습니다(사람이 새로고침을 누른 동안은 예외). 다만 옆 컬럼이 되살아나는 값까지 없앨 수는 없으므로, 잠든 컬럼이 있어도 x.com 쪽 부담이 완전히 0 이 되지는 않습니다.

번개를 끄면 그 자리에서 최신 글을 받아옵니다. 절전 중에도 새로고침 단추는 그대로 동작합니다 — 절전이 막는 것은 저절로 도는 일이지 직접 누르는 조작이 아닙니다.

탭이 가려졌는지로 자동 판단하지는 않습니다. 모니터가 둘이면 다른 일을 하는 동안에도 덱은 브라우저가 보기에 '보이는' 탭이라, 그 값으로는 가려낼 수 없습니다.

사진은 그 자리에 필요한 만큼만 받습니다

컬럼은 창 크기와 컬럼 수에 따라 폭이 크게 달라집니다. 사진을 받을 때 드는 메모리는 화면에 그려지는 크기가 아니라 받아온 픽셀 수 로 정해지므로, 컬럼 폭을 실제로 재서 그에 맞는 크기를 고릅니다. 사진이 여러 장이면 격자로 깔려 칸이 절반이 되는 것까지 함께 셉니다.

목록에서는 1200px 을 넘겨 받지 않습니다. 확대해서 볼 때는 라이트박스가 원본을 따로 받으므로 목록이 더 큰 사진을 들고 있을 이유가 없습니다. 설정의 미디어 크기 는 화면에 그리는 높이를 정하는 것이고, 받아오는 크기는 이렇게 폭에서 자동으로 정해집니다.

갱신이 멈추지 않게 하는 장치

x.com 은 document.hidden 이면 새 게시물 폴링을 멈춥니다. 인터셉터가 visibilityState · hidden · hasFocus 를 항상 '보임' 으로 유지하고 visibilitychange · blur 이벤트를 캡처 단계에서 삼켜 폴링이 계속 돌게 합니다. 덱이 얹히는 문서에서만 적용되므로 평소에 쓰는 다른 x.com 탭은 영향을 받지 않습니다.

인터셉터가 깨어날지 판단하는 기준은 덱이 얹힐지 판단하는 기준과 같아야 합니다. 확장 아이콘으로 연 탭에는 역할 표시(xdeck_role)가 주소에 붙지만, 홈 화면에 자동으로 얹힌 덱에는 그런 표시가 없습니다. 이때도 덱은 그 문서를 추천 담당으로 세우므로 인터셉터 역시 깨어나야 합니다 — 두 판단이 어긋나면 그 문서는 응답을 한 건도 가로채지 못한 채 컬럼이 준비 중 에 멈춥니다. 자동 적용 설정은 확장 저장소에 있어 MAIN world 에서 읽을 수 없으므로, 설정을 저장할 때 페이지에 함께 남겨두는 사본을 봅니다.

'새 게시물 보기' 알림이 떠 있으면 눌러 다음 타임라인을 끌어옵니다. 그것으로도 컬럼이 조용하면 강제 갱신 사다리를 차례로 오릅니다 — 내비 링크 재클릭 → 탭 재클릭 → . 단축키 → 탭 튕기기 → 프레임 재적재. 내비 링크는 담당 화면의 것을 씁니다(추천 · 팔로잉은 홈 링크, 멘션 · 알림은 알림 링크). 팔로잉만 순서가 다릅니다 — 홈 링크로 받아오는 것은 홈의 기본 탭인 추천이라 그 칸을 맨 뒤로 미룹니다.

한 번에 한 칸만 밟습니다. 이미 열려 있는 탭을 다시 눌러서는 x.com 이 아무 요청도 내지 않기 때문에, 옆 탭에 들렀다 돌아오는 '탭 튕기기' 는 확실히 듣는 수단입니다. 다만 들른 타임라인을 통째로 받아 그리고 돌아오며 또 그리므로 한 번에 전체 렌더가 두세 번입니다. 그래서 맨 뒤에 두고, 앞의 싼 수단이 모두 실패했을 때만 씁니다.

새로고침 단추도 같은 사다리를 탑니다. 알림이 떠 있으면 그것만 누르고, 없으면 첫 칸부터 시작해 새 목록이 오지 않을 때만 다음 칸으로 넘어갑니다. 사람이 기다리고 있으므로 다음 칸까지의 간격은 2.5초로 짧습니다(자동 갱신은 20초). 결과 안내는 새 글이 들어오면 그 자리에서 내고, 끝내 없으면 사다리를 다 밟고 난 뒤에 냅니다 — 첫 칸이 헛돌아 같은 목록이 돌아온 것만으로 새 글 없음 을 띄우면 몇 초 뒤 뒷칸이 물어온 새 글과 어긋납니다.

판정 기준은 하나뿐입니다 — 그 컬럼에 새 목록이 들어왔는가. 알림을 눌렀다거나 탭을 눌렀다는 '시도' 는 근거로 치지 않습니다. 옆 컬럼의 응답, 목록과 무관한 응답도 마찬가지입니다(멘션 · 알림 프레임은 응답 이름을 가릴 수 없어 그 화면의 응답을 전부 받으므로, 목록처럼 생긴 것만 골라 셉니다).

응답이 온 것과 새 목록이 온 것은 다릅니다. 이미 열려 있는 화면을 다시 두드리면 x.com 은 대개 방금 준 것과 똑같은 목록을 한 번 더 줍니다. 그래서 응답에 실린 항목 목록을 지문으로 떠 두고 직전 것과 맞춰 봅니다(값이 매번 달라지는 커서와 광고 항목은 지문에서 뺍니다). 새 목록이면 사다리는 맨 아래로 내려오고, 같은 목록이면 다음 칸으로 오릅니다. 컬럼 머리글의 수신 중 배지는 목록이 그대로여도 응답만 오면 켜집니다 — 그쪽은 문서가 살아 있는지를 보는 표시이기 때문입니다.

컬럼이 조용한지도 새 목록으로 잽니다. 폴링을 계속 돌게 해 두었으므로 두드리지 않아도 응답은 꾸준히 들어오는데, 추천은 알고리즘 타임라인이라 그 응답이 늘 같은 목록입니다. 응답이 왔다는 것만으로 유휴 시계를 되감으면 그 컬럼은 영영 '조용하지 않은' 것이 되어 사다리가 시작조차 하지 못합니다. 팔로잉은 시간순이라 폴링 응답에 새 글이 실려 오므로 같은 자리에서도 멀쩡히 돌아갑니다.

마지막 칸의 재적재는 숨은 프레임에만, 그리고 사다리를 오르는 내내 응답이 한 건도 없었을 때만 적용합니다. 응답은 오는데 목록만 그대로인 것은 문서가 죽은 것이 아니라 x.com 에 내놓을 새 글이 없는 것이므로, 다시 띄우지 않고 사다리를 맨 아래로 되돌려 유휴 간격만큼 쉰 뒤 한 번 더 오릅니다. 최상위 탭에는 덱이 얹혀 있어 컬럼 하나 때문에 다시 띄우는 일 자체가 없습니다. 대신 그 컬럼을 숨은 프레임에 넘깁니다 — 이 문서의 x.com 이 세션째로 막히면(브라우저 로그가 viewer_context 500 으로 뒤덮이는 상태) 어떤 수단으로도 타임라인이 돌아오지 않는데, 새로 뜬 프레임은 그 바깥에서 처음부터 시작하므로 사람이 탭을 새로고침하는 것과 같은 효과를 냅니다. 넘긴 뒤 최상위 문서는 그 컬럼에서 손을 뗍니다 — 두 문서가 한 컬럼을 함께 채우면 서로의 탭 선택을 밀어냅니다. 숨은 프레임을 띄우지 못하는 환경(교대 수집)에서는 넘길 곳이 없으므로 그대로 둡니다.

담당 화면을 벗어나 있으면 그리로 돌아갑니다. 홈 컬럼은 홈에서, 알림 컬럼은 알림 화면에서만 나오므로, 로그인을 마치고 엉뚱한 자리에 떨어지면 탭도 알림도 찾을 수 없습니다. 사용자가 직접 x.com 을 쓰는 동안(통과 모드)에는 이 되돌림도 멈춥니다.

프레임이 첫 타임라인을 아직 못 내놓았거나 오래 조용하면, 최상위 문서가 그 탭에 잠깐 들러 대신 훑고 옵니다(대타 방문). 프레임이 살아 있는 컬럼은 유휴 갱신 간격의 두 배가 넘게 조용할 때만 건드립니다 — 그보다 짧게 잡으면 멀쩡히 도는 프레임을 주기마다 방해하게 됩니다. 대타로 받은 옆 컬럼의 응답은 최상위 문서가 맡은 컬럼의 갱신으로 세지 않고, 다녀왔다는 이유로 그 컬럼의 유휴 시계를 되감지도 않습니다.

숨은 프레임을 끝내 띄우지 못하는 환경에서는 교대 수집 으로 물러섭니다. 최상위 문서 하나가 여러 탭을 번갈아 방문하며 수집하는 방식으로, 갱신은 느려지지만 멈추지는 않습니다. 이 상태에서는 컬럼 머리글에 교대 수집 배지가 표시됩니다.

번역

번역은 Papago 화면을 그대로 빌려서 합니다. 유료 API 를 쓰지 않으므로 키도 비용도 없습니다.

도착 언어는 한국어로 못박혀 있습니다. 브라우저 언어를 따라가지 않습니다 — 브라우저 UI 가 한국어가 아닌 환경에서 영어 번역문이 돌아오기 때문입니다. 번역을 권할지 가르는 기준도 같습니다. x.com 이 붙인 언어 코드는 한국어 글에도 자주 틀린 값이 오므로, 글자에 섞인 한글 비율도 함께 보고 이미 한국어인 글에는 단추를 달지 않습니다. 출발과 도착이 같아지면 Papago 가 도착 언어를 제멋대로 영어로 바꿔버립니다. 프레임 안에서도 주소에 남은 도착 언어가 부탁과 어긋나면 한 번 다시 띄워 바로잡습니다.

보이지 않는 프레임에 papago.naver.com 을 띄우되, 번역할 글월은 주소의 st 파라미터에 실어 보냅니다 — Papago 가 공유 링크에 쓰는 방식이라 우리가 입력란을 건드릴 일이 없습니다. 주소에 담기엔 긴 글만 프레임 안에서 직접 넣으며, 이때도 값을 써넣지 않고 브라우저의 편집 파이프라인(insertText)을 태웁니다. 요즘 편집기는 제 모델을 따로 들고 있어서 DOM 만 고치면 다시 그릴 때 지워버리기 때문입니다.

x.com 과 Papago 는 출처가 달라 서로의 DOM 을 읽을 수 없습니다. 그래서 Papago 문서 안에서 도는 papago.ts 가 번역문을 읽어 메시지로 넘깁니다. 이 스크립트는 덱이 띄운 프레임에서만 돌며, 사람이 직접 연 Papago 탭은 건드리지 않습니다.

Papago 가 막히면 브라우저에 내장된 번역기로 물러섭니다. 기기 안에서 도는 번역이라 네트워크도 권한도 필요 없지만, 최신 크롬에만 있고 게시물의 언어를 알 수 있을 때만 쓸 수 있습니다. 번역문 아래에 어느 쪽이 옮겼는지(Papago 번역 · 브라우저 번역) 적습니다.

사진 속 글자 번역

글 번역과 달리 이쪽은 브라우저 밖의 도움을 받습니다. 확장은 파일도 프로세스도 만질 수 없는데, 쓰려는 것은 이 PC 에 깔린 codex · claude 명령이기 때문입니다. 그 사이를 bridge/host.mjs 가 잇습니다 — 의존성 없는 작은 Node 프로그램입니다.

라이트박스 '사진 번역'
└─ 배경 워커            ← 덱은 바깥 프로그램을 부를 수 없습니다
   └─ connectNative      ← 브라우저가 브리지를 켜고 표준 입출력으로 이어줍니다
      ├─ 이미지를 내려받아 임시 파일로 둡니다
      ├─ codex exec -i <파일> …  또는  claude -p …
      └─ 결과를 덱으로 돌려주고 임시 파일을 지웁니다

포트도 주소도 없습니다. 등록해 둔 프로그램을 브라우저가 직접 켜서 표준 입출력으로 잇고, 연결을 놓으면 함께 내립니다. 등록된 확장(allowed_origins)이 아니면 애초에 켜지지도 않으므로 통로가 하나뿐입니다.

확장 ID 는 manifest.json 의 key 가 정합니다. 폴더째 얹는 방식으로 나눠주면 크롬은 폴더 경로에서 ID 를 만들어내는데, 그러면 사용자마다 달라져 등록 스크립트가 대상을 미리 적어둘 수 없습니다. 공개키를 넣어두면 ID 가 경로와 무관해져 모두가 같은 값을 갖습니다.

말은 4바이트 길이 뒤에 UTF-8 JSON 한 덩이로 오갑니다. 브라우저가 한 덩이를 1MB 로 끊으므로 다시 그린 그림은 잘라서 여러 덩이로 보내고, 받는 쪽이 순번을 세어 이어 붙입니다.

두 명령은 각자의 구독 계정 으로 이미 로그인돼 있고, 브리지는 그것을 그대로 빌립니다. 로그인 여부는 자격증명 파일을 뜯어보지 않고 짧은 명령을 한 번 시켜보고 판정합니다. 파일의 모양은 판이 바뀌면 함께 바뀌지만 이 판정은 그대로 맞고, 비밀값을 우리가 만질 이유도 없습니다.

로그인 자체는 브라우저를 열어 사람이 마치는 절차라 확장 안에서 시작할 수 없습니다. 브리지가 콘솔 창을 하나 띄워주고, 설정 화면은 끝났는지를 다시 확인하는 것까지만 합니다.

번역은 한 번에 하나씩 돕니다. Codex 가 만든 그림은 $CODEX_HOME/generated_images/ 아래에 떨어지는데, 어느 파일인지는 무엇이 새로 생겼는지 로 찾아야 합니다 — 명령이 최종 메시지로 알려주는 경로에는 치환되지 않은 자리표시자가 그대로 오는 경우가 있어 믿을 수 없습니다. 동시에 돌리면 서로의 결과를 집어가므로 줄을 세웁니다.

결과는 원본 주소를 열쇠로 IndexedDB 에 쟁여둡니다. 한 번에 30초 넘게 걸리고 구독 한도도 함께 닳으므로, 같은 사진을 두 번 청하지 않는 것이 여기서는 성능이 아니라 비용 문제입니다.

하트 · 리포스트를 처리하는 방식

x.com 의 내부 뮤테이션을 직접 호출하려면 요청 서명까지 위조해야 하고, 이는 깨지기 쉬운 동시에 계정 위험을 집니다. 그래서 게시물 상세 페이지를 보이지 않는 프레임에 띄운 뒤 x.com 자신의 버튼을 누르는 방식을 씁니다. 요청 생성과 서명, 낙관적 갱신을 전부 x.com 코드가 수행하며 확장은 버튼을 누르기만 합니다. 답글 · 인용 · 새 글도 같은 원칙으로, x.com 의 공식 작성 화면을 덱 안에 그대로 띄웁니다.


기술 스택

분류 사용 기술
확장 Chrome Manifest V3 (content script · service worker · declarativeNetRequest)
언어 TypeScript 5.9 (strict)
UI React 19, Tailwind CSS 4
저장소 IndexedDB (idb), chrome.storage.sync
번들러 Vite 7 (덱 UI · IIFE + CSS 인라인), esbuild (인터셉터 · 브리지 · 백그라운드)
테스트 Vitest 4, happy-dom
격리 Shadow DOM + Constructable Stylesheets
사진 번역 브리지 의존성 없는 Node 서버 (선택 기능)

외부 UI 라이브러리나 상태 관리 라이브러리는 쓰지 않습니다. 아이콘도 src/ui/components/icons.tsx 에 인라인 SVG 로 두었습니다.


프로젝트 구조

src/
├─ background/    서비스 워커 (덱 탭 열기 · 규칙 상태 조회)
├─ content/       x.com 문서에서 도는 코드 (수집기 · 선택자 · 동작)
├─ injected/      MAIN world 인터셉터
├─ core/          양쪽이 함께 쓰는 순수 로직 (타입 · 파서 · DB · 설정)
└─ ui/            덱 UI (React)

bridge/          사진 번역용 네이티브 메시징 호스트 (선택 기능. 확장 번들에 들어가지 않습니다)
scripts/         빌드 오케스트레이터와 그 곁가지 (출력 자리 결정 · 아이콘 · 확장 키 생성)
build.bat        두 번 눌러 굽는 길 (윈도우). 감시 모드는 dev.bat

tests/
├─ core/          파서 · 설정 · 세션 힌트 테스트
├─ content/       선택자 판단 규칙 · 강제 갱신 사다리 · 번역 도착 언어 테스트 (손으로 만든 DOM)
├─ ui/            표시 형식 · 안 본 수 세기 · 계정 읽기 · 카드 클릭 판정 테스트
├─ bridge/        사진 번역 브리지의 실패 문구 · codex 설정 자동 수리 테스트
├─ scripts/       빌드 출력 자리 결정 테스트
├─ fixtures/      실제 x.com 화면을 떠 온 파일 (커밋하지 않습니다)
└─ setup/         happy-dom 보정
경로 역할
src/content/mount.tsx 덱 진입점. 그림자 DOM 을 만들고 최상위 문서의 수집기를 띄웁니다
src/content/underlay.ts 아무도 보지 않는 x.com 을 그리지 않게 합니다 (최상위 문서 · 수집 프레임)
src/content/collector.ts 수집 본체. 탭 유지, 알림 감지 · 클릭, 강제 갱신 사다리, 교대 수집
src/content/selectors.ts x.com DOM 선택자 전부. UI 개편 시 이 파일만 고칩니다
src/content/actions.ts 하트 · 리포스트 수행, 작성 화면 주소
src/content/translate.ts 게시물 번역. Papago 화면을 빌려 쓰고, 막히면 브라우저 내장 번역기로 물러섭니다
src/content/papago.ts 번역 프레임 안에서 도는 스크립트. 글월을 받아 넣고 결과만 돌려줍니다
src/content/imageTranslate.ts 사진 번역 창구. 배경 워커를 거쳐 브리지에 말을 걸고, 쓸 수 있는 명령을 고릅니다
bridge/host.mjs codex · claude 를 대신 부르는 네이티브 메시징 호스트. 로그인 판정도 여기서 합니다
bridge/messages.mjs 그 호스트가 실패를 사람 말로 옮기는 자리. 명령을 띄우지 않고 잴 수 있게 떼어 두었습니다
bridge/codex-config.mjs codex 가 자기 config.toml 때문에 못 뜰 때 그 줄을 꺼줍니다 (원본은 .bak 로 보관)
bridge/install.mjs 그 호스트를 브라우저에 등록·해제하고, codex · claude 를 최신 판으로 받습니다 (최초 1회)
src/content/frameQueue.ts 숨은 프레임 작업을 하나씩 줄 세웁니다
src/content/frameBlock.ts 프레임이 막힌 원인(CSP vs X-Frame-Options)을 가려내는 관측점
src/injected/interceptor.ts 타임라인 응답만 복제해 넘기고, 문서를 '보임' 상태로 유지하며, 아무도 보지 않는 영상을 틀지 않습니다
src/core/parser.ts GraphQL 응답을 Tweet · DeckNotification 으로 정규화합니다
src/core/db.ts IndexedDB 영속 저장과 보관 정책
src/core/settings.ts 설정 스키마 · 저장 · 마이그레이션
src/core/types.ts 컬럼 종류와 데이터 모델의 단일 정의
src/core/role.ts 이 문서가 덱인지 · 어느 컬럼 담당인지의 판정. 덱과 인터셉터가 함께 씁니다
src/core/playback.ts 어느 영상을 틀지 않을지의 판정. 인터셉터가 씁니다
src/ui/ 덱 UI. x.com DOM 을 아는 코드가 한 줄도 없습니다
scripts/out-dir.mjs 빌드 결과를 놓을 자리를 고릅니다. 남의 폴더를 지우지 않게 막고, 동기화 폴더면 알려줍니다

@core · @ui 경로 별칭을 사용하며, Vite 와 esbuild 양쪽에 같은 별칭을 등록해 두었습니다.


권한 안내

권한 필요한 이유
storage · unlimitedStorage 설정 저장과 게시물 보관(IndexedDB)에 사용합니다
tabs 확장 아이콘을 눌렀을 때 덱 탭을 열고, 이미 열린 탭을 다시 찾아옵니다
declarativeNetRequest x.com 응답의 X-Frame-Options 헤더를 제거합니다. frame-ancestors 'self' 는 동일 출처 임베드를 허용하지만 X-Frame-Options: DENY 는 동일 출처까지 막기 때문에, 이 헤더를 걷어내야 수집 프레임을 띄울 수 있습니다. 광고·분석 도메인 차단에도 같은 권한을 씁니다
declarativeNetRequestFeedback 프레임이 막혔을 때 규칙이 실제로 적용되었는지 진단합니다
host_permissions: https://x.com/* 덱과 수집기가 도는 도메인입니다
host_permissions: https://papago.naver.com/* 게시물 번역에 Papago 화면을 빌려 씁니다. 유료 API 대신 사람이 쓰는 번역 화면을 보이지 않는 프레임에 띄우고, 그 안에서 도는 스크립트가 결과만 덱으로 넘깁니다
nativeMessaging 사진 번역 브리지를 부르는 데 씁니다. 등록해 둔 그 프로그램 하나만 켤 수 있으며, 사진 번역을 켜지 않으면 한 번도 쓰이지 않습니다

규칙은 rules.json 에 전부 적혀 있습니다. 헤더를 걷어내는 x.com 규칙, 번역 프레임을 위한 papago.naver.com 규칙, 그리고 광고·분석 도메인 차단 규칙 세 갈래입니다. 뒤의 둘은 initiatorDomains 로 x.com 문서가 시작한 요청에만 걸립니다.

광고 · 분석 도메인 차단

숨은 수집 프레임은 컬럼 수만큼 x.com 웹앱을 통째로 띄웁니다. 그 사본마다 광고·분석 스크립트가 따라 붙으면 그만큼 요청과 메모리가 배로 늘어나므로, x.com 문서가 시작한 요청 중 아래 도메인으로 가는 것을 막습니다.

googlesyndication.com · doubleclick.net · googletagservices.com · googleadservices.com · adservice.google.com · ads.google.com · google-analytics.com · analytics.google.com · static.ads-twitter.com · analytics.twitter.com · intercom.io · intercomcdn.com

로그인 · 봇 탐지 · 결제 · 영상 재생에 쓰이는 외부 도메인(arkoselabs · castle · reCAPTCHA · Google/Apple 로그인 · Stripe · Plaid · gstatic 캐스트)은 막지 않습니다. 막으면 로그인이나 재생이 깨집니다. 주소창에 직접 입력하거나 링크로 이동하는 경우(main_frame)도 차단 대상에서 뺐습니다.

Note

declarativeNetRequest 조건에는 프레임 깊이를 가리는 항목이 없습니다. 그래서 이 규칙은 숨은 수집 프레임뿐 아니라 같은 탭의 x.com 화면 전체 에 걸립니다. 통과 모드로 x.com 원본을 볼 때도 위 도메인은 막힌 채입니다.

Important

이 규칙에는 범위를 좁히지 못한 부작용 이 있습니다. 조건이 "x.com 으로 가는 요청" 이라, 덱이 띄운 프레임뿐 아니라 다른 사이트가 x.com 을 프레임에 싣는 경우에도 헤더가 제거됩니다. 즉 이 확장을 설치한 브라우저에서는 임의의 사이트가 로그인된 x.com 을 iframe 으로 실을 수 있어 클릭재킹에 노출됩니다.

initiatorDomains 로 좁히는 방법과 덱 탭에만 거는 세션 규칙(tabIds) 을 모두 시험했지만, 두 경우 모두 수집 프레임 자체가 뜨지 못했습니다. 범위를 좁히면서 기능을 유지할 방법을 아직 찾지 못해 현재는 넓은 규칙을 그대로 두고 있습니다.


개인정보와 보안

  • 수집한 데이터는 사용자 브라우저 안에만 저장됩니다. 외부로 전송하는 코드가 없습니다
  • 로그인은 x.com 이 직접 처리하며, 확장은 아이디 · 비밀번호 · 토큰을 읽거나 저장하지 않습니다
  • 분석 도구나 원격 스크립트를 포함하지 않습니다
  • 설정의 보관 데이터 비우기 로 저장된 게시물을 언제든 전부 삭제할 수 있습니다

사진 번역을 켠 경우

이 기능은 위의 "외부로 전송하는 코드가 없습니다" 에 대한 유일한 예외입니다. 켜고 사용자가 단추를 눌렀을 때에 한해, 그 사진 한 장 이 codex · claude 를 거쳐 각 회사의 서버로 나갑니다. 게시물 내용이나 계정 정보는 함께 나가지 않습니다.

브리지는 이렇게 좁혀 둡니다.

  • 네트워크에 열린 자리가 없습니다. 브리지는 표준 입출력으로만 말하며, 브라우저가 켜준 그 통로 하나뿐입니다
  • 등록해 둔 확장(allowed_origins)이 아니면 브리지가 켜지지도 않습니다
  • 등록은 HKEY_CURRENT_USER 와 브리지 폴더만 건드립니다. 관리자 권한이 필요 없고, 해제하면 흔적이 남지 않습니다
  • 확장은 codex · claude 의 자격증명을 읽지 않습니다. 로그인 여부는 명령을 한 번 시켜보고 판정합니다

한때 로컬 서버와 공유 열쇠를 썼지만 걷어냈습니다 — 열쇠 파일은 브리지 옆에 평문으로 놓이므로 이 PC 에서 도는 악성 프로그램에게는 무의미했고, 막아야 할 웹페이지는 어차피 브라우저가 막고 있었습니다. 사용자에게 붙여넣기라는 단계만 하나 더 얹고 있었습니다.

브리지를 등록하지 않으면 이 경로는 존재하지 않습니다.


알려진 제약

  • x.com 의 화면 구조나 응답 형식이 바뀌면 수집이 멈출 수 있습니다. 컬럼 머리글의 상태 배지(준비 중 · 수신 중 · 폴백 파싱 · 교대 수집)가 진단 지점이며, 배지에 마우스를 올리면 수집기가 마지막으로 시도한 일과 그 컬럼이 마지막으로 글을 받은 시각이 함께 표시됩니다
  • 띄우는 컬럼이 많을수록 x.com 화면을 그만큼 더 열어두게 되므로 메모리 사용량과 갱신 지연이 늘어납니다. 종으로 지켜보는 타임라인도 같은 값을 치릅니다 — 화면 자리만 아낄 뿐 수집은 컬럼과 똑같이 합니다
  • 크로미움 계열 브라우저를 대상으로 합니다. Firefox 는 지원하지 않습니다
  • 아직 정식 릴리스 전이며, 스토어에 등록되어 있지 않습니다
  • 사진 번역은 브리지를 한 번 등록해야 쓸 수 있고, Node.js 와 codex/claude CLI 가 미리 깔려 있어야 합니다. Codex 로 사진을 다시 그리는 데는 한 장에 30초 ~ 1분 남짓 걸리므로, 눌러두고 나중에 확인하는 쪽에 가깝습니다
  • 사진 번역은 각 구독 요금제의 사용량을 씁니다. 같은 계정으로 다른 작업을 하고 있다면 한도를 나눠 쓰게 됩니다

개발 규칙

  • 버전의 단일 출처는 package.json 의 version 입니다. 빌드 시 dist/manifest.json 으로 주입되므로 저장소의 manifest.json 값은 직접 고치지 않습니다
  • 사용자가 체감하는 변경은 CHANGELOG.md 에 기록합니다
  • 소스를 고쳤으면 커밋 전에 npm run check 로 타입 검사와 테스트를 통과시킵니다
  • 커밋 메시지는 한글로 작성합니다
  • 저장소에서 작업할 때의 상세 규약은 CLAUDE.md 에 정리되어 있습니다

라이선스

MIT

이 프로젝트는 X Corp. 와 아무런 제휴 관계가 없으며, 공식 제품이 아닙니다.

About

트위터(X) 타임라인을 일정 주기별로 자동갱신하며 여러 탭을 한 화면에 보여줌

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages