[HTTP]blob과 Content-Disposition을 통한 파일 다운로드 예시
Blob과 Contetn-Disposition을 사용하여 다운로드와 미리보기 기능 구현하기
서버에 요청해서 받아온 pdf 파일을 blob과 Content-Disposition으로 처리한 내용을 담았다.
이번에 pdf 파일 다운로드 작업을 하게 되었는데 내가 한 방식이 흔하지 않은 방식인건지 적당한 예제를 못 찾아서(예제를 복붙하는 날먹을 못해서) 그냥 내가 남겨두려고 글을 쓴다.
1. Blob
-
Blob이란 Binary Large Object(이진 대형 객체)의 약자로 이진 데이터를 나타내는 JavaScript 객체다. 주로 텍스트, 이미지 같은 이진 데이터를 다룰때나 파일 관련 작업에서 유용하게 사용된다.
- Blob 생성자
blob 객체는 생성자를 사용하여 생성된다.const blob = new Blob(array, options)
array
: blob 객체에 포함시킬 배열 또는 데이터options
: MIME 타입이나 다른 설정을 담고있는 객체(선택)
- 예시
서버에서 받아온 image를 blob 객체에 담아주는 예시이다.const imageBlob = new Blob(responseImage, { type: 'image/png' })
2. 서버 호출 시 blob, Contetn-Disposition 적용
처음부터 막힌게 좀 웃기지만 서버 호출하는 것 부터 잘 안됐다.
api가 배포되기 전이라 endpoint와 query 값, headers에 어떤 값을 넣어서 보내주는지 백엔드와 이야기해서 로직을 먼저 짰다.
그리고 api 작업이 완료되고 연결을 해보았는데…
-
1트
const handleDownload = async(filename: string) => { try { const pdfResponse = await ApiKeyInstance.get(`/download/pdf`, { headers: { 'Content-Disposition': `attachment; filename=${filename}.pdf`, } }) } }
이렇게 하면 아래와 같은 에러가 뜬다
일반 문자를 인코딩해야하는데 안해서 나는 에러다.
-
2트
찾아보다가 랜덤 블로그에서 response type에 blob을 넣어줘야 한다는 것을 보고 그것만 넣어줬다.const handleDownload = async(filename: string) => { try { const pdfResponse = await ApiKeyInstance.get(`/download/pdf`, { responseType: 'blob', headers: { 'Content-Disposition': `attachment; filename=${filename}.pdf`, } }) } }
1트에서 났던 에러가 똑같이 난다.
에러가 어떻게하면 해결되는지 분명하게 메세지를 주고 있는데 말을 듣지 않는 나의 심리는.,…,,..,.. 나도 나를 모르겠다.???: 그니까 내가 인코딩 해야한다고 말했자나..
-
3트
이제 정신차리고 인코딩을 제대로 해줬다.
headers에 content type도 추가해줬다.const handleDownload = async(filename: string) => { try { const encodedFilename = encodeURIComponent(`${filename}.pdf`); const pdfResponse = await ApiKeyInstance.get(`/download/pdf`, { responseType: 'blob', headers: { 'Content-Type': 'application/pdf', 'Content-Disposition': `attachment; filename=${encodedFilename}.pdf`, } }) } }
이렇게 해주면 파일이 잘 들어온다. 굳굳
3. blob 객체를 이용한 다운로드 기능 구현하기
사용자의 화면에는 보이지 않는 가상 <a/>
요소와 다운로드 링크를 생성하고 바로 다운로드 이벤트가 발생하도록 하여 버튼 클릭 시 파일을 다운로드 받을 수 있도록 한다.
const url = URL.createObjectURL(new Blob([pdfResponse.data], {type: 'application/pdf'}));
// 1. 위에서 호출한 데이터의 결과 pdfResponse를 기반으로 blob 객체를 생성하고, 해당 blob 객체를 가리키는 가상의 url 생성
const downloadLink = document.createElement('a');
// 2. 다운로드 링크를 나타낼 가상 요소
downloadLink.href = url;
// 3. 1번에서 생성한 가상의 url을 다운로드 링크의 href 속성에 할당
downloadLink.setAttribute('download', `${filename}.pdf`);
// 4. 클릭했을때 파일이 다운로드 되도록 download 속성 설정, 파일이름은 `${filename}.pdf` 부분으로 설정 가능(본인은 filename이라는 값을 함수의 매개변수로 받았다)
document.body.appendChild(downloadLink);
// 5. body에 위에서 생성한 가상 다운로드 링크를 추가하여 클릭 이벤트가 발생하도록 해줌
downloadLink.click();
// 6. 생성한 가상 다운로드 링크가 클릭되도록 함
3. 미리보기 추가하기
파일을 다운로드 받기 전 미리보기 기능이 있으면 좋을 것 같아서 미리보기 기능도 추가해주었다.
함수의 매개변수로 type으로 ‘preview’와 ‘download’를 받도록 하여서 어떤 버튼을 클릭한건지 알 수 있도록 했다.
const url = URL.createObjectURL(new Blob([pdfResponse.data], {type: 'application/pdf'}));
if (type === 'preview') {
return window.open(url);
}
if (type === 'download') {
const downloadLink = document.createElement('a');
downloadLink.href = url;
downloadLink.setAttribute('download', `${filename}.pdf`);
document.body.appendChild(downloadLink);
downloadLink.click();
}
완성본
- fetch 시 파일명에 특수문자나 공백이 포함되어있을경우 이를 url에 직접사용하기 위해 파일명을 encoding 한다.
- PDF 파일을 다룰때는 blob 객체로 받게 하여 클라이언트 측에서 다운로드, 미리보기 등의 작업을 수행할 수 있도록 한다.
-
최종 코드
const handleDownload = async(filename: string, type: string) => { try { const encodedFilename = encodeURIComponent(`${filename}.pdf`); const pdfResponse = await ApiKeyInstance.get(`/download/pdf`, { responseType: 'blob', headers: { 'Content-Type': 'application/pdf', 'Content-Disposition': `attachment; filename=${encodedFilename}.pdf`, } }) const url = URL.createObjectURL(new Blob([pdfResponse.data], {type: 'application/pdf'})); if (type === 'preview') { return window.open(url); } if (type === 'download') { const downloadLink = document.createElement('a'); downloadLink.href = url; downloadLink.setAttribute('download', `${filename}.pdf`); document.body.appendChild(downloadLink); downloadLink.click(); } } catch (error) { console.error('Error donloading PDF:', error); } }