안녕하세요! 💻 코드 리뷰와 자동화에 진심인 블로거입니다. 오늘은 개발자라면 한 번쯤 꿈꿔봤을 '글쓰기 자동화', 그중에서도 티스토리 자동 포스팅 스크립트를 심층적으로 파헤쳐 보려고 합니다.
매일 혹은 매주 정해진 시간에 콘텐츠를 발행하는 것은 생각보다 끈기가 필요한 일입니다. 저 또한 다른 프로젝트와 병행하며 블로그를 운영하다 보니, 가끔 포스팅 시간을 놓치거나 귀찮음에 미루게 되는 경우가 잦았죠. 😅 그래서 "이 지루하고 반복적인 작업을 자동화할 수는 없을까?"라는 생각에서 이 프로젝트를 시작하게 되었습니다.
이 포스팅에서는 제가 직접 작성하고 여러 시행착오를 거쳐 완성한 파이썬 기반의 티스토리 자동 포스팅 스크립트 전체 코드를 공유하고, 각 부분이 어떤 역할을 하는지, 그리고 어떤 문제들을 해결해야 했는지 상세히 설명해 드리고자 합니다. 특히, 많은 분들이 자동화 과정에서 부딪히는 '저장된 글이 있습니다' 알림 창 문제에 대한 해결책도 포함되어 있으니 끝까지 함께해주세요!
✨ 1. 프로젝트 개요 및 핵심 준비물
이 스크립트의 목표는 명확합니다. 제목, 본문, 태그를 인자로 넘겨주면, 스크립트가 알아서 티스토리에 로그인하고 새 글을 작성하여 '공개'로 발행하는 것입니다. 이 모든 과정은 Selenium이라는 강력한 웹 브라우저 자동화 도구를 사용해 이루어집니다.
[준비물]
- Python 3.x: 스크립트 실행 환경
- Selenium: 웹 브라우저를 코드로 제어하는 라이브러리
- webdriver-manager: Chrome 드라이버를 자동으로 관리해주는 라이브러리
- Google Chrome: Selenium이 제어할 웹 브라우저
핵심 동작 원리는 다음과 같습니다.
- Selenium으로 Chrome 브라우저를 실행 (사용자 인터페이스가 없는 헤드리스 모드)
- 카카오 계정 로그인 페이지로 이동하여 ID와 비밀번호를 입력하고 로그인
- 내 블로그의 글쓰기 페이지(/manage/newpost)로 직접 이동
- (만약 나타난다면) '저장된 글' 관련 알림(Alert) 창을 감지하고 '취소'하여 닫기
- 제목, 본문(iframe 내부), 태그 입력란에 각각의 텍스트를 채워 넣기
- '발행' 버튼을 눌러 발행 옵션 창을 띄우고, 최종적으로 '공개 발행' 버튼 클릭
자, 그럼 이제 실제 코드를 보면서 한 단계씩 자세히 살펴보겠습니다.
🤔 2. 코드 심층 분석: 설정부터 로그인까지
모든 자동화의 첫 관문은 '로그인'입니다. 티스토리는 카카오 계정 기반 로그인을 사용하므로, 이 부분을 안정적으로 처리하는 것이 매우 중요합니다.
Python
`# -- coding: utf-8 --
"""
이 스크립트는 주어진 제목, 내용, 태그로 티스토리에 자동으로 글을 발행하는 함수를 포함합니다.
헤드리스 모드로 실행되며, "저장된 글" 알림 발생 시 "취소" 후 새 글 작성을 시도합니다.
"""
Selenium 관련
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager # ChromeDriver 자동 관리를 위해 사용
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.options import Options
from selenium.common.exceptions import TimeoutException, NoAlertPresentException
표준 라이브러리
import time
def post_to_tistory(title_text, content_text, tags_text):
"""
주어진 제목, 내용, 태그로 티스토리에 자동으로 글을 발행하는 함수 (헤드리스 모드).
"저장된 글" 알림 발생 시 "취소" 후 새 글 작성을 시도합니다.
Args:
title_text (str): 포스팅할 글의 제목.
content_text (str): 포스팅할 글의 내용.
tags_text (str): 포스팅할 글의 태그 (쉼표로 구분된 문자열).
Returns:
bool: 포스팅 성공 시 True, 실패 시 False.
"""
print(f"티스토리 자동 포스팅 시작: '{title_text}'")
chrome_options = Options()
chrome_options.add_argument("--headless")
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument("--window-size=1920,1080")
chrome_options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
# 중요: 아래 email, password, blog_name은 실제 값으로 채워야 합니다.
# 이 정보는 함수 외부에서 관리하거나, 더 안전한 방식으로 전달하는 것이 좋습니다.
email = "email" # 실제 카카오 계정 이메일로 변경
password = "password" # 실제 카카오 계정 비밀번호로 변경
blog_name = "blog_name" # 실제 티스토리 블로그 이름으로 변경
driver = None
posting_successful = False
try:
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=chrome_options)
print("WebDriver 시작됨 (헤드리스 모드)")
driver.get('<https://accounts.kakao.com/login/?continue=https%3A%2F%2Fkauth.kakao.com%2Foauth%2Fauthorize%3Fclient_id%3D3e6ddd834b023f24221217e370daed18%26prompt%3Dselect_account%26redirect_uri%3Dhttps%253A%252F%252Fwww.tistory.com%252Fauth%252Fkakao%252Fredirect%26response_type%3Dcode%26auth_tran_id%3D0jyki8ku4znd3e6ddd834b023f24221217e370daed18maxym4gt%26ka%3Dsdk%252F1.43.6%2520os%252Fjavascript%2520sdk_type%252Fjavascript%2520lang%252Fko%2520device%252FWin32%2520origin%252Fhttps%25253A%25252F%25252Fwww.tistory.com%26is_popup%3Dfalse%26through_account%3Dtrue#login>')
print(f"카카오 로그인 페이지 접속: {driver.current_url}")
# 로그인 시도
try:
WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.NAME, 'loginId')))
email_input = driver.find_element(By.NAME, 'loginId')
email_input.send_keys(email)
password_input = driver.find_element(By.NAME, 'password')
password_input.send_keys(password)
print("이메일 및 비밀번호 입력 완료.")
login_button = driver.find_element(By.CSS_SELECTOR, 'button[type="submit"]')
login_button.click()
print("로그인 버튼 클릭.")
print("로그인 버튼 클릭 후 10초 대기 시작...")
time.sleep(10) # 로그인 및 페이지 전환 대기 시간 (필요에 따라 조절)
current_url_after_click_and_wait = driver.current_url
print(f"로그인 버튼 클릭 10초 후 URL: {current_url_after_click_and_wait}")
screenshot_name = "debug_screenshot_after_login_attempt.png"
driver.save_screenshot(screenshot_name)
print(f"로그인 시도 후 스크린샷 저장됨: {screenshot_name}.")
if "tistory.com" in current_url_after_click_and_wait and \\
not ("accounts.kakao.com" in current_url_after_click_and_wait and "login" in current_url_after_click_and_wait):
print("로그인 성공! 🎉")
else:
print("로그인 실패: 최종 URL이 티스토리 로그인 성공 상태가 아닙니다.")
raise Exception("로그인 최종 실패 (URL 조건 불일치 또는 카카오 페이지에 머무름)")
except Exception as e:
print(f"로그인 과정 중 오류 발생: {e}")
print("저장된 스크린샷(예: debug_screenshot_after_login_attempt.png)을 확인하여 원인을 파악하세요.")
raise`
- Chrome Options: headless는 UI 없이 백그라운드에서 크롬을 실행하는 옵션입니다. 서버 환경에서 자동화 스크립트를 돌릴 때 필수적이죠. user-agent를 설정하는 이유는, 일부 웹사이트가 자동화 툴(봇)의 접근을 막는 경우가 있는데, 일반적인 브라우저처럼 보이게 하여 이를 우회하기 위함입니다.
- 보안 경고 ⚠️: 코드에 email, password 같은 민감한 정보를 직접 하드코딩하는 것은 보안상 매우 취약합니다. 실제 운영 환경에서는 환경 변수(.env 파일)나 별도의 설정 파일로 분리하여 관리하는 것을 강력히 권장합니다.
- WebDriverWait: 자동화 스크립트의 안정성을 좌우하는 핵심 요소입니다. time.sleep(10)처럼 고정된 시간만큼 기다리는 대신, WebDriverWait은 특정 요소가 나타나거나 클릭 가능해질 때까지 '지능적으로' 기다려줍니다. 네트워크 속도나 PC 성능에 따라 페이지 로딩 시간이 달라져도 스크립트가 깨지는 것을 방지해주죠.
- 디버깅을 위한 스크린샷: 로그인 실패는 자동화에서 가장 흔히 발생하는 문제입니다. driver.save_screenshot() 코드를 넣어두면 실패했을 때의 화면을 그대로 저장해주기 때문에, "왜 실패했지?"라는 물음에 대한 아주 강력한 단서가 됩니다. 비밀번호가 틀렸는지, 2단계 인증이 떴는지, 아니면 예상치 못한 팝업이 떴는지 바로 알 수 있죠.
🤔 3. 코드 심층 분석: '저장된 글'이라는 함정 피하기
로그인에 성공하고 글쓰기 페이지로 이동했을 때, 저는 예상치 못한 복병을 만났습니다. 비정상적으로 자동화 스크립트가 종료되었을 경우, 티스토리는 임시 저장된 글을 복구할지 묻는 알림(Alert) 창을 띄웁니다. 이 알림 창은 Selenium의 정상적인 요소 감지를 방해하여 스크립트를 멈추게 만들었죠.
이 문제를 해결하기 위해 글쓰기 페이지에 진입한 직후, 알림 창의 존재 여부를 먼저 확인하고 처리하는 로직을 추가했습니다.
Python
`# 글쓰기 페이지 이동
write_page_url = f'https://{blog_name}.tistory.com/manage/newpost'
driver.get(write_page_url)
print(f"글쓰기 페이지로 이동 시도: {write_page_url}")
# === "저장된 글" 알림 처리 로직 추가 ===
try:
print("알림 창 확인 시도 (최대 5초 대기)...")
WebDriverWait(driver, 5).until(EC.alert_is_present())
alert = driver.switch_to.alert
alert_text = alert.text
print(f"알림 발견! 내용: '{alert_text}'")
if "저장된 글이 있습니다" in alert_text:
print("저장된 글 관련 알림 확인. '취소'를 클릭하여 새 글 작성을 시작합니다.")
alert.dismiss() # "취소" 클릭
time.sleep(1) # 알림 닫은 후 잠시 대기
print("알림을 닫았습니다 (취소 선택).")
else:
print(f"예상치 못한 다른 알림입니다: '{alert_text}'. 일단 '확인'을 클릭합니다.")
alert.accept() # 다른 종류의 알림이면 일단 '확인'
time.sleep(1)
print("알림을 닫았습니다 (확인 선택).")
except TimeoutException:
print("알림 창이 5초 내에 나타나지 않았습니다. 정상 진행합니다.")
except NoAlertPresentException: # WebDriverWait이 성공해도 alert이 없을 수 있으므로 이 예외는 보통 발생 안 함
print("NoAlertPresentException: 알림이 없습니다. 정상 진행합니다.")
except Exception as e_alert:
print(f"알림 처리 중 예외 발생: {e_alert}")
driver.save_screenshot("debug_screenshot_alert_handling_error.png")
raise
# =======================================`
- EC.alert_is_present(): WebDriverWait과 함께 사용하여, 최대 5초 동안 알림 창이 나타나는지 기다립니다.
- try-except TimeoutException: 만약 5초가 지나도 알림 창이 나타나지 않으면 TimeoutException이 발생합니다. 이는 알림 창이 없는 '정상적인' 상황이므로, 예외를 잡아 무시하고 다음 코드를 실행하도록 처리했습니다. 이 부분이 없으면 알림 창이 안 뜰 때마다 스크립트가 오류를 내고 멈추게 됩니다.
- alert.dismiss() vs alert.accept(): dismiss()는 '취소'나 '닫기'에 해당하고, accept()는 '확인'에 해당합니다. "저장된 글" 알림의 경우, 새 글을 써야 하므로 '취소' 버튼에 해당하는 dismiss()를 호출하여 창을 닫습니다.
이 로직 덕분에 스크립트는 어떤 상황에서도 유연하게 대처하며 안정적으로 글쓰기 단계로 넘어갈 수 있게 되었습니다.
✍️ 4. 코드 심층 분석: 내용 입력과 최종 발행
이제 마지막 단계입니다. 제목, 본문, 태그를 입력하고 최종적으로 발행 버튼을 누르는 과정입니다. 여기서도 한 가지 중요한 포인트가 있습니다. 바로 'iframe'입니다.
티스토리의 글쓰기 에디터는 <iframe>이라는 별도의 HTML 문서 안에 존재합니다. 따라서 본문을 입력하려면 메인 페이지가 아닌, 이 iframe으로 제어권을 전환해야 합니다.
Python
`# 글쓰기 페이지 로드 및 제목 필드 확인
try:
title_input_element = WebDriverWait(driver, 20).until(
EC.presence_of_element_located((By.ID, "post-title-inp"))
)
print("글쓰기 페이지 로드 및 제목 필드 확인 완료 (알림 처리 후).")
except Exception as e:
print(f"글쓰기 페이지 로드 실패 또는 제목 필드를 찾을 수 없음 (알림 처리 후): {e}")
print("현재 URL:", driver.current_url)
driver.save_screenshot("debug_screenshot_editor_page_load_error_after_alert.png")
raise
# 제목 입력
title_input_element.send_keys(title_text)
print("제목 입력 완료: ", title_text)
# 본문 입력
try:
WebDriverWait(driver, 20).until(
EC.frame_to_be_available_and_switch_to_it((By.ID, "editor-tistory_ifr"))
)
print("본문 편집 iframe (id='editor-tistory_ifr')으로 전환 성공. 👍")
body_element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.ID, "tinymce"))
)
body_element.clear()
body_element.send_keys(content_text) # 여기서 content_text는 일반 텍스트여야 합니다.
print("본문 입력 완료. ✍️")
driver.switch_to.default_content()
print("기본 콘텐츠로 돌아오기 완료.")
except Exception as e:
print(f"본문 입력 중 오류 발생: {e} 😥")
driver.save_screenshot("debug_screenshot_body_input_error.png")
raise
# 태그 입력
tag_input_element = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.ID, "tagText"))
)
tag_input_element.send_keys(tags_text)
print("태그 입력 완료: ", tags_text, "🏷️")
time.sleep(1) # 태그 입력 후 안정화 시간
# 발행 버튼 클릭
try:
complete_button = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.ID, "publish-layer-btn"))
)
complete_button.click()
print("'완료' 버튼 클릭 완료. 🎉")
# '공개' 옵션이 포함된 발행 설정 창 확인 및 클릭
public_option_span_xpath = "//span[@class='checkbox-text' and normalize-space()='공개']"
public_option_element = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.XPATH, public_option_span_xpath))
)
public_option_element.click()
print("'공개' 옵션 클릭 시도 완료. ☑️")
time.sleep(0.5)
final_publish_button = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.ID, "publish-btn"))
)
final_publish_button.click()
print("최종 '공개 발행' 버튼 클릭 완료! 🚀")
time.sleep(5) # 발행 후 페이지 이동 또는 확인 대기
print("발행 후 현재 URL:", driver.current_url)
posting_successful = True
except Exception as e:
print(f"발행 과정 중 오류 발생: {e} 😥")
driver.save_screenshot("debug_screenshot_publish_process_error.png")
raise
print("🎉 자동 포스팅 모든 단계 성공적으로 실행 시도 완료 🎉")`
- driver.switch_to.frame(): id가 editor-tistory_ifr인 iframe으로 제어권을 넘깁니다. 이 명령 이후 driver의 모든 제어는 해당 iframe 내부에서만 동작합니다.
- driver.switch_to.default_content(): iframe 내부에서의 작업(본문 입력)을 마친 후, 다시 원래의 메인 페이지로 제어권을 돌려놓는 중요한 명령어입니다. 이걸 빼먹으면 iframe 바깥에 있는 태그 입력란이나 발행 버튼을 찾지 못해 오류가 발생합니다.
- 발행 프로세스: 티스토리 글 발행은 '완료' 버튼을 누르고, 나타나는 레이어 팝업에서 다시 '공개 발행' 버튼을 누르는 2단계로 이루어집니다. 이 순서를 코드에서도 그대로 따라야 합니다. 각 버튼이 클릭 가능할 때까지 WebDriverWait으로 기다려주는 것이 안정성을 높입니다.
✅ 5. 전체 코드 및 마무리
마지막으로, 예외 처리와 테스트를 위한 전체 코드를 보여드리겠습니다. try...finally 구문을 사용하여 스크립트 실행 중 어떤 오류가 발생하더라도 driver.quit()이 반드시 호출되도록 하여, 불필요한 Chrome 프로세스가 남지 않도록 깔끔하게 종료하는 것이 중요합니다.
Python
`except Exception as e_global:
print(f"💥 스크립트 실행 중 예외 발생: {e_global}")
if driver:
try:
final_error_screenshot_name = "debug_screenshot_function_fatal_error.png"
driver.save_screenshot(final_error_screenshot_name)
print(f"함수 실행 중 치명적 오류 발생 시 스크린샷 저장됨: {final_error_screenshot_name}")
except Exception as screenshot_err:
print(f"치명적 오류 스크린샷 저장 실패: {screenshot_err}")
finally:
if driver:
driver.quit()
print("WebDriver 종료됨.")
return posting_successful
if name == 'main':
# 아래는 함수 테스트를 위한 예시입니다.
test_title = "티스토리 자동 포스팅 테스트 제목"
test_content_plain = """
안녕하세요. 티스토리 자동 포스팅 테스트 본문입니다.
이 내용은 Selenium을 사용하여 자동으로 작성되었습니다.
줄바꿈도 잘 되는지 확인합니다.
- 첫 번째 항목
- 두 번째 항목
감사합니다.
"""
test_tags = "자동화,테스트,파이썬" # 쉼표로 구분
print(f"테스트를 위해 post_to_tistory 함수를 호출합니다.")
# 카카오 계정 정보(email, password)와 블로그 이름(blog_name)이
# post_to_tistory 함수 내에 정확히 설정되어 있어야 합니다.
success = post_to_tistory(test_title, test_content_plain, test_tags)
if success:
print("\\n✅ 테스트 실행 결과: 포스팅 성공!")
else:
print("\\n❌ 테스트 실행 결과: 포스팅 실패 또는 오류 발생.")`
- if __name__ == '__main__':: 이 스크립트 파일을 직접 실행했을 때만 내부에 있는 테스트 코드가 동작하도록 하는 파이썬의 표준적인 방법입니다. 다른 파일에서 이 스크립트를 import하여 post_to_tistory 함수만 가져다 쓸 때는 테스트 코드가 실행되지 않습니다.
오늘은 이렇게 파이썬과 Selenium을 활용한 티스토리 자동 포스팅 스크립트를 처음부터 끝까지 살펴보았습니다. 단순 반복 작업을 자동화하는 것은 개발자에게 큰 즐거움과 효율성을 가져다줍니다. 이 코드를 기반으로 여러분만의 자동화 스크립트를 만들어 보시는 것은 어떨까요? 예를 들어, 특정 웹사이트의 데이터를 크롤링하여 자동으로 분석하고 그 결과를 티스토리에 포스팅하는 봇을 만들 수도 있을 겁니다.
이 글이 여러분의 자동화 여정에 작은 도움이 되었기를 바랍니다. 궁금한 점이 있으시면 언제든지 댓글 남겨주세요! 긴 글 읽어주셔서 감사합니다. 👋