React - 파일 다운로드

sarang_daddy·2023년 12월 16일
0

React

목록 보기
24/26
post-thumbnail

사용자의 요청을 처리하는 기능 구현 중 사용자의 첨부 자료를 다운로드하여 확인해야하는 기능이 필요하다. React에서 파일을 다운로드 하는 방법을 구현하고 정리해보자.

서버로부터 파일 소스 받아오기

  • 사용자 정보에서 첨부된 파일의 소스를 알 수 있다.
  • 서버주소에 해당 파일소스를 연결하여 요청하면 파일 다운로드가 가능하다.
  • ${serverUrl}${creatorDetailData.file_src}
{
  ...
  "affiliation": "Tesggr",
  "grade": "초3",
  "nickname": "스파냐",
  "name": "테스트",
  ...
  "state": 1,
  "file_src": "/creator/20069242-dec1-4284-be9c-2bc030ce0aa9.jpg",
  "category": [24],
  ...
}

1차 시도 : window.open() 메서드

  • 처음 시도는 window.open() 메서드로 해당 주소로 이동하면 다운로드가 가능하다고 생각했다.
const handleFileDownload = () => {
    window.open(`${serverUrl}/${creatorDetailData.file_src}`);
  };
  • 첨부된 파일의 확인이 가능하지만 새로운 브라우저 탭에서의 내용 확인으로 동작되었다.
  • 기획서에서 요구하는 구현방식은 컴퓨터에 파일이 직접 다운로드되는 방식이다.

2차 시도 : <a> 요소의 download

  • 링크 이동이 아닌 파일을 직접 다운로드하기 위해서는 <a> 요소의 다운로드 속성을 사용한다.
  const handleFileDownload = () => {
    const a = document.createElement("a");
    a.href = `${process.env.NEXT_PUBLIC_BASIC_URL}${creatorDetailData.file_src}`;
    a.download = creatorDetailData.file_src.split("/").pop() || "download";
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
  };
  • 하지만, window.open()과 같이 다운로드가 아닌 새 탭에서 파일을 보여주는 방식으로 동작한다.
  • mdn을 자세히 살펴보니 CORS 문제로 다른 출처의 URL을 가리키면 보안상의 이유로 download 속성을 무시하고 파일을 새 탭이나 창에서 열도록 동작함을 알 수 있다.

CORS란?

  • Cross-Oring Resource Sharing (교차 출처 정책)
  • 웹 페이지는 기본적으로 자신과 다른 도메인의 리소스 접근을 제한한다.
  • 이것을 동일 출처 정책(same-orgin policy)이라 한다.
  • 하지만 현대의 웹은 다양한 서비스가 통합되면서 다른 출처의 리소스 요청이 필수적이다.
  • 이를 해결하기 위해 CORS가 등장했다.
  • 서버에서 특정 출처 요청을 수락하고 이를 HTTP 응답 헤더를 통해 브라우저에게 알려준다.
  • 브라우저는 서버로부터 받은 CORS 헤더를 확인하고 특정 출처의 요청을 허용한다.

    CORS는 SOP의 제한을 안전하게 완화하기 위한 방법이다.

3차 시도 : fetch API

  • fetch API를 사용해 파일을 blob형태로 가져오고 이 blob을 이용해 브라우저에서 직접 다운로드할 수 있는 URL을 생성할 수 있다.
const handleFileDownload = () => {
    setIsDownloading(true);
    const fileUrl = `${serverUrl}${creatorDetailData.file_src}`;

    fetch(fileUrl)
      .then((response) => response.blob())
      .then((blob) => {
        const url = window.URL.createObjectURL(new Blob([blob]));
        const a = document.createElement("a");
        a.href = url;
        a.download = creatorDetailData.file_src.split("/").pop() || "download";
        document.body.appendChild(a);
        a.click();
        window.URL.revokeObjectURL(url);
        document.body.removeChild(a); 
        setIsDownloading(false);
      })
      .catch((error) => {
        console.error("파일 다운로드 오류:", error);
        setIsDownloading(false);
      });
  };

🧐 fetch API 방식을 사용한 이유

  • CORS 설정 변경 없이도 클라이언트 측에서 다운로드가 가능하다.
  • promise 메서드를 사용하여 에러 핸들링이 가능하다.
  • promise 객체의 상태값으로 다운로드 중임과 완료를 시각적으로 알려주어 UX를 향상시킨다.

결과

  • Axios를 사용중임으로 fecth를 Axios로 변경
export function CreatorSettingForm({
  creatorDetailData,
  categoryList,
}: CreatorSettingFormProps) {
  const [isDownloading, setIsDownloading] = useState(false);

  const handleFileDownload = () => {
    setIsDownloading(true);
    const fileUrl = `${creatorDetailData.file_src}`;

    client // AxiosInstance
      .get(fileUrl, { responseType: "blob" })
      .then((response) => {
        const url = window.URL.createObjectURL(new Blob([response.data]));
        const a = document.createElement("a");
        a.href = url;
        a.download = creatorDetailData.file_src.split("/").pop() || "download";
        document.body.appendChild(a);
        a.click();
        window.URL.revokeObjectURL(url);
        document.body.removeChild(a); 
        setIsDownloading(false);
      })
      .catch((error) => {
        console.error("파일 다운로드 오류:", error);
        setIsDownloading(false);
      });
  };

  return (
    <div className="space-y-6">
      <div>
        <h3 className="text-lg font-medium sticky">크리에이터 신청</h3>
        <CreatorDetailTable
          creatorDetailData={creatorDetailData}
          categoryList={categoryList}
        />
        <div>
          {isDownloading ? (
            <Button disabled>
              <Loader2 className="mr-2 h-4 w-4 animate-spin" />
              다운로드 중
            </Button>
          ) : (
            <Button onClick={handleFileDownload}>첨부파일 다운로드</Button>
          )}
        </div>
      </div>
    </div>
  );
}

profile
한 발자국, 한 걸음 느리더라도 하루하루 발전하는 삶을 살자.

0개의 댓글