확장 프로그램 없이 트윗 대량 삭제하기
지난 X 게시물을 정리하려고 크롬 콘솔에 붙여 넣는 무료 스크립트입니다: 먼저 시뮬레이션, 최근 트윗은 보호, 상한을 둔 묶음 단위 삭제. 확장 프로그램도, 제삼자 서비스도 없고, 어떤 것도 브라우저 밖으로 나가지 않습니다.
DRY_RUN = true)로 제공됩니다. 아무것도 건드리지 않고 삭제될 목록만 보여 줍니다. 미리 X 보관 데이터를 내려받고, 시뮬레이션을 확인한 다음에만 실제 삭제로 넘어가세요. 웹 인터페이스를 자동화하는 것은 X 이용 약관에서 예정한 방식이 아닙니다. 천천히, 본인 책임으로 진행하세요.
진행 방법
- 프로필을 여세요: 계정에 로그인한 채
x.com/your_username의 게시물 탭으로 이동하세요. - 콘솔을 여세요:
F12(맥에서는Cmd+Option+J)를 누른 다음 콘솔 탭을 클릭하세요. - 스크립트를 설정하세요: 아래 코드를 복사한 뒤
your_username을 자신의 X 아이디(@ 제외)로 바꾸고,KEEP_RECENT(남길 최근 트윗 개수)를 조정하세요. 필요하면KEEP_STATUS_IDS에 절대 삭제하지 않을 트윗의 식별자를 넣으세요. - 시뮬레이션하세요: 스크립트를 콘솔에 붙여 넣고 Enter로 실행하세요.
DRY_RUN = true(기본 설정)이면 아무것도 삭제하지 않고 삭제될 트윗 목록만 표시합니다. - 삭제하세요: 시뮬레이션 결과가 괜찮으면
DRY_RUN을false로 바꾸고 다시 실행하세요. 스크립트는 한 번에 최대 40개까지 삭제하며, 6초에서 14초 사이의 대기 시간을 둡니다. - 반복하세요: 남은 트윗을 묶음 단위로 처리하려면 필요한 만큼 스크립트를 다시 실행하세요.
스크립트
(() => {
// ============ 설정 ============
const HANDLE = "your_username"; // 자신의 X 아이디, @ 제외
const KEEP_RECENT = 15; // 가장 최근 트윗 15개(맨 위)는 절대 삭제하지 않음
const KEEP_STATUS_IDS = [ // 추가 보호(선택): 절대 삭제하지 않을 식별자
"1234567890123456789",
];
let DRY_RUN = true; // true = 시뮬레이션(목록만). false = 실제 삭제.
const MIN_DELAY_MS = 6000;
const MAX_DELAY_MS = 14000;
const MAX_DELETIONS = 40; // 실행당 상한(계속하려면 다시 실행)
// ================================
const me = HANDLE.toLowerCase();
const KEEP = new Set(KEEP_STATUS_IDS.map(String));
const protectedRecent = new Set();
const RX_DELETE = /(삭제|delete|supprimer)/i;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const rand = (a, b) => Math.floor(a + Math.random() * (b - a));
const skipped = new Set();
if (!location.pathname.toLowerCase().startsWith("/" + me)) {
console.warn(`%c[중지] https://x.com/${HANDLE} (게시물 탭)로 이동한 뒤 다시 실행하세요.`,
"color:#e0245e;font-weight:bold");
return;
}
const articles = () => [...document.querySelectorAll('article[data-testid="tweet"]')];
const idOf = (art) => {
const href = [...art.querySelectorAll('a[href*="/status/"]')]
.map((x) => x.getAttribute("href"))
.find((h) => new RegExp(`^/${HANDLE}/status/\\d+`, "i").test(h));
if (!href) return null;
const m = href.match(/status\/(\d+)/);
return m ? m[1] : null;
};
// 가장 최근 트윗 KEEP_RECENT개(맨 위)를 불러와 보호함.
async function protectRecent() {
const order = [], seen = new Set();
let noNew = 0;
window.scrollTo(0, 0);
await sleep(1200);
while (order.length < KEEP_RECENT && noNew < 6) {
let added = 0;
for (const art of articles()) {
const id = idOf(art);
if (id && !seen.has(id)) { seen.add(id); order.push(id); added++; }
}
noNew = added ? 0 : noNew + 1;
if (order.length >= KEEP_RECENT) break;
const cs = articles();
(cs[cs.length - 1] || document.body).scrollIntoView({ block: "end" });
await sleep(1400);
}
for (const id of order.slice(0, KEEP_RECENT)) protectedRecent.add(id);
return order.slice(0, KEEP_RECENT);
}
const eligible = (art) => {
const id = idOf(art);
if (!id) return null;
if (protectedRecent.has(id)) return null; // 최근 트윗 중 하나
if (KEEP.has(id)) return null; // 추가로 보호된 ID
if (skipped.has(id)) return null;
return id;
};
async function deleteOne(art, id) {
const caret = art.querySelector('[data-testid="caret"]');
if (!caret) { skipped.add(id); return false; }
caret.click();
await sleep(700);
const del = [...document.querySelectorAll('[role="menuitem"]')]
.find((i) => RX_DELETE.test((i.innerText || "").trim()));
if (!del) { document.body.click(); skipped.add(id); return false; }
del.click();
await sleep(700);
const confirmBtn = document.querySelector('[data-testid="confirmationSheetConfirm"]');
if (!confirmBtn) { skipped.add(id); return false; }
confirmBtn.click();
await sleep(1500);
return true;
}
async function dryScan() {
const seen = new Set();
let noNew = 0;
console.log("%c[시뮬레이션] 아무것도 삭제되지 않습니다. 집계 중...", "color:#1d9bf0;font-weight:bold");
while (noNew < 4) {
let added = 0;
for (const art of articles()) {
const id = eligible(art);
if (id && !seen.has(id)) { seen.add(id); added++; }
}
noNew = added ? 0 : noNew + 1;
const cs = articles();
(cs[cs.length - 1] || document.body).scrollIntoView({ block: "end" });
await sleep(1200);
}
console.log(`%c[시뮬레이션] 트윗 ${seen.size}개가 삭제될 예정입니다(최근 트윗은 보호됨).`,
"color:#1d9bf0;font-weight:bold");
console.log([...seen].slice(0, 60));
}
async function run() {
let count = 0, guard = 0, noHit = 0, started = false;
while (count < MAX_DELETIONS && guard++ < 1500) {
const pair = articles().map((a) => [a, eligible(a)]).find(([, id]) => id);
if (!pair) {
if ((started && noHit >= 6) || noHit >= 40) break;
const cs = articles();
(cs[cs.length - 1] || document.body).scrollIntoView({ block: "end" });
await sleep(1500);
noHit++;
continue;
}
started = true; noHit = 0;
const [node, id] = pair;
node.scrollIntoView({ block: "center" });
await sleep(600);
const ok = await deleteOne(node, id);
if (ok) count++;
console.log(`${ok ? "삭제됨" : "건너뜀"} ${id} (합계: ${count}/${MAX_DELETIONS})`);
await sleep(rand(MIN_DELAY_MS, MAX_DELAY_MS));
}
console.log(`%c[완료] 이번 회차에 ${count}개 삭제됨. 남은 것이 있으면 스크립트를 다시 실행하세요.`,
"color:#00ba7c;font-weight:bold");
}
(async () => {
console.log(`가장 최근 트윗 ${KEEP_RECENT}개를 불러와 보호하는 중...`);
const recent = await protectRecent();
if (recent.length < KEEP_RECENT) {
console.warn(`%c[중지] 트윗을 ${recent.length}개만 불러왔습니다(X가 속도를 제한함). 페이지를 새로 고침(F5)한 뒤 다시 시도하세요.`,
"color:#e0245e;font-weight:bold");
return;
}
console.log("%c보호됨(가장 최근 트윗):", "color:#00ba7c;font-weight:bold", recent);
if (DRY_RUN) { await dryScan(); return; }
await run();
})();
})();
스크립트가 여러분을 지키는 방법
네 가지 안전장치가 들어 있습니다. 먼저 기본 시뮬레이션입니다. DRY_RUN이 true인 동안에는 아무것도 삭제되지 않고, 대상 트윗 목록만 얻습니다. 다음은 최근 트윗 보호입니다. 스크립트는 가장 먼저 최근 트윗(기본값 15개)을 기억해 두고 절대 건드리지 않습니다. 그다음은 화이트리스트입니다. KEEP_STATUS_IDS에 넣은 식별자는 손대지 않으므로, 고정한 트윗이나 공지에 유용합니다. 마지막은 신중한 속도입니다. 삭제 사이에 6초에서 14초의 무작위 대기를 두고 40개 후 자동으로 멈춰, X의 사용 제한을 유발하지 않도록 합니다.
대청소를 하기 전에 소중한 것은 보관해 두세요: 이메일 및 URL 추출기는 트윗 내보내기에서 모든 링크를 뽑아 주고, 단어 수 카운터는 여러 해에 걸친 게시물의 분량을 알려 주며, 텍스트 정리기는 보관 파일에서 복사한 텍스트를 깔끔하게 다듬어 줍니다. 앞으로의 트윗에는 유니코드 스타일 텍스트와 대소문자 변환기가 함께합니다. 도구 모음의 모든 도구는 이 가이드처럼 브라우저 안에서 작동하며, 저희 개인정보 처리방침의 정신을 따릅니다.
자주 묻는 질문
삭제한 트윗을 되돌릴 수 있나요?
아니요. 삭제한 트윗은 영구적으로 사라지며, X에는 휴지통이 없습니다. 그래서 이 스크립트는 시뮬레이션 모드(DRY_RUN = true)로 제공됩니다. 먼저 그대로 실행해 삭제될 목록을 확인하고, 검토한 뒤 확신이 설 때만 false로 바꾸세요. 그 전에 X 보관 데이터도 내려받아 두는 것이 좋습니다(설정, 내 계정, 내 데이터 보관 파일 다운로드 순).
이 스크립트로 다른 사람의 트윗도 삭제할 수 있나요?
아니요. X는 자신의 게시물에만 삭제 버튼을 표시합니다. 따라서 스크립트는 로그인한 계정에서, 자신의 프로필 페이지에 있는 게시물에만 작동합니다. 다른 사람의 답글이나 제삼자의 콘텐츠는 건드리지 않습니다.
삭제 40개 상한과 무작위 대기 시간은 왜 있나요?
플랫폼에 부담을 주지 않기 위해서입니다. 너무 빠르거나 너무 많은 삭제는 X의 사용 제한을 유발하거나 계정을 일시적으로 차단시킬 수 있습니다. 이 스크립트는 삭제 사이에 6초에서 14초를 기다리고 40개 후에 멈춥니다. 이어서 하려면 다시 실행해 묶음 단위로 진행하세요.
이것이 X에서 허용되나요?
자신의 콘텐츠를 삭제하는 것은 여러분의 권리입니다. 다만 웹 인터페이스를 자동화하는 것은 X 이용 약관에서 예정한 방식이 아닙니다. 이 스크립트는 절제해서, 본인 책임으로 사용하세요. 공식적인 대안은 수동 삭제이거나, API를 이용하는 전용 서비스입니다.
특정 트윗을 어떻게 보호하나요?
두 가지 보호가 함께 적용됩니다. KEEP_RECENT는 가장 최근 트윗(기본값 15개)을 자동으로 지켜 주고, KEEP_STATUS_IDS는 절대 삭제하지 않을 식별자 목록을 받습니다. 트윗의 식별자는 주소 끝에 있는 긴 숫자입니다: x.com/your_username/status/1234567890123456789.