๐ข ํ์ต๋ชฉํ
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 ํ์ผ์ ๋ก๋