시작은 포기에서 시작됐다
글 쓰는 건 너무 편한데 티스토리로 옮길 때마다 미치는 줄 알았음
서식 다 깨지고, 이미지는 하나하나 다시 올려야 하고... 진짜 이거 할 바엔 차라리 티스토리에서 직접 쓰는 게 나을 것 같았음
그래서 찾아본 게 티스토리 API였는데...
티스토리는 글쓰기 API를 제공하지 않음 ㅋㅋㅋ
OAuth랑 글 목록 조회는 되는데 정작 글 작성/수정은 막혀있더라. 완전 포기 모드였음
근데 어느 날 브라우저 자동화라는 걸 알게 됐고, "이거면 되는 거 아니야?" 싶어서 Playwright 삽질을 시작함
결과적으로는 성공했고, 지금은 노션에서 상태만 바꾸면 자동으로 티스토리에 글이 올라감
이 과정에서 겪은 온갖 삽질들을 정리해봄
원하는 게 뭐였냐면
Pain Point:
- 노션의 작성 경험은 포기 못함 (진짜 편함)
- 근데 티스토리로 옮길 때마다 서식 깨지고 이미지 재업로드 지옥
- 복붙 반복 작업으로 생산성 ㅈ됨
Goal:
- One Source Multi Use: 노션에 글만 쓰고 상태값 변경하면 자동으로 티스토리 '공개 발행'
- API 없으면 뚫어버리자: Playwright로 브라우저 자동화
Target:
- 나같이 생산성 중시하는 개발자들
- RPA나 브라우저 자동화 관심 있는 사람들
보스 0: 로그인부터 막혔다 (Authentication Hell)
본격적인 자동화 전에 제일 먼저 막힌 게 로그인이었음
"그냥 ID/PW 입력하면 되는 거 아냐?" 했는데... 카카오 로그인은 생각보다 훨씬 빡셌음
처음에 로그인시도하면 계속 타임아웃이 나길래 스크린샷을 떠보게 했더니 이렇게..창이 띄워진채로 기다리다가 그냥 끝난 것…
ID/PW 입력 방식이 실패한 이유
처음엔 당연히 page.fill()로 아이디랑 비번 넣게 했음. 로컬에서는 완벽하게 돌아감
근데 GitHub Actions에 올리자마자 지옥 시작됨
로컬 vs GitHub Actions 차이
GitHub Actions의 IP는 매번 바뀜 → 카카오 입장에선 완전 해킹 시도로 보임 → 2단계 인증(전화/이메일) 요구하거나 이미지 캡차 띄움 → 헤드리스 브라우저로는 절대 통과 불가
해결: 세션 주입(Session Injection)
정문(로그인 화면) 못 뚫으면 이미 통과한 사람의 출입증 빌려 쓰면 됨
핵심 원리:
- 내 로컬 PC(신뢰받는 기기)에서 정상 로그인
- 브라우저 인증 토큰(Cookie, LocalStorage) 덤프 떠서 state.json 저장
- 이 파일을 GitHub Actions에 전송
- 서버가 마치 내 PC인 것처럼 위장
간단해 보이지? 근데 이것도 삽질 3종 세트가 있었음
삽질 1: 하루만 살고 죽는 세션
현상:state.json 만들고 성공했는데 다음 날 아침이면 "로그인 풀렸습니다" 에러 뜸
원인: 로그인할 때 [로그인 상태 유지] 체크박스를 안 눌렀음 → 브라우저 닫으면 세션 파기되는 휘발성 쿠키만 생성됨
해결:
# get_session.py 실행 시 반드시 체크박스 활성화
# 최소 30일간 유효한 영구 토큰 발급받기
삽질 2: "넌 아까 그 사람 아니잖아?" (User-Agent Mismatch)
현상: 쿠키 완벽하게 복사했는데도 서버에서 강제 로그아웃 시킴
원인: 티스토리가 세션 발급받은 브라우저랑 현재 접속한 브라우저 지문(Fingerprint) 대조함 로컬 PC(Chrome)랑 GitHub Actions(Headless Chromium)의 User-Agent가 달라서 위조로 걸림
해결:
# 로컬과 서버 모두 동일한 User-Agent 사용 필수
context = browser.new_context(
user_agent="Mozilla/5.0 (Windows NT 10.0; ...) Chrome/122.0.0.0 Safari/537.36"
)
로컬이랑 서버가 완전 똑같은 브라우저인 척 연기하게 만듦
삽질 3: 권한 부족 (400 Bad Request)
현상: 로그인은 됐는데 글쓰기 페이지 가면 "잘못된 요청입니다" 에러
원인:tistory.com 통합 로그인 쿠키만 챙기고 실제 내 블로그(myblog.tistory.com)의 관리자 권한 쿠키는 안 만들어진 상태로 저장함 도메인 스코프 문제였음
해결: 세션 저장하기 직전에 반드시 관리자 페이지까지 진입해야 함
# 서브 도메인 권한 쿠키까지 확실하게 구워진 상태로 저장
page.goto(f"https://{blog_name}.tistory.com/manage")
context.storage_state(path="state.json")
결론: 왜 세션 주입을 선택했나
자동화의 핵심은 **"사람처럼 행동하는 것"**이 아니라 "시스템이 사람이라고 착각하게 만드는 것"
세션 주입 덕분에 GitHub Actions라는 낯선 환경에서도 당당하게 주인 행세하며 글 발행 가능해짐
어떻게 동작하는지
CI/CD 파이프라인이랑 비슷한 흐름으로 만들었음
[Notion 작성]
↓ (Status='Ready')
[GitHub Actions 트리거]
↓
[Python Script 실행]
├─ 1. Notion API로 콘텐츠 파싱 & HTML 변환
├─ 2. WordPress API로 이미지 업로드 & CDN 주소 확보
└─ 3. Playwright로 티스토리 자동 로그인 & 글쓰기
↓
[Tistory 발행 완료]
↓
[Notion Status='Published'로 업데이트]
핵심 프로세스 3단계
1. Extract (Notion)
- 노션 API로 페이지 블록 데이터 가져옴
- HTML로 변환
2. Transform (Media)
- 노션 임시 이미지 URL은 시간 지나면 엑박됨
- 워드프레스 미디어 서버로 이관해서 영구 URL 확보
3. Load (Tistory)
- Playwright로 티스토리 관리자 페이지 접속
- HTML 주입하고 발행 버튼 클릭
삽질의 연속이었던 4대 보스전
보스 1: 에디터 주소 리다이렉트 문제 (400 Bad Request)
현상: 구형 에디터 주소(/manage/post/write)로 접근하면 리다이렉트 과정에서 세션 꼬여서 400 에러 뜸
해결: 그냥 내가 로그인 해서 새 에디터 직접 주소 찾아냄
AI가 자꾸 구주소를 알려줘서 문제였다… 역시 사람의 손맛이 제맛 ㅎ
리다이렉트 없이 바로 진입하니까 해결됨
보스 2: 유령 글 현상 (Ghost Content)
현상:innerHTML로 본문 강제 주입했는데 티스토리가 변경 사항을 못 느낌 제목만 있고 본문 없는 유령 글이 저장됨 ㅋㅋ
해결: 더미 타이핑 기법 사용
# 에디터 깨우기
frame.type("body#tinymce", " ") # 스페이스 입력
frame.press("body#tinymce", "Backspace") # 바로 지우기
이렇게 하니까 에디터가 "오 뭔가 입력됐네?" 하고 인식함
보스 3: 불안정한 클릭과 팝업 (Flaky Clicks)
현상:
- [완료] 버튼 클릭 시 설정 레이어 뜨기 전에 다음 로직 넘어감
- 맞춤법 검사 팝업이 가로막음
해결:
# 팝업 자동 수락
page.on("dialog", lambda dialog: dialog.accept())
# 레이어 뜰 때까지 재시도
max_retries = 3
for i in range(max_retries):
try:
layer = page.wait_for_selector(".layer", timeout=2000)
break
except:
if i == max_retries - 1:
raise
보스 4: 동적 클래스명과 클릭 차단 (The Final Boss)
현상:
- '공개 발행' 버튼이 눈에 보이는데 클릭이 안됨
- CSS Selector 맞는데도 타임아웃 발생
- 투명 레이어가 버튼을 가리고 있는 듯
해결: 무식하게 JS 강제 클릭으로 때려박음
page.evaluate("""
() => {
const buttons = Array.from(document.querySelectorAll('button'));
const target = buttons.find(b => b.innerText.includes('공개 발행'));
if (target) target.click();
else throw new Error("버튼 못 찾음");
}
""")
Selector 의존 버리고 텍스트로 찾아서 직접 .click() 날림
핵심 코드 (제일 삽질 많이 한 부분)
티스토리 발행 함수 최종 완성본:
def publish_to_tistory(title, html_content):
# ... (브라우저 초기화 생략) ...
try:
# 1. 새 에디터 직접 접속 (리다이렉트 회피)
write_url = f"https://{blog_name}.tistory.com/manage/newpost/?type=post&returnURL=%2Fmanage%2Fposts%2F"
page.goto(write_url, wait_until="domcontentloaded")
# 2. 본문 주입 및 더미 타이핑 (에디터 깨우기)
frame = page.wait_for_selector("iframe#editor-tistory_ifr").content_frame()
frame.evaluate(
f'(html) => {{ document.querySelector("body#tinymce").innerHTML = html; }}',
html_content
)
# [핵심] Change 이벤트 강제 발생
frame.type("body#tinymce", " ")
frame.press("body#tinymce", "Backspace")
# 3. JS 필살기: '공개 발행' 버튼 강제 클릭
page.evaluate("""
() => {
const buttons = Array.from(document.querySelectorAll('button'));
const target = buttons.find(b => b.innerText.includes('공개 발행'));
if (target) target.click();
else throw new Error("버튼을 찾을 수 없습니다.");
}
""")
# 4. 발행 완료까지 대기
page.wait_for_url(lambda url: "entry" in url or "manage/posts" in url)
print("발행 성공!")
except Exception as e:
page.screenshot(path="error.png") # 디버깅용
raise e
앞으로 해야 할 것들
1. 썸네일 자동 지정
- 대표이미지를 자동으로 설정해주는 것도 필요함
2. 실패 알림 연동
- GitHub Actions 실패하면 슬랙이나 카카오톡으로 알림
- 지금은 실패해도 모르고 있음 ㅋㅋ
3. 태그 /카테고리 자동으로 넣어주기
- Wordpress같은 경우에는 넣어주고 있는데, 여긴 아직 안했다.
- 세션 정기 유지
- 일단 자동 로그인 유지로 한동안 세션은 유지되겠지만, 30일마다 state.json파일을 갱신해줘야한다. 주기적으로 알림을 주던.. 뭔가 조금 더 생명연장을 정기적으로 해줄 수 있는 과정을……
로그인까지 오기가 너무나 힘든 과정이었기에..
이부분은 차차 넣을 예정.
사실 이미 워드프레스는 API로 자동화 연결을 해놔서 어느정도 삽질을 통해 좀 정상화 시켰기 때문에 그나마 수월했다.
이놈도 이미지 사이즈 조정 등 곤란한 상황이 많았음…
배운 것들
이 프로젝트 하면서 깨달은 것:
- "보이는 게 전부가 아니다": UI 자동화는 생각보다 훨씬 복잡함. 클릭 가능해 보이는데 안 되는 경우 진짜 많음
- "안 되면 되게 하라": API 없으면 JS Injection으로 뚫어버리면 됨. 방법은 항상 있음
- "디버깅은 스크린샷": 에러 나면 스크린샷 찍어두는 게 진짜 도움 많이 됨. AI도 모든걸 알지는 못하기 때문에 리다이렉션 주소를 구 주소로 찾아오는 등 약간의 오류가 있기 때문에 이 부분을 바로 잡아주려면 내가 스크린샷 보고 어디서 막히는지 해보는게 더 빠를 수 있다.
브라우저 레벨에서 제어권 확보하는 과정은 고통스러웠지만 확실히 성장한 느낌
API 없다고 포기할 필요 없음. 브라우저 자동화라는 무기가 있으니까
…
이게 또 언제까지 가능할지 모르겠지만, 인간은 언제나 그렇듯 답을 찾을 것…..*
'Tech' 카테고리의 다른 글
| 디자이너들은 대체 되는가? Claude Design기능 출시 (0) | 2026.04.20 |
|---|---|
| Microsoft AI Tour Seoul 2026 후기 — 밥 공짜 그러나 다소 아쉬웠던.. (1) | 2026.04.01 |
| 워드프레스 자동 포스팅 이미지 리사이징 해결 방법 | Python API 이미지 최적화 (feat. 중앙정렬) (0) | 2026.02.01 |
| 8년차 FullStack개발자, DevSecOps로 전환을 결심하다 [1] (0) | 2026.01.25 |
| GitHub Actions로 소스코드 보안 자동화 하기 [2]- Github Actions 설정하는 방법 (0) | 2025.12.05 |