Huny's Dev Blog
huny.dev

다운로드 버튼을 눌렀는데 파일이 없다면: HTTP·브라우저 이벤트·저장 단계 나누기

Chrome Network, Playwright, curl로 다운로드 실패 지점을 구분하고 인증 정보가 섞인 로그를 안전하게 다루는 점검 순서를 정리한다.

Hun Jang
Hun Jang Oct 28, 2026

브라우저에서 파일 저장까지 이어지는 경로를 표현한 AI 생성 개념 이미지. 실제 오류 화면은 아니다.

다운로드 버튼을 눌렀는데 파일이 생기지 않는다. 같은 주소를 curl로 요청해도 결과가 다르다. 이때 바로 “서버가 막았다”거나 “브라우저 자동화가 고장 났다”고 결론 내리면 진단이 꼬인다.

먼저 클릭, HTTP 응답, 다운로드 시작, 파일 저장을 각각 확인해야 한다. 어느 단계까지 성공했는지 알면 다음에 볼 로그도 좁혀진다. 이 글은 특정 서비스의 장애 원인을 확정하는 글이 아니라, 일반적인 웹 다운로드를 점검하는 순서다.

1. 버튼 클릭과 파일 저장을 따로 기록한다

실무에서는 다음 네 가지 증거를 구분해 남기는 편이 좋다.

  • 클릭: 의도한 버튼에 동작이 전달됐는가?
  • 요청: 관련 HTTP 요청이 발생했고 어떤 응답을 받았는가?
  • 다운로드: 브라우저가 파일 다운로드를 시작했는가?
  • 저장: 기대한 위치에 파일이 생겼고 내용이 맞는가?

클릭 함수가 정상 반환했다고 마지막 단계까지 끝난 것은 아니다. 반대로 다운로드 이벤트를 기다리다 시간 초과가 났다고 HTTP 403을 받았다고 말할 수도 없다. 관찰하지 않은 상태 코드는 알 수 없는 값으로 남겨야 한다.

2. Network를 먼저 열고 한 번 재현한다

Chrome DevTools의 Network를 열고 기록 상태를 확인한다. 페이지 전환이 섞이면 Preserve log를 켠다. 기존 요청을 지운 뒤 다운로드를 한 번 눌러, 그 시점에 추가된 요청을 본다.

처음에는 필터를 너무 좁히지 않는 편이 좋다. 파일을 준비하는 API와 실제 파일 전송 요청이 따로 있을 수 있기 때문이다. 요청의 상태 코드, Headers, 응답 내용과 이동 경로를 함께 확인한다. Chrome Network 공식 문서

“200이니 성공”도 충분한 설명은 아니다. 기대한 파일 대신 로그인 화면이나 오류 안내 HTML을 받은 것은 아닌지 확인해야 한다. 파일 확장자는 내용 검증을 대신하지 못한다.

403은 서버가 요청을 이해했지만 처리를 거부했다는 뜻이다. 그 숫자 하나만으로 쿠키, 계정 권한, URL 만료 중 무엇이 원인인지는 특정할 수 없다. 서비스가 제공하는 오류 본문과 공식 안내를 확인하고, 권한이 없는 작업을 반복 시도하지 않는다. MDN 403

3. 화면에서 열리는 응답인지 확인한다

HTTP 응답의 Content-Disposition은 내용을 화면에 표시할지, 첨부 파일로 내려받도록 할지 판단하는 데 쓰인다.

이런 응답과 브라우저 안에 내용을 표시하는 inline 응답은 동작이 다를 수 있다. 또한 HTML의 download 속성이 적용되는 경우도 있으므로 헤더 하나만으로 모든 동작을 단정하지 않는다. MDN Content-Disposition

특히 동영상이 재생된다는 사실은 다운로드용 파일 응답까지 확인했다는 뜻이 아니다. 재생 경로와 파일 내보내기 경로가 같은지는 실제 요청으로 확인해야 한다.

4. Playwright에서는 클릭 전에 다운로드를 기다린다

Playwright 공식 예제는 다운로드 대기를 먼저 등록한 다음 클릭한다. 빠르게 시작한 이벤트를 놓치지 않기 위해서다. 다운로드 이벤트는 시작 시점에 발생하며, 저장 완료와는 구분된다. Playwright Downloads

아래는 Node.js에서 사용할 점검 패턴이다. page는 이미 준비되어 있고, 버튼 이름은 실제 화면에 맞춰 바꾼다는 전제다.

failure()는 다운로드가 끝날 때까지 기다린 뒤 실패 내용을 돌려주며, 성공하면 null이다. saveAs()는 지정한 경로에 저장한다. 브라우저 컨텍스트를 닫으면 임시 다운로드 파일이 삭제되므로 필요한 파일은 그 전에 보관해야 한다. Playwright Download API

이 예제의 30초는 다운로드 이벤트를 기다리는 제한이다. 파일 전체 전송 시간의 공통 제한으로 해석하면 안 된다. 파일이 존재해도 기대한 CSV·PDF·영상인지 별도로 확인한다.

검증 범위: 위 코드는 공식 API에 맞춘 예제이며 실행 성공 결과를 제시하는 코드는 아니다. 이 글을 준비한 환경에서는 로컬 브라우저 실행이 완료되지 않아 다운로드·저장까지의 동작을 검증하지 못했다.

5. curl 비교는 같은 조건인지부터 본다

브라우저와 curl은 별도의 클라이언트다. 브라우저에 로그인했다고 curl에도 인증 상태가 자동으로 생기지는 않는다. 계정이 필요한 서비스라면 먼저 공식 다운로드 방법을 사용하고, 쿠키를 무작정 복사해 해결하려 하지 않는다.

아래 명령은 직접 관리하거나 테스트 권한이 있는 공개 파일 URL을 넣어 사용하는 점검 예제다. 실제 실행 결과가 아니다.

--location은 리디렉션을 따라가고, --range는 일부 바이트를 요청한다. 다만 서버가 범위 요청을 무시하면 전체 응답을 보낼 수 있다. 시간과 크기 제한을 함께 두고 curl 버전에서 지원하는 옵션을 확인한다. HEAD 요청만 성공했다고 GET 다운로드도 성공한다고 보장할 수는 없다. curl 공식 매뉴얼

브라우저만 성공한다면 인증·요청 조건·실행 환경의 차이를 비교할 단서가 생긴 것이다. 곧바로 특정 헤더를 바꾸거나 보안 설정을 끄는 결론으로 넘어가지는 않는다.

로그는 공유 전에 다시 확인한다

Network의 Copy as cURL에는 인증 정보가 포함될 수 있다. HAR도 원본 그대로 공개 이슈에 붙이지 않는다. Chrome의 sanitized HAR는 Cookie·Set-Cookie·Authorization 같은 헤더를 제외하지만, URL 쿼리나 응답 내용까지 모두 안전하다고 가정하면 안 된다. Chrome HAR 내보내기 안내

공유용 기록에는 시각, 실패 단계, 상태 코드, 호스트, 오류 종류 정도를 먼저 남긴다. 계정 정보, 서명된 전체 URL, 토큰, 쿠키는 제거한다.

다운로드 실패를 빠르게 좁히는 방법은 클릭을 반복하는 것이 아니다. 마지막으로 확인된 성공 단계와 아직 확인하지 못한 단계를 나누는 것부터 시작하면 된다.

추천 글