Node
- 진짜 죽을 맛이었다…S3를 다 정리하고 넘어가려 했는데, 뭘 어디서부터 정리해야 할지 까마득하여 출처 블로그에 맡기고 그간의 삽질을 작성하겠다.
Frontend
- 프론트는 Next를 쓰나 React를 쓰나 우선 구조부터 작성해본다.
- 이미지 전송을 위해서는
form
태그를 사용해야 한다. - 여기에는 또 여러가지 사항을 지켜줘야 하는데, 하도 많았어서 놓치는 게 있을지도 모른다.
1
2
3
4
5
6
7
8
9
<Form onSubmit={submitHandler} method="post" encType="multipart/form-data">
<input
type="file"
accept="image/*"
name="profileImage"
onChange={imageHandler}
/>
<button type="submit" />
</Form>
- 우선 프론트 코드는 이런 식으로 진행된다.
- 이것도 태그에서 지켜야 할 규칙들을 작성한 것일 뿐, 함수나 api요청을 할 때는 더 많은 부가 사항들을 지켜야 한다…
encType
: 인코딩 방식을 정하는 것인데,method
가post
인 경우에만 사용할 수 있다.- 총 3가지 옵션이 있는데,
multipart~
는 인코딩을 하지 않는 방식으로, 주로 파일이나 이미지를 전송할 때에 사용되는 옵션이다. application/x-www-form-urlencoded
이 기본값인데, 모든 문자들을 서버로 보내기 전에 인코딩한다.
- 총 3가지 옵션이 있는데,
accpet
:input
이 허용할 파일의 유형을 결정한다.- 간단한 옵션이다. 특별할 것도 없고, 그냥 모든 이미지 파일을 허용한다고 보면 된다.
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
31
32
33
34
35
36
37
38
39
40
const [imageInfo, setImageInfo] = (useState < File) | (null > null);
const imageHandler = (e: React.EventHandler<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
setSelectedImage(e.target.files[0]);
}
};
const submitHandler = (e: FormEvent) => {
e.preventDefault();
const formData = new FormData();
// 이미지를 추가
if (selectedImage) {
formData.append("profileImage", selectedImage);
}
// 다른 입력 데이터 추가
formData.append("id", id.userId);
formData.append("password", checkPass.secondPass);
formData.append("name", nickname.name);
formData.append("mail", `${userMail.mail}@${userMail.domain}`);
const signIn = () => {
api
.post("/signup", formData, {
headers: {
"Content-Type": "multipart/form-data", // 이 부분은 중요하다.
},
})
.then((res) => {
alert("회원가입이 완료되었습니다.");
})
.catch((err) => console.log(err));
};
if (checkSecurity.compareSecurityCode) signIn();
else alert("회원 가입에 실패했습니다. 다시 진행해주세요.");
};
- 리팩토링 하려고 더 편하게 작성한 것도 있는데, 언제 하고 있지 까마득하다…또 어떤 에러들을 겪을지
- 이미지를 선택하고 나면
e.target.files
에 여러가지 정보가 들어가게 된다.- 이미지명, 생성시간, 크기 등 등… 이런 정보들이 담긴다.
- 전송할 데이터들을 넣어줄 객체를 만들어주고
append
를 이용하여 객체에 정보를 저장한다.- 이렇게 담긴 정보들은
console.log
로는 파악할 수 없어entries
메서드를 사용하여 접근하여야 한다.
- 이렇게 담긴 정보들은
headers
에는encType
과 같이 옵션값을 넣어준다.
나의 포른트 코드는 이러했다.
Backend - S3, Express, MongoDB
- 서버 코드도 꽤나 복잡하다.
- 우선 기본적으로 S3를 사용하기 위한 패키지들을 설치해준다.
npm i multer, multer-s3 , aws-sdk , shortid + @types
뭐 얼추 이 정도 있을 거다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/config/index.ts
import AWS from 'aws-sdk';
import dotenv from 'dotenv';
dotenv.config();
const s3AccessKey = process.env.S3_ACCESS_KEY;
const s3SecretKey = process.env.S3_SECRET_KEY;
const bucketName = process.env.BUCKET_NAME;
const storage: AWS.S3 = new AWS.S3({
accessKeyId: s3AccessKey,
secretAccessKey: s3SecretKey,
region: 'ap-northeast-2',
});
export default storage;
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
import multer from 'multer';
import multerS3 from 'multer-s3';
import shortId from 'shortid';
import { S3Client } from '@aws-sdk/client-s3';
export const upload = multer({
storage: multerS3({
// https://stackoverflow.com/questions/68264237/how-to-set-credentials-in-aws-sdk-v3-javascript
// error
s3: new S3Client({
credentials: {
accessKeyId: s3AccessKey as string,
secretAccessKey: s3SecretKey as string,
},
region: 'ap-northeast-2',
}),
bucket: 'choigirang-why-community', // 객체를 업로드할 버킷 이름
contentType: multerS3.AUTO_CONTENT_TYPE,
key: function (req, file: Express.MulterS3.File, cb) {
// id 랜덤 생성
const fileId = shortId.generate();
const type = file.mimetype.split('/')[1];
const fileName = `${fileId}.${type}`;
cb(null, fileName);
},
acl: 'public-read-write',
}),
});
- S3 사용을 위한 설정이 끝났으면 마이페이지의 접근 보안 항목에서 액세스 키를 발급받는다.
- 환경변수 등록하여
key
를 작성하고 숨겨준다. - 최상위 파일인
app.ts
에 작성하는 경우도 많이 보이지만 대부분 config 폴더에 따로 작성하길래 우선 따라간다.AWS
로 기본 설정을 마쳐준다.
- 파일 전송을 직접적으로 관장한다고 생각할 수 있는
multer
을 작성한다.- 원래 기본 형태는 이렇지 않지만, 오류를 수정하려다 보니 위와 같은 형태로 바뀌었다.
- 지금은
AWS
에 작성한 설정들을multer
에서 직접 사용하고 있다고 생각하면 된다.
storage
와s3
부분에서 게속 타입 에러가 발생하여,new S3Client
를 따로 생성해 준 것이다.- 이 부분은 기능적으로는 동작하나 본적인 원인을 해결하지 못 했다.
- 또 업로드 될 파일명이 유일해야 하기 때문에 임의의
id
를 생성하는shortId
라이브러리나 다른 사람들은 날짜와 시간을 연결하여 작성하는 경우도 봤다. - 이렇게 하면 기본적인 셋팅은 끝난 것이다.
- 워낙 에러가 많이 발생했어서 어떤 상황에서 또 어떤 에러가 발생할 지 장담할 수 없지만, 우선 기본적인 골조는 이 정도로 작성된다.
router, controller
- 라우터 자체와 실행 함수를 동시에 작성하는 경우도 많이 보았지만, 본인은 컨트롤러와 라우터를 나눠 작성하였기에 본인 스타일로 작성하겠다.
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
// router.ts
import upload from "../config/multer";
exampleRouter.post("/signup", upload.single("profileImage"), createUser);
// exmaple.controller.ts
async function createUser(req: Request, res: Response) {
const { id, password, name, mail } = req.body;
try {
const profileImage = req.file;
const user = new User({
id,
password,
name: name,
mail: mail,
img: profileImage.path,
});
await user.save();
res.status(200).json(user);
} catch (err) {
res.status(500).json(err);
}
}
- 앞서, 프론트에서 보낸
formData
를 들여다 보자. Content-Type : multipart/form-data
로 파일과 일반 텍스트로 된 데이터를 전송한다.signup
주소로 요청이 왔을 때,multer
미들웨어로 파일과 일반 텍스트를 파악하고, 하나밖에 없는single
데이터를profileImage
라는 키로 저장된다.- 이것이
req.file
이 된다.
- 이것이
- 프론트에서 전송한
selectedImage
가profileImage
가 되는 것이고, 서버에서 전송받은file
의 메서드를 사용하여 파일 경로를 저장하면 된다. - 좀 더 심화하여 파일의 최대 크기를 제한한다던지, 서버 요청에 따라 이미지를 저장할 디렉토리를 나눈다던지, 여러가지를 추가하여 좀 더 안정성이 높은 코드를 작성할 수 있다.
- 우선 여기까지 완료.
- 사실 개념을 배우는 것도 오류를 해결하는 것도 정말 많은 시간이 소요되고 너무 많은 것을 한 번에 배운 느낌이라 완벽하지 않고 잘못 알고 있는 개념도 있을 거라 생각한다.
- 사용을 하면서 계속 찾아보고 더 익혀가보기로 하자.