STUDY/React

[React] axios로 서버에서 받은 파일 다운로드 구현

_JJ_ 2023. 12. 18. 16:56

 

이번에 파일 다운로드 기능을 구현하다가 엄청난 삽질을 하게 돼서 기록 삼아 글을 쓴다.

 

엑셀 파일이 서버 측에 저장되어 있고, api를 호출하면 서버에서 response로 파일을 보내주고 해당 파일을 다운로드 구현하는 일이었다.

서버 응답은 이런 식으로 샬라샬라 내려옴

 

구글링을 해보니 responseType을 추가해줘야 한다고 한다.

const options = {
      responseType: "blob",
    };

이리저리 검색해보니 responseType: "arraybuffer"로 넣는 사람도 있고 "blob"을 넣는 사람도 있던데

쓰기/수정 기능을 써야 하는 게 아니라면 불변성이 있는 blob을 권장한다고 한다. 읽기 전용이므로 blob으로 넣었다

 

그리고 불러온 데이터를 new Blob으로 생성해 준다

const blobData = new Blob([response.payload.data], {
            type: response.payload.headers["content-type"],
          });

 

파일을 불러올 때 file-saver 라이브러리를 이용하는 방법도 있지만, 나는 라이브러리를 쓰지 않는 방법으로 했다

            // Blob을 사용하여 다운로드 링크를 생성
            const link = document.createElement("a");
            link.href = URL.createObjectURL(blobData);
            link.download = "fileName.xlsx";
            link.target = "_blank"; // 다운로드 링크를 새 창에서 열도록 설정 (옵션)
            th.appendChild(link);
            link.click();
            th.removeChild(link);
            URL.revokeObjectURL(blobData);

 

response 응답받은 이후 blob 데이터 생성해 주고, a 태그를 만들어서 필요한 속성들 추가해 주고 클릭시킨 다음

할 일이 끝나면 링크를 삭제시켰다.  

URL.createObjectURL()로 생성한 url을 URL.revokeObjectURL()로 폐기하지 않으면 가비지 콜렉터로 설정되지 않아서 메모리 누수가 있을 수 있다고 한다.

 

 

이리저리 수정을 했는데도 자꾸 파일이 손상되었다고 나옴.

하다 하다 파일 이름이 문제인가 싶어서 파일 이름까지 그대로 불러와보기로 함.

 

서버에서는 response Headers에서 Content-Disposition로 파일 이름을 넘겨주고 있는 상황이었음.

그런데 아무리 response를 찍어봐도 cache-control, content-length, content-type, expires, pragma 같은 정보만 나오고

정작 필요한 Content-Disposition 접근이 안 됐음. 

예전에 Authorization 작업할 때도 이런 적이 있어서 감이 딱 왔다. 이건 서버에서 설정해줘야 함!!!

서버에서 Access-Control-Expose-Headers: * 에서 별 외에 따로 명시를 해줘야 한다.

 

Access-Control-Expose-Headers - HTTP | MDN

The Access-Control-Expose-Headers response header allows a server to indicate which response headers should be made available to scripts running in the browser, in response to a cross-origin request.

developer.mozilla.org

 

 

서버에서 설정을 해주면 이렇게 content-disposition에 접근해서 파일 이름을 가져올 수 있게 됨.

const contentDisposition =
            response.payload.headers["content-disposition"]; // 파일 이름

 

content-disposition을 가져와서 쓸데없는 부분은 다 떼주고 파일 이름이 한글 이름이었어서 디코딩까지 시켜주었다.

let fileName = "unknown";
          if (contentDisposition) {
            const [fileNameMatch] = contentDisposition
              .split(";")
              .filter((str) => str.includes("filename"));
            if (fileNameMatch) [, fileName] = fileNameMatch.split("=");
          }
          fileName = decodeURIComponent(fileName).slice(7);

 

파일 이름은 서버에서 보내주는 그대로 설정돼서 다운로드는 되나, 자꾸 파일이 손상됐다고 나왔다.

암만 뒤져봐도 도대체 뭐가 잘못됐는지 모르던 찰나 깨달았다.

api 호출할 때 header랑 위치를 바꿔서 호출했다는 사실을 ㅠ 옵션 다음 header를 넣어야 한다. 완전 허무..

 

그렇게 완성된 코드..

link.click() 때문에 해당 함수가 두 번 호출될 수 있어서 이 부분은 필터를 걸어줘야 한다

const request = axios
    .get(
      `${process.env.REACT_APP_DEFAULT_URL}/api`,
      options,
      { headers }
    )
    .then((response) => response);
const header = {
      Authorization: token,
    };
    const options = {
      responseType: "blob",
    };
// 성공
          const contentDisposition =
            response.payload.headers["content-disposition"]; // 파일 이름
          let fileName = "unknown";
          if (contentDisposition) {
            const [fileNameMatch] = contentDisposition
              .split(";")
              .filter((str) => str.includes("filename"));
            if (fileNameMatch) [, fileName] = fileNameMatch.split("=");
          }
          fileName = decodeURIComponent(fileName).slice(7);
          const blobData = new Blob([response.payload.data], {
            type: response.payload.headers["content-type"],
          });

          if (blobData) {
            // Blob을 사용하여 다운로드 링크를 생성
            const link = document.createElement("a");
            link.href = URL.createObjectURL(blobData);
            link.download = fileName;
            link.target = "_blank"; // 다운로드 링크를 새 창에서 열도록 설정 (옵션)
            th.appendChild(link);
            link.click();
            th.removeChild(link);
            URL.revokeObjectURL(blobData);
          }