예전에는 X 개발 API를 이용해서 자동으로 포스팅이 가능했습니다.
그러나, 돈에 환장한 일론 머스크는 그런 것을 용납하지 않고 돈을 지불해야만 API를 사용가능하게 했습니다.

그래서, '브라우저 자동제어(Puppeteer)' 방식을 Node.js에 적용하여 X에 포스팅하는 방법에 대해서 설명을 해보겠습니다.
1단계: 설계 (어떻게 돌아가는가?)
단순히 글만 올리는 게 아니라, "헤더 기사가 바뀌었는지"를 감시하는 로직이 필요합니다.
- 감시(Scraping): Node.js가 5분마다 뉴스 사이트 메인 페이지를 방문합니다.
- 비교(Check): 현재 헤더 기사의 URL이 마지막으로 올린 URL과 다른지 확인합니다. (데이터베이스나 파일에 저장된 값과 비교)
- 실행(Puppeteer): URL이 바뀌었다면 가상 브라우저를 열어 X에 로그인하고 포스팅합니다.
- 갱신(Update): 새로 올린 URL을 DB(MySQL)에 저장하여 다음 비교에 사용합니다.
2단계: 환경 준비 (Node.js)
프로젝트 폴더를 만들고 필요한 라이브러리를 설치합니다.
mkdir news-auto-poster
cd news-auto-poster
npm init -y
npm install puppeteer-core puppeteer-extra puppeteer-extra-plugin-stealth axios cheerio mysql2
- cheerio: 뉴스 사이트의 HTML에서 제목과 URL을 빠르게 긁어옵니다.
- stealth: X의 봇 감지 시스템을 피하기 위한 필수 플러그인입니다.
npm install 중에 puppeteer나 puppeteer-core가 멈추는 현상은 개발자들 사이에서 아주 유명한 '악몽' 같은 상황입니다. 원인은 거의 100% 브라우저(Chromium) 바이너리 다운로드 때문입니다.
그냥 puppeteer를 설치하면 약 200~300MB 정도 되는 브라우저 실행 파일을 구글 서버에서 받아오는데, 네트워크 환경에 따라 이게 극도로 느려지거나 타임아웃이 걸립니다.
# 환경 변수로 다운로드를 끄고 설치
SET PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
npm install puppeteer-core
- puppeteer: 라이브러리 + 전용 브라우저(Chromium) 포함 (설치 시 매우 무거움)
- puppeteer-core: 라이브러리만 포함 (이미 크롬이 깔려 있다면 이게 훨씬 가볍고 빠름)
3단계: 핵심 코드 구현 (뼈대)
전체 로직을 하나로 합친 흐름입니다.
1. 뉴스 사이트 헤더 긁어오기 (Scraper)
const axios = require('axios');
const cheerio = require('cheerio');
async function getLatestNews() {
const { data } = await axios.get('https://your-news-site.com');
const $ = cheerio.load(data);
// 메인 헤더의 CSS 선택자를 찾아야 합니다 (예: .main-header a)
const title = $('.main-header-title').text();
const url = $('.main-header-title a').attr('href');
return { title, url };
}
2. X에 자동 포스팅하기 (Puppeteer)
가장 중요한 부분입니다. API 없이 브라우저를 직접 조작합니다.
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
async function postToX(text) {
const browser = await puppeteer.launch({
executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
userDataDir: 'E:\\Dev\\news-bot\\auto_session',
//headless: false,
headless: "new",
ignoreDefaultArgs: ['--enable-automation'], // ★ 중요: "자동화" 관련 기본 설정을 아예 무시함
args: [
'--start-maximized',
'--disable-blink-features=AutomationControlled', // 브라우저 엔진의 봇 신호 차단
'--no-sandbox',
'--disable-setuid-sandbox',
// 아래 옵션들이 "자동화 소프트웨어..." 문구를 지워줍니다.
'--disable-infobars',
'--excludeSwitches=enable-automation',
'--use-gl=desktop'
]
});
const [page] = await browser.pages(); // 새 탭 대신 첫 번째 탭 사용
// 웹페이지 속성 변조 (봇 감지 스크립트 무력화)
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'webdriver', { get: () => false });
});
try {
// 포스팅 페이지로 이동
await page.goto('https://x.com/compose/post', { waitUntil: 'networkidle2' });
// 포스팅 입력창이 뜰 때까지 대기
await page.waitForSelector('.public-DraftEditor-content', { timeout: 10000 });
// 내용 입력
await page.click('.public-DraftEditor-content');
await page.keyboard.type(content);
const randomWait = Math.floor(Math.random() * 4000) + 3000;
await new Promise(r => setTimeout(r, randomWait));
// '게시하기' 버튼 클릭 (data-testid 사용이 가장 정확합니다)
await page.click('[data-testid="tweetButton"]');
console.log("✅ 포스팅 완료!");
await new Promise(r => setTimeout(r, 5000)); // 완료 확인을 위해 5초 대기
} catch (error) {
console.error("포스팅 중 에러:", error);
} finally {
await browser.close();
}
}
4단계: 무한 루프 시스템 (Scheduler)
이 프로그램을 서버(AWS 등)에서 계속 돌아가게 만듭니다.
async function runOnce() {
console.log("--- 단발성 테스트 시작 ---");
const news = await getLatestNews();
if (!news || !news.url) {
console.log("❌ 뉴스를 가져오지 못했습니다.");
return;
}
// 2. 이전에 포스팅한 URL 불러오기
let lastPostedUrl = "";
if (fs.existsSync(LAST_URL_FILE)) {
lastPostedUrl = fs.readFileSync(LAST_URL_FILE, 'utf8').trim();
}
// 3. 중복 체크 (DB의 PK 비교와 같습니다)
if (news.url === lastPostedUrl) {
console.log("ℹ️ 이미 포스팅된 기사입니다. (중복 방지)");
return;
}
console.log(`추출된 기사: ${news.content}`);
const tweetContent = `${news.content}\n${news.url}`;
const success = await postToX(tweetContent);
// 5. 포스팅 성공 시에만 마지막 URL 업데이트
if (success !== false) {
fs.writeFileSync(LAST_URL_FILE, news.url, 'utf8');
console.log("✅ 마지막 포스팅 URL이 업데이트되었습니다.");
}
console.log("--- 테스트 종료 ---");
}
// 프로그램 맨 아래에 추가
async function startApp() {
// 1. 실행하자마자 한 번 체크
await runOnce();
// 2. 이후 5분(300,000ms)마다 반복
setInterval(async () => {
const now = new Date().toLocaleString();
console.log(`[${now}] 정기 체크 중...`);
await runOnce();
}, 1000 * 60 * 5);
}
startApp();
로그인 세션 유지: 매번 로그인하면 X에서 계정을 차단합니다. userDataDir 옵션을 사용해 한 번 로그인한 정보를 로컬에 저장해두고 계속 재사용하세요.
이 파일을 js로 저장하고나서 node로 실행해주면 됩니다.
반응형
'꼰대개발자 > 프로그래밍 언어' 카테고리의 다른 글
| AWS WAF 데이터를 DB에 저장하기 (0) | 2026.05.28 |
|---|---|
| Laravel 13 세팅할 때 발생한 오류에 대한 정리 (0) | 2026.04.15 |
| PHP5.2와 JAVA에서 호환이 가능한 AES256 암호화(2) (1) | 2025.09.01 |
| PHP5.2와 JAVA에서 호환이 가능한 AES256 암호화 (0) | 2025.09.01 |
| 스프링 실무에서 MyBatis와 JPA를 비교해 본다. (feat. 영속성 컨텍스트) (4) | 2025.08.14 |