๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

์นดํ…Œ๊ณ ๋ฆฌ ์—†์Œ

[aws-s3] ์—ฐ๊ฒฐํ•˜๊ธฐ

728x90
๋ฐ˜์‘ํ˜•

๐ŸŸข ํ•™์Šต๋ชฉํ‘œ

aws-s3 ์™€ ๋‚˜์˜ project๋ฅผ ์—ฐ๊ฒฐํ•˜์—ฌ ๊ฐ„๋‹จํ•˜๊ฒŒ file upload๋ฅผ test ํ•ด๋ณธ๋‹ค.

๐Ÿ”ต ์ˆœ์„œ

๐Ÿฌ aws ๋กœ๊ทธ์ธ ํ›„ S3 ์„œ๋น„์Šค์—์„œ ๋ฒ„ํ‚ท์ƒ์„ฑ

์ด๋ฆ„์ž‘์„ฑ ํ›„ region ์€ ์ด ๋ฒ„ํ‚ท์„ ๋ฐฐํฌํ•˜๋Š” ์ง€์—ญ ์ •๋„๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค. ์•„๋ฌด๊ฑฐ๋‚˜ ์„ ํƒํ›„

## ๊ฐ์ฒด ์†Œ์œ ๊ถŒ์„ ํ™œ์„ฑํ™”ํ•ด์ค€๋‹ค.

๋ชจ๋“  ํผ๋ธ”๋ฆญ ์•ก์„ธ์Šค ์ฐจ๋‹จ์„ ๋น„ํ™œ์„ฑํ™” ํ•ด์ค€๋‹ค.

๐Ÿฌ ๋ฒ„ํ‚ท ์ •์ฑ… ์„ค์ •

์ •์ฑ…์„ ์„ค์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ์—…๋กœ๋“œ๊ฐ€ ๋œ ํŒŒ์ผ์„ ๋งํฌ๋กœ ๋ฐ›์œผ๋ ค๊ณ  ํ•ด๋„ ๋ฐ›์•„์ง€์ง€๊ฐ€ ์•Š๋Š”๋‹ค. -> ๊ฐ์ฒด uri denied

๋งŒ๋“ค์–ด์ง„ ๋ฒ„ํ‚ท์— ๋“ค์–ด๊ฐ€ ๊ถŒํ•œ tap ์œผ๋กœ ๋„˜์–ด๊ฐ€ ์ •์ฑ…์„ค์ • ํŽธ์ง‘์„ ํ•œ๋‹ค.

์•„๋ž˜ ๋‚ด์šฉ์„ ๋ถ™์—ฌ๋„ฃ๋Š”๋‹ค.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicRead",
      "Effect": "Allow",
      "Principal": "*",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:PutObjectAcl"
      ],
      "Resource": "arn:aws:s3:::testcolor99b/*"
    }
  ]
}

- version : aws iam ์ •์ฑ… ๋ฌธ์„œ์˜ ๋ฒ„์ „

- statement : ์ •์ฑ… ๋ฌธ์„œ์˜ ์ฃผ์š” ๋ถ€๋ถ„์œผ๋กœ, ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•˜๋Š” ํ•˜๋‚˜ ์ด์ƒ์˜ ์„ ์–ธ๋ฌธ์„ ํฌํ•จํ•œ๋‹ค.
 - sid : statement ์— ๋Œ€ํ•œ ๊ณ ์œ  ์‹๋ณ„์ž ์ œ๊ณต
 - effect : ์ •์ฑ… ์Šคํ…Œ์ดํŠธ๋จผํŠธ์—์„œ ์ง€์ •๋œ ์ž‘์—…(action ์— ํ•ด๋‹นํ•˜๋Š” ๋‚ด์šฉ)์— ๋Œ€ํ•œ ๊ถŒํ•œ์„ ํ—ˆ์šฉํ•œ๋‹ค.
 - Principal : ๊ถŒํ•œ์„ ๋ถ€์—ฌ๋ฐ›๋Š” ๋Œ€์ƒ์„ ์ง€์ •ํ•œ๋‹ค. * ์€ ๋ชจ๋“  ์‚ฌ์šฉ์ž๋ฅผ ์˜๋ฏธํ•œ๋‹ค.
 - Action : ์Šคํ…Œ์ดํŠธ๋จผํŠธ์— ๋Œ€ํ•œ ํ—ˆ์šฉ ๋˜๋Š” ๊ฑฐ๋ถ€ํ•  aws ์„œ๋น„์Šค ์ž‘์—…์˜ ๋ชฉ๋ก์„ ๋‚˜ํƒ€๋‚ธ๋‹ค.
    - s3:GetObject : ํŠน์ • ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š” ๊ถŒํ•œ
    - s3:PutObject : ์ƒˆ ๊ฐ์ฒด๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ๊ธฐ์กด ๊ฐ์ฒด๋ฅผ ๋ฎ์–ด์“ธ ์ˆ˜ ์žˆ๋Š” ๊ถŒํ•œ
    - s3:PutObjectAcl : ๊ฐ์ฒด์— ๋Œ€ํ•œ ์•ก์„ธ์Šค ์ œ์–ด ๋ชฉ๋ก์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋Š” ๊ถŒํ•œ
 - Resource : ์ด ์ •์ฑ…์ด ์ ์šฉ๋˜๋Š” resource ์ง€์ •. ์—ฌ๊ธฐ์„  testcolor99b ๋ผ๋Š” ์ด๋ฆ„์˜ s3 ๋ฒ„ํ‚ท์˜ ๋ชจ๋“  ๊ฐ์ฒด์— ์ ์šฉํ•˜๊ฒŒ ๋œ๋‹ค.

๐Ÿฌ cors ์„ค์ • : ์›น app์—์„œ ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์˜ ๋ฆฌ์†Œ์Šค์— ์•ก์„ธ์Šค ํ•˜๋Š” ๊ฒƒ์„ ํ—ˆ์šฉํ•˜๊ฑฐ๋‚˜ ์ œํ•œํ•˜๋Š” ์„ค์ •

[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "GET",
            "PUT",
            "POST",
            "HEAD"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": [
            "x-amz-server-side-encryption",
            "x-amz-request-id",
            "x-amz-id-2"
        ],
        "MaxAgeSeconds": 3000
    }
]

- AllowedHeaders : cors ์š”์ฒญ์—์„œ ํ—ˆ์šฉ๋˜๋Š” http ํ—ค๋”๋ฅผ ๋‚˜์—ดํ•œ๋‹ค. * ์€ ๋ชจ๋“  ํ—ค๋”๋ฅผ ํ—ˆ์šฉํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•œ๋‹ค.

- AllowedMethods : cors ์š”์ฒญ์—์„œ ํ—ˆ์šฉ๋˜๋Š” http ๋ฉ”์„œ๋“œ. ์—ฌ๊ธฐ์„œ๋Š” get, put, post, head

- AllowedOrigins : cors ์š”์ฒญ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋Š” ์›น ํŽ˜์ด์ง€์˜ ๋„๋ฉ”์ธ์„ ๋‚˜์—ด. *์€ ๋ชจ๋“  ๋„๋ฉ”์ธ์—์„œ ์˜ค๋Š” ์š”์ฒญ์„ ํ—ˆ๋ฝํ•œ๋‹ค๋Š” ๋œป

- ExposeHeaders : cors ์š”์ฒญ์— ๋Œ€ํ•œ ์‘๋‹ต์—์„œ ๋…ธ์ถœ๋˜๋Š” ํ—ค๋”๋ฅผ ๋‚˜์—ดํ•œ๋‹ค. ์ด ํ—ค๋”๋“ค์€ js์—์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋œ๋‹ค.

- MaxAgeSeconds : cors ๊ด€๋ จ ์ •๋ณด๋ฅผ ๋ธŒ๋ผ์šฐ์ €์˜ ์บ์‹œ์— ์ €์žฅํ•˜๋Š” ์‹œ๊ฐ„์ดˆ๋‹จ์œ„. 3000์ดˆ๋Š” 50๋ถ„์œผ๋กœ ์ด ๊ธฐ๊ฐ„ ๋™์•ˆ ๋™์ผํ•œ ์š”์ฒญ์ด ๋ฐœ์ƒํ•˜๋ฉด ๋ธŒ๋ผ์šฐ์ €๋Š” ์„œ๋ฒ„์—๊ฒŒ ์ƒˆ๋กœ์šด cors ์š”์ฒญ์„ ๋ณด๋‚ด์ง€ ์•Š๊ณ  ์บ์‹œ๋œ ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

๐ŸŸฃ myproject front ์—์„œ input ์ž‘์„ฑ

๐Ÿฌ formData ํ˜•ํƒœ๋กœ ์ž‘์„ฑ

๋ณดํ†ต image๋งŒ ํˆญ ๋ณด๋‚ด๋Š” ๊ฒฝ์šฐ๋Š” ๊ฑฐ์˜ ์—†์œผ๋ฏ€๋กœ, ์—ฌ๋Ÿฌ๊ฐ€์ง€ ๋ถ€๊ฐ€์ ์ธ ์„ค๋ช…, ์ด๋ฆ„, ๋“ฑ๋“ฑ์„ ๋„ฃ๋Š”๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๊ณ  formData ๋กœ ์ง„ํ–‰ํ•˜๊ธฐ ์œ„ํ•ด Upload๋ฅผ ํ•˜๋Š” component๋ฅผ ๋”ฐ๋กœ test์šฉ์œผ๋กœ ๋งŒ๋“ค์—ˆ๋‹ค.

// UploadForm.tsx

// components/UploadForm.tsx
import React, { ChangeEvent, FormEvent, useState } from "react";
import { upload } from "@/apis";
const UploadForm: React.FC = () => {
  const [file, setFile] = useState<File | null>(null);

  const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
    if (event.target.files && event.target.files[0]) {
      setFile(event.target.files[0]);
    }
  };

  const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();

    if (!file) return;

    const formData = new FormData();
    formData.append("image", file);
    for (const [key, value] of formData.entries()) {
      console.log(`${key}:`, value);
    }

    try {
      console.log(formData);
      const response = await upload(formData);
      console.log(response);
    } catch (error) {
      console.error("Error uploading image:", error);
    }
  };

  return (
    <>
      <form onSubmit={handleSubmit}>
        <input type="file" accept="image/*" onChange={handleFileChange} />

        <button type="submit">Upload</button>
      </form>
      <button
        onClick={(e) => {
          console.log(file);
        }}
      >
        ํ™•์ธ
      </button>
    </>
  );
};

export default UploadForm;

handleFileChange ํ•จ์ˆ˜๋Š” input ํƒœ๊ทธ๋กœ ์„ ํƒํ•œ ํŒŒ์ผ์ด ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค state๋ฅผ ๋ณ€๊ฒฝํ•ด์ฃผ๋Š” ์—ญํ• .

handleSubmit ํ•จ์ˆ˜๋Š” new FormData() ํ˜•ํƒœ๋กœ ์••์ถ•ํ•ด์„œ server๋กœ ๋ณด๋‚ด์ฃผ๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.

์ค‘๊ฐ„์— ์žˆ๋Š” console.log key์™€ value๋Š” formData ๋Š” ๋‹จ์ˆœ console.log๋กœ๋Š” ๋นˆ๊ฐ’์œผ๋กœ ๋ฐ–์— ํ™•์ธํ•  ์ˆ˜ ์—†๋Š”๋ฐ ์ด๋Š” ๋‹ค๋ฅธ ํฌ์ŠคํŒ…์—์„œ ์„ค๋ช…ํ•œ๋‹ค.

https://yjunvlog.tistory.com/42

๐ŸŸ  front api code ์ž‘์„ฑ

export const upload = async (formData: FormData) => {
  console.log("ํผ๋ฐํƒ€");
  for (const [key, value] of formData.entries()) {
    console.log(`${key}:`, value);
  }

  return await request.post("/upload", formData);
};

๐Ÿ”ด server code ์ž‘์„ฑ

app.post("/api/upload", upload.single("image"), (req: MulterRequest, res) => {
  console.log(req.body);
  console.log(req.file);
  if (!req.file) {
    return res.status(400).json({ error: "No file was provided" });
  }

  console.log(req.file.location);
  //DB ์ถ”๊ฐ€
  res.status(200).json({ location: req.file.location });
});

โญ ์ œ์ผ ์ค‘์š”ํ•œ s3Multer module ์ž‘์„ฑ

import multer from "multer";
import multerS3 from "multer-s3";
import AWS from "aws-sdk";

const S3_BUCKET = "testcolor99b";

AWS.config.update({
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  region: process.env.AWS_REGION,
});

const myBucket = new AWS.S3({
  region: process.env.AWS_REGION,
});
const upload = multer({
  storage: multerS3({
    s3: myBucket as any,
    bucket: S3_BUCKET,
    contentType: multerS3.AUTO_CONTENT_TYPE,
    acl: "public-read",
    contentDisposition: "inline",
    key: (req, file, cb) => {
      file.originalname = Buffer.from(file.originalname, "latin1").toString(
        "utf8"
      );
      cb(null, Date.now().toString() + "_" + file.originalname);
    },
  }),
});

export default upload;

AWS.config~~~ : aws ์„ค์ •์„ ์—…๋ฐ์ดํŠธํ•˜๋Š”๋ฐ ํ™˜๊ฒฝ ๋ณ€์ˆ˜์—์„œ ๊ฐ€์ ธ์˜จ ๊ฐ’์œผ๋กœ ์„ธํŒ…ํ•œ๋‹ค.

const myBucket = new ~~ : aws s3 instance๋ฅผ code์ƒ์—์„œ ์ƒ์„ฑํ•œ๋‹ค.

upload ~~ : multer ๊ฐ์ฒด๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๊ณ  ์ €์žฅ์†Œ๋ฅผ multerS3๋กœ ์„ค์ •ํ•œ๋‹ค. ์ด๋Š” ํŒŒ์ผ์ด S3์— ์ €์žฅ๋˜๋„๋ก ์„ค์ •ํ•œ๋‹ค.
   s3 : s3 ์ธ์Šคํ„ด์Šค
   bucket : s3bucket ์ด๋ฆ„ ์„ค์ •
   acl : ํŒŒ์ผ์˜ ์ ‘๊ทผ ๊ถŒํ•œ public-read ๋Š” ๋ˆ„๊ตฌ๋‚˜ ์ฝ์„ ์ˆ˜ ์žˆ๋„๋ก ํ—ˆ์šฉ
   contentDisposition : ํŒŒ์ผ์˜ ์ „์†กํ˜•ํƒœ๋ฅผ inline์œผ๋กœ ์„ค์ •. ๋ธŒ๋ผ์šฐ์ €์—์„œ ํŒŒ์ผ์ด ํ‘œ์‹œ๋˜๋„๋ก ํ—ˆ์šฉ
   key : ํŒŒ์ผ์ด s3์— ์ €์žฅ๋  ๋•Œ ์‚ฌ์šฉํ•  ํ‚ค(ํŒŒ์ผ ๊ฒฝ๋กœ ๋ฐ ์ด๋ฆ„) ๋ฅผ ์ •์˜ํ•˜๋Š” ํ•จ์ˆ˜.

๐Ÿฌ file.originame ์„ ์™œ ์ˆ˜์ •ํ•ด??

์˜์–ด์™€ ์ˆซ์ž๋กœ ๊ตฌ์„ฑ๋œ .image* ํŒŒ์ผ์„ upload ํ•˜๋ฉด ์ž˜ ์˜ฌ๋ผ๊ฐ„๋‹ค. ํ•˜์ง€๋งŒ aws๋„ ๊ทธ๋ ‡๊ณ  vs-code ๋„ ๊ทธ๋ ‡๊ณ  ํ•œ๊ธ€ ๊ธฐ์ค€์ด ์•„๋‹ˆ๊ธฐ์— ํ•œ๊ธ€๋กœ upload๋ฅผ ํ•˜๋ฉด ์ด์ƒํ•˜๊ฒŒ ๊ธ€์ž๊ฐ€ ๊นจ์ง€๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

๊ธฐ๋ณธ์ ์œผ๋กœ multer๋Š” ํŒŒ์ผ ์ด๋ฆ„์„ latin1 ์ธ์ฝ”๋”ฉ์œผ๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค. latin1์€ ASCII ๋ฌธ์ž์™€ ์ผ๋ถ€ ํ™•์žฅ๋œ Latin ๋ฌธ์ž๋งŒ ์ง€์›ํ•˜๊ธฐ์— ํ•œ๊ธ€๊ณผ ๊ฐ™์€ ์œ ๋‹ˆ์ฝ”๋“œ ๋ฌธ์ž๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํ‘œํ˜„ํ•  ์ˆ˜ ์—†๋Š” ๊ฒƒ์ด๋‹ค.

๋”ฐ๋ผ์„œ ์œ ๋‹ˆ์ฝ”๋“œ ๋ฌธ์ž๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” utf8 ์ธ์ฝ”๋”ฉ ๋ฐฉ์‹์œผ๋กœ ๋ฐ”๊ฟ”์„œ ์ €์žฅ์„ ํ•ด์ฃผ๋Š” ๊ฒƒ์ด๋‹ค.

๋‹ค๋งŒ, ์ด๋ฏธ์ง€๋ฅผ ๋‹ค์‹œ front๋กœ ๋ถˆ๋Ÿฌ๋‚ผ๋•Œ img URI ๋ฅผ ์‚ฌ์šฉํ•ด์•ผํ•˜๋Š”๋ฐ ์ด URI๋Š” s3์— upload ํ• ๋•Œ response๋กœ ๋ฐ˜ํ™˜๋ฐ›๋Š” ๊ฐ’์œผ๋กœ, ์ด URI๋Š” %3DX% ์ด๋Ÿฐ์‹์œผ๋กœ ๋‚˜์˜ค๋Š”๋ฐ ์ด๊ฑธ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์€ ๋” ์ฐพ์•„๋ด์•ผ ํ•  ๊ฒƒ ๊ฐ™๋‹ค.
--> ํ•ด๊ฒฐ

๐Ÿฌ percentEncoding

๋”ฐ๋กœ ์„ค์ •ํ•ด์ค€ ๊ฒƒ์ด ์—†๋Š”๋ฐ, ๊ธฐ๋ณธ์ ์œผ๋กœ return ๋ฐ›๋Š” ๊ฐ’์ด %3DX% ์ด๋Ÿฐ์‹์œผ๋กœ ๋‚˜ํƒ€๋‚˜๋Š” ๊ฒƒ์€ ํ•œ๊ธ€์ด percent-encoded ๋˜์–ด ์žˆ๋Š” ๊ฒƒ์ธ๋ฐ. ์›น์—์„œ๋Š” ํŠน์ˆ˜๋ฌธ์ž, ๊ณต๋ฐฑ, ํ•œ๊ธ€ ๋“ฑ๊ณผ ๊ฐ™์€ ๋ฌธ์ž๋“ค์„ ์•ˆ์ „ํ•˜๊ฒŒ ์ „์†กํ•˜๊ธฐ ์œ„ํ•ด ์ด๋Ÿฐ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•œ๋‹ค.

ํ•œ๊ธ€๋กœ ๋œ uri๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ์ธํ„ฐ๋„ท ํ‘œ์ค€ ๊ทœ์•ฝ ๊ทœ์ •์— ์–ด์šธ๋ฆฌ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์›๋ณธ ํ•œ๊ธ€์ด๋ฆ„์„ ์‚ฌ์šฉํ•˜๋Š” ๋Œ€์‹  ์•ˆ์ „ํ•œ ASCII ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.

๐ŸŸฃ front์—์„œ s3์— ์—…๋กœ๋“œ๋œ ์ด๋ฏธ์ง€ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ

์•„์ฃผ ๊ฐ„๋‹จํ•˜๋‹ค. img uri ๋ฅผ img ํƒœ๊ทธ์˜ src์— ๋„ฃ์–ด์ฃผ๊ธฐ๋งŒ ํ•˜๋ฉด ok

์ฆ‰ db์™€ ์—ฐ๋™ํ•˜์—ฌ imguri๋ฅผ ๊ฐ™์ด ์ €์žฅ๋งŒ ํ•ด์ฃผ๋ฉด ๋ถˆ๋Ÿฌ์˜ค๊ธฐ๋Š” ์‰ฝ๋‹ค๋Š” ๋œป์ด๋‹ค.

 

 

๐ŸŸค Reference

https://www.youtube.com/watch?v=LRsC8_tvLcA    =>  aws-s3 ํŒŒ์ผ์—…๋กœ๋“œ
728x90
๋ฐ˜์‘ํ˜•


Calendar
ยซ   2024/09   ยป
์ผ ์›” ํ™” ์ˆ˜ ๋ชฉ ๊ธˆ ํ† 
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Archives
Visits
Today
Yesterday