"이거 혹시 왜 그런 지 알아요?"
갑자기, 옆에 계시던 팀장님께서 어느 날 문의를 주셨다.
문의에 대한 내용은 html a 태그를 토대로 이미지 다운로드 기능을 구현중인데,
이상하게 새창으로 띄워진다는 것 이었다.
그러면서, 이런 경우가 있냐고 물어보셨다.
내 상식으로는 당연하게도 아래 코드는 잘 작동해야 했다.
psuedo
코드 이므로 감안하고 봐 주시길 바란다.
<a href="파일 다운로드 주소" download>
<button/>
</a>
일반적으로 파일 다운로드 기능을 활용할 때는, 저렇게 html의 a 태그
로 감싸고, href
주소와 download
링크 속성만 허용되지 않나?라는 아주 단순한 생각을 한다.
필자 역시 그랬기에...
아마도 이 포스팅을 쓰지 않았을까 싶다.
잘 모를땐 뭐다?
찾아봐야지...
그래서 바로 MDN의 A태그 공식 문서를 찾아봤다.
이 참에 a 태그도 다시 살펴볼 겸.
하나씩 속성을 살펴봤다.
그리고 깨달음을 조금 씩 얻을 수 있었다.
그렇다.
떡하니, 나와 계시다...
동일 출처 URL과 blob:, data: 스킴에서만 작동한다.
결론적으로, 외부 url을 저기에 아무리 삽입한들 작동하지 않는다.
그렇다면, 어떻게 접근하고 다뤄야 할까?
어쨌든 우리는 다운을 받아야하지 않는가?
결국 방법은 우회전술이다.
간단히 살펴보자면, 아래와 같다.
- 프록시 서버 사용:
• 외부 파일을 서버로 가져온 뒤, 서버에서 클라이언트로 파일을 전달합니다. 이를 통해 파일이 같은 도메인(origin)에서 제공되는 것처럼 보이게 할 수 있습니다.- 파일을 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를 허용해 달라고 서버 측에 요청해 주는 것이 좋겠다.
아니면, 프론트에서 잘 분산처리해서 로드 밸런싱을 저런 프록시마다 세부 설정하는 수고를 해줘야 할 것이다.
간단하다고 생각하는 html 태그 역시, 제대로 원리를 알고 쓰자.
html은 고도로 발전되어 있고, 보안적으로도 신경을 많이 쓴 기술이다.
따라서, 항상 왜 안 될까보다는 그렇게 기술이 나온 원리를 파악하고 대처하는 것이 좋겠다.
추가로, 위에서 언급했듯이 해결책 및 그에 따른 방지책 역시 구비해 둬야 한다.
그래야만, 뒤탈이 없다....
그럼 오늘은 이만 이렇게 짧게 마쳐본다.
다음 주제로 다시 돌아오겠다.