티스토리 뷰

기록

React에 MongoDB, AWS S3 적용해보기

als982001 2023. 7. 29. 23:00

Pixabay로부터 입수된 StartupStockPhotos님의 이미지 입니다.

 


 

 리팩토링을 위해 React 프로젝트에 MongoDB와 AWS S3를 적용하기로 하였다. 기존에는 로컬에 저장을 했었는데 아무리 봐도 이건 좀 아니다 싶었다. 그래서 이미지는 S3에, 나머지는 MongoDB에 저장할 것이다. 이를 위해 구글링으로 관련 글들을 찾아본 후 연결에 성공했다. 이 글은 적용하기까지의 과정을 기록하는 글이다.

 

1. MongoDB 적용하기

1-1. 데이터베이스 만들기

 우선 몽고db에서 데이터베이스를 만들어야 한다. 나는 이미 만들어져 있던 데이터베이스를 이용하였다. 처음부터 데이터베이스를 만드는 과정은 이 글의 제일 하단의 '참고한 글'에서 확인할 수 있다.

데이터베이스를 만들었다. 이제 서버와 이것을 연결해야 한다. 서버는 Express를 이용하였다. 우선 mongoose를 설치한 후, 서버의 index.js 파일에 다음과 같이 코드를 작성한다.

npm i mongoose
import app from "./server";

const PORT = 4000;
const database = "데이터베이스_이름";

const mongoose = require("mongoose");

mongoose.connect(
  "나중에_복사_붙여넣기할_곳", 
  {
    useUnifiedTopology: true,
    useNewUrlParser: true,
  }
);

const db = mongoose.connection;
db.on("error", (err) => console.error(err));
db.once("open", () => console.log("MongoDB 연결 성공"));

const handleListening = () =>
  console.log(`✅ Server listening on port http://localhost:${PORT} 🚀`);

app.listen(PORT, handleListening);

코드는 아래의 '참고한 글'에서 가져왔다. mongoose를 통해 몽고db와 연결할 수 있다. mongoose.connect에 "나중에 복사_붙여넣기할_곳"이라고 되어 있다. 이는 데이터베이스 사진 가운데 쯤에 있는 Connect를 클릭하여 얻을 수 있다.

 

Connect 클릭 후, 빨간 동그라미 클릭
3. Add your~의 코드 북사 후 붙여넣기

3. Add your ~의 코드를 복사한 후, "나중에 복사_붙여넣기할_곳"에 붙여넣기 하면 된다. 

mongoose.connect(
  `mongodb+srv://아이디:${process.env.MONGODB_PASSWORD}@cluster0.cysggjr.mongodb.net/${database}?retryWrites=true&w=majority`,
  {
    useUnifiedTopology: true,
    useNewUrlParser: true,
  }
);

이 때, <password>는 설정했던 비밀번호로 바꾸면 된다. ('<', '>'도 지워져야 한다.) 그리고 붙여넣기 한 부분에 ${database}라는 것이 있다. 이는 아래 이미지의 파란색 동그라미친 부분, 데이터베이스를 정하는 것이다. 저 부분을 비워도 되는데, 비울 경우 자동으로 "test"라는 이름의 데이터베이스를 이용하게 된다.

 

1-2. 스키마 만들기

 몽고db의 스키마란 어떤 형태와 구조를 가지는지를 정의하는 것이다. 예를 들어, 특정 상품 등에 관한 리뷰의 스키마를 생각해보자. 리뷰에는 최소한 이미지, 작성자, 내용, 상품 id가 필요할 것이다. 이에 대한 스키마는 아래처럼 만들 수 있다.

// models/Review.js

import mongoose from "mongoose";

const reviewSchema = new mongoose.Schema({
  productId: { type: mongoose.Schema.Types.ObjectId, ref: "Product" },
  imageUrl: { type: String, required: true },
  name: { type: String, required: true },
  content: { type: String, required: true },
});

const Review = mongoose.model("Review", reviewSchema);

export default Review;

우선, 이미지는 나중에 S3를 이용할 것이기에 String 타입으로 지정하였다. 그리고 작성자 이름(name)과 리뷰 내용(content) 역시 String으로 타입을 지정했다. 그리고 특정 상품에 대한 리뷰이기에 무슨 상품인지를 알아야 한다. 이를 productId라고 표현했다. productId의 타입은 ~.ObjectId이며 ref는 Product(무슨 스키마인지)이다. 이는 상품 역시 몽고db의 데이터이며, 몽고db의 데이터들은 전부 ObjectId를 가지기 때문이다. 마지막으로 전부 꼭 필요한 것이기에 required: true로 설정하였다. String, ObjectId 외에도 더 다양한 타입과 설정이 존재하며 이는 구글링을 통해 쉽게 찾을 수 있으니 여기서는 생략한다.

 

1-3. 데이터 POST 하기

이제 이 스키마를 이용해서 리뷰들을 GET, POST 등을 할 수 있다. 아래는 POST의 예시이다. 

// 프론트쪽 코드

// handlePostReview: 리뷰를 등록하는 함수
const handlePostReview = async (data: FormValues) => {
    const { content } = data;

    const imageUrl = await getImageUrl(image.name, image); // AWS S3를 이용

    if (imageUrl) {
      const newRecord = {
        imageUrl,
        name: userState.userInfo.name,
        content,
      };
	
      const success = await postRecord(id, newRecord); // id: 상품의 id

      if (success) {
	// 등록 성공
      } else {
	// 등록 실패
      }
    } else {
	// 이미지 업로드 실패
    }

    return;
  };
  
// postRecord: 서버에 POST 요청을 보내는 함수
export const postRecord = async (
  productId: string,
  review: {
    imageUrl: string;
    name: string;
    content: string;
  }
) => {
  try {
    const response = await axios.post(
      `${process.env.REACT_APP_BACK}/review/${placeId}`,
      review,
      {
        headers: {
          "Content-Type": "multipart/form-data",
        },
      }
    );

	// 성공했음
  } catch (error) {
    console.log(error);
    // 실패했음
  }
};

우선, 프론트의 코드이다. 리뷰에서 꼭 필요한 imageUrl, name, content, productId를 서버로 보낸다. 그리고 이 값들을 백에서 처리해야 한다.

 

import Review from "../models/Review";
import Product from "../models/Product";

export const postReviews = async (req, res) => {
  const { id } = req.params;

  try {
    const product = await Product.findById(id);

    if (product) {
      const newReview = await Review.create({
        placeId: new ObjectId(id),
        ...req.body,
      });

      product.reviews.push(newReview._id);
      product.save();

      return res.status(codes.ok).end();
    } else {
      return res
        .status(codes.notFound)
        .send("해당하는 상품을 찾을 수 없습니다.");
    }
  } catch (error) {
    return res.status(400).send("Error");
  }
};

// Product 스키마 중 reviews 부분

const productSchema = new mongoose.Schema({
  // 생략
  reviews: [{ type: mongoose.Schema.Types.ObjectId, ref: "Review" }],
});

const Product = mongoose.model("Product", productSchema);

export default Product;

Product, Review 둘 다 스키마이다. 백에서의 코드 흐름은 다음과 같다. 우선, req.params에서 상품 id를, req.body에서 리뷰를 받아온다. 그리고 await Product.findById(id); 를 통해 id에 해당하는 상품을 찾는다. findById는 id를 인자로 받으며 id에 해당하는 데이터가 없을 경우 null을 반환한다. 만약 product가 있을 경우  await Review.create()를 통해 리뷰를 만들고 자동으로 저장하며 만들어진 데이터를 반환한다. 이 때, 리뷰는 imageUrl, name 등의 키-값 쌍을 가지는 객체이므로 객체를 인자로 넣어야 한다. 마지막으로 상품 스키마는 reviews라는 키와  ObjectId 배열을 값으로 가진다. 상품의 reviews는 ObjectId 배열이므로 리뷰의 _id(id 아님)를 push한 후 저장(save)한다. (newReview는 몽고db의 데이터이므로 _id를 가진다.) 아래의 참고한 글에서 GET, DELETE 등의 예시를 확인할 수 있다.

 

* find 함수에서 id를 이용할 때

 findById는 일치하는 id의 데이터를 찾는다. 그리고 find라는 함수도 있는데 이는 더 다양한 조건으로 데이터를 찾을 수 있다. 예를 들어, 특정 상품의 리뷰들을 찾고 싶을 경우, Review.find({ 상품Id: 내가_찾고_싶은_상품_id })로 찾으면 된다. 그런데 상품의 _id는 ObjectId이다. 그래서 아래 코드처럼 ObjectId를 명시해줘야 한다고 생각할 수 있다.

const reviews = await Review.find({ placeId: new ObjectId(id) });
const reviews = await Review.find({ placeId: id });

 하지만 두 코드 똑같이 동작한다. 왜냐하면 mongoose 라이브러리는 자동으로 string 형식의 ID를 ObjectID로 변환해주기 때문이다. 

 

2. AWS S3

 

 S3는 코딩애플 강의와 개인 프로젝트에서 Next.js와 연결했었다. 그래서 Next.js의 코드를 그대로 이용해도 될 거라 생각했고 실제로도 성공했다. 아래는 대략적인 과정이다.

 

2-1. S3 버킷 만들기 

 버킷 만들기는 하단의 '참고한 글'의 'AWS S3로 React 배포하기'를 참고하여 만들 수 있다. 저 글을 참고해 버킷을 만든 후 따로 설정해야할 것들이 있다. 우선, 버킷에 들어간 후 권한 탭을 클릭한다.

권한

그리고 조금 내리면 버킷 정책이라는 것이 있다. 버킷 정책의 '편집'을 클릭한 후, 아래처럼 입력하면 된다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AddPerm",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:DeleteObject",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": [
                "arn:aws:s3:::버킷이름",
                "arn:aws:s3:::버킷이름/*"
            ]
        }
    ]
}

Resource의 버킷이름은 자신의 버킷이름으로 변경하면 된다. 이 JSON은 코딩애플 강의를 통해 알게 되었고, chatGPT를 이용해 이 JSON에 대해 설명을 들었다. 간단히 설명하자면 다음과 같다.

  • Version: 정책 문서에서 사용하는 IAM 정책 버전
  • Statement: 하나 이상의 권한 세트
    • Sid: 이 선언문의 식별자. (선택사항)
    • Effect: 이 선언문이 허용하는 작업인지 거부하는 작업인지 지정. 현재는 허용(Allow)
    • Principal: 이 정책이 적용되는 주체를 지정. "*"는 모든 사용자를 의미함
    • Action: 이 선언문이 허용하거나 거부하는 작업 목록. 현재는 순서대로 삭제, 가져오기, 객체 목록 조회, 객체 추가, 접근 제어 목록 수정하는 권한을 허용중
    • Resource: 이 정책이 적용되는 리소스를 지정

그리고 권한 탭의 마지막에 CORS(Cross-origin 리소스 공유)라는 것이 있다. 이 부분도 아래처럼 채워넣으면 된다.

[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "PUT",
            "POST"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": [
            "ETag"
        ]
    }
]

 우선, AllowedHeaders는 클라이언트 요청의 어떤 헤더를 허용할 지를 나타내며 "*"는 모든 헤더를 허용한다는 뜻이다. 그리고 AllowedMethods는 허용되는 HTTP 메소드를 의미한다. 현재는 PUT, POST를 허용하였다. AllowedOrigins는 허용할 수 있는 origin(출처)이며 현재 모든 출처를 허용한다. 마지막으로 ExposeHeaders는 브라우저가 접근할 수 있도록 허용된 응답 헤더를 나타낸다. 현재는 Etag 헤더를 접근 가능하게 허용하고 있다. 현재 버캣 정책과 CORS 부분은 개인 개발 중이기에 허용을 많이 해놨는데 실제 서비스를 해야 할 상황이라면 보안을 위해 어느 정도 제한을 해야할 것이다.

 

2-2. Access Key, Secret Access Key 만들기

 

 오른쪽 위의 아이디를 클릭하면 나오는 목록 중, '보안 자격 증명'을 클릭한 후, 조금 내리면 나오는 '액세스 키'에서 액세스 키를 만들 수 있다. 이 액세스 키와 시크릿 키는 절대 유출되면 안된다. 그리고 한 번 확인한 후 확인 페이지를 벗어나면 다시는 시크릿 키를 확인할 수 없으니 잘 보관해야 한다. 

 

2-3. 이미지를 업로드할 수 있는 URL 확인 (서버)

npm i aws-sdk
// imageController.js

import aws from "aws-sdk";

export const getBucketUrl = async (req, res) => {
  aws.config.update({
    accessKeyId: process.env.AWS_ACCESS_KEY,
    secretAccessKey: process.env.AWS_SECRET_KEY,
    region: "ap-northeast-2",
    signatureVersion: "v4",
  });

  const s3 = new aws.S3();

  const url = await s3.createPresignedPost({
    Bucket: process.env.AWS_BUCKET,
    Fields: { key: req.query.file },
    Expires: 60, // seconds
    Conditions: [
      ["content-length-range", 0, 5242880], //파일용량 5MB 까지 제한
    ],
  });

  res.status(200).json(url);
};

현재 코드는 '/image?file=이미지이름'으로 GET 요청을 보냈을 때 처리할 서버 쪽의 코드이다. 이 코드는 코딩애플 강의에 있던 코드이다. 

  • aws.config.update({...}): AWS SDK 설정을 업데이트 하는 부분. 이를 위해 방금 만들었던 액세스 키, 시크릿 키, 지역, signatureVersion를 설정해야 한다. 
  • await s3.createPresignedPost({...}): S3 객체의 createPresignedPost 메서드를 호출해 사전 서명된 URL을 생성한다. 이를 위해 버킷 이름(Bucket), 파일이 업로드될 때 사용할 키(이미지 이름), 만료 기간(Expires), 여러 조건(Conditions)이 필요하다. 현재 content-length-range를 통해 업로드할 파일의 크기를 0MB부터 5MB까지의 범위로 제한하고 있다. 이 때 5242880은 5 * 1024 * 1024이며 1 * 1024 * 1024가 1MB이다.

 

2-4. 이미지를 업로드할 URL 받아오기 (클라이언트)

export const getImageUrl = async (imageName: string, image: File) => {
  imageName = encodeURIComponent(imageName);

  const imageUploadResponse: AxiosResponse<any, any> = await axios.get(
    `http://localhost:4000/image?file=${imageName}`
  );

  const formData = new FormData();
  Object.entries({
    ...imageUploadResponse.data.fields,
    file: image,
  }).forEach(([key, value]) => {
    formData.append(key, value as string | Blob);
  });

  let uploadResult = await fetch(imageUploadResponse.data.url, {
    method: "POST",
    body: formData,
  });

  if (uploadResult.ok) {
    return `${uploadResult.url}/${imageName}`;
  } else {
    return null;
  }
};

 이 코드는 이미지를 업로드한 후, 이미지를 받아올 수 있는 url을 반환하는 함수이다. 우선, `http://localhost:4000/image?file=${imageName}`에 GET 요청을 보낸다. 그리고 response로 이미지를 업로드할 수 있는 url을 받는다. 그리고 이미지 파일을 formData 객체로 만든 후, 이를 업로드한다. 이러한 과정을 통해 이미지를 s3에 저장하고 url을 통해 접근할 수 있다.

 

 


참고한 글

https://merrily-code.tistory.com/10

 

Express.js & MongoDB 기반 REST api 구현하기 - 2편

지난 글에서 익스프레스를 활용한 간단한 REST api 서버를 만들었다. 서버만 만들고 마치기는 2% 아쉬우니, 몽고디비(MongoDB)와 연계해 CRUD 동작을 구현해 보자. 이번 글에서는 몽고디비를 클라우드

merrily-code.tistory.com

https://velog.io/@krkorklo58/AWS-S3%EB%A1%9C-React-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0

 

AWS S3로 React 배포하기

AWS S3, CloudFront, Route 53을 사용해서 React 배포하기

velog.io

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
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
글 보관함