Node.js, Express, Typescript로 S3에 image upload 하기 (Feat. multer, aws-sdk)

Server The SOPT·2022년 5월 20일
14

tips

목록 보기
2/2
post-thumbnail

✏️ 작성자: 서팟장 잡채
📌 작성자의 한마디: "7차 세미나 때 할 내용이었는데..."

안녕하세요! 👻
오늘은 우리가 사용하는 Node.js 와 Express, Typescript 환경에서 아주 간단하게 이미지를 받아서 S3 bucket 에 upload 하는 코드를 작성해보려 합니다.
사실 이 내용은 7차 세미나에서 다룰 거지만, 혹시라도 합동 세미나/솝커톤에서 이미지 업로드를 다뤄보고 싶으신 분이 계시다면 유용할 것 같아 작성합니다.

기본 지식

우리가 지금까지 했던 POST API 들의 Request 가 JSON 형식이었다면, 파일은 multipart/form-data 를 사용합니다.
이러한 multipart/form-data 를 위해 사용하는 미들웨어가 바로 Multer 입니다.
더욱 자세한 내용은 7차 세미나에서 공유할게요 :-)

S3 시크릿 키 생성

우리 서버에서 AWS S3 Bucket 에 접근하기 위해 access key id, secret key 를 받아와야 합니다.
aws 콘솔에 들어가셔서

  • 보안 자격 증명

으로 들어갑니다.

액세스 키를 누르고 새 액세스 키를 만들어줍시다!
키 파일 다운로드를 누르고, 자신만 알 수 있는 어딘가에 꼭 저장해둡시다 (csv 파일입니다)

env, config 설정

aws 에서 받은 시크릿 키는 절대로 github 이든 공개된 곳에 올라가면 안됩니다.
따라서 .env 에 설정해둘겁니다!
아까 받은 키파일을 열어서 복붙합니다.

.env

S3_ACCESS_KEY=access_key_id
S3_SECRET_KEY=secret_key
BUCKET_NAME=jobchae

이후 config 파일에 가서 export 해줍시다!

/config/index.ts

  /**
   * AWS S3
   */
  s3AccessKey: process.env.S3_ACCESS_KEY as string,
  s3SecretKey: process.env.S3_SECRET_KEY as string,
  bucketName: process.env.BUCKET_NAME as string

본격적으로 시작하기 전에 몇가지 라이브러리를 설치합시다.

설치

yarn add multer aws-sdk
yarn add @types/multer --dev

s3 업로드를 도와 줄 aws-sdk와 multer 를 설치해줍니다.

File Collection 생성

우리는 이미지 파일을 받아 s3에 올리고 해당 링크를 File Collection 에 저장하려고 합니다.
먼저 model info 부터 작성합시다.

/interface/file/FileInfo.ts

export interface FileInfo {
    link: string;
    fileName: string;
}

/models/File.ts

import mongoose from "mongoose";
import { FileInfo } from "../interfaces/file/FileInfo";

const FileSchema = new mongoose.Schema({
    link: {
        type: String,
        required: true
    },
    fileName: {
        type: String,
        required: true
    }
}, {
    timestamps: true // createdAt, updatedAt 자동기록
});

export default mongoose.model<FileInfo & mongoose.Document>("File", FileSchema);

s3 설정

우리의 bucket 에 접근할 수 있도록 시크릿 키 파일을 활용해 초기 설정을 해줍시다.

/config/s3Config.ts

import AWS from "aws-sdk";
import config from ".";

const storage: AWS.S3 = new AWS.S3({
    accessKeyId: config.s3AccessKey,
    secretAccessKey: config.s3SecretKey,
    region: 'ap-northeast-2'
});

export default storage;

multer 설정

/config/multerConfig.ts

import { Request } from "express";
import multer, { FileFilterCallback } from "multer";

type FileNameCallback = (error: Error | null, filename: string) => void

export const multerConfig = {
    storage: multer.diskStorage({
        destination: 'uploads/',
        filename: function (req: Request, file: Express.Multer.File, cb: FileNameCallback) {
            cb(null, file.originalname);
        }
    })
}

destination 은 파일을 어디에 저장할 지를 정하는 함수입니다.

destination 옵션은 어느 폴더안에 업로드 한 파일을 저장할 지를 결정합니다.

결정을 돕기 위해 각각의 함수는 요청 정보 (req) 와 파일 (file) 에 대한 정보를 모두 전달 받습니다.

req.body 는 완전히 채워지지 않았을 수도 있습니다. 이는 클라이언트가 필드와 파일을 서버로 전송하는 순서에 따라 다릅니다.

(출처: Multer README)

Controller

/controllers/FileController.ts

import express, { Request, Response } from "express";
import message from "../modules/responseMessage";
import statusCode from "../modules/statusCode";
import util from "../modules/util";
import { FileService } from "../services";

const uploadFileToS3 = async (req: Request, res: Response) => {
    if (!req.file) return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, message.NULL_VALUE));

    const fileData: Express.Multer.File = req.file;

    try {
        const data = await FileService.uploadFileToS3(fileData);

        res.status(statusCode.CREATED).send(util.success(statusCode.CREATED, message.CREATE_FILE_SUCCESS, data));
    } catch (error) {
        console.log(error);
        res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, message.INTERNAL_SERVER_ERROR));
    }
}

export default {
    uploadFileToS3
}

연습이니 아주 간단하게 작성해봅시다.
조금 다른 부분이 있죠?
기존에 있던 req.body 가 아닌 req.file 을 사용합니다.
multipart/form-data 로 들어오는 file 은 Express.Multer.File type 을 사용하고, req.file 에서 받아올 수 있습니다.
file이 없을 수 있기 때문에 upload controller 에서는 null value 처리를 해줍시다!

fileData 를 받아온 후에 서비스 로직으로 보내줍시다!

Service

/services/FileService.ts

import config from "../config";
import storage from "../config/s3Config";
import File from "../models/File";
import fs from 'fs';
import { FileResponseDto } from "../interfaces/file/FileResponseDto";

const uploadFileToS3 = async (fileData: Express.Multer.File): Promise<FileResponseDto> => {
    try {
        const fileContent: Buffer = fs.readFileSync(fileData.path);

        const params: {
            Bucket: string;
            Key: string;
            Body: Buffer;
        } = {
            Bucket: config.bucketName,
            Key: fileData.originalname,
            Body: fileContent
        };

        const result = await storage.upload(params).promise();

        const file = new File({
            link: result.Location,
            fileName: fileData.originalname
        });

        await file.save();

        const data = {
            _id: file._id,
            link: result.Location
        };

        return data;
    } catch (error) {
        console.log(error);
        throw error;
    }
}

export default {
    uploadFileToS3
}

service 로직에서는 s3에 파일을 upload 하고, 해당 링크를 File DB 내부에 저장해줄겁니다.
업로드 코드를 먼저 봅시다.

import fs from 'fs';

const fileContent: Buffer = fs.readFileSync(fileData.path);

const params: {
      Bucket: string;
      Key: string;
      Body: Buffer;
} = {
      Bucket: config.bucketName,
      Key: fileData.originalname,
      Body: fileContent
};

const result = await storage.upload(params).promise();

Node.js 가 기본적으로 제공하는 file system fs 를 사용하여 파일을 Buffer type으로 읽어옵니다.
readFileSync 는 파일의 상대 경로 (path) 를 사용하여 읽어올 수 있습니다.

params 는 업로드에 필요합니다.
내부적으로는 Bucket 이름, Key (저장 될 경로), Body (보낼 파일 스트림)이 필요합니다.

이후 storage (우리가 s3Config.ts 에서 정의함) 에서 제공하는 upload 함수를 통해 upload 할 수 있습니다.
sync 하게 호출하기 위해 promise() 를 붙여줘야 합니다.

const file = new File({
            link: result.Location,
            fileName: fileData.originalname
        });

        await file.save();

        const data = {
            _id: file._id,
            link: result.Location
        };

        return data;

반환 받은 result 에서 s3 link 는 Location 안에 있습니다.
이를 활용하여 File DB 에 저장해줍니다.
originalname 을 file 의 원래 이름입니다.

/interfaces/FileResponseDto.ts

import mongoose from "mongoose";

export interface FileResponseDto {
    _id: mongoose.Schema.Types.ObjectId;
    link: string;
}

Response DTO 입니다!

Router

/routes/index.ts

import { Router } from 'express';
import FileRouter from "./FileRouter";

const router: Router = Router();

router.use('/file', FileRouter);

export default router;

/routes/FileRouter.ts

import { Router } from "express";
import { FileController } from "../controllers";
import multer from 'multer';
import { multerConfig } from "../config/multer";

const router: Router = Router();

const upload = multer(multerConfig);

router.post('/upload', upload.single('image'), FileController.uploadFileToS3);

export default router;
const upload = multer(multerConfig);

multer 에 아까 생성한 multerConfig (diskStorage) 를 보내 생성해줍니다.
multer 미들웨어를 router 중간에 (controller 로 들어가기 전) 넣어줍시다!
'image' 는 파일을 보낼 form field name 입니다!

테스트

이제 이미지 파일을 보내볼까요?
서버를 실행시키고, Body 를 Form 으로 선택합니다.

제 서버 파트장 사진을 보내볼게요!
field name 은 아까 정한 image 로 둡니다.

upload 성공과 함께 s3 객체 link 가 보입니다.
s3 에서 확인해봅시다.

제대로 들어왔네요!
정말 간단한 코드로 클라이언트한테 받은 이미지를 s3 bucket 에 업로드해보았습니다.

마찬가지로 mongo DB 내부에도 제대로 저장된 걸 확인 할 수 있습니다!

s3 upload 쉽죠?! 🤔

마치며

솝커톤이나 합동세미나에서 도움이 되었다면 좋겠습니다!
언제든 궁금한 점이 있다면 저를 찾아주세요 🎉
글로 적느라 많은 설명을 못 넣은 점 양해 부탁드리며 7차세미나 때 더 다뤄봐요!

profile
대학생연합 IT벤처창업 동아리 SOPT 30기 SERVER 파트 기술 블로그입니다.

3개의 댓글

comment-user-thumbnail
2022년 5월 28일

정말 필요했던 내용입니다 ㅠㅠ

답글 달기
comment-user-thumbnail
2022년 5월 28일

정말 필요했던 내용입니다 ㅠㅠ

답글 달기
comment-user-thumbnail
2022년 5월 30일

감사합니다.

답글 달기