[TIL/Amazon S3] 2025/05/17

์›๋ฏผ๊ด€ยท2025๋…„ 5์›” 17์ผ

[TIL]

๋ชฉ๋ก ๋ณด๊ธฐ
181/201
post-thumbnail

Image Processing Optimization ๐ŸŽจ

0. Overview โœ…

Question Content์— ํฌํ•จ๋˜๋Š”, ์ด๋ฏธ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์†Œ๊ฐœํ•˜๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค.

Amazon S3๋ž€, ์ด๋ฏธ์ง€ / ๋™์˜์ƒ / ๋ฌธ์„œ / ๋ฐฑ์—… ๋ฐ์ดํ„ฐ ๋“ฑ ๋‹ค์–‘ํ•œ ํŒŒ์ผ์„ ์ €์žฅํ•˜๊ณ  ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š”, AWS์—์„œ ์ œ๊ณตํ•˜๋Š” ํด๋ผ์šฐ๋“œ ๊ฐ์ฒด ์Šคํ† ๋ฆฌ์ง€ ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค.

Base64๋กœ ์‚ฝ์ž…๋œ ์ด๋ฏธ์ง€๋ฅผ WebP ํŒŒ์ผ๋กœ ๋ณ€ํ™˜ํ•ด AWS S3์— ์—…๋กœ๋“œํ•˜๊ณ , HTML ์ฝ˜ํ…์ธ ์˜ <img> ํƒœ๊ทธ src๋ฅผ S3 ์ด๋ฏธ์ง€ URL๋กœ ๊ต์ฒดํ•˜๋Š” ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ์ตœ์ ํ™”์— ๋Œ€ํ•ด ์„ค๋ช…ํ•˜๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค.

1. processContentImages() โœ…

  async processContentImages(content: string): Promise<string> {
    const parser = new DOMParser();

    const doc = parser.parseFromString(content, "text/html");

    const images = doc.querySelectorAll("img");

    for (const img of images) {
      if (img.src.startsWith("data:image")) {
        try {
          const file = await this.convertBase64ToWebPFileWithFallback(img.src);
          const uploadURL = await this.uploadFileToS3(file);
          img.src = uploadURL;
        } catch (error) {
          console.error("Error processing image:", error);
        }
      }
    }

    return doc.getElementsByTagName("body")[0].innerHTML;
  }

์งˆ๋ฌธ ์ œ์ถœ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋Š” ์ˆœ๊ฐ„ ๊ฐ€์žฅ ๋จผ์ € ๋™์ž‘ํ•˜๊ฒŒ ๋˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

์ด ํ•จ์ˆ˜๋Š” HTML ๋ฌธ์ž์—ด(content)์„ ๋ฐ›์•„, ๊ทธ ์•ˆ์˜ ์ด๋ฏธ์ง€ ํƒœ๊ทธ ์ค‘ base64๋กœ ์ธ์ฝ”๋”ฉ๋œ ์ด๋ฏธ์ง€(data:image/...)๋ฅผ ์ฐพ์•„ WebP ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•œ ๋’ค AWS S3์— ์—…๋กœ๋“œํ•˜๊ณ , ํ•ด๋‹น ์ด๋ฏธ์ง€ ํƒœ๊ทธ์˜ src๋ฅผ ์—…๋กœ๋“œ๋œ S3 URL๋กœ ๊ต์ฒดํ•œ ํ›„, ์ตœ์ข…์ ์œผ๋กœ ์ˆ˜์ •๋œ HTML ๋ฌธ์ž์—ด์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.


๋จผ์ € base64๋กœ ์ธ์ฝ”๋”ฉ๋˜์—ˆ๋‹ค๋Š” ๊ฒƒ์ด ์–ด๋–ค ์˜๋ฏธ์ธ์ง€ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

์ด๋ฏธ์ง€๋Š” ํ”ฝ์…€์˜ ์ง‘ํ•ฉ์ž…๋‹ˆ๋‹ค. RGB ์ƒ‰์ƒ ๋ชจ๋ธ์˜ ๊ด€์ ์—์„œ ๋ณด๋ฉด, ๊ฐ ํ”ฝ์…€์€ 0๋ถ€ํ„ฐ 255 ์‚ฌ์ด์˜ ์ˆซ์ž ๊ฐ’์œผ๋กœ ํ‘œํ˜„๋ฉ๋‹ˆ๋‹ค. 0์ด ์ƒ‰์ƒ์ด ์ „ํ˜€ ์—†๋Š” ์ƒํƒœ๋ผ๋ฉด, 255๋Š” ๊ฐ€์žฅ ๊ฐ•ํ•œ ์ƒ‰์ƒ์„ ๋œปํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, ์ด๋ฏธ์ง€๋Š” ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ์ž…๋‹ˆ๋‹ค.

Base64๋Š” ์ปดํ“จํ„ฐ๊ฐ€ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐ”์ด๋„ˆ๋ฆฌ(0๊ณผ 1๋กœ ์ด๋ฃจ์–ด์ง„ ์ด์ง„ ๋ฐ์ดํ„ฐ)๋ฅผ ํ…์ŠคํŠธ ๋ฌธ์ž(์•ŒํŒŒ๋ฒณ, ์ˆซ์ž, ํŠน์ˆ˜๊ธฐํ˜ธ)๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณ€ํ™˜ํ•˜๋Š” ์ธ์ฝ”๋”ฉ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ์ด๋ฏธ์ง€๋ฅผ ๋น„๋กฏํ•œ ๋ชจ๋“  ์ด์ง„ ํŒŒ์ผ์€ ์›๋ž˜ RGB ๊ฐ’ ๋“ฑ์œผ๋กœ ๊ตฌ์„ฑ๋œ ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ์ง€๋งŒ, HTML์ด๋‚˜ JSON ๊ฐ™์€ ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๊ทธ๋Œ€๋กœ ์“ธ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์—, base64๋กœ ์ธ์ฝ”๋”ฉํ•ด ํ…์ŠคํŠธ ํ˜•ํƒœ๋กœ ๋ฐ”๊ฟ” ์ €์žฅํ•˜๊ฑฐ๋‚˜ ์ „์†กํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฌธ์ž์—ด๋กœ ํ‘œํ˜„ํ•˜๊ณ  ์›น ๋ฌธ์„œ๋‚˜ API ๋“ฑ์— ์‚ฝ์ž…ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.


๋‹ค์Œ์œผ๋กœ parser์— ๋Œ€ํ•ด ์•Œ์•„๋ด์•ผ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

parser๋Š”, ๋ฌธ์ž์—ด ํ˜•ํƒœ์˜ HTML, XML ๊ฐ™์€ ๋ฌธ์„œ๋ฅผ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ดํ•ดํ•˜๋Š” DOM(Document Object Model) ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•ด ์ฃผ๋Š” ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. ํ…์ŠคํŠธ๋ฅผ DOM ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•ด์•ผ ํ˜„์žฌ์˜ document์—์„œ ์ด๋ฏธ์ง€ 'ํƒœ๊ทธ'๋ฅผ ์ถ”์ถœํ•  ์ˆ˜ ์žˆ๊ฒ ์ฃ .


parser์—์„œ ์ œ๊ณตํ•˜๋Š” parseFromString() ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด, ๋ฌธ์ž์—ด ํ˜•ํƒœ์ธ content๋ฅผ HTML ๋ฌธ์„œ ๊ตฌ์กฐ(DOM)๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

content์— ์ด๋ฏธ์ง€ ํ•œ ์žฅ์„ ๋‹ด์•„์„œ ์ „๋‹ฌํ•˜๋ฉด doc ๋ณ€์ˆ˜์—๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ DOM ์ •๋ณด๊ฐ€ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.

 #document (http://localhost:5173/edit)
    <html>
      <head></head>
       <body>
         <p>
          <img src="https://react-nest-bucket.s3.ap-northeast-2.amazonaws.com/images/image-1747463476560.webp">
        </p>
      </body>
    </html>

doc(DOM ๊ฐ์ฒด)์— querySelectorAll() ๋ฉ”์„œ๋“œ๋ฅผ ์ ์šฉํ•˜์—ฌ, ์ด๋ฏธ์ง€ ํƒœ๊ทธ๋งŒ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.

์ด์ œ ์ด๋ฏธ์ง€ ํƒœ๊ทธ์— ํฌํ•จ๋œ src๋ฅผ ํ†ตํ•ด convertBase64ToWebPFileWithFallback() ํ•จ์ˆ˜์™€ uploadFileToS3() ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•œ ๋’ค, ์ด๋ฏธ์ง€ URL์„ S3์˜ URL๋กœ ๊ต์ฒดํ•˜๊ณ , ๋ณ€ํ˜•๋œ doc์˜ boby์˜ ์ฒซ ๋ฒˆ์งธ ์š”์†Œ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

<p>
 <img src="https://react-nest-bucket.s3.ap-northeast-2.amazonaws.com/images/image-1747463476560.webp">
</p>

2. convertBase64ToWebPFileWithFallback() โœ…

processContentImages() ํ•จ์ˆ˜์—์„œ ์ด๋ฏธ์ง€ ํƒœ๊ทธ์˜ src๋ฅผ ์ถ”์ถœํ•œ ๋’ค ๊ฐ€์žฅ ๋จผ์ € ์‹คํ–‰๋˜๋Š” ํ•จ์ˆ˜๊ฐ€ ๋ฐ”๋กœ convertBase64ToWebPFileWithFallback() ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

async convertBase64ToWebPFileWithFallback(base64: string): Promise<File> {
    return new Promise((resolve) => {
      const img = new Image();
      img.src = base64;

      img.onload = () => {
        const canvas = document.createElement("canvas");
        canvas.width = img.width;
        canvas.height = img.height;

        const ctx = canvas.getContext("2d");
        if (!ctx) {
          resolve(this.convertBase64ToOriginal(base64));
          return;
        }

        ctx.drawImage(img, 0, 0);

        canvas.toBlob(
          (blob) => {
            if (blob) {
              const webpFile = new File([blob], `image-${Date.now()}.webp`, {
                type: "image/webp",
              });
              resolve(webpFile);
            } else {
              resolve(this.convertBase64ToOriginal(base64));
            }
          },
          "image/webp",
          0.8
        );
      };

      img.onerror = () => resolve(this.convertBase64ToOriginal(base64));
    });
  }

convertBase64ToWebPFileWithFallback() ํ•จ์ˆ˜์— ๋“ค์–ด์˜ค๋Š” base64๋Š” "data:image"๋กœ ์‹œ์ž‘ํ•˜๋Š” ์ด๋ฏธ์ง€ URL์ž…๋‹ˆ๋‹ค.

const img = new Image();
img.src = base64;

์œ„ ๊ณผ์ •์ด ์„ ํ–‰๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

base64 ๋ฌธ์ž์—ด์€ ๋‹จ์ˆœ ํ…์ŠคํŠธ ๋ฐ์ดํ„ฐ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ฐ”๋กœ ์ด๋ฏธ์ง€ ํŒŒ์ผ๋กœ ๋‹ค๋ฃฐ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ํ…์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ์‹ค์ œ ์ด๋ฏธ์ง€์ฒ˜๋Ÿผ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ธ์‹ํ•˜๊ณ  ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” Image ๊ฐ์ฒด์— src๋ฅผ ์ง€์ •ํ•˜์—ฌ ์ด๋ฏธ์ง€๋ฅผ ๋กœ๋“œํ•˜๊ณ  ํฌ๊ธฐ๋‚˜ ํ”ฝ์…€ ๋ฐ์ดํ„ฐ ๋“ฑ์„ ์–ป๋Š” ๊ณผ์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.


const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;

const ctx = canvas.getContext("2d");

if (!ctx) {
  resolve(this.convertBase64ToOriginal(base64));
  return;
}

ctx.drawImage(img, 0, 0);

img.onload๋Š”, ์ด๋ฏธ์ง€๊ฐ€ img.src์— ์ง€์ •๋œ base64 ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค ์ฝ์–ด์„œ ์™„์ „ํžˆ ๋กœ๋“œ๋˜๋ฉด ์‹คํ–‰๋˜๋Š” ์ฝœ๋ฐฑ ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

์›น ํŽ˜์ด์ง€์— ์‹ค์ œ๋กœ ํ‘œ์‹œ๋˜์ง€ ์•Š๋Š” ๊ฐ€์ƒ์˜ <canvas> ์š”์†Œ๋ฅผ ์ƒˆ๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์ด๋ฏธ์ง€ ๋ณ€ํ™˜์„ ์œ„ํ•ด ์ž„์‹œ๋กœ ์ด๋ฏธ์ง€๋ฅผ ๊ทธ๋ฆฌ๊ฒŒ ๋  ๊ณต๊ฐ„์— ํ•ด๋‹นํ•ฉ๋‹ˆ๋‹ค. ์ดํ›„, ์บ”๋ฒ„์Šค์˜ ํฌ๊ธฐ๋ฅผ ์›๋ณธ ์ด๋ฏธ์ง€์˜ ํฌ๊ธฐ ์™€ ๋™์ผํ•˜๊ฒŒ ๋งž์ถฐ์„œ, ์ด๋ฏธ์ง€๊ฐ€ ์™œ๊ณก ์—†์ด ๊ทธ๋Œ€๋กœ ๊ทธ๋ ค์งˆ ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

const ctx = canvas.getContext("2d");

canvas์— getContext("2d") ๋ฉ”์„œ๋“œ๋ฅผ ์ ์šฉํ•œ ๋ชจ์Šต์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. canvas๊ฐ€ ๋„ํ™”์ง€๋ผ๋ฉด, ctx๋Š” ๋„ํ™”์ง€์— ๊ทธ๋ฆผ์„ ๊ทธ๋ฆด ์ˆ˜ ์žˆ๋Š” ๋ถ“์„ ๋ฐ›๋Š” ๊ณผ์ •์ž…๋‹ˆ๋‹ค. ์ •ํ™•ํžˆ๋Š” 2d ํ˜•ํƒœ์˜ ๊ทธ๋ฆผ์„ ๊ทธ๋ฆฌ๊ธฐ ์œ„ํ•œ ๋ถ“์„ ๋ฐ›์€ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ดํ›„ drawImage() ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์บ”๋ฒ„์Šค ์ขŒ์ƒ๋‹จ(0,0)๋ถ€ํ„ฐ ๊ทธ๋ฆผ์„ ๊ทธ๋ฆฌ๊ธฐ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.

  canvas.toBlob(
          (blob) => {
            if (blob) {
              const webpFile = new File([blob], `image-${Date.now()}.webp`, {
                type: "image/webp",
              });
              resolve(webpFile);
            } else {
              resolve(this.convertBase64ToOriginal(base64));
            }
          },
          "image/webp",
          0.8
        );

์œ„ ์ฝ”๋“œ๊ฐ€ convertBase64ToWebPFileWithFallback() ํ•จ์ˆ˜์˜ ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค.

Blob์€ Binary Large Object์˜ ์•ฝ์ž๋กœ, 0๊ณผ 1๋กœ ์ด๋ฃจ์–ด์ง„ ์ด์ง„ ๋ฐ์ดํ„ฐ์ž…๋‹ˆ๋‹ค. ์œ„ ์ฝ”๋“œ๋Š”, ์บ”๋ฒ„์Šค์— ๊ทธ๋ฆฐ ๊ทธ๋ฆผ์„ ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ๋กœ ๋ณ€ํ™˜ํ•œ ๋’ค webp ํŒŒ์ผ ํ˜•์‹์œผ๋กœ ๋‹ค์‹œ ๋ฐ˜ํ™˜ํ•˜๋Š” ์ฝ”๋“œ๋ผ๊ณ  ์š”์•ฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์บ”๋ฒ„์Šค์˜ ์‹œ๊ฐ์  ์ •๋ณด๋Š” ๋ฉ”๋ชจ๋ฆฌ์— ํ”ฝ์…€ ๋‹จ์œ„๋กœ ์ €์žฅ๋˜๋Š”๋ฐ, ์ด ์ •๋ณด๋ฅผ ๋ฐ”๋กœ webp ํŒŒ์ผ ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•˜๊ธฐ ์–ด๋ ต๊ธฐ ๋•Œ๋ฌธ์—, ์ค‘๊ฐ„ ๋‹จ๊ณ„๋กœ Blob์ด๋ผ๋Š” ๋ฒ”์šฉ ์ด์ง„ ๋ฐ์ดํ„ฐ ํ˜•์‹์œผ๋กœ ๋จผ์ € ๋ณ€ํ™˜ํ•ด ์ฃผ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.


convertBase64ToWebPFileWithFallback() ํ•จ์ˆ˜์—์„œ webp ํŒŒ์ผ๋กœ ๋ณ€ํ™˜์— ์‹คํŒจํ•˜๋Š” ๋ชจ๋“  ์ƒํ™ฉ์—์„œ๋Š”, convertBase64ToOriginal() ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์ด๋ฏธ์ง€๋ฅผ ๋” ํšจ์œจ์ ์ธ WebP ํฌ๋งท์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋˜, ๋ธŒ๋ผ์šฐ์ €๊ฐ€ WebP๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋ณ€ํ™˜์— ์‹คํŒจํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•ด ์›๋ณธ JPEG๋กœ ๋Œ์•„๊ฐ€๋Š” ์•ˆ์ „์žฅ์น˜(fallback)๋ฅผ ๊ตฌํ˜„ํ•œ ํ•จ์ˆ˜๋ผ๊ณ  ์ •๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒ ์Šต๋‹ˆ๋‹ค.

3. convertBase64ToOriginal() โœ…

  async convertBase64ToOriginal(src: string): Promise<File> {
    const base = atob(src.split(",")[1]);

    const blob = Uint8Array.from(base, (char) => char.charCodeAt(0));

    return new File([blob], `image-${Date.now()}.jpeg`, { type: "image/jpeg" });
  }

convertBase64ToWebPFileWithFallback() ํ•จ์ˆ˜์˜ ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. WebP ๋ณ€ํ™˜์— ์‹คํŒจํ–ˆ์„ ๊ฒฝ์šฐ ์›๋ณธ JPEG ํŒŒ์ผ์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

data:image/jpeg;base64,/9j/4AAQSkZJRg...๊ณผ ๊ฐ™์€ ํ˜•์‹์—์„œ ์‹ค์ œ base64 ๋ฐ์ดํ„ฐ ๋ถ€๋ถ„๋งŒ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฅผ atob() ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์ด์ง„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ดํ›„ '์ด์ง„ ๋ฌธ์ž์—ด'์„ ์‹ค์ œ ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ๋กœ ๋ณ€ํ™˜ํ•œ ๊ฐ’์ด blob์ด ๋ฉ๋‹ˆ๋‹ค. ํ•ด๋‹น ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ๋กœ jpeg ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ฒŒ ๋˜์ฃ .

์‹ค์ œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ด์ง„ ๋ฌธ์ž์—ด์ด ๋˜๊ณ , ์ด์ง„ ๋ฌธ์ž์—ด์ด ์‹ค์ œ ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ๋กœ ๋ณ€ํ™˜๋˜๋Š” ๊ณผ์ •์„ ์ดํ•ดํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


๋งŒ์•ฝ , ์ดํ›„์˜ ์‹ค์ œ ๋ฐ์ดํ„ฐ๊ฐ€ "SGk="๋ผ๋ฉด ์ด์ง„ ๋ฌธ์ž์—ด์€ "Hi"์ด๊ณ  ๋ฐ”์ดํŠธ ๋ฐฐ์—ด์€ [72, 105]๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

์—ญ์œผ๋กœ ์ƒ๊ฐํ•ด ๋ณด์ฃ .

H์˜ ASCII ๊ฐ’์€ 72(01001000), i์˜ ASCII ๊ฐ’์€ 105(01101001)์ž…๋‹ˆ๋‹ค. ์ด๋Š” ์ด 16๋น„ํŠธ์ด์ฃ . ํ•˜์ง€๋งŒ Base64๋Š” ํ•ญ์ƒ 3๋ฐ”์ดํŠธ(24๋น„ํŠธ) ๋‹จ์œ„๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ธ์ฝ”๋”ฉํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ๋ถ€์กฑํ•œ 1๋ฐ”์ดํŠธ(8๋น„ํŠธ)๋Š” 0์œผ๋กœ ํŒจ๋”ฉํ•˜์—ฌ ์•„๋ž˜์™€ ๊ฐ™์ด 24๋น„ํŠธ๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

01001000 01101001 00000000

์ด์ œ ์ด 24๋น„ํŠธ๋ฅผ 6๋น„ํŠธ ๋‹จ์œ„๋กœ ์ชผ๊ฐœ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

010010 / 000110 / 100100 / 000000

๊ฐ 6๋น„ํŠธ ๋ธ”๋ก์„ 10์ง„์ˆ˜๋กœ ๋ณ€ํ™˜ํ•˜๋ฉด 18, 6, 36, 0์ด ๋˜๊ณ , Base64 ์ธ์ฝ”๋”ฉ ํ…Œ์ด๋ธ”์—์„œ ๊ฐ๊ฐ์— ๋Œ€์‘ํ•˜๋Š” ๋ฌธ์ž๋Š” "S", "G", "k", "A"์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋Š” 2๋ฐ”์ดํŠธ(Hi)์˜€๊ณ , ๋งˆ์ง€๋ง‰ ๋ฐ”์ดํŠธ๋Š” ํŒจ๋”ฉ์œผ๋กœ ์ถ”๊ฐ€๋œ ๊ฒƒ์ด๋ฏ€๋กœ "A"๋Š” ์ œ๊ฑฐ๋˜๊ณ , ๊ทธ ์ž๋ฆฌ๋Š” =๋กœ ๋Œ€์ฒด๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ตœ์ข… ๊ฒฐ๊ณผ๋Š” "SGk="์ž…๋‹ˆ๋‹ค.


Base64๋Š” ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฅผ ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ํ™˜๊ฒฝ(HTML, JSON ๋“ฑ)์— ์•ˆ์ „ํ•˜๊ฒŒ ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•œ ์ธ์ฝ”๋”ฉ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ์ด ๋ฐฉ์‹์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์›๋ฆฌ๋กœ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์ปดํ“จํ„ฐ์˜ ๋ฐ์ดํ„ฐ๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ 8๋น„ํŠธ(1๋ฐ”์ดํŠธ) ๋‹จ์œ„๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.

Base64๋Š” 6๋น„ํŠธ ๋‹จ์œ„๋กœ ์ชผ๊ฐญ๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด 6๋น„ํŠธ๋Š” 2^6 = 64๊ฐœ์˜ ๊ฐ’์„ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋Š” ํ…์ŠคํŠธ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” 64๊ฐœ์˜ ๋ฌธ์ž(A-Z, a-z, 0-9, +, /)์™€ ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ 8๋น„ํŠธ์™€ 6๋น„ํŠธ๋Š” ์„œ๋กœ ๋”ฑ ๋งž์•„๋–จ์–ด์ง€์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์—, 6๊ณผ 8์˜ ์ตœ์†Œ๊ณต๋ฐฐ์ˆ˜์ธ 24๋น„ํŠธ ๋‹จ์œ„๋กœ ๋ฌถ์–ด ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ธ์ฝ”๋”ฉ ๋ฉ๋‹ˆ๋‹ค.

3๊ฐœ์˜ 8๋น„ํŠธ(3๋ฐ”์ดํŠธ) = 24๋น„ํŠธ โ†’ 6๋น„ํŠธ์”ฉ 4์กฐ๊ฐ์œผ๋กœ ๋‚˜๋ˆ„์–ด 4๊ธ€์ž๋กœ ์ธ์ฝ”๋”ฉ

๋ฐ˜๋Œ€๋กœ ๋””์ฝ”๋”ฉ ํ•  ๋•Œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์›๋ž˜๋Œ€๋กœ ๋ณต์›๋ฉ๋‹ˆ๋‹ค.

4๊ฐœ์˜ Base64 ๋ฌธ์ž โ†’ ๊ฐ๊ฐ 6๋น„ํŠธ = 24๋น„ํŠธ โ†’ 3๋ฐ”์ดํŠธ(8๋น„ํŠธ์”ฉ)๋กœ ๋ณต์›

"SGk="๋Š” ์‹ค์ œ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ ๋ถ€๋ถ„์— ๋Œ€ํ•œ ์˜ˆ์‹œ์˜€์Šต๋‹ˆ๋‹ค.

์ด ๊ฐ’์€ base64 ์ธ์ฝ”๋”ฉ์„ ๊ฑฐ์นœ ๊ฐ’์ธ๋ฐ "Hi"๋ฅผ ์ธ์ฝ”๋”ฉํ•˜๋ฉด "SGk="๊ฐ€ ๋œ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ๋ฐ˜๋Œ€๋กœ "SGk="๋ฅผ ๋””์ฝ”๋”ฉ ํ•˜๋ฉด, "Hi"๊ฐ€ ๋˜๊ฒ ์ฃ . ์ด๋•Œ "Hi"์— ๋Œ€์‘๋˜๋Š” ๋ฐ”์ด๋„ˆ๋ฆฌ๊ฐ€ [72, 105]๊ฐ€ ๋œ๋‹ค๊ณ  ์ดํ•ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

4. uploadFileToS3() โœ…

  constructor() {
    AWS.config.update({
      accessKeyId: import.meta.env.VITE_AWS_ACCESS_KEY_ID,
      secretAccessKey: import.meta.env.VITE_AWS_SECRET_ACCESS_KEY,
      region: import.meta.env.VITE_AWS_REGION,
    });

    this.s3 = new AWS.S3();
  }

์ฝ”๋“œ ์ƒ๋‹จ์—์„œ S3 Service๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ๊ธฐ๋ณธ์ ์ธ Configuration ์„ค์ •์„ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

  async uploadFileToS3(file: File): Promise<string> {
    const params = {
      Bucket: import.meta.env.VITE_AWS_BUCKET_NAME,
      Key: `images/${file.name}`,
      Body: file,
      ContentType: file.type,
    };

    return new Promise<string>((resolve, reject) => {
      this.s3.upload(
        params,
        (
          err: Error | null,
          data: AWS.S3.ManagedUpload.SendData | undefined
        ) => {
          if (err) {
            reject(err);
          } else {
            resolve(data?.Location || "");
          }
        }
      );
    });
  }

๋‚˜๋ฆ„๋Œ€๋กœ์˜ ๋ณ€ํ™˜ ๊ณผ์ •์„ ๊ฑฐ์นœ ์ด๋ฏธ์ง€ ํŒŒ์ผ์„, ์ตœ์ข…์ ์œผ๋กœ S3 Bucket์— ์—…๋กœ๋“œํ•˜๋Š” ๋กœ์ง์ž…๋‹ˆ๋‹ค. ๋‹จ์ˆœํ•œ ์‚ฌ์šฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

5. Conclusion โœ…

HTML ์ฝ˜ํ…์ธ ์— ํฌํ•จ๋œ, Base64๋กœ ์ธ์ฝ”๋”ฉ๋œ ์ด๋ฏธ์ง€๋ฅผ WebP ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ AWS S3์— ์—…๋กœ๋“œํ•˜๋Š” ๊ณผ์ •์„ ์„ค๋ช…ํ–ˆ์Šต๋‹ˆ๋‹ค.

processContentImages() ํ•จ์ˆ˜์—์„œ ์‹œ์ž‘ํ•˜์—ฌ, HTML ๋ฌธ์„œ์—์„œ Base64 ์ด๋ฏธ์ง€๋ฅผ ์ฐพ์•„ convertBase64ToWebPFileWithFallback() ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๋” ํšจ์œจ์ ์ธ WebP ํฌ๋งท์œผ๋กœ ๋ณ€ํ™˜ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ณ€ํ™˜์— ์‹คํŒจํ•  ๊ฒฝ์šฐ์—๋Š” convertBase64ToOriginal() ํ•จ์ˆ˜๋กœ ์›๋ณธ JPEG ํ˜•์‹์„ ์œ ์ง€ํ–ˆ์ฃ .

์ตœ์ข…์ ์œผ๋กœ uploadFileToS3() ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๋ณ€ํ™˜๋œ ์ด๋ฏธ์ง€๋ฅผ S3์— ์—…๋กœ๋“œํ•˜๊ณ  HTML์˜ ์ด๋ฏธ์ง€ ํƒœ๊ทธ src ์†์„ฑ์„ S3 URL๋กœ ๊ต์ฒดํ•จ์œผ๋กœ์จ ์›น ํŽ˜์ด์ง€์˜ ๋กœ๋”ฉ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ค๋Š” ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ์†”๋ฃจ์…˜์„ ๊ฐœ๋ฐœํ–ˆ์Šต๋‹ˆ๋‹ค.

profile
Write a little every day, without hope, without despair โœ๏ธ

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