<당신의 A태그는 다운로드를 허용하지 않습니다.(feat. html a 태그의 작동원리)>

강민수·2024년 11월 24일
1

아하 모먼트

목록 보기
8/9
post-thumbnail

1. 발단

"이거 혹시 왜 그런 지 알아요?"

갑자기, 옆에 계시던 팀장님께서 어느 날 문의를 주셨다.
문의에 대한 내용은 html a 태그를 토대로 이미지 다운로드 기능을 구현중인데,
이상하게 새창으로 띄워진다는 것 이었다.

그러면서, 이런 경우가 있냐고 물어보셨다.

내 상식으로는 당연하게도 아래 코드는 잘 작동해야 했다.

psuedo코드 이므로 감안하고 봐 주시길 바란다.

<a href="파일 다운로드 주소" download>
	<button/>
</a>

일반적으로 파일 다운로드 기능을 활용할 때는, 저렇게 html의 a 태그로 감싸고, href 주소와 download 링크 속성만 허용되지 않나?라는 아주 단순한 생각을 한다.

필자 역시 그랬기에...

아마도 이 포스팅을 쓰지 않았을까 싶다.

2. 모르면 MDN.

잘 모를땐 뭐다?

찾아봐야지...

그래서 바로 MDN의 A태그 공식 문서를 찾아봤다.

이 참에 a 태그도 다시 살펴볼 겸.

하나씩 속성을 살펴봤다.

그리고 깨달음을 조금 씩 얻을 수 있었다.

  • gpt의 첨언

그렇다.

떡하니, 나와 계시다...

동일 출처 URL과 blob:, data: 스킴에서만 작동한다.

결론적으로, 외부 url을 저기에 아무리 삽입한들 작동하지 않는다.

그렇다면, 어떻게 접근하고 다뤄야 할까?

어쨌든 우리는 다운을 받아야하지 않는가?

3. 결국 방법은 우회다.

결국 방법은 우회전술이다.

간단히 살펴보자면, 아래와 같다.

  1. 프록시 서버 사용:
    • 외부 파일을 서버로 가져온 뒤, 서버에서 클라이언트로 파일을 전달합니다. 이를 통해 파일이 같은 도메인(origin)에서 제공되는 것처럼 보이게 할 수 있습니다.
  2. 파일을 Blob으로 처리:
    • 외부 파일을 fetch로 가져온 뒤, Blob 객체를 생성하고 이를 URL.createObjectURL()로 처리하여 다운로드 링크를 생성합니다.

정말 간단히는, 위의 2번 처럼 하면 된다.


import React from 'react';

const DownloadButton: React.FC<{ fileUrl: string; fileName: string }> = ({
  fileUrl,
  fileName,
}) => {
  const handleDownload = async () => {
    try {
      // 외부 URL에서 파일 가져오기
      const response = await fetch(fileUrl);
      if (!response.ok) {
        throw new Error('파일을 가져오는 데 실패했습니다.');
      }

      // Blob 객체 생성
      const blob = await response.blob();

      // Blob URL 생성
      const url = URL.createObjectURL(blob);

      // 임시 a 태그 생성 및 클릭 트리거
      const link = document.createElement('a');
      link.href = url;
      link.download = fileName;
      document.body.appendChild(link);
      link.click();

      // 사용 후 URL 및 태그 정리
      URL.revokeObjectURL(url);
      document.body.removeChild(link);
    } catch (error) {
      console.error('다운로드 중 오류 발생:', error);
    }
  };

  return (
    <button onClick={handleDownload} style={{ padding: '10px', fontSize: '16px' }}>
      Download
    </button>
  );
};

export default DownloadButton;

하지만,

만약 2번의 경우에서도 cors 등의 에러가 난다면? (우리 팀의 경우가 그랬다...)

1) nextjs api route (app router 기준)

import { NextRequest, NextResponse } from 'next/server';

export async function GET(req: NextRequest) {
  const { searchParams } = req.nextUrl;
  const fileUrl = searchParams.get('url'); // 외부 URL 가져오기

  if (!fileUrl) {
    return NextResponse.json({ error: 'Missing file URL' }, { status: 400 });
  }

  try {
    // 외부 URL에서 파일 가져오기
    const externalResponse = await fetch(fileUrl);

    if (!externalResponse.ok) {
      return NextResponse.json(
        { error: 'Failed to fetch the file' },
        { status: externalResponse.status }
      );
    }

    // 파일 데이터 읽기
    const fileBlob = await externalResponse.blob();

    // 파일 스트림 반환
    return new Response(fileBlob, {
      headers: {
        'Content-Type': externalResponse.headers.get('content-type') || 'application/octet-stream',
        'Content-Disposition': `attachment; filename="${fileUrl.split('/').pop()}"`,
      },
    });
  } catch (error) {
    console.error('Error fetching external file:', error);
    return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
  }
}

2) 클라이언트 단 사용

'use client';

import React from 'react';

const DownloadButton: React.FC<{ externalUrl: string; fileName: string }> = ({
  externalUrl,
  fileName,
}) => {
  const handleDownload = () => {
    const apiUrl = `/api/download?url=${encodeURIComponent(externalUrl)}`;

    // 임시 a 태그 생성
    const link = document.createElement('a');
    link.href = apiUrl;
    link.download = fileName;
    document.body.appendChild(link);
    link.click();

    // 링크 정리
    document.body.removeChild(link);
  };

  return (
    <button onClick={handleDownload} style={{ padding: '10px', fontSize: '16px' }}>
      Download
    </button>
  );
};

export default DownloadButton;

그때는 사실 방법은 1번(nextjs 기준 프록시 api route.ts 활용)으로 해야 한다.

그런데 이건 사실 좋지는 않은 방법이라고 생각한다.

왜냐면, 동시에 수만명이 요청을 건다면 저 프록시 서버는 제대로 분산처리를 해 놓지 않았다면 뻗을 것이다.

그래서...

2번의 경우라면, 웬만하면 서버 단에서 cors를 허용해 달라고 서버 측에 요청해 주는 것이 좋겠다.

아니면, 프론트에서 잘 분산처리해서 로드 밸런싱을 저런 프록시마다 세부 설정하는 수고를 해줘야 할 것이다.

4. 결론

간단하다고 생각하는 html 태그 역시, 제대로 원리를 알고 쓰자.

html은 고도로 발전되어 있고, 보안적으로도 신경을 많이 쓴 기술이다.

따라서, 항상 왜 안 될까보다는 그렇게 기술이 나온 원리를 파악하고 대처하는 것이 좋겠다.

추가로, 위에서 언급했듯이 해결책 및 그에 따른 방지책 역시 구비해 둬야 한다.

그래야만, 뒤탈이 없다....

그럼 오늘은 이만 이렇게 짧게 마쳐본다.

다음 주제로 다시 돌아오겠다.

profile
개발도 예능처럼 재미지게~

0개의 댓글

관련 채용 정보