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. 1트

     const handleDownload = async(filename: string) => {
       try {
         const pdfResponse = await ApiKeyInstance.get(`/download/pdf`, {
           headers: {
             'Content-Disposition': `attachment; filename=${filename}.pdf`,
           }
         })
       } 
     }
    

    이렇게 하면 아래와 같은 에러가 뜬다

    image

    일반 문자를 인코딩해야하는데 안해서 나는 에러다.

  2. 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. 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);
        }
      }