Node 4장 - Express, S3를 이용한 이미지 업로드 (2)
포스트
취소

Node 4장 - Express, S3를 이용한 이미지 업로드 (2)

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요청을 할 때는 더 많은 부가 사항들을 지켜야 한다…
  1. encType : 인코딩 방식을 정하는 것인데, methodpost인 경우에만 사용할 수 있다.
    • 총 3가지 옵션이 있는데, multipart~는 인코딩을 하지 않는 방식으로, 주로 파일이나 이미지를 전송할 때에 사용되는 옵션이다.
    • application/x-www-form-urlencoded이 기본값인데, 모든 문자들을 서버로 보내기 전에 인코딩한다.
  2. 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("회원 가입에 실패했습니다. 다시 진행해주세요.");
};
  • 리팩토링 하려고 더 편하게 작성한 것도 있는데, 언제 하고 있지 까마득하다…또 어떤 에러들을 겪을지
  1. 이미지를 선택하고 나면 e.target.files에 여러가지 정보가 들어가게 된다.
    • 이미지명, 생성시간, 크기 등 등… 이런 정보들이 담긴다.
  2. 전송할 데이터들을 넣어줄 객체를 만들어주고 append를 이용하여 객체에 정보를 저장한다.
    • 이렇게 담긴 정보들은 console.log로는 파악할 수 없어 entries 메서드를 사용하여 접근하여야 한다.
  3. 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에서 직접 사용하고 있다고 생각하면 된다.
  • storages3 부분에서 게속 타입 에러가 발생하여, 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이 된다.
  • 프론트에서 전송한 selectedImageprofileImage가 되는 것이고, 서버에서 전송받은 file의 메서드를 사용하여 파일 경로를 저장하면 된다.
  • 좀 더 심화하여 파일의 최대 크기를 제한한다던지, 서버 요청에 따라 이미지를 저장할 디렉토리를 나눈다던지, 여러가지를 추가하여 좀 더 안정성이 높은 코드를 작성할 수 있다.
  • 우선 여기까지 완료.
  • 사실 개념을 배우는 것도 오류를 해결하는 것도 정말 많은 시간이 소요되고 너무 많은 것을 한 번에 배운 느낌이라 완벽하지 않고 잘못 알고 있는 개념도 있을 거라 생각한다.
  • 사용을 하면서 계속 찾아보고 더 익혀가보기로 하자.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.