Node에서 S3 이미지 저장 및 삭제

김래영·2022년 5월 7일
2

Backend

목록 보기
6/7

Node & AWS S3

Install

npm i aws-sdk

AWS S3 버킷 생성 및 권한 설정에서 이미지 저장 및 삭제는 권한(IAM 정책)을 부여했다.

  • IAM 정책 생성시 받은 액세스 키(AWS_ACCESS_KEY), 시크릿 키(AWS_SECRET_KEY)S3 리전(AWS_REGION).env에서 관리한다.

    ex)

      import aws from 'aws-sdk';
    
      const { AWS_SECRET_KEY, AWS_ACCESS_KEY, AWS_REGION } = process.env;
    
      export const s3 = new aws.S3({
        secretAccessKey: AWS_SECRET_KEY,
        accessKeyId: AWS_ACCESS_KEY,
        region: AWS_REGION,
      });

이미지 저장

이미지를 저장하는 방법 중에 아래와 두 가지 방법이 있다.

  • Client에서 -> Backend로 이미지 파일을 전송 후 multer-s3를 이용해 s3에 이미지를 저장하는 방법.

    • 위의 방법은 이미지 용량이 크거나 여러 장 저장할 때 서버에 부하를 줄 수도 있고 이미지를 모두 저장 후 url을 받아 사용자에게 보여준다면 시간이 너무 오래 걸려 사용자 경험이 좋지 못하다.
  • Client or Backend에서 PresignedUrl을 이용해 S3에 이미지를 업로드하는 방법.

    • S3에서 특정 파일(Object)에 PresignedUrl(제한된 시간 동안 접근할 수 있도록 권한이 부여된 서명된 url(업로드 url))로 S3에 이미지를 저장하는 방법.
    • 클라이언트에서 바로 S3로 업로드가 가능하기 때문에, 보안을 위해 백엔드를 걸쳐 이미지를 업로드 시켰다.

Node에서 PresignedUrl 생성

  • createPresignedPost를 이용해 PresignedUrl을 생성한다.
  • key부분은 이미지 저장 경로 및 파일 이름
  • ex) image/<파일 이름>.<파일 확장자>
import aws from 'aws-sdk';

const { AWS_SECRET_KEY, AWS_ACCESS_KEY, AWS_REGION } = process.env;

export const s3 = new aws.S3({
  secretAccessKey: AWS_SECRET_KEY,
  accessKeyId: AWS_ACCESS_KEY,
  region: AWS_REGION,
});

export const getSignedUrl = ({ key }: { key: string }) => {
  return new Promise((resolve, reject) => {
    s3.createPresignedPost(
      {
        Bucket: '<your bucket name>',
        Fields: {
          key, /* ex) image/<파일 이름>.<jpeg or png> */
        },
        Conditions: [
          ['content-length-range', 0, 50 * 1000 * 1000],
          ['starts-with', '$Content-Type', 'image/'],
        ],
      },
      (err, data) => {
        if (err) reject(err);
        resolve(data);
      }
    );
  });
};
  • getSignedUrl이용해 생성된 PresignedUrl하고 imageKey(image/<파일 이름>.<jpeg or png>)를 응답한다.
import express, { Request, Response, NextFunction } from 'express';
import { s3, getSignedUrl } from '../aws';

const router = express.Router();

/* ... */
router.post('/presigned', async (req, res, next) => {
  try {
    const { contentTypes } = req.body;

    if (!Array.isArray(contentTypes)) throw new Error('error');

    const presignedData = await Promise.all(
      contentTypes.map(async (conteType) => {
        const imageKey = `${uuid()}.${mime.extension(conteType)}`;
        const key = `image/${imageKey}`;
        const presigned = await getSignedUrl({ key });
        return { imageKey, presigned };
      })
    );

    res.json(presignedData);
  } catch (err) {
    next(err);
  }
});

/* ... */

export default router;

Client(React)에서 PresignedUrl로 이미지 업로드

  • PresignedUrl 생성하고 응답받은 presignedData(imageKey(이미지 파일 이름), PresignedUrl(업로드 url))을 이용해 Client에서 S3에 이미지를 저장한다.
  const imgContentTypes = imgFile.map((el) => el?.type);

  /* presignedUrl 생성하기 */
  const res = await request.post('/upload/presigned', {
    contentTypes: imgContentTypes,
  });

  /* s3 이미지 저장 */
  await Promise.all(
    imgFile.map((file, index) => {
      const { presigned } = res.data[index];
      const formData = new FormData();
      for (const key in presigned.fields) {
        formData.append(key, presigned.fields[key]);
      }
      file && formData.append('Content-Type', file.type);
      file && formData.append('file', file);
      return axios.post(presigned.url, formData);
    })
  );

이미지 삭제

  • Bucket부분에 해당 S3 버킷 이름을 적어준다.
  • Key부분에 버킷에서 삭재할 이미지 경로와 파일 이름을 적어준다.
    • 버킷에서 image폴더에 이미지 저장했기 때문에 Key부분에 <폴더>/<삭제할 이미지 파일 이름>.<파일 확장자>를 입력했다.
    • ex) image/<img 파일 이름>.jpeg
  • 콜백으로 에러와 데이터를 확인할 수 있다.

ex)

import express, { Request, Response, NextFunction } from 'express';
import { s3, getSignedUrl } from '../aws';

const router = express.Router();

/* ... */
router.delete(
  '/image/:id',
  async (req: Request, res: Response, next: NextFunction) => {
    try {
      await s3.deleteObject(
        {
          Bucket: '<your bucket name>',
          Key: `image/${req.params.id}`,
        },
        (err, data) => {
          if (err) throw err;
        }
      );

      res.json({
        message: 'success',
      });
    } catch (err) {
      next(err);
    }
  }
);
/* ... */

export default router;
profile
개발 노트

0개의 댓글