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 처리
'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 |