✏️ 작성자: 서팟장 잡채
📌 작성자의 한마디: "7차 세미나 때 할 내용이었는데..."
안녕하세요! 👻
오늘은 우리가 사용하는 Node.js 와 Express, Typescript 환경에서 아주 간단하게 이미지를 받아서 S3 bucket 에 upload 하는 코드를 작성해보려 합니다.
사실 이 내용은 7차 세미나에서 다룰 거지만, 혹시라도 합동 세미나/솝커톤에서 이미지 업로드를 다뤄보고 싶으신 분이 계시다면 유용할 것 같아 작성합니다.
우리가 지금까지 했던 POST API 들의 Request 가 JSON 형식이었다면, 파일은 multipart/form-data 를 사용합니다.
이러한 multipart/form-data 를 위해 사용하는 미들웨어가 바로 Multer 입니다.
더욱 자세한 내용은 7차 세미나에서 공유할게요 :-)
우리 서버에서 AWS S3 Bucket 에 접근하기 위해 access key id, secret key 를 받아와야 합니다.
aws 콘솔에 들어가셔서
으로 들어갑니다.
액세스 키를 누르고 새 액세스 키를 만들어줍시다!
키 파일 다운로드를 누르고, 자신만 알 수 있는 어딘가에 꼭 저장해둡시다 (csv 파일입니다)
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 를 설치해줍니다.
우리는 이미지 파일을 받아 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);
우리의 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;
/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)
/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 를 받아온 후에 서비스 로직으로 보내줍시다!
/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 입니다!
/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차세미나 때 더 다뤄봐요!
정말 필요했던 내용입니다 ㅠㅠ