티스토리 이미지 업로드 URL 만료, 서명 주소를 저장하면 안 되는 이유

2026. 9. 12. 13:00·IT
728x90
반응형

티스토리에 이미지를 자동으로 올리고 본문에 넣는 작업을 하다가, 업로드 직후 에디터에 삽입된 이미지 주소를 그대로 HTML에 박아 넣으면 몇 주 뒤에 깨질 수 있다는 것을 알았다. 주소에 만료 시각과 서명이 붙어 있었기 때문이다. 이 글은 그 주소의 정체와, 주소 대신 티스토리가 만든 마크업을 통째로 옮기는 방식으로 우회한 과정을 다룬다.

업로드된 이미지 주소에는 만료 시각과 서명이 붙어 있다

기본모드 에디터에서 사진 첨부로 PNG 하나를 올리자 편집 영역 iframe 안에 이런 마크업이 생겼다.

<figure data-ke-type="image" data-ke-mobilestyle="widthOrigin" data-ke-style="alignCenter">
  <img src="https://blog.kakaocdn.net/dna/.../img.png?credential=...&expires=1790780399&allow_ip=&allow_referer=&signature=..."
       data-image-src="...같은 주소...">
</figure>

src의 쿼리스트링을 보면 credential, expires, signature가 있다. expires=1790780399는 유닉스 시각으로 2026년 9월 말이다. 업로드한 날로부터 약 3주 뒤다. 클라우드 스토리지에서 흔히 쓰는 서명된 URL, 즉 특정 기간 동안만 접근을 허용하는 임시 주소다. 편집 중인 글에서 미리보기를 보여주기 위한 것이지, 발행된 글이 영구히 참조할 주소가 아니다.

내 첫 설계는 이 src만 뽑아서, HTML 모드에 넣을 본문의 로컬 이미지 경로와 치환하는 것이었다. 그랬다면 발행 뒤 3주 동안은 멀쩡하다가 어느 날 이미지가 전부 깨졌을 것이다. 발행 시점에는 검증이 통과하고 만료 시점에는 아무도 지켜보지 않는, 가장 나쁜 종류의 지연 폭탄이다.

티스토리는 저장할 때 자기 규칙으로 주소를 바꾼다

발행된 글의 이미지가 어떻게 되는지 확인하려고, 업로드 직후의 글을 임시저장하고 마크다운 모드로 다시 열어봤다. 그러자 이미지가 이런 형태로 바뀌어 있었다.

는 티스토리의 치환자다. 저장 시점에 에디터가 삽입한 마크업을 서버가 자기 규칙으로 바꿔 보관하고, 글을 렌더링할 때 그때그때 유효한 주소로 풀어낸다. 즉 티스토리는 서명 URL을 저장하지 않는다. data-ke-type="image"가 붙은 <figure> 마크업을 보고 어떤 이미지인지 인식한 뒤 자기 식으로 변환한다.

그렇다면 답은 분명하다. 주소를 뽑아 쓰지 말고, 티스토리가 만든 <figure> 요소를 통째로 가져다 쓰면 된다. 서버는 그 마크업을 알아보고 알아서 영구 참조로 바꿔준다.

figure 마크업을 통째로 옮기면 서버가 알아서 처리한다

최종 흐름은 이렇다. HTML 모드에는 업로드 UI가 없으므로 기본모드에서 먼저 이미지를 올린다. 업로드마다 iframe 안의 <figure> 개수가 늘어나는 것을 기다렸다가 마지막 <figure>의 outerHTML을 회수한다. 모든 이미지를 올린 뒤 편집 영역을 비우고 HTML 모드로 전환한 다음, 본문의 로컬 <img>를 회수한 마크업으로 바꿔 넣는다.

def upload_image(page, path):
    before = figure_count(page)
    page.click("div[aria-label='첨부'] button")
    with page.expect_file_chooser() as chooser:
        page.click("#attach-image")
    chooser.value.set_files(str(path))
    deadline = time.monotonic() + 90
    while time.monotonic() < deadline:
        page.wait_for_timeout(1000)
        if figure_count(page) > before:
            return last_figure_html(page)   # data-mce-selected 속성은 떼고 반환
    raise RuntimeError(f"업로드가 끝나지 않았습니다: {path.name}")

회수한 마크업에서 data-mce-selected 속성은 떼어냈다. 에디터가 현재 선택된 요소에 붙이는 편집용 표시라 본문에 남길 이유가 없다. 이렇게 발행한 글을 다시 열자 <figure> 2개(대표 썸네일, 본문 도해)가 모두 살아 있었고, src가 http로 시작하지 않는 로컬 경로는 하나도 남지 않았다.

그래도 만료 주소가 남았는지 발행 직전에 검사한다

이 방식에도 한계는 있다. 티스토리가 <figure> 마크업의 규칙을 바꾸면 서버가 이미지를 인식하지 못할 수 있다. 그 경우 서명 URL이 그대로 저장되고, 3주 뒤에 조용히 깨진다. 지금 동작한다는 사실이 앞으로도 동작한다는 보장은 아니다.

그래서 두 겹의 방어를 두었다. 하나는 발행 직전 게이트에서 본문에 로컬 경로 이미지가 남아 있으면 발행 자체를 막는 규칙이다. 업로드가 누락된 채 나가는 것을 막는다. 다른 하나는 발행 후 저장된 글을 다시 열어 <img src>가 전부 http로 시작하는지, <figure> 개수가 올린 수와 같은지 세는 검증이다.

"주소를 저장하지 말고 서비스가 알아보는 마크업을 저장하라." 티스토리뿐 아니라 서명 URL을 쓰는 어떤 서비스에서든 통하는 원칙이다. 만료 시각이 박힌 주소는 그 시각이 지나면 반드시 깨진다. 발행 시점에 멀쩡해 보이는 것이 가장 위험한 이유다.

참고 자료

  • Playwright Python — Input: Upload files — expect_file_chooser로 파일 선택창을 받는 방법
  • Playwright Python — Actionability — 업로드 완료를 기다릴 때 참고한 자동 대기 규칙
  • 티스토리 Open API 종료 안내 — API 로 이미지를 올릴 수 없게 된 배경
728x90
반응형

'IT' 카테고리의 다른 글

주니어 개발자 채용이 줄었다는 말의 진짜 의미  (0) 2026.09.12
카카오 로그인 세션이 브라우저 닫으면 풀리는 이유와 자동 재로그인  (0) 2026.09.11
AI 코딩 에이전트 생산성, 90%가 쓰는데 왜 일정은 그대로인가  (0) 2026.09.11
Playwright로 티스토리 글 올리기, 본문이 빈 채 저장되던 이유  (4) 2026.09.10
D램 가격 4배 폭등, 서버 비용은 어디까지 오르나  (0) 2026.09.10
'IT' 카테고리의 다른 글
  • 주니어 개발자 채용이 줄었다는 말의 진짜 의미
  • 카카오 로그인 세션이 브라우저 닫으면 풀리는 이유와 자동 재로그인
  • AI 코딩 에이전트 생산성, 90%가 쓰는데 왜 일정은 그대로인가
  • Playwright로 티스토리 글 올리기, 본문이 빈 채 저장되던 이유
밍글링글링
밍글링글링
mingling - 밍글링, 밍글밍글링. 코드와 어우러지다. IT/ 프로그래밍/소스
    반응형
    250x250
  • 밍글링글링
    mingling
    밍글링글링
  • 전체
    오늘
    어제
    • 밍글링글링 (407) N
      • Flutter (2)
      • 일상생활 (8)
        • 리뷰 (1)
        • 생활정보 (4)
        • 맛집 (0)
        • 여행 (0)
        • 모든정보 (3)
      • JAVA (126)
        • 개념 (6)
        • 예제 (115)
        • Exception (2)
      • C (1)
        • C (1)
        • C++ (0)
        • C# (0)
      • JS (29)
        • JavaScript (18)
        • JQuery (5)
        • AJax (0)
        • NODE.JS (6)
        • Angular.JS 2.0 (0)
      • WEB (87)
        • HTML (6)
        • CSS (61)
        • JSP (20)
        • JSTL (0)
      • FrameWork (8)
        • Spring (8)
        • BootStrap (0)
        • MyBATIS (0)
        • JUnit (0)
      • 외부 라이브러리 (5)
      • 공유 소스 관리 (5)
        • Git (5)
        • SVN (0)
      • 빅데이터 프로그래밍 (37)
        • Python (37)
        • R Programming (0)
      • DB (7)
        • ORACLE (0)
        • MySql (6)
      • Development Tools (7)
        • StarUML (0)
        • eXERD (0)
        • Eclipse (4)
      • SKILL (6)
        • Migration (0)
        • Security (6)
      • MicroSoft (0)
        • Excel (0)
        • Word (0)
      • Android (0)
      • Server (21)
        • Ubuntu (5)
        • Linux (15)
      • IOS (0)
      • XML (0)
      • 미디어 (0)
      • 공지사항 (3)
      • NETWORK (1)
      • 게임 (4)
        • 피파 (1)
        • 리니지M (0)
        • 배틀그라운드 (1)
        • 듀랑고 (2)
      • 세상 이슈 (6)
      • 일렉트론 (0)
      • 대회 소식 (4)
      • 업무 (2)
      • Express, Vue (6)
      • docker (11)
      • svelte (3)
      • 블록체인 (1)
      • IT (10) N
      • Rust (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 미디어로그
    • 위치로그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    css list
    Java Array
    ssl 인증서 발급
    러스트
    css table
    VS Code 팁
    API 설계
    servlet class
    자바 객체 지향
    css transition
    자바 배열
    에디터 팁
    spring java
    Rust lang
    클론코딩
    proxy pass
    css perspective
    vue cli
    css block
    자바 for문
    ubuntu
    자바 생성자
    lang rust
    nginx ssl 설정
    자바 클래스
    티스토리 자동화
    css float
    프런트엔드
    자바 exception
    오류
    Extension Bisect
    vscode
    jsp parameter
    css tb
    java casting
    rust linux
    AI 코딩 에이전트
    리눅스 설치
    React Compiler
    mysql db
    gitlab 설치
    vue 설치
    브라우저 자동화
    svelte
    nginx ssl 적용
    nginx
    docker
    Node
    jsp include
    SSL 인증서
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
밍글링글링
티스토리 이미지 업로드 URL 만료, 서명 주소를 저장하면 안 되는 이유
상단으로

티스토리툴바