AWS/S3

[AWS 3S] Node.js 에서 .env 파일을 이용하여 S3 버킷 이용하기 ( 버킷에 파일 저장 + 버킷 객체 목록 읽기 )

찰리-누나 2023. 1. 2.

 

 

 

 

앞서 프론트에서 진행했을 경우의 위험성을 살펴 보았다. 우리는 가난하기 때문에 과금을 감당할 수 없다. 과금당하지 않기 위해 .env 파일을 만들어, 소중한 key값을 환경변수에 등록하고 사용해보자. 이 글은 Express를 기본적으로 사용할 수 있다는 전제로 진행하지만, 전체 코드와 부분 부분 Node.js를 설명하는 글을 첨부하였기 때문에 조금 서툴더라도 따라할 수 있을 것이다.

 


 

 

 

aws 서비스를 이용하려면, 내가 '이 서비스를 이용할 자격이 있는 사람임' 을 알려주어야 한다. 공식 가이드에서는 다음과 같은 5가지의 방법을 제시하고 있다.

 

 

 

  1. 서버(EC2)로부터 IAM Roles를 얻어 연동
  2. 서버의 ~/.aws/credentials 경로의 파일에 자격증명 입력하여 연동
  3. 환경변수에 자격증명을 저장하고 연동
  4. config.json 파일에 자격증명을 입력하여 연동 (권장되지 않음)
  5. 그냥 소스 코드에 직접 입력하여 연동 (권장되지 않음)

 

 

 

보안을 위해서는 1번을 사용해야 하며, 실무에서도 1번을 사용한다고 한다. 이전 글에서 살펴본 공식문서는 2번의 방법을 안내해 주고 있었다. 2번은 EC2를 통해 백엔드를 배포할 때 해 볼 것이므로, 우선은 3번을 이용해 처리해보자. 매우 쉽다.

만일 Node.js에서 .env 파일을 작성하고 활용하는 방법이 무엇인지 모른다면 다음 글을 참고한다 : https://make-somthing.tistory.com/70

 

[Node.js] .env 환경변수 파일 생성과 이용

Node.js에서는 프로젝트 디렉토리에 .env라는 파일이 존재하면, 환경변수처럼 소스코드로 가져와서 사용할 수 있다. .env 파일은 Object처럼 key=value 형식으로 작성하며, 주석을 작성하고자 할 때는 문

make-somthing.tistory.com

 

 

 

 

 

위 글을 따라 .env 파일을 생성하고 설정을 완료했다면, 이전 글에서 득템해왔던 엑세스 키와 시크릿 키를 등록해주자. 띄워쓰기 하면 안된다. .env 파일에 아래 내용을 작성하고 저장한다.

S3_ACCESS_KEY_ID=엑세스키
S3_SECRET_ACCESS_KEY=시크릿키
BUCKET_NAME=버킷이름
AWS_REGION=버킷리전

 

 

 

 

 

 

Node.js에서 AWS의 S3 버킷에 이미지를 저장하는 예제를 진행하기 위해 설치해 주어야 할 라이브러리는 총 3개이다. npm install을 통해 설치해준다. (aws-sdk 버전이 맞지 않으면 에러가 뜬다. multer-s3를 받을 때 2버전으로 다운하기 위해 @^2 를 꼭 붙인다.)

 

aws-sdk : AWS Software Development Kit

● multer : node.js 파일 업로드 라이브러리

● multer-s3 : multer 전용 s3 파일 업로드 라이브러리

 

npm install aws-sdk
npm install multer
npm install multer-s3@^2 --save

 

 

 

 

 

라우터를 사용해 aws 요청을 모아 관리해 줄 것이다. ( Node.js의 Express에서 Router 사용하는 법 : LINK ) 프론트에서 /api/aws/이하경로 를 통해 요청을 보내면, 해당 라우터가 요청에 응답한다. index.js에 라우터의 경로를 입력한다.

app.use('/api/aws', require('./routes/aws.js'))

 

 

 

 

 

라우터인 aws.js 파일을 본격적으로 작성한다. 이전 글을 통해, 프론트단에서 처리하는 방법으로 s3에 파일 몇개를 업로드한 상태이니 먼저 버킷 속 객체를 조회하여 프론트로 전송해 보자. /api/aws/getBucketObj 로 요청할 것이다. 조회 코드는 지난시간에 사용한 코드를 그대로 사용하면 된다. 원래 node.js에서 사용한 코드를 React에서 사용한 것이었다.

 

해당 코드에서는 aws-sdk를 사용해 s3객체를 만들어, 버킷을 조회하는 형태였다. 따라서

 

1. 라우터에 aws-sdk를 require로 import 한 후

2. AWS.config.update를 통해 자격증명을 설정해 주고,

3. s3 객체를 생성

 

할 것이다. 

 

우선은 프론트에 응답하지 말고, 조회부터 해 보자. 

 

const express = require('express')
const router = express.Router()

const AWS = require('aws-sdk');

// aws region 및 자격증명 설정
AWS.config.update({
    accessKeyId: process.env.S3_ACCESS_KEY_ID,
    secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
    region: process.env.AWS_REGION,
 });
 const s3 = new AWS.S3();

router.get('/getBucketObj', function(req,res){
    let objectlists = [];
    
    s3
   .listObjectsV2({ Bucket: process.env.BUCKET_NAME })
   .promise()
   .then((data) => {
      console.log('Object Lists : ', data);
      for (let i of data.Contents) {
         objectlists.push(i.Key);
      }
      console.log('objectlists : ', objectlists);
   })
   .catch((error) => {
      console.error(error);
   });

})

module.exports = router;

▶ 결과. 리스트가 정상적으로 출력되었다.

 

 

 

 

코드가 정상적으로 작동하는 것을 확인하였으니, 결과를 담은 objectlists를 프론트로 전달해준다. promise의 then은 성공했을 경우에만 작동하는 코드이다. 따라서, promise가 성공했을 경우에 data에서 key값만 저장한 objectlists를 전달해주면 버킷의 정보를 프론트에 노출하지 않고 파일 리스트를 조회할 수 있을 것이다. then문장을 다음과 같이 수정한다.

 

.then((data) => {
      for (let i of data.Contents) {
         objectlists.push(i.Key);
      }
      return res.status(200).json({
            success:true, message:'send bucket obj list',objectlists
      })
   })

 

 

 

 

 

이제 [객체 리스트 가져오기] 버튼을 클릭했을 때 실행할 프론트엔드 코드를 작성해보자. 이전에 사용했던 Previews.js를 복사하고, 버튼 이벤트 내용들을 모두 지워준 뒤 새로 작성했다. 지금 당장은 조회한 내용을 화면에까지 뿌려줄 것이 아니므로 단순히 결괏값을 받아와 출력해본다.

 

먼저, /api/aws 부분을 config 파일에 등록해주자. 자주 사용될 것은 이렇게 변수로 만들어놓으면 관리하기 편리하다.

//client/src/config/config.js

export const AWS_API = '/api/aws'
//Previews2.js
...

async function getButton() {
    
    // 객체 리스트 얻어오기
     axios.get(`${config.AWS_API}/getBucketObj`).then(
        response => {
            console.log(response)
        }

    )
    
  }

백엔드를 통해 전달받은 결과가 콘솔로 출력되었다.

.

 

 

 

조회에 성공하였으므로 'Node.js Express 백엔드에서 S3에 이미지를 업로드' 하는 과정을 진행해보자. 라우터에 multer, multer-s3, path를 임포트 해 준다.

// ==============================================
//                    multer 
// ==============================================

const multer = require('multer')
const multerS3 = require('multer-s3')
const path = require('path')

 

 

 

 

이미지 파일만 업로드되게 하고 싶다면 필터를 작성한다. 이 과정은 생략해도 괜찮다. 나는 사진첩을 만들 것이므로, 이미지만 받을 것이다.

// Multer를 위한 필터. 이미지 파일만 Multer를 통해 업로드하고 아닌 경우 에러 발생.
const multerfilter = (req, file, cb) => {
    if (file.mimetype.startsWith('image')){
        cb(null, true);
    } else {
        cb(console.log('Not image file upload tried'), false);
    }
}

 

 

 

이제 multer-s3를 통해, 서버에 저장하지 않고 s3에 바로 받은 이미지 파일을 업로드한다.

// multer 저장소 및 필터 설정
const upload = multer({
    storage: multerS3({
        s3: s3, //s3 객체
        bucket: process.env.BUCKET_NAME, //버킷 이름
        acl: 'public-read', //public-read 상태의 acl 설정(아무나 읽을 수 있음)
        contentType: multerS3.AUTO_CONTENT_TYPE, // content type 들어오는대로 설정
        key: function(req, file, cb){
                if(file.originalname === undefined || file.originalname === null){ 

                }
                else {
                     // 콜백 함수 두 번째 인자에 파일명(경로 포함)을 입력한다.
                     // 파일 이름 설정을 위한 callback function
                    cb(null, `${Date.now()}_${path.basename(file.originalname)}`); 
                }
        }
    }),
    fileFilter: multerfilter, //필터를 연결한다. 필터를 사용하지 않을 경우 이 코드는 삭제한다.
    limits: { fileSize: 10 * 1024 * 1024 }, //파일 사이즈를 10mb미만으로 제한한다.
});

 

 

 

 

다 되었다. 이런 것은 외우는게 아니고 필요할 때 찾아서 복사해 쓰는 것이다. api 요청을 받을 라우터를 작성해주자. 프론트에서 /api/aws/putimage로 요청하면, s3에 이미지를 업로드 한 후, 업로드된 url을 프론트로 반환해준다. 

router.post('/putimage',upload.single('image'), function(req,res){
    console.log(req.file)
    res.status(200).json({
        success:true, url:req.file.location
    })

})

 

 

 

 

req.file 객체 데이터는 아래의 종류들이 있다. 여기서 우리는 location 을 프론트로 반환해 준 것이다. 

Key 설명 Note
size 바이트 크기  
bucket 버켓 이름 S3Storage
key 키 이름 S3Storage
acl 이 파일에 대한 접근 권한 S3Storage
contentType MIME 타입  (파일의 확장자가 없을 경우 반드시 MIME 타입을 설정) S3Storage
metadata The metadata object to be sent to S3 S3Storage
location 파일 url 경로 S3Storage
contentDisposition The contentDisposition used to upload the file S3Storage
storageClass S3 티어 스토리지 클래스 S3Storage
versionId S3 버저닝을 활성화했을시 부여되는 버전 아이디 S3Storage
contentEncoding The contentEncoding used to upload the file S3Storage

 

 

 

프론트에 api를 요청하는 코드를 작성해 주었다.

  function saveEventhandler() {  
      
        if (files && files[0].size > (10 * 1024 * 1024)) {
          alert("10mb 이하의 파일만 업로드할 수 있습니다.");
       } else {

            let formData = new FormData();
            formData.append('image',files[0])

            // Todo S3에 파일 저장하는 api 요청
            
            axios.post(`${config.AWS_API}/putimage`,formData)
            .then(response=> {
                if(response.data.success) {
                    console.log(response.data)
                } else {
                    console.log(response)
                }
            })        
        
          
        // Todo axios로 파일 이름과 함께 경로를 몽고db에 저장하는 api 요청

          }

 

 

 

 

실행하면..!

에러가 발생한다. AccessControlListNotSupported: The bucket does not allow ACLs 라는 에러 코드이다. 님 ACL 권한 설정 안하셨어요 라는 뜻이다.

 

까꿍

 

Access List 는 접근하는 것을 허용 또는 거부하는 접근제어 리스트를 뜻한다. ACL을 통해 필터링 이라는 기능을 수행할 수 있는데, 특정 주소를 가진 호스트의 접근을 막거나 특정 서비스를 차단하는 등의 여러 목적으로 사용될 수 있다. (출처 : https://net-gate.tistory.com/18

 

 

 

 

이 에러를 해결하려면, 버킷 창에 들어가 객체 소유권을 편집해야 한다. 아래와 같이 설정해주자.

 

버킷의 ACL 권한 설정

 

 

 

다시 이미지를 업로드 하고, 돌려받은 URL을 주소창을 통해 접속해본다. 

 

결과

 

 

 

 


생각보다 어렵지 않고 코드도 짧다. 사실 S3는 권한과의 싸움이 치열할 뿐 코드는 별거 아니다. 다음 글에서는 버킷 속 이미지를 조회해 메인 페이지에 띄우고, 페이지네이션하며, 삭제해본다. 프론트와 백엔드의 적절한 조화가 필요한 기능이라고 할 수 있다.

 

 

 


 

 

참고한 글 : https://inpa.tistory.com/entry/AWS-SDK-%F0%9F%91%A8%F0%9F%8F%BB%E2%80%8D%F0%9F%92%BB-Multer-S3-%EC%97%B0%EB%8F%99-%EB%B0%8F-%EC%82%AC%EC%9A%A9%EB%B2%95-%EC%A0%95%EB%A6%AC

https://inpa.tistory.com/entry/AWS-SDK-%F0%9F%91%A8%F0%9F%8F%BB%E2%80%8D%F0%9F%92%BB-Multer-S3-%EC%97%B0%EB%8F%99-%EB%B0%8F-%EC%82%AC%EC%9A%A9%EB%B2%95-%EC%A0%95%EB%A6%AC

 


 

전체 코드

 

프론트엔드

더보기

 

 

import React, { useRef, useState } from "react";
import axios from 'axios';
import AWS from "aws-sdk"
import * as config from '../config/config.js'

function Previews2() {

  
  //  =========================== set AWS ===========================



    //  =========================== set state ===========================

  const uploadIcon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA0klEQVR4nO2UWQ6CQBAF+8cbavjQ4+BJ1VNAUgaXxI1lIJPuwVcJP8yQVE1nMBMiLkDVPVYiwA5ogBY4WKHyT8qJ4Fu+nAj65eNHcL+wn/JNz7uqhJNvgT2w7VmLMYkh+Zc9MSOYIB82ggT5cBHMkA8Twfiv8saP78Zos0dMlZ8ZkDciRX5BQJ6IVPmFAW4X+43UdXcU4I0m4I0m4I0m4I0m4M3qJjCGRQO4MJ2TRQM4JgTUFg1g84g4D4h3a3W319tXCCEyQXBMAc7Y6icgxJ9xBUOuyY7C/8ZJAAAAAElFTkSuQmCC'
  const imageInput = useRef();  


  const[files,setFiles] = useState('')
  const [imageSrc, setImageSrc] = useState('');

  const [imgname,setImgname] = useState('')

  let objlist = []


  // <=========== test Click event ===========>
 

 async function getButton() {
    
    // 객체 리스트 얻어오기
    axios.get(`${config.AWS_API}/getBucketObj`).then(
        response => {
            console.log(response)
        }

    )
    
  }
    // <=========== Savebutton Click event ===========>

  function saveEventhandler() {  
      
        if (files && files[0].size > (10 * 1024 * 1024)) {
          alert("10mb 이하의 파일만 업로드할 수 있습니다.");
       } else {

            let formData = new FormData();
            formData.append('image',files[0])

            // S3에 파일 저장하는 api 요청
            
            axios.post(`${config.AWS_API}/putimage`,formData)
            .then(response=> {
                if(response.data.success) {
                    console.log(response.data)
                } else {
                    console.log(response)
                }
            })               
          

          }

      

  
  } 
  
const onClickInput = () => {
  imageInput.current.click();
  }   


  const onLoadFile = (e) => {

    const file = e.target.files;
    console.log('onLoadFile',file)
    setFiles(file);
    console.log('state에 저장 완료 files',files)

    let fileBlob = e.target.files[0]
    
      const reader = new FileReader();
      reader.readAsDataURL(fileBlob);
      return new Promise((resolve) => {
        reader.onload = () => {
          setImageSrc(reader.result);
          resolve();
        };
      });

  }
  

  return(
    <div style={{position:'relative'}}>
      <div>
        <input type="file" 
        style={{display:'none'}} 
        
        onChange={onLoadFile}
        ref={imageInput} 
        />
        
        <img 
        style={{
          position:'absolute',
          top:'20%',
          left:'44%',
          cursor:'pointer'
        }}
        src={ imageSrc ? '' : uploadIcon}
        onClick={onClickInput}/>
        
        </div>
        
        <img

        className="uploadImage"
        style={{
         minWidth:'150px', minHeight:'150px',maxWidth:'500px', border:'1px solid lightgray',
        alignItems:'center', justifyContent:'center', cursor:'pointer'
       
        }}
        onClick={onClickInput}
        
        src={imageSrc? imageSrc : ''}/>

      <div style={{marginTop:'50px'}} >
        <input type="text" placeholder="제목을 입력해주세요."
      style={{width:'300px',height:'30px',outline:'none',
      fontSize:'25px', color:'white',
      borderLeftWidth:0,borderRightWidth:0,borderTopWidth:0,borderBottomWidth:1,
      backgroundColor:"transparent"
      }}
      onChange={(e)=>{setImgname(e.target.value)}}
      /><span style={{marginLeft:'20px', fontSize:'20px', cursor:'pointer' }}
      onClick={saveEventhandler}
      >업로드</span>
      </div>
      <div
      style={{marginTop:'20px',cursor:'pointer'}}
      onClick={getButton}>
        객체 리스트 가져오기
      </div>
      

    </div>
  )
}

  export default Previews2;

 

 

 

백엔드

 

더보기

 

index.js
const express = require('express')
const app = express()
app.use(express.json());

var cors = require('cors');
app.use(cors());

require('dotenv').config();
const AWS = require('aws-sdk');

app.listen(5000,function(){
    console.log('서버를 열었습니다.')
})


app.get('/api/hello', function(req,res){
    res.send('Open with server')
})


app.use('/api/aws', require('./routes/aws.js'))

  

 

aws.js
// ==============================================
//                    router 
// ==============================================
const express = require('express')
const router = express.Router()

// ==============================================
//                    multer 
// ==============================================

const multer = require('multer')
const multerS3 = require('multer-s3')
const path = require('path')

// ==============================================
//                      aws 
// ==============================================

const AWS = require('aws-sdk');

//           aws region 및 자격증명 설정
// ----------------------------------------------

AWS.config.update({
    accessKeyId: process.env.S3_ACCESS_KEY_ID,
    secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
    region: process.env.AWS_REGION,
 });

//                   aws s3 생성
// ----------------------------------------------
 const s3 = new AWS.S3();


 // ==============================================
//              버킷에 이미지 업로드
// ==============================================

// Multer를 위한 필터. 이미지 파일만 Multer를 통해 업로드하고 아닌 경우 에러 발생.
// ----------------------------------------------------------------------------------
const multerfilter = (req, file, cb) => {
    if (file.mimetype.startsWith('image')){
        cb(null, true);
    } else {
        cb(console.log('Not image file upload tried'), false);
    }
}


// multer 저장소 및 필터 설정
// -------------------------------------------------------------
const upload = multer({
    storage: multerS3({
        s3: s3,
        bucket: process.env.BUCKET_NAME,
        acl: 'public-read',
        contentType: multerS3.AUTO_CONTENT_TYPE, // content type 들어오는대로 설정
        key: function(req, file, cb){
                if(file.originalname === undefined || file.originalname === null){ 

                }
                else {
                     // 콜백 함수 두 번째 인자에 파일명(경로 포함)을 입력한다.
                     // 파일 이름 설정을 위한 callback function
                    cb(null, `${Date.now()}_${path.basename(file.originalname)}`); 
                }
        }
    }),
    fileFilter: multerfilter, //파일 필터
    limits: { fileSize: 10 * 1024 * 1024 }, //이미지 파일 크기를 10MB 미만으로 제한
});


// 이미지 저장 API 처리 라우터
// -------------------------------------------------------------

router.post('/putimage',upload.single('image'), function(req,res){
    console.log(req.file)
    res.status(200).json({
        success:true, url:req.file.location
    })

})



// ==============================================
//           버킷 내 객체 데이터 조회
// ==============================================

router.get('/getBucketObj', function(req,res){
    let objectlists = [];

    s3
   .listObjectsV2({ Bucket: process.env.BUCKET_NAME })
   .promise()
   .then((data) => {
      for (let i of data.Contents) {
         objectlists.push(i.Key);
      }
      return res.status(200).json({
            success:true, message:'send bucket obj list',objectlists
      })
   })
   .catch((error) => {
      console.error(error);
   });
})

module.exports = router;

 

 

 

댓글