...
AWS SDK S3 사용법
AWS 클라우드에는 정말 많은 서비스들이 있고, AWS SDK에서도 프로그래밍적으로 다양한 서비스를 제어할 수 있게 많은 api를 제공한다.
그중에 S3 서비스는 값싸고 관리하기 좋은 원격 저장소로서 정말 안쓰이는 곳이 없을정도로 많이 쓰이는 서비스이다.
정말 많이 사용되는 만큼 AWS SDK에 입문하는데 있어서 최적이다.
이번 포스팅에서 Node.js를 S3에 연동해서 버킷을 만들고 객체(파일)을 넣고 뺄수있는지 전반적인 메소드를 살펴볼 예정이다.
..앞서 기본적인 S3 서비스에 대한 지식과 더불어 기본적인 SDK 사용법에 대한 선수지식이 필요하다.
const AWS = require('aws-sdk');
const dotenv = require('dotenv');
dotenv.config(); // .env 환경변수 사용
// aws region 및 자격증명 설정
AWS.config.update({
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
region: 'ap-northeast-2',
});
// 자격증명 데이터를 따로 파일로 관리한다면 다음으로 호출할수 있다.
// AWS.config.loadFromPath('./config.json');
// S3 객체 얻기
const s3 = new AWS.S3();
위처럼 따로따로 생성자를 통해 서비스 객체를 얻을수 있지만, 바로 개별 서비스 클래스를 바로 import 할수도 있다.
이런식으로 구성할경우 메모리를 절약할 수 있다는 장점이 있다.
SDK 버킷 메소드
버킷 목록 조회
//* 버킷 목록 가져오기
await s3
.listBuckets() // s3 버킷 정보 가져오기
.promise() // 메소드를 프로미스 객체화
.then((data) => {
console.log('S3 : ', JSON.stringify(data, null, 2));
});
버킷 생성
//* 버킷 생성
const bucketName = 'node-sdk-sample-123123123';
await s3
.createBucket({ Bucket: bucketName })
.promise()
.then((bucket) => {
console.log('bucket : ', bucket);
})
.catch((error) => {
console.error(error);
});
버킷 삭제
버킷 삭제는 구성이 약간 복잡하다.
AWS CLI 같은 경우, $ aws s3 rb s3://버킷명 --force 명령어를 통해 버킷안에 내용물이 있어도 강제적으로 삭제할 수가 있다.
그러나 기본적으로 AWS SDK 에서는 버킷 강제 삭제를 지원하지 않는다.
따라서 먼저 버킷의 내용물 목록을 조회한후, 내용물을 삭제하고, 그다음 버킷을 삭제하는 프로세스를 짜야 한다.
s3.listObjectsV2() : 버킷의 내용물(객체) 목록을 얻는다.
s3.deleteObjects() : 버킷의 내용물(객체)들을 여러개 한꺼번에 삭제한다.
바로 뒤에서 자세히 배운다.
const bucketName = 'node-sdk-sample-123123123';
//* 버킷 삭제 (cli와 달리 --force 옵션이 없다. 따라서 직접 버킷 내용물을 지우고 삭제해줘야 한다.)
async function emptyS3Bucket(bucketName) {
// 해당 버킷의 내용물(객체)들을 얻어와 저장
const listedObjects = await s3.listObjectsV2({ Bucket: bucketName }).promise();
console.log('listedObjects: ', listedObjects);
// 만일 내용물이 없으면 빈 버킷이니 리턴
if (listedObjects.Contents.length === 0) return;
// ... 내용물이 있다면
const deleteParams = {
Bucket: bucketName,
Delete: { Objects: [] },
};
listedObjects.Contents.forEach(({ Key }) => {
// 객체 키명을 얻어와 deleteParams의 배열에 저장
deleteParams.Delete.Objects.push({ Key });
});
await s3.deleteObjects(deleteParams).promise(); // 객체 삭제
// 기본적으로 listObjectsV2는 최대 1000개까지의 객체만 얻어온다. 만일 내용물이 1000개 이상이 있을 경우 재귀적으로 처리
if (listedObjects.IsTruncated) await emptyS3Bucket(bucket);
}
await emptyS3Bucket(bucketName); // 버킷 내용물 비우기
await s3
.deleteBucket({ Bucket: bucketName })
.promise()
.then((data) => {
console.log('success : ', data);
})
.catch((error) => {
console.error(error);
});
SDK 객체 메소드
객체 목록 조회
const bucketName = 'node-sdk-sample-123123123';
//* 버켓의 객체 리스트 출력
let objectlists = [];
await s3
.listObjectsV2({ Bucket: bucketName })
.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);
});
listObjects() 와 listObjectsV2()
listObjectsV2는 listObjects의 개선 버젼이라고 보면 된다.
다만 내부 로직이 완전히 바뀌었기 때문에 기존 메소드를 업데이트하는 방식이 불가능하여 이렇게 아예 새로운 메서드명으로 추가하였다.
기본적으로 라이브러리는 구 코드의 호환성을 항상 고려해야 되기 때문에 불가피하게 생긴 조치라고 보면 된다.
객체 업로드
간단하게 파일명(keyName) 과 파일 내용(keyValue)을 지정하고 upload() 메소드로 보낸 예제이다.
버킷 폴더 지정하기
sdk 내에 폴더 지정에 대한 메서드는 없다.
다만 객체 이름에 들어가는 후행문자 /로 폴더를 구분해줄 수 있다.
객체 생성 시 key를 "uploads/file.txt"로 작성하면 자동으로 uploads라는 폴더를 생성하고, 그 안에 객체를 저장한다.
파일명이 한글일 경우
한글이 들어간 파일을 S3에 업로드할 경우, S3 콘솔에서는 한글을 출력하지만 , node에서 해당 객체를 읽을 경우 인코딩된 문자열이 나온다.
따라서 본인의 코드 구현 시 필요에 따라 decodeURIComponent()를 사용해 디코딩이 필요하다.
const bucketName = 'node-sdk-sample-123123123';
const keyName = 'hello_world.txt'; // 파일명
const keyValue = 'Hello World !!'; // 성능을 위해 파일 내용을 chunck로 나누어 읽는다.
//* 단순 객체 업로드
const objectParams = {
Bucket: bucketName,
Key: `imgFolder/${keyName}`, // 폴더 안에다가 업로드
Body: keyValue, // 파일 내용
};
await s3
.upload(objectParams)
.promise()
.then((data) => {
console.log('Upload Success! : ', data.Location);
})
.catch((error) => {
console.error(error);
});
위와 같이 해당 버킷 경로에 파일이 들어있음을 확인 할 수 있다.
하지만 만일 이미지 와 같은 대용량 파일을 S3에 업로드할때는 어떻게 할까?
이때 사용하는 것이 Node 모듈의 스트림 모듈 이다.
스트림을 통해 대용량 이미지 파일 내용을 읽어오고, 그 스트림 파일을 S3로 보내주면 알아서 파싱되어 버킷에 저장되게 된다.
const fs = require('fs');
const path = require('path');
//* 대용량 객체 업로드
const bucketName = 'node-sdk-sample-123123123';
const keyName = 'image.webp'; // 파일명
const keyValue = fs.createReadStream(`./${keyName}`); // 성능을 위해 파일 내용을 chunck로 나누어 읽는다.
keyValue.on('error', (err) => {
// stream도 비동기라, 비동기들은 항상 에러 처리를 직접 정의해 줘야 한다.
console.log('Stream error : ', err);
});
const objectParams = {
Bucket: bucketName,
Key: `imgFolder/${keyName}`, // 폴더 안에다가 업로드
Body: keyValue, // 파일 내용 (스트림 파일을 그대로 넣는다)
ContentType: `$image/${path.extname(keyName).substring(1)}`, // 파일 타입. image.webp' -> .webp -> webp
ACL: 'public-read', // 파일 권한
};
await s3
.upload(objectParams)
.promise()
.then((data) => {
console.log('Upload Success! : ', data.Location);
})
.catch((error) => {
console.error(error);
});
객체 다운
위에서 업로드를 해봤으니 이번엔 버킷의 이미지 파일을 로컬로 다운로드 해보자.
//* 객체 다운로드
const fs = require('fs');
const path = require('path');
const bucketName = 'node-sdk-sample-123123123';
const keyName = 'image.webp'; // 파일명
const objectParams2 = {
Bucket: bucketName, // 다운할 버킷
Key: `imgFolder/${keyName}`, // 다운할 파일 경로
};
// 파일을 스트림(쓰기)으로 생성 (구분을 위해 파일명을 달리함)
const file = fs.createWriteStream('./image_copy.webp');
try {
await s3
.getObject(objectParams2)
.createReadStream() // 얻은 객체 데이터를 스트림으로 읽어
.pipe(file); // 조금씩 복사해서 파이프로 write스트림으로 데이터 복사해서 파일 생성
} catch (error) {
console.error(error);
}
객체 복사
이번엔 버킷의 파일을 버킷에 복사해보도록 해보자 (s3 → s3)
다만 copyObject의 파라미터가 약간 혼동이 올수가 있어 정리해본다.
- Bucket : 복사될 파일의 버킷
- Key : 복사될 파일명
- CopySource : 복사할 파일 경로 지정 (버킷 이름 까지 풀로 지정해주어야 한다)
const bucketName = 'node-sdk-sample-123123123';
const keyName = 'image.webp'; // 파일명
//* 객체 복사
const objectParams_dl = {
Bucket: bucketName, // 복사될 파일의 버킷 지정
Key: `imgFolder/copy.webp`, // 복사될 파일
CopySource: `${bucketName}/imgFolder/${keyName}`, // 복사할 파일 경로 지정
//? CopySource에는 root에서의 경로가 아니라, 버킷 이름까지 포함 시켜야 한다.
};
await s3
.copyObject(objectParams_dl)
.promise()
.then((data) => {
console.log('Copy Success! : ', data);
})
.catch((error) => {
console.error(error);
});
객체 삭제
파일 단일로만 삭제할 때는 다음과 같다.
const bucketName = 'node-sdk-sample-123123123';
//* 단일 객체 삭제
const objectParams_del = {
Bucket: bucketName,
Key: `imgFolder/hello_world.txt`,
};
await s3
.deleteObject(objectParams_del)
.promise()
.then((data) => {
console.log('Delete Success! : ', data);
})
.catch((error) => {
console.error(error);
});
하지만 여러개의 파일을 삭제하려면 계속 요청을 보내야되서 서버에 부하가 올수 있다.
이때는 deleteObjects 라는 s 붙인 메소드를 이용해 한꺼번에 삭제 처리가 가능하다.
const bucketName = 'node-sdk-sample-123123123';
//* 여러개 객체 삭제
const objectParams_dels = {
Bucket: bucketName,
Delete: {
Objects: [
{
Key: 'imgFolder/copy.webp',
// VersionId: '2LWg7lQLnY41.maGB5Z6SWW.dcq0vx7b', 버저닝 쓸 경우
},
{
Key: 'imgFolder/image.webp',
// VersionId: 'yoz3HB.ZhCS_tKVEmIOr7qYyyAaZSKVd', 버저닝 쓸 경우
},
],
Quiet: false,
},
};
await s3
.deleteObjects(objectParams_dels)
.promise()
.then((data) => {
console.log('Delete Success! : ', data);
})
.catch((error) => {
console.error(error);
});
폴더 생성하기
const bucketName = 'node-sdk-sample-123123123';
//* 폴더 생성
const objectFolder = {
Bucket: bucketName,
Key: `imgFolder/uploads/`, // 폴더 경로만 써준다
};
s3.putObject(objectFolder)
.promise()
.then((data) => {
console.log('Make Folder Success! : ', data);
})
.catch((error) => {
console.error(error);
});
})();
객체가 존재하는지 확인
버킷에 찾고자하는 파일이 있는지 없는지 확인할 때 사용되는 로직이다.
const bucketName = 'node-sdk-sample-123123123';
//* 객체 있는지 없는지 확인
const objectParams_url = {
Bucket: bucketName,
Key: `imgFolder/image.webp`, // 존재하지 않는 파일
};
await s3
.headObject(objectParams_url)
.promise()
.then(async (data) => {
console.log(data)
// 만일 객체가 있으면 getSignedUrl을 통해 객체 url을 가져온다.
const obj_url = s3.getSignedUrl('getObject', objectParams_url);
console.log('obj_url: ', obj_url);
})
.catch((error) => {
if (error.code === 'NotFound') console.error('파일이 존재하지 않음');
});
객체 Pre Signed URL 얻기
코드를 통해 객체에 대한 임시 권한을 얻는 방법이다.
실무에서도 정말 자주 사용되는 메서드 이니 필히 익혀주자.
Expires 파라미터를 안써주면, 그냥 파일의 url을 가져오게 된다.
단, 이때 public 설정이 되어있지 않으면 엑세스가 불가능하다.
const bucketName = 'node-sdk-sample-123123123';
//* 객체 Pre Signed URL 얻기
const objectParams_preSign = {
Bucket: bucketName,
Key: `imgFolder/copy.webp`,
Expires: 60, // 60초 동안만 url 유지
};
await s3
.getSignedUrlPromise('getObject', objectParams_preSign)
.then((data) => {
console.log('Presigned URL : ', data);
})
.catch((error) => {
console.error(error);
});
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.