Playwright 파일 업로드, input 없는 사이트 처리법

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

Playwright로 파일을 올릴 때 첫 선택지는 set_input_files()다. 그런데 어떤 사이트는 DOM에 <input type="file">이 아예 없다. 버튼을 누르는 순간 브라우저의 네이티브 파일 선택창이 뜨고, 자동화 도구는 그 창을 조작할 수 없어 멈춘다. 이 글은 티스토리 에디터가 정확히 이런 구조였고, expect_file_chooser()로 어떻게 넘겼는지를 다룬다. 핵심은 DOM에서 input을 찾는 것을 포기하고, 파일 선택창이 열리는 이벤트를 받는 것이다.

DOM 어디에도 input type=file 이 없었다

티스토리 에디터의 첨부 메뉴를 열면 사진, 파일, 슬라이드쇼 세 항목이 나온다. 사진(#attach-image)을 누르면 운영체제의 파일 선택창이 뜬다. 자동화라면 당연히 그 뒤에 숨은 <input type="file">을 찾아 set_input_files를 부르면 될 것 같았다.

print(page.evaluate("""() => [...document.querySelectorAll('input[type=file]')]
    .map(e => ({id: e.id, accept: e.accept}))"""))
# → []

메뉴를 열기 전에도, 연 뒤에도 결과는 빈 배열이었다. 숨겨진 input이 있는 게 아니라 정말로 없었다. 클릭 시점에 스크립트가 input 요소를 만들어 click()을 호출하고 곧바로 버리는 방식이거나, 브라우저 API로 직접 선택창을 여는 방식으로 짐작된다. 어느 쪽이든 셀렉터로 잡을 대상이 없다.

set_input_files 가 아니라 expect_file_chooser 를 쓴다

Playwright 문서는 이 상황을 "input 요소가 동적으로 생성될 때"로 분류하고 expect_file_chooser() 패턴을 안내한다.

with page.expect_file_chooser() as fc_info:
    page.get_by_label("Upload file").click()
file_chooser = fc_info.value
file_chooser.set_files("myfile.pdf")

원리는 요소를 찾는 것이 아니라 사건을 기다리는 것이다. with 블록 안에서 클릭을 하면 Playwright가 브라우저 수준에서 "파일 선택창이 열렸다"는 이벤트를 가로채고, 블록을 빠져나온 뒤 fc_info.value에 FileChooser 객체가 들어온다. 운영체제 창은 실제로 뜨지 않는다. headless에서도 동작한다.

티스토리에 적용한 코드는 이렇다.

page.click("div[aria-label='첨부'] button")     # 첨부 메뉴 열기
page.wait_for_timeout(800)
with page.expect_file_chooser(timeout=15000) as chooser:
    page.click("#attach-image")                  # 사진
chooser.value.set_files("content/post/assets/thumbnail.png")

첫 실행에서 바로 통과했다. 9초 뒤 편집 영역에 <figure data-ke-type="image">가 나타났고, src는 blog.kakaocdn.net의 주소였다.

업로드 완료는 DOM 변화로 판단해야 한다

set_files는 파일을 선택창에 넘긴 시점에 돌아온다. 업로드가 끝난 시점이 아니다. 그 다음에 무엇을 기다릴지는 사이트마다 다르다. 티스토리는 업로드가 끝나면 편집 영역에 <figure>를 삽입하므로, 그 개수가 늘어나는 것을 완료 신호로 삼았다.

def figure_count(page):
    return page.evaluate("""() => {
        const f = document.querySelector('iframe');
        return f && f.contentDocument
            ? f.contentDocument.querySelectorAll('figure').length : -1;
    }""")

before = figure_count(page)
# ... 파일 선택 ...
deadline = time.monotonic() + 90
while time.monotonic() < deadline:
    page.wait_for_timeout(1000)
    if figure_count(page) > before:
        break
else:
    raise RuntimeError("업로드가 끝나지 않았습니다")

wait_for_timeout(9000) 같은 고정 대기로도 이번엔 됐겠지만, 파일이 크거나 회선이 느린 날에는 깨진다. 실제로 걸린 시간은 4초에서 9초 사이로 편차가 있었다. 상태 변화를 기다리는 쪽이 느린 날에도 맞고 빠른 날에는 더 빨리 끝난다.

그래도 input 이 있는지 먼저 확인하는 게 맞다

expect_file_chooser가 만능이니 처음부터 이걸 쓰면 되지 않느냐는 반론이 있을 수 있다. 나는 반대다. set_input_files가 가능한 곳에서는 그쪽이 낫다. 클릭이 필요 없어서 오버레이나 스크롤 문제에 걸리지 않고, 메뉴를 여는 중간 단계도 없다. 요소가 있으면 요소를 쓰고, 없을 때만 사건을 기다린다.

또 하나. expect_file_chooser의 with 블록 안에서 하는 클릭이 실제로 선택창을 열지 못하면, 블록은 timeout까지 기다렸다가 TimeoutError를 낸다. 첨부 메뉴가 안 열려서 #attach-image가 보이지 않는 경우가 그렇다. 그래서 메뉴를 여는 클릭과 사진 버튼 클릭 사이에 짧은 대기를 두고, 사진 버튼은 with 블록 안에서만 누른다. 사건을 기다리는 코드는 사건이 안 일어났을 때 어떻게 실패하는지까지 정해둬야 한다.

참고 자료

  • Playwright Python — Input: Upload files — set_input_files와 expect_file_chooser 패턴
  • Playwright Python — Actionability — 클릭 전 자동 대기 규칙과 timeout 실패
  • Playwright Python — Dialogs — 파일 선택창과 별개로 뜨는 confirm 처리
728x90
반응형

'IT' 카테고리의 다른 글

프론트엔드 프레임워크 2026, 시그널이 표준이 되고 논쟁은 끝났다  (0) 2026.09.13
티스토리 이미지 업로드 URL 만료, 서명 주소를 저장하면 안 되는 이유  (0) 2026.09.12
주니어 개발자 채용이 줄었다는 말의 진짜 의미  (0) 2026.09.12
카카오 로그인 세션이 브라우저 닫으면 풀리는 이유와 자동 재로그인  (0) 2026.09.11
AI 코딩 에이전트 생산성, 90%가 쓰는데 왜 일정은 그대로인가  (0) 2026.09.11
'IT' 카테고리의 다른 글
  • 프론트엔드 프레임워크 2026, 시그널이 표준이 되고 논쟁은 끝났다
  • 티스토리 이미지 업로드 URL 만료, 서명 주소를 저장하면 안 되는 이유
  • 주니어 개발자 채용이 줄었다는 말의 진짜 의미
  • 카카오 로그인 세션이 브라우저 닫으면 풀리는 이유와 자동 재로그인
밍글링글링
밍글링글링
mingling - 밍글링, 밍글밍글링. 코드와 어우러지다. IT/ 프로그래밍/소스
    반응형
    250x250
  • 밍글링글링
    mingling
    밍글링글링
  • 전체
    오늘
    어제
    • 밍글링글링 (409) 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 (12) N
      • Rust (0)
  • 블로그 메뉴

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

  • 공지사항

  • 인기 글

  • 태그

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

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
밍글링글링
Playwright 파일 업로드, input 없는 사이트 처리법
상단으로

티스토리툴바