
๊ณต๋ถํ๋ ๋ด์ฉ๋ค์ ์์ฃผ ๊ธฐ๋กํ๋ ๊ณณ์ด velog์ด๋ค๋ณด๋, velog๋ฅผ ์ดํด๋ณด๋ฉฐ ๋์ํ๋ ์๋ฆฌ๊ฐ ๊ถ๊ธํด์ง ๊ฒ๋ค์ด ๋ช ๊ฐ์ง๊ฐ ์์๋ค. ๊ทธ ์ค ํ๋๊ฐ CDN!
CDN์ ๋ง์ ์ฅ์์ ๊ฑธ์ณ ๋ถ์ฐ๋ ์๋ฒ๋ค์ ๊ทธ๋ฃน์ ๋๋ค.
์ด๋ฌํ ์๋ฒ๋ค์ ๋ฐ์ดํฐ์ ์ค๋ณต ๋ณต์ฌ๋ณธ๋ค์ ์ ์ฅํ์ฌ, ์๋ฒ๊ฐ ์ต์ข ์ฌ์ฉ์์ ๊ฐ์ฅ ๊ฐ๊น์ด ์๋ฒ๋ฅผ ๊ธฐ์ค์ผ๋ก ๋ฐ์ดํฐ ์์ฒญ์ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
CDN์ ๋์ ํธ๋ํฝ์๋ ์ํฅ์ ๋ ๋ฐ๋ ๋น ๋ฅธ ์๋น์ค๋ฅผ ์ ๊ณตํ ์ ์์ต๋๋ค.๐ค CDN์ ์ฌ์ฉํ๋ ์ด์ ๋?
- CDN์ ํตํด์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ ์ asset์ ์ ๊ณตํ๋ฉด ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์์ฒด ์๋ฒ์ ์์ฒญ ๋ถ๋ด์ด ์ค์ด๋ญ๋๋ค.
- ๋๋ถ๋ถ์ CDN๋ค์ ์ ์ธ๊ณ์ ์๋ฒ๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค. ๋ฐ๋ผ์ CDN ์๋ฒ๋ค์ ์ง๋ฆฌํ์ ์ผ๋ก ํน์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์๋ฒ๋ณด๋ค ์ฌ์ฉ์์ ๋ ๊ฐ๊น์ต๋๋ค. ์ง๋ฆฌํ์ ๊ฑฐ๋ฆฌ๋ ์ง์ฐ์๊ฐ์ ๋น๋กํ์ฌ ์ํฅ์ ๋ฏธ์นฉ๋๋ค.
- CDN๋ค์ ์ด๋ฏธ ์ ์ ํ ์บ์ ์ค์ ์ด ๋์ด์์ต๋๋ค. CDN์ ์ฌ์ฉํ๋ฉด ์์ฒด ์๋ฒ์ ์ ์ asset์ ์ํ ์ถ๊ฐ์ ์ธ ์ค์ ์ ํ์ง ์์๋ ๋ฉ๋๋ค.
blob: URLvelog ๊ธ ์์ฑ ์ ์ ๋ก๋ํ๊ณ ์ ํ๋ ์ฌ์ง์ ๋ณต์ฌ-๋ถ์ฌ๋ฃ๊ธฐ ํ๋ฉด ๋ค์๊ณผ ๊ฐ์ด ๋ฌ๋ค.

์ด๊ฒ์ ๋ธ๋ผ์ฐ์ ์ Blob API ์์ ์ ๊ณตํ๋ ๊ธฐ๋ฅ์ผ๋ก, ๋ธ๋ผ์ฐ์ ๋ด๋ถ์์๋ง ์กด์ฌํ๋ ์์ URL์ด๋ค.
์ฌ์ฉ์๊ฐ ์ ํํ๊ฑฐ๋ ๋ณต์ฌํ ์ด๋ฏธ์ง ํ์ผ์ ํด๋ผ์ด์ธํธ ๋ฉ๋ชจ๋ฆฌ์์ ์์๋ก ์ ๊ทผํ ์ ์๊ฒ ํ๋ URL์ด๊ธฐ ๋๋ฌธ์, ๋ค๋ฅธ ๋ธ๋ผ์ฐ์ ๋ ์ปดํจํฐ์์๋ ์ ๊ทผ์ด ๋ถ๊ฐ๋ฅํ๋ค.
blob: ๋ค์ ์กด์ฌํ๋ UUID(ex> 01ba3af8-7827-4c0e-9e97-9e97d2f2011d26)๋ ๋ธ๋ผ์ฐ์ ๊ฐ ์์๋ก ๋ถ์ฌํ ๊ณ ์ ์๋ณ์์ด๋ฉฐ, ์๋ฒ์ ์ ์ก๋๊ธฐ ์ ๊น์ง๋ง ์กด์ฌํ๊ธฐ ๋๋ฌธ์ 1์ด๋ ์ ๋๋ ์๊ฐ ๋์๋ง ์กด์ฌํ๋ค๊ฐ CDN URL๋ก ๊ต์ฒด๋๋ค.
์ฌ์ฉ์๊ฐ ctrl + c & ctrl + v ํ๊ฑฐ๋ drag & drop ํ๋ฉด ๋ธ๋ผ์ฐ์ ๊ฐ ๋จผ์ ์์ URL์ ๋ง๋ค์ด์ ์ด๋ฏธ์ง๊ฐ ์
๋ก๋ ์ค์์ ํ์ํ๋ ๊ฒ์ด๋ค.
Blob์ด๋?Blob(Binary Large Object) ์ ๋ฐ์ด๋๋ฆฌ ํ์
์ ํฐ ๊ฐ์ฒด๋ฅผ ๋ํ๋ธ๋ค. ์ผ๋ฐ์ ์ผ๋ก ์ด๋ฏธ์ง, ์๋ฆฌ, ๋ฉํฐ๋ฏธ๋์ด ํ์ผ์ด Blob์ ์ํ๋ค.
const myBlob = new Blob(["Hello to the world!"], {type: "text/plain"})
// myBlob
// Blob {size: 20, type: 'text/plain'}
์์ ๊ฐ์ด Blob ์์ฑ์๋ฅผ ์ด์ฉํด myBlob ๊ฐ์ฒด๋ฅผ ๋ง๋ค์๋ค. myBlob ๊ฐ์ฒด๋ size์ type์ 2๊ฐ์ง ์์ฑ์ ๊ฐ๋ ๊ฒ์ ์ ์ ์๋๋ฐ, size ์์ฑ์ ๋ฐ์ดํฐ์ ํฌ๊ธฐ(๋ฐ์ดํธ)๋ฅผ, type ์์ฑ์ ๋ฐ์ดํฐ์ ํ์
์ MIME ํ์
์ผ๋ก ๋ํ๋ธ๋ค.
Blob ๊ฐ์ฒด๋ optional type๊ณผ blobParts๋ก ์ด๋ฃจ์ด์ ธ ์๋ค.

Blob API๋ ๋ค์๊ณผ ๊ฐ์ ๊ฒฝ์ฐ๋ค์ ์ฃผ๋ก ์ฌ์ฉํ๋ค.

File ๊ฐ์ฒด๋ Blob ์ ํน์ํ ํ์
์ด๋ฉฐ, ๋ชจ๋ Blob ํ์
์ ๋งฅ๋ฝ์์ ์ฌ์ฉํ ์ ์๋ค. ๋ฐ๋ผ์ ํฐ ํ์ผ์ ์ ์กํ๊ธฐ ์ํด slice ๋ฉ์๋๋ฅผ ์ฌ์ฉํด ํ์ผ์ ๋๋๊ณ , ์ฒญํฌ ๋จ์๋ก ์
๋ก๋๋ฅผ ํ ์ ์๋ค.
const file = new File(["a".repeat(1000000)], "bytefer.txt");
const chunkSize = 40000;
const url = "https://httpbin.org/post";
async function chunkedUpload() {
for (let start = 0; start < file.size; start += chunkSize) {
const chunk = file.slice(start, start + chunkSize + 1);
const fd = new FormData();
fd.append("data", chunk);
await fetch(url, { method: "post", body: fd })
.then((res) => res.text());
}
}
const downloadBlob = (url, callback) => {
const xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'blob'
xhr.onload = () => {
callback(xhr.response)
}
xhr.send(null)
}
์ ์์ ์ downloadBlob ํจ์๋ฅผ ์ฌ์ฉํด์ ์ธํฐ๋ท์์ ๋ฐ์ดํฐ๋ฅผ ๋ค์ด๋ก๋ํ๊ณ ์ด๋ฅผ Blob ๊ฐ์ฒด์ ์ ์ฅํ ์ ์๋ค.
์คํธ๋ฆผ์ ๋ฐ์ด๋๋ฆฌ ๋ฐ์ดํฐ๋ฅผ ์ป๊ธฐ ์ํด, XMLHttpRequest API ๋์ fetch API๋ฅผ ์ฌ์ฉํ ์๋ ์๋ค.
const myImage = document.querySelector('img');
const myRequest = new Request('flowers.jpg');
fetch(myRequest)
.then(function(response) {
return response.blob();
})
.then(function(myBlob) {
let objectURL = URL.createObjectURL(myBlob);
myImage.src = objectURL;
});
์ ์ฝ๋์์๋ fetch ์์ฒญ์ ์ฑ๊ณตํ๋ฉด response ๊ฐ์ฒด์ blob() ๋ฉ์๋๋ฅผ ํธ์ถํด, response ๊ฐ์ฒด๋ก๋ถํฐ Blob ๊ฐ์ฒด๋ฅผ ์ฝ์ด์จ๋ค. ๊ทธ๋ฐ ๋ค์ createObjectURL() ๋ฉ์๋๋ฅผ ํธ์ถํด objectURL์ ์์ฑํ๊ณ , ์ด URL์ ์ด๋ฏธ์ง ์์์ src ์์ฑ์ ํ ๋นํด ์ด๋ฏธ์ง๊ฐ ํ์๋๋๋ก ํ๋ค.
์ด๋ฏธ์ง ์
๋ก๋ ์ ์ ํ์ผ์ ํฌ๊ธฐ๋ฅผ ์ค์ฌ ์ ์ก ์๋๋ฅผ ๊ฐ์ ํ๊ณ ์ถ์ ๋, Canvas API๋ฅผ ํ์ฉํ ์ ์๋ค.
1. canvas.toDataURL(mimeType, quality)
image/pngimage/jpeg ๋๋ image/webp ๋ quality(0~1) ์ต์
์ผ๋ก ์์ถ๋ฅ ์กฐ์ ์ด ๊ฐ๋ฅํ๋ค.2. Data URL โ Blob ๋ณํ
์ ์ก ํจ์จ์ ์ํด base64 ๋ฌธ์์ด์ ๋ค์ Blob ์ผ๋ก ๋ณํํ ์ ์๋ค.
canvas.toBlob((blob) => {
// blob์ ์๋ฒ๋ก ์
๋ก๋ or ๋ก์ปฌ์ ์ ์ฅ or html ํ๊ทธ์ ๋ฃ๊ธฐ
}, "image/jpeg", 0.8);
๐ ์๋ฒ๋ก ์ ์กํ๋ ์์
canvas.toBlob((blob) => {
const formData = new FormData();
formData.append("file", blob, "canvas.jpg");
fetch("/upload", {
method: "POST",
body: formData,
});
}, "image/jpeg", 0.8);
toDataURL() ์ ๋๊ธฐ๋ก ๋์ํ๋ฉฐ ๋ฌธ์์ด์ ๋ฐํํ๊ณ , toBlob() ์ ๋น๋๊ธฐ๋ก ๋์ํ๋ฉฐ Blob ๊ฐ์ฒด๋ฅผ ๋ฐํํ๊ธฐ ๋๋ฌธ์, toBlob() ์ด ๋ ์ ํฉํ๋ค.
์ ๋ฆฌํ๋ฉด, Canvas API๋ก ์ด๋ฏธ์ง๋ฅผ ๋ฆฌ์ฌ์ด์ง ํน์ ์์ถํ๊ณ , Blob์ผ๋ก ๋ณํํ ํ, ์๋ฒ๋ก ์ ์กํ๋ ๊ฒ์ด ์ผ๋ฐ์ ์ผ๋ก ์ฐ์ด๋ ํ๋ฆ์ด๋ค.
Blob URL์ Blob ๋ฐ File ๊ฐ์ฒด๋ฅผ ์ด๋ฏธ์ง์ URL ์์ค๋ ๋ฐ์ด๋๋ฆฌ ๋ฐ์ดํฐ๋ฅผ ๋ค์ด๋ก๋ํ๊ธฐ ์ํ ๋งํฌ ๋ฑ์ผ๋ก ์ฌ์ฉํ ์ ์๋๋ก ํ๋ ์์ฌ ํ๋กํ ์ฝ(pseudo-protocol)์ด๋ค.
๋ธ๋ผ์ฐ์ ์์ Blob URL์ ์์ฑํ๊ธฐ ์ํด URL.createObjectURL() ํจ์๋ฅผ ์ฌ์ฉํ๋๋ฐ, ์ด ํจ์๋ Blob ๊ฐ์ฒด๋ฅผ ๊ฐ์ ธ์์ blob:<origin>/<uuid> ํํ์ ๊ณ ์ ํ URL์ ์์ฑํ๋ค.
์์ฑ๋ URL ํํ๋ ๋ค์๊ณผ ๊ฐ์ ํํ์ด๋ค.
blob:https://velog.io/01ba3af8-7827-4c0e-9e97-9e97d2f2011d26
๋ธ๋ผ์ฐ์ ๋ Blob ๊ฐ์ฒด๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ์ ์งํ๊ณ , ๋ด๋ถ์ ์ผ๋ก URL โ Blob ๋งตํ์ ๊ด๋ฆฌํ๋ค. Blob ๊ฐ์ฒด๋ ๋ฌธ์๊ฐ ์ธ๋ก๋๋ ๋ ์๋์ผ๋ก ํด์ ๋์ง๋ง, ์ฑ์ด ์ค๋ ์คํ๋๋ฉด ๋ถํ์ํ๊ฒ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์ฐจ์งํ ์ ์๋ค. ๋ฐ๋ผ์ Blob์ด ๋ ์ด์ ํ์ ์์ ๋๋ URL.revokeObjectURL ์ ํธ์ถํด์ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ํด์ ํด์ฃผ์ด์ผ ํ๋ค.
const download = (fileName, blob) => {
const link = document.createElement("a");
link.href = URL.createObjectURL(blob); // Blob URL ์์ฑ
link.download = fileName;
link.click();
URL.revokeObjectURL(link.href); // ๋ฉ๋ชจ๋ฆฌ ํด์
};
const btn = document.querySelector("#downloadBtn");
btn.addEventListener("click", () => {
const blob = new Blob(["Hello Blob!"], { type: "text/plain" });
download("blob.txt", blob);
});
์ ๋ฆฌํ์๋ฉด Blob URL์ ํ์ผ์ ๋ฏธ๋ฆฌ๋ณด๊ธฐ/๋ค์ด๋ก๋ํ ๋๋ ์ ์ฉํ์ง๋ง, ์ฌ์ฉ ํ์๋ ๋ฐ๋์ ํด์ ํด์ผ ๋ฉ๋ชจ๋ฆฌ ๋์๋ฅผ ๋ง์ ์ ์๋ค.
์ค๋๋ ๋๋ถ๋ถ์ ๋ธ๋ผ์ฐ์ ๋ Data URL์ด๋ผ๊ณ ๋ถ๋ฆฌ๋ ๊ธฐ๋ฅ์ ์ง์ํ๋๋ฐ, ์ด ๊ธฐ๋ฅ์ ์ด๋ฏธ์ง์ ๋ค๋ฅธ ํ์ผ์ ๋ฐ์ด๋๋ฆฌ ๋ฐ์ดํฐ๋ฅผ base64๋ก ์ธ์ฝ๋ฉํด ์น ํ์ด์ง์ ํ ์คํธ ๋ฌธ์์ด ํํ๋ก ํฌํจ๋ ์ ์๋๋ก ํด์ฃผ๋ ๊ธฐ๋ฅ์ด๋ค.
Data URL์ ๋ค์๊ณผ ๊ฐ์ ๊ตฌ์กฐ๋ก ์ด๋ฃจ์ด์ ธ ์๋ค.
data:[<mediatype>][;base64],<data>
์ ๋์ฌ(data:)์ ๋ฐ์ดํฐ์ ํ์ ์ ๋ํ๋ด๋ MIME ํ์ , optional ์์์ธ base64 ํ๊ทธ, ๋ฐ์ดํฐ ์์ฒด ์ด๋ ๊ฒ ๋ค ๊ฐ์ง ๋ถ๋ถ์ผ๋ก ์ด๋ฃจ์ด์ ธ ์๋ค.
mediatype ์ MIME ํ์
์ ๋ฌธ์์ด๋ก, ๊ธฐ๋ณธ๊ฐ์ text/plain;charset=US-ASCII์ด๋ค. ๋ฐ๋ผ์ ๋ฐ์ดํฐ๊ฐ ํ
์คํธ ํ์
์ด๋ผ๋ฉด ํ
์คํธ๋ฅผ ๋ฐ๋ก ํฌํจ์ํฌ ์ ์๋ค. ๋ง์ฝ ๋ฐ์ด๋๋ฆฌ ํ์
์ด๋ผ๋ฉด, ํฌํจ์ํค๊ธฐ ์ ์ base64 ์ธ์ฝ๋ฉ์ ํ ์ ์๋ค.
Data URL์ ์์ ์ด๋ฏธ์ง๋ ์์ด์ฝ์ Data URL๋ก ๋ณํํด HTML/CSS์ ์ง์ ํฌํจํ๋ฉด ๋คํธ์ํฌ ์์ฒญ ์๋ฅผ ์ค์ผ ์ ์๋ค๋ ์ฅ์ ์ด ์์ง๋ง, base64 ์ธ์ฝ๋ฉ์ ์๋ณธ๋ณด๋ค ํฌ๊ธฐ๊ฐ ์ฝ 33% ์ ๋ ๋์ด๋๊ธฐ ๋๋ฌธ์ ํฐ ์ด๋ฏธ์ง์๋ ๋นํจ์จ์ ์ด๋ฉฐ, ํ์ด์ง ๋ก๋ฉ ์๋๋ฅผ ์ ํ์ํฌ ์ ์๋ค๋ ๋จ์ ์ด ์๋ค.
๊ทธ๋์ FileReader API๋ฅผ ํตํด ๊ฐํธํ๊ฒ ์ด๋ฏธ์ง์ ๋ก์ปฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ธฐ๋ฅ์ ๊ตฌํํ ์ ์๋ค.
<input type="file" accept="image/*" onchange="loadFile(event)">
<img id="output"/>
<script>
const loadFile = function(event) {
const reader = new FileReader();
reader.onload = function(){
const output = document.querySelector('output');
output.src = reader.result;
};
reader.readAsDataURL(event.target.files[0]);
};
</script>
์ ์์ ์ฝ๋์์๋ ํ์ผ ํ์
์
๋ ฅ ๋ฐ์ค์ onChange ์ด๋ฒคํธ ํธ๋ค๋ฌ์ loadFile() ํจ์๋ฅผ ๋ฐ์ธ๋ฉํ๋ค. ์ด ํจ์์์๋ FileReader ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ณ ํด๋น ๊ฐ์ฒด์ onload ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ๋ฐ์ธ๋ฉํ ๋ค์, FileReader ๊ฐ์ฒด์ readAsDataURL() ๋ฉ์๋๋ฅผ ํธ์ถํด ๋ก์ปฌ ์ด๋ฏธ์ง์ ํด๋นํ๋ File ๊ฐ์ฒด๋ฅผ Data URL๋ก ๋ณํํ๋ค.
ํด๋ผ์ด์ธํธ์์ Data URL ํํ๋ก ์๋ฒ์ ์ ์กํ๋ฉด, ์๋ฒ๋ base64๋ก ์ธ์ฝ๋ฉ๋ ๋ฐ์ดํฐ๋ฅผ ๋ค์ ๋ฐ์ด๋๋ฆฌ๋ก ๋์ฝ๋ฉํด์ ํ์ผ๋ก ์ ์ฅํ ์ ์๋ค.
ํ์ง๋ง ์ค๋ฌด์์๋ ๋ณดํต Data URL๋ณด๋ค FormData๋ multipart ์
๋ก๋ ๋ฐฉ์์ ๋ ์์ฃผ ์ฌ์ฉํ๋ค.

์ด๊ฒ์ ์ด๋ฏธ์ง ํ์ผ์ URL ์์์ด๋ค. ์ด URL์ ์ด๋ป๊ฒ ์๊ธฐ๋ ๊ฑธ๊น?
multipart/form-data ์ ๊ฐ์ ํ์์ผ๋ก ์๋ฒ์ ์ ์กํ๋ฉด, ์๋ฒ๋ ๊ทธ ํ์ผ์ ํ์ผ ์คํ ๋ฆฌ์ง์ ์ ์ฅํ๋ค.https://velog.velcdn.com/... ์ ๊ฐ์ URL์ ๋ฐํํ๋ค.blob: URL์ CDN์ผ๋ก๋ถํฐ ๋ฐ์ URL๋ก ๊ต์ฒดํ๋ค.post/d3fbbd80-db70-48eb-95d7-fe84b1bb189a
์ด๊ฒ์ ๊ฒ์๊ธ ๋จ์(post)์ ์ด๋ฏธ์ง ๋จ์(image) UUID๋ฅผ ํฌํจํ URL ๊ตฌ์กฐ์ด๋ค. velog์์๋ ๊ฐ์ ์ด๋ฏธ์ง๋๋ผ๋ ๋ณต์ฌ-๋ถ์ฌ๋ฃ๊ธฐํ ๋๋ง๋ค ์๋ก์ด UUID๋ฅผ ์์ฑํ๋๋ฐ, ์ ๊ทธ๋ฐ ๊ฒ์ผ๊น? ๊ถ๊ธํด์ก๋ค.
์ด๊ฒ์ ๋ค์๊ณผ ๊ฐ์ ๋ชฉ์ ์ด ์๋ค.
์์ ๊ฐ์ ์ด์ ๋ก, ๋์ผํ ํ์ผ์ด๋๋ผ๋ ์๋ฒ์์๋ ์ ํ์ผ์ฒ๋ผ ์ทจ๊ธํด์ URL์ ์๋ก ๋ถ์ฌํ๋ ๊ฒ์ด๋ค.
๐ ์ ๋ฆฌํ๋ฉด ์ฌ์ฉ์๋ ๋จ์ํ ์ด๋ฏธ์ง๋ฅผ ๋ถ์ฌ๋ฃ๊ธฐ๋ง ํ์ง๋ง, ์ค์ ๋ก๋ ๋ธ๋ผ์ฐ์ ๊ฐ Blob API๋ฅผ ํตํด ์์ URL์ ์์ฑํ๊ณ , ์๋ฒ-์คํ ๋ฆฌ์ง-CDN์ ๊ฑฐ์ณ ์ต์ข ์ ์ผ๋ก ์์ ์ ์ธ URL๋ก ๊ต์ฒด๋๋ ํ๋ฆ์ด ๋์ํ๋ ๊ฒ์ด๋ค.
CDN(์ฝํ
์ธ ์ ์ก ๋คํธ์ํฌ)์ด๋ ๋ฌด์์ผ๊น์? CDN์ ์ด๋ป๊ฒ ์๋ํ๋์? | Akamai
MDN Web Docs ์ฉ์ด ์ฌ์ : ์น ์ฉ์ด ์ ์ > CDN
An Introduction to Blob API & Its Use Cases