한글 이름으로 올린 파일을 내려받았더니 파일명이 깨져 있던 이유
문제 발생
첨부파일 다운로드 API에서 업로드할 때의 원래 파일명을 그대로 헤더에 실어 보내고 있었습니다.
Content-Disposition: attachment; filename="9월 정산 내역.xlsx"report.pdf 같은 파일은 아무 문제가 없었는데, 한글이 들어간 파일은 내려받고 보니 이름이 알아볼 수 없게 깨져 있었습니다. 처음엔 브라우저 문제인가 싶었는데, 어느 브라우저로 받아도 한글 파일만 멀쩡하지 않았습니다.
원인 분석
filename은 한글을 담으라고 있는 자리가 아니었습니다. MDN의 Content-Disposition 문서에 따르면 filename과 filename*은 딱 한 가지가 다릅니다 — filename*은 RFC 5987의 3.2절에 정의된 인코딩을 씁니다. UTF-8 파일명을 제대로 실어 보내는 자리는 filename*이고, 모양은 이렇습니다.
Content-Disposition: attachment; filename*=UTF-8''file%20name.jpgUTF-8 뒤에 작은따옴표 두 개를 붙이고, 그 뒤에 퍼센트 인코딩한 파일명이 옵니다.
그렇다고 filename에 퍼센트 인코딩을 넣는 것도 답이 아니었습니다. 처음엔 filename 값만 encodeURIComponent로 감싸서 고쳤는데, MDN은 이걸 피하라고 따로 적어 둡니다. filename 안의 퍼센트 이스케이프는 브라우저마다 다르게 처리돼서, Firefox와 Chrome은 디코딩하지만 Safari는 하지 않는다고요. 크롬에서만 확인하고 넘어갔다면 사파리 쪽 사용자는 %EC%A0%95... 같은 이름을 받았을 겁니다.
해결 방안
filename과filename*을 같이 보냅니다. MDN에 따르면 둘이 함께 있고 브라우저가 둘 다 이해하면filename*이 우선하고, 호환성을 위해 둘 다 넣는 것을 권합니다.filename에는 ASCII로 바꾼 이름을 넣습니다.
Content-Disposition: attachment; filename="settlement.xlsx"; filename*=UTF-8''9%EC%9B%94%20%EC%A0%95%EC%82%B0%20%EB%82%B4%EC%97%AD.xlsxencodeURIComponent만으로는 조금 모자랍니다. RFC 5987은',(,),*까지 인코딩해야 하는데,encodeURIComponent는 이 네 글자를 그대로 둡니다. MDN이 바로 이 경우를 위한 함수를 예제로 실어 두었습니다.
function encodeRFC5987ValueChars(str) {
return encodeURIComponent(str)
.replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)
.replace(/%(7C|60|5E)/g, (str, hex) => String.fromCharCode(parseInt(hex, 16)));
}보고서(1).pdf처럼 괄호가 들어간 이름에서 차이가 납니다. 다운로드한 파일 이름 뒤에 (1)이 붙는 일은 생각보다 흔합니다.
- 헤더 만드는 곳을 한 군데로 모읍니다. 다운로드 API가 여러 개면 어딘가 하나는 예전 방식으로 남습니다.
function attachmentHeader(name, asciiFallback = "download") {
return `attachment; filename="${asciiFallback}"; filename*=UTF-8''${encodeRFC5987ValueChars(name)}`;
}- 프런트에서
fetch로 받는다면 헤더를 노출합니다. 다른 출처의 API를fetch로 받아서 파일명을Content-Disposition에서 꺼내 쓰는 구조라면, 헤더가 분명히 있는데도response.headers.get("Content-Disposition")이null일 수 있습니다. MDN에 따르면 교차 출처 응답에서 스크립트에 기본으로 보이는 헤더는 CORS 안전 목록에 있는Cache-Control,Content-Language,Content-Length,Content-Type,Expires,Last-Modified,Pragma뿐이고, 나머지는 서버가 직접 열어 줘야 합니다.
Access-Control-Expose-Headers: Content-Disposition- 파일명은 어디까지나 제안이라는 걸 기억합니다. MDN은
filename이 대체로 참고용 정보이고, 브라우저가 파일 시스템에 맞게/나\같은 경로 구분자를 밑줄로 바꾸는 식의 변환을 할 수 있다고 적어 둡니다. 원래 이름을 정확히 보여 줘야 하는 화면이라면 다운로드 목록에 원래 이름을 따로 표시해 둡니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.