๐Ÿ“ก velog์—์„œ ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•  ๋•Œ CDN์„ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•

์ง€์ฝ”ยท2025๋…„ 9์›” 23์ผ

FE STUDY

๋ชฉ๋ก ๋ณด๊ธฐ
6/8
post-thumbnail

๊ณต๋ถ€ํ•˜๋Š” ๋‚ด์šฉ๋“ค์„ ์ž์ฃผ ๊ธฐ๋กํ•˜๋Š” ๊ณณ์ด velog์ด๋‹ค๋ณด๋‹ˆ, velog๋ฅผ ์‚ดํŽด๋ณด๋ฉฐ ๋™์ž‘ํ•˜๋Š” ์›๋ฆฌ๊ฐ€ ๊ถ๊ธˆํ•ด์ง„ ๊ฒƒ๋“ค์ด ๋ช‡ ๊ฐ€์ง€๊ฐ€ ์žˆ์—ˆ๋‹ค. ๊ทธ ์ค‘ ํ•˜๋‚˜๊ฐ€ CDN!

CDN(Content Delivery Network)์ด๋ž€?

CDN์€ ๋งŽ์€ ์žฅ์†Œ์— ๊ฑธ์ณ ๋ถ„์‚ฐ๋œ ์„œ๋ฒ„๋“ค์˜ ๊ทธ๋ฃน์ž…๋‹ˆ๋‹ค.
์ด๋Ÿฌํ•œ ์„œ๋ฒ„๋“ค์€ ๋ฐ์ดํ„ฐ์˜ ์ค‘๋ณต ๋ณต์‚ฌ๋ณธ๋“ค์„ ์ €์žฅํ•˜์—ฌ, ์„œ๋ฒ„๊ฐ€ ์ตœ์ข… ์‚ฌ์šฉ์ž์™€ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ์„œ๋ฒ„๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋ฐ์ดํ„ฐ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
CDN์€ ๋†’์€ ํŠธ๋ž˜ํ”ฝ์—๋„ ์˜ํ–ฅ์„ ๋œ ๋ฐ›๋Š” ๋น ๋ฅธ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿค” CDN์„ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ ๋Š”?

  • CDN์„ ํ†ตํ•ด์„œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์ •์  asset์„ ์ œ๊ณตํ•˜๋ฉด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์ž์ฒด ์„œ๋ฒ„์— ์š”์ฒญ ๋ถ€๋‹ด์ด ์ค„์–ด๋“ญ๋‹ˆ๋‹ค.
  • ๋Œ€๋ถ€๋ถ„์˜ CDN๋“ค์€ ์ „ ์„ธ๊ณ„์— ์„œ๋ฒ„๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ CDN ์„œ๋ฒ„๋“ค์€ ์ง€๋ฆฌํ•™์ ์œผ๋กœ ํŠน์ • ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์„œ๋ฒ„๋ณด๋‹ค ์‚ฌ์šฉ์ž์™€ ๋” ๊ฐ€๊น์Šต๋‹ˆ๋‹ค. ์ง€๋ฆฌํ•™์  ๊ฑฐ๋ฆฌ๋Š” ์ง€์—ฐ์‹œ๊ฐ„์— ๋น„๋ก€ํ•˜์—ฌ ์˜ํ–ฅ์„ ๋ฏธ์นฉ๋‹ˆ๋‹ค.
  • CDN๋“ค์€ ์ด๋ฏธ ์ ์ ˆํ•œ ์บ์‹œ ์„ค์ •์ด ๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค. CDN์„ ์‚ฌ์šฉํ•˜๋ฉด ์ž์ฒด ์„œ๋ฒ„์˜ ์ •์  asset์„ ์œ„ํ•œ ์ถ”๊ฐ€์ ์ธ ์„ค์ •์„ ํ•˜์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค.

1๏ธโƒฃ blob: URL

velog ๊ธ€ ์ž‘์„ฑ ์‹œ ์—…๋กœ๋“œํ•˜๊ณ ์ž ํ•˜๋Š” ์‚ฌ์ง„์„ ๋ณต์‚ฌ-๋ถ™์—ฌ๋„ฃ๊ธฐ ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋œฌ๋‹ค.

![์—…๋กœ๋“œ์ค‘..](blob:https://velog.io/01ba3af8-7827-4c0e-9e97-9e97d2f2011d26)

์ด๊ฒƒ์€ ๋ธŒ๋ผ์šฐ์ €์˜ 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๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฒฝ์šฐ๋“ค์— ์ฃผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.

1. ์ฒญํฌ๋กœ ์—…๋กœ๋“œํ•˜๋Š” ๊ฒฝ์šฐ

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());
  }
}

2. ์ธํ„ฐ๋„ท์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์šด๋กœ๋“œํ•˜๋Š” ๊ฒฝ์šฐ

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 ์†์„ฑ์— ํ• ๋‹นํ•ด ์ด๋ฏธ์ง€๊ฐ€ ํ‘œ์‹œ๋˜๋„๋ก ํ•œ๋‹ค.

3. ์ด๋ฏธ์ง€๋ฅผ ์••์ถ•ํ•˜๋Š” ๊ฒฝ์šฐ

์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์ „์— ํŒŒ์ผ์˜ ํฌ๊ธฐ๋ฅผ ์ค„์—ฌ ์ „์†ก ์†๋„๋ฅผ ๊ฐœ์„ ํ•˜๊ณ  ์‹ถ์„ ๋•Œ, Canvas API๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

1. canvas.toDataURL(mimeType, quality)

  • mimeType์˜ default ๊ฐ’์€ image/png
  • image/jpeg ๋˜๋Š” image/webp ๋Š” quality(0~1) ์˜ต์…˜์œผ๋กœ ์••์ถ•๋ฅ  ์กฐ์ •์ด ๊ฐ€๋Šฅํ•˜๋‹ค.
  • ๋ฐ˜ํ™˜๊ฐ’์€ base64 ํ˜•ํƒœ์˜ DataURL.

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์œผ๋กœ ๋ณ€ํ™˜ํ•œ ํ›„, ์„œ๋ฒ„๋กœ ์ „์†กํ•˜๋Š” ๊ฒƒ์ด ์ผ๋ฐ˜์ ์œผ๋กœ ์“ฐ์ด๋Š” ํ๋ฆ„์ด๋‹ค.

4. Blob URL์„ ์ƒ์„ฑํ•˜๋Š” ๊ฒฝ์šฐ

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์€ ํŒŒ์ผ์„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ/๋‹ค์šด๋กœ๋“œํ•  ๋•Œ๋Š” ์œ ์šฉํ•˜์ง€๋งŒ, ์‚ฌ์šฉ ํ›„์—๋Š” ๋ฐ˜๋“œ์‹œ ํ•ด์ œํ•ด์•ผ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๋ฅผ ๋ง‰์„ ์ˆ˜ ์žˆ๋‹ค.

5. Blob์—์„œ Data 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 ์—…๋กœ๋“œ ๋ฐฉ์‹์„ ๋” ์ž์ฃผ ์‚ฌ์šฉํ•œ๋‹ค.

2๏ธโƒฃ ์„œ๋ฒ„ ์—…๋กœ๋“œ ํ›„ CDN URL๋กœ ๊ต์ฒด

![](https://velog.velcdn.com/images/id/post/d3fbbd80-db70-48eb-95d7-fe84b1bb189a/image.png)

์ด๊ฒƒ์€ ์ด๋ฏธ์ง€ ํŒŒ์ผ์˜ URL ์˜ˆ์‹œ์ด๋‹ค. ์ด URL์€ ์–ด๋–ป๊ฒŒ ์ƒ๊ธฐ๋Š” ๊ฑธ๊นŒ?

  1. ๋จผ์ € ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ multipart/form-data ์™€ ๊ฐ™์€ ํ˜•์‹์œผ๋กœ ์„œ๋ฒ„์— ์ „์†กํ•˜๋ฉด, ์„œ๋ฒ„๋Š” ๊ทธ ํŒŒ์ผ์„ ํŒŒ์ผ ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅํ•œ๋‹ค.
  2. CDN์€ ์ €์žฅ์†Œ ํŒŒ์ผ์„ ์ „ ์„ธ๊ณ„์— ์žˆ๋Š” ๋…ธ๋“œ๋“ค์— ์บ์‹ฑํ•˜๊ณ  ๋ฐฐํฌํ•œ๋‹ค.
  3. ์„œ๋ฒ„๋Š” ๋ธŒ๋ผ์šฐ์ €์—๊ฒŒ ์ตœ์ข…์ ์œผ๋กœ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ https://velog.velcdn.com/... ์™€ ๊ฐ™์€ URL์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  4. ๋ธŒ๋ผ์šฐ์ €๋Š” blob: URL์„ CDN์œผ๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ URL๋กœ ๊ต์ฒดํ•œ๋‹ค.

๐Ÿค” ์—…๋กœ๋“œํ•˜๋Š” ์ปจํ…์ธ ๊ฐ€ ๋™์ผํ•˜๋”๋ผ๋„ UUID๋Š” ์™œ ๊ณ„์† ๋ณ€๊ฒฝ๋ ๊นŒ?

post/d3fbbd80-db70-48eb-95d7-fe84b1bb189a

์ด๊ฒƒ์€ ๊ฒŒ์‹œ๊ธ€ ๋‹จ์œ„(post)์™€ ์ด๋ฏธ์ง€ ๋‹จ์œ„(image) UUID๋ฅผ ํฌํ•จํ•œ URL ๊ตฌ์กฐ์ด๋‹ค. velog์—์„œ๋Š” ๊ฐ™์€ ์ด๋ฏธ์ง€๋”๋ผ๋„ ๋ณต์‚ฌ-๋ถ™์—ฌ๋„ฃ๊ธฐํ•  ๋•Œ๋งˆ๋‹ค ์ƒˆ๋กœ์šด UUID๋ฅผ ์ƒ์„ฑํ•˜๋Š”๋ฐ, ์™œ ๊ทธ๋Ÿฐ ๊ฒƒ์ผ๊นŒ? ๊ถ๊ธˆํ•ด์กŒ๋‹ค.

์ด๊ฒƒ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ชฉ์ ์ด ์žˆ๋‹ค.

  1. ๊ฒŒ์‹œ๊ธ€์— ์ด๋ฏธ์ง€๊ฐ€ ์—ฌ๋Ÿฌ ๋ฒˆ ์‚ฌ์šฉ๋  ๋•Œ ์ถฉ๋Œํ•˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•œ๋‹ค.
  2. CDN ์บ์‹ฑ ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•œ๋‹ค.
  3. ์ˆ˜์ •/์‚ญ์ œ์— ๋Œ€ํ•ด ์ถ”์ ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

์œ„์™€ ๊ฐ™์€ ์ด์œ ๋กœ, ๋™์ผํ•œ ํŒŒ์ผ์ด๋”๋ผ๋„ ์„œ๋ฒ„์—์„œ๋Š” ์ƒˆ ํŒŒ์ผ์ฒ˜๋Ÿผ ์ทจ๊ธ‰ํ•ด์„œ URL์„ ์ƒˆ๋กœ ๋ถ€์—ฌํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

๐Ÿคก ์ •๋ฆฌํ•ด๋ณด์ž!

  1. CDN์€ ์ „ ์„ธ๊ณ„์— ๋ถ„์‚ฐ๋œ ์„œ๋ฒ„๋ง์œผ๋กœ, ์‚ฌ์šฉ์ž์™€ ๊ฐ€๊นŒ์šด ์„œ๋ฒ„์—์„œ ์ฝ˜ํ…์ธ ๋ฅผ ์ œ๊ณตํ•˜์—ฌ ์†๋„์™€ ์•ˆ์ •์„ฑ์„ ๋†’์ธ๋‹ค.
  2. ๋ธŒ๋ผ์šฐ์ €๋Š” ์—…๋กœ๋“œ ๊ณผ์ •์—์„œ blob URL์„ ๋จผ์ € ๋งŒ๋“ค์–ด ์ž„์‹œ๋กœ ์ด๋ฏธ์ง€๋ฅผ ํ‘œ์‹œํ•œ๋‹ค.
  3. Blob์€ ์ด๋ฏธ์ง€/๋™์˜์ƒ ๊ฐ™์€ ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃจ๊ธฐ ์œ„ํ•œ ๊ฐ์ฒด์ด๋ฉฐ, Data URL, Blob URL ๋“ฑ ๋‹ค์–‘ํ•œ ๋ฐฉ์‹์œผ๋กœ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
  4. ์—…๋กœ๋“œ๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ์„œ๋ฒ„๊ฐ€ ํŒŒ์ผ์„ ์ €์žฅํ•˜๊ณ , CDN์ด ์ตœ์ข…์ ์œผ๋กœ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ URL์„ ๋ฐœ๊ธ‰ํ•ด Blob URL์„ ๊ต์ฒดํ•œ๋‹ค.
  5. ๋™์ผํ•œ ํŒŒ์ผ์ด๋ผ๋„ ์„œ๋ฒ„๋Š” ์ƒˆ UUID๋ฅผ ๋ถ€์—ฌํ•ด ์บ์‹ฑ ๋ฌธ์ œ๋‚˜ ์ถฉ๋Œ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ๊ด€๋ฆฌ์„ฑ์„ ๋†’์ธ๋‹ค.

๐Ÿ‘‰ ์ •๋ฆฌํ•˜๋ฉด ์‚ฌ์šฉ์ž๋Š” ๋‹จ์ˆœํžˆ ์ด๋ฏธ์ง€๋ฅผ ๋ถ™์—ฌ๋„ฃ๊ธฐ๋งŒ ํ•˜์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” ๋ธŒ๋ผ์šฐ์ €๊ฐ€ Blob API๋ฅผ ํ†ตํ•ด ์ž„์‹œ URL์„ ์ƒ์„ฑํ•˜๊ณ , ์„œ๋ฒ„-์Šคํ† ๋ฆฌ์ง€-CDN์„ ๊ฑฐ์ณ ์ตœ์ข…์ ์œผ๋กœ ์•ˆ์ •์ ์ธ URL๋กœ ๊ต์ฒด๋˜๋Š” ํ๋ฆ„์ด ๋™์ž‘ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

Reference

CDN(์ฝ˜ํ…์ธ  ์ „์†ก ๋„คํŠธ์›Œํฌ)์ด๋ž€ ๋ฌด์—‡์ผ๊นŒ์š”? CDN์€ ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋‚˜์š”? | Akamai
MDN Web Docs ์šฉ์–ด ์‚ฌ์ „: ์›น ์šฉ์–ด ์ •์˜ > CDN
An Introduction to Blob API & Its Use Cases

profile
๊พธ์ค€ํ•จ์ด ๋ฌด๊ธฐ

0๊ฐœ์˜ ๋Œ“๊ธ€