만들고, 클릭하고, 지워라: Blob 다운로드 Object URL(Next.js 경계)
한 문장 결론: Blob 다운로드는 object URL을 만들고 클릭으로 트리거한 뒤 즉시 revoke로 해제하는 흐름이 반복 실행에서도 누적 리스크를 줄인다. Next.js에서는 브라우저 API 실행 위치를 Client Component로 고정하는 게 갈린다.
**한 문장 결론: Blob 다운로드는 `URL.createObjectURL()`로 object URL(객체 URL, `blob:` URL)을 만들고, 다운로드를 트리거한 뒤 `URL.revokeObjectURL()`로 즉시 해제하는 흐름이 가장 안전하게 간다.**
다운로드는 “한 번 되면 끝”이 아니다. 반복 실행에서 리소스가 쌓이면 UX/성능/안정성이 같이 무너진다. 핵심은 이거다. **만들되, 쓰자마자 지운다.**
배경/문제
프런트엔드에서 데이터(CSV/로그/임시 리포트)를 파일로 저장시키는 흔한 흐름은 “데이터 → Blob(블롭: 브라우저의 파일 같은 원시 데이터 컨테이너) → object URL(객체 URL: `blob:` 스킴의 임시 URL) → `<a download>` 클릭”이다. 문제는 다운로드가 되느냐가 아니라, **반복 실행에서 정리가 되느냐**다. object URL을 해제하지 않으면 누적될 수 있다(환경/정책에 따라 체감은 달라질 수 있다).
근거: [MDN – `URL.createObjectURL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static), [MDN – `URL.revokeObjectURL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL_static), [MDN – `blob:` URLs (Memory management)](https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/blob)
핵심 개념
흐름 고정(Mermaid)
```mermaid
flowchart LR
A["데이터 생성<br/>(string/ArrayBuffer 등)"] -->|"Blob로 감싼다"| B["Blob 생성"]
B -->|"URL.createObjectURL(blob)"| C["object URL 생성<br/>(blob:...)"]
C -->|"a.href에 연결"| D["<a download> 준비"]
D -->|"a.click()로 트리거"| E["브라우저 다운로드 시작"]
E -->|"URL.revokeObjectURL(url)"| F["object URL 해제<br/>(리소스 정리 가능)"]
```
→ 기대 결과/무엇이 달라졌는지: 트리거와 해제가 한 흐름으로 묶여, 반복 다운로드에서 누적 위험이 줄어든다.
Next.js 경계
`document`, `window`, `URL.createObjectURL()`는 브라우저 전용 API다. Next.js에선 **Client Component(클라이언트 컴포넌트: `'use client'`로 브라우저 실행을 고정하는 컴포넌트)** 로 실행 위치를 못 박아야 한다.
근거: [Next.js – Server and Client Components](https://nextjs.org/docs/app/getting-started/server-and-client-components), [Next.js – `use client` Directive](https://nextjs.org/docs/app/api-reference/directives/use-client)
해결 접근
1) Blob 만들기(왜: 다운로드 대상을 표준 객체로 표현) → 기대 결과: 타입/인코딩을 명시해 후속 로직이 단순해진다.
2) object URL 만들기 + `<a download>` 클릭(왜: 브라우저 다운로드 UX 재사용) → 기대 결과: 라이브러리 없이도 자연스러운 다운로드가 된다.
3) `URL.revokeObjectURL()`로 해제(왜: 반복 실행 누적 방지) → 기대 결과: 같은 화면에서 여러 번 내려받아도 리소스 정리 포인트가 생긴다.
구현(코드)
1) “Blob → 다운로드” 유틸
```javascript
export function downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.style.display = "none";
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 0);
}
```
→ 기대 결과/무엇이 달라졌는지: 호출할 때마다 다운로드가 시작되고, object URL이 즉시 해제되어 반복 실행 시 누적 위험이 줄어든다.
2) Next.js Client Component 트리거
```typescript
'use client';
import { downloadBlob } from './downloadBlob';
export default function ExportButton() {
const onClick = () => {
const blob = new Blob(["hello\nworld\n"], { type: "text/plain;charset=utf-8" });
downloadBlob(blob, "example.txt");
};
return (
<button type="button" onClick={onClick}>
Download example.txt
</button>
);
}
```
→ 기대 결과/무엇이 달라졌는지: 서버 렌더링과 충돌하지 않고, 클릭 시 다운로드가 안정적으로 동작한다.
3) `fetch()` 응답 다운로드
```javascript
import { downloadBlob } from "./downloadBlob";
export async function downloadFromUrl(fileUrl, filename) {
const res = await fetch(fileUrl);
if (!res.ok) throw new Error(`Download failed:${res.status}`);
const blob = await res.blob();
downloadBlob(blob, filename);
}
```
→ 기대 결과/무엇이 달라졌는지: 원격 응답을 Blob으로 변환한 뒤 동일 유틸로 처리해 구현이 한 덩어리로 정리된다.
검증 방법(체크리스트)
흔한 실수/FAQ
- Q. `revokeObjectURL()`을 안 하면? → A. 자동 정리를 단정하기 어렵다. 반복 다운로드 화면이라면 해제를 기본값으로 강제하는 편이 안전하다. 근거: [MDN – `URL.revokeObjectURL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL_static)
- Q. `setTimeout(..., 0)`는 왜? → A. 다운로드는 브라우저 내부 동작이라 시작 이벤트를 직접 받기 어렵다. 클릭 직후 다음 틱에서 해제하는 패턴을 많이 쓴다.
- Q. `<a download>`는 아무 URL에나? → A. 환경/정책에 따라 제약이 있을 수 있다. 외부 URL 강제 다운로드가 목표라면 서버에서 `Content-Disposition`으로 내려주는 방식이 더 예측 가능하다. 근거: [MDN – `<a>` `download` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a)
요약(3~5줄)
Blob 다운로드는 “object URL 생성 → `<a download>` 트리거 → 즉시 revoke”가 정석이다. `revokeObjectURL`은 반복 실행 누적 리스크를 줄이는 핵심이다. Next.js에서는 브라우저 API를 Client Component로 격리해 실행 위치를 고정한다. 외부 URL/정책 통제가 필요하면 서버 `Content-Disposition` 응답이 더 예측 가능하다.
결론
다운로드는 “한 번 동작”보다 “반복해도 안전”이 기준이다. **만들고, 쓰고, 지우기.** 이 흐름을 기본값으로 두면 Next.js에서도 흔들리지 않는다.
참고
- [Next.js – Server and Client Components](https://nextjs.org/docs/app/getting-started/server-and-client-components)
- [Next.js – `use client` Directive](https://nextjs.org/docs/app/api-reference/directives/use-client)
- [Next.js – Route Handlers](https://nextjs.org/docs/app/getting-started/route-handlers)
- [MDN – `URL.createObjectURL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static)
- [MDN – `URL.revokeObjectURL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL_static)
- [MDN – `blob:` URLs (Memory management)](https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/blob)
- [MDN – `<a>` `download` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a)
- [MDN – `Content-Disposition` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Disposition)
대안/비교: 서버에서 “첨부 다운로드”로 고정하기
클라이언트 메모리에 Blob을 크게 올리고 싶지 않거나, 다운로드 정책/권한/로그를 서버에서 통제해야 한다면 Route Handler에서 Content-Disposition으로 “첨부(attachment)” 응답을 내려주는 방식이 더 예측 가능하다.
근거: Next.js – Route Handlers, MDN – Content-Disposition header
// app/api/export/route.ts
export async function GET() {
const body = "id,name\n1,Alice\n2,Bob\n";
return new Response(body, {
headers: {
"Content-Type": "text/csv; charset=utf-8",
"Content-Disposition": 'attachment; filename="export.csv"',
},
});
}→ 기대 결과/무엇이 달라졌는지: 브라우저가 “파일 다운로드”로 인식하는 응답을 받아 object URL 없이도 다운로드 UX를 고정할 수 있다.