AWS S3를 이용해서 웹브라우저에서 사용할 수 있는 포토 앨범 만들기
이번 포스팅에서는 AWS S3를 이용해서 웹브라우저에서 사진을 업로드하고 보고, 삭제 할 수 있는 포토 앨범을 만드는 예제를 정리해보겠습니다.
시나리오
이 예제에서는 아래 그림과 같이, 웹브라우저에서 AWS SDK를 이용해서 AWS S3에 사진앨범을 만들게 됩니다.
이 예제에는 포토앨범에 대해서 아래와 같은 기능이 포함되어 있습니다.
- listAlbums
- createAlbums
- viewAlbums
- addPhoto
- deleteAlbum
- deletePhoto
0. 선행 작업
이 예제를 진행하기 위해서 3가지 선행 작업이 필요합니다.
0-1. AWS S3 Bucket 만들기
포토앨범들을 저장하기 위한 Bucket을 하나 만들어 줍니다.
그리고 Bucket의 Permissions 설정에서 하위메뉴에 있는 CORS configuration
에 아래의 문서를 복사하고 저장해줍니다.
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>POST</AllowedMethod>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<AllowedMethod>DELETE</AllowedMethod>
<AllowedMethod>HEAD</AllowedMethod>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
0-2. AWS Cognito Identity Pool 생성하기
0-3. Identity Pool 생성할 때 함께 생성된 IAM Role에 S3에 접근할 수 있는 Policy 추가하기.
원문에서는 IAM role을 생성하라고 되어 있는데, 그럴 필요 없습니다.
0-2 단계에서 Identity Pool을 생성할 때 IAM role이 함께 생성되는데, 그 role을 찾아서 거기에 S3에 접근 할 수 있게 하기위한 Policy를 추가해주면 됩니다.
이 과정을 조금 자세히 살펴 보겠습니다.
(1) Policy 생성
Identity Pool을 생성할 때 함께 만들어진 IAM role을 찾으시면
Inline Policies
는 무언가 자동으로 만들어진게 있는데
Managed Policies
에는 아무것도 없습니다.
Attach Policy
를 눌러서 S3에 접근할 수 있는 Policy를 찾아서 추가하면 되는데, 이렇게 하면 기본적으로 모든 Bucket에 대한 권한을 주게 됩니다.
앨범으로 쓸 Bucket은 하나인데 다른 Bucket까지 권한을 주면 보안 측면에서 좋지 않은 건 당연합니다.
그래서 권한을 줄 Bucket만을 대상으로하는 Policy를 새로 생성합니다.
AWS IAM에서 좌측 메뉴의 Policies를 누르면 기본적으로 제공되는 Policy들이 나타납니다. 그리고 그 위에 보면
create policy
가 있습니다. 클릭합니다.
다음 화면에서 Policy를 어떤 방식으로 생성할 건지 묻는데, 여기서는
Create Your Own Policy
를 선택합니다.
왜냐하면 Policy에 작성할 내용은 원문에서 제공하고 있거든요.
Policy Name은 본인이 Policy를 식별할 수 있게 잘 지어주시면 되고 Description도 Policy에 대한 설명을 잘 작성해주시면 됩니다.
중요한건 Policy Document입니다. 여기에 아래 Json 문서를 복사하고 BUCKET_NAME
을 앨범으로 쓸 Bucket이름으로 바꿔줍니다.
다 작성하고나면 Validate Policy 버튼을 눌러서 작성한 Policy가 올바른지 확인하고 확인이 되면 Create Policy 버튼을 클릭합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::BUCKET_NAME/*"
]
}
]
}
(2) Cognito Identity Pool을 생성하면서 함께 생성된 Role에 방금 생성한 Policy 추가
Policy를 생성하고 나면 아까 Role에서 아무것도 없던 Managed Policies에 Attach Policy
를 눌러서 방금 생성한 Policy를 추가해줍니다.
선행작업 3번째가 좀 길었네요.. 선행작업 여기까지입니다ㅎㅎㅎ 쉬었다가 아래부터는 웹브라우저에서 포토앨범을 만들 html, javascript 코드들을 보시죠 ㅎㅎ
1. HTML 코드 작성
HTML 코드는 간결합니다. 사실 원문에서는 더 간결한데, 원문에서는 익명사용자를 허용하기 때문에 로그인 할 필요가 없어서 더 짧은 것이고, 본 포스팅에서는 로그인을 한 사용자만 허용하기 위해서 코드가 조금 더 들어갔습니다.
<!DOCTYPE html>
<html>
<head>
<!--
AWS Cognito를 사용하려면 아래 2개의 라이브러리가 필요합니다.
자세한 사항은 https://walkinpcm.blogspot.kr/2017/05/aws-aws-cognito.html 를 참고해주세요.
-->
<script src="./res/js/aws-cognito-sdk.min.js"></script>
<script src="./res/js/amazon-cognito-identity.min.js"></script>
<!-- AWS SDK for Javascript -->
<script src="https://sdk.amazonaws.com/js/aws-sdk-2.7.20.min.js"></script>
<!-- 로그인, 앨범 관련 함수를 작성할 js 파일 -->
<script src="./res/js/app.js"></script>
<script>
function getHtml(template) {
return template.join('\n');
}
</script>
</head>
<body>
<!-- 로그인을 위한 부분입니다. -->
<div>
<h1>Cognito Sign in</h1>
Username : <input id="signin_username" type="text" size="20" placeholder="Username"><br>
Password : <input id="signin_pwd" type="password" size="20" placeholder="Password"><br>
<input type="button" onclick="submitSignin()" value="Sign in">
</div>
<h1>My Photo Albums App</h1>
<!-- app div 내부에 함수들의 실행 결과가 출력될 것입니다. -->
<div id="app"></div>
</body>
</html>
코드를 실행하면 아래와 같이 스타일 없는 밋밋한 화면이 출력됩니다.
원문대로라면 앨범 리스트가 나와야하지만, 본 포스팅에서는 로그인에 성공하면 앨범 리스트가 출력되도록 수정하였습니다.
2. Javascript 코드 작성
Javascript코드는 HTML의 head 내부에 script 태그로 넣어도 되지만, 내용이 조금 기니깐 따로 파일로 저장하시는게 좋습니다.
위의 HTML 코드에서도 보이듯이 저는 app.js 라고 파일을 작성해서 HTML 코드에서 불러왔습니다.
참고로, 제 포스팅의 코드와 원문의 코드가 조금씩 다른 부분은 다 로그인한 사용자만 허용하기 위해서 코드를 조금 수정한 것입니다.
2-1. AWS SDK와 AWS Cognito를 사용하기 위한 환경설정
// 리전 정보
var regionInfo = '<region>';
// 포토앨범으로 사용할 Bucket 이름
var albumBucketName = '<bucket-name>';
// 로그인 기능을 위한 Id 설정들
var userPoolId = '<user pool id>';
var appClientId = '<app client id>'; // 위의 userpool에서 생성한 app client id를 입력하시면 됩니다.
var identityPoolId = '<identity pool id>'; // 위의 userpool을 이용해서 생성한 federated identity pool의 id를 입력하시면 됩니다.
// AWS.config에 리전 정보 설정
AWS.config.update({
region: regionInfo
});
// AWSCognito 객체에도 리전 정보 설정
AWSCognito.config.region = regionInfo;
// 사용할 User Pool의 정보를 저장
var poolData = {
UserPoolId : userPoolId,
ClientId : appClientId
};
// 입력한 User Pool 정보를 가지고 실제 User Pool에 접근할 수 있는 객체 생성
var userPool = new AWSCognito.CognitoIdentityServiceProvider.CognitoUserPool(poolData);
// 로그인 성공시 로그인 한 사용자의 정보를 반환받는 변수
var cognitoUser;
// s3 객체를 담기 위한 전역변수 선언
var s3;
2-2. 로그인 함수
function submitSignin() {
// 입력 폼에서 ID와 비밀번호를 입력받습니다.
var user_Name = document.getElementById("signin_username").value;
var user_Pw = document.getElementById("signin_pwd").value;
// ID와 Password를 정해진 속성명인 Username과 Password에 담습니다.
var authenticationData = {
Username : user_Name,
Password : user_Pw
};
// 여기에는 ID와 User Pool 정보를 담아 놓습니다.
var userData = {
Username : user_Name, // your username here
Pool : userPool
};
// 로그인을 위해 Cognito 객체 2개를 준비합니다.
var authenticationDetails = new AWSCognito.CognitoIdentityServiceProvider.AuthenticationDetails(authenticationData);
cognitoUser = new AWSCognito.CognitoIdentityServiceProvider.CognitoUser(userData);
// authenticateUser 함수로 로그인을 시도합니다.
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: function (result) {
// credentials를 가져오기 위해 필요한 정보들을 입력합니다.
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: identityPoolId,
Logins: {}
});
AWS.config.credentials.params.Logins['cognito-idp.' + regionInfo + '.amazonaws.com/'+ userPoolId] = result.getIdToken().getJwtToken();
// get() 함수로 credentials을 획득합니다.
AWS.config.credentials.get(function(err){
if (err) {
alert(err);
}else{
// !!! 주의 !!! S3 객체를 만들때는 credentials이 획득된 상태에서 S3를 생성해야합니다.
// 그렇지 않으면, Missing credentials in config 에러가 발생합니다.
s3 = new AWS.S3({
apiVersion: '2006-03-01',
params: {Bucket: albumBucketName}
});
// s3객체를 획득하고 앨범 리스트를 출력합니다.
listAlbums();
}
});
},
onFailure: function(err) {
alert(err);
}
});
}
2-3. 포토앨범 제어 함수
이 부분은 원문의 코드와 다른점이 없기에 상세한 설명은 생략하도록 하겠습니다.
다만, 원문의 오타인것인지 viewAlbum() 함수의 첫줄에서 슬래시가 2개면 정상적으로 안되고 1개면 정상적으로 되는 것 같더군요.
그래서 저는 슬래시 1개만 썼습니다.
관련해서 AWS에 Feedback을 남겼는데, 답변을 받아봐야 진상을 알 것 같네요 ㅎㅎㅎ
전체 코드를 보시려면 아래를 펼쳐주세요.
원문을 복사한것이 아니라 제가 작성해서 작동을 확인한 코드입니다.
전체 코드 숨기기
function createAlbum(albumName) {
albumName = albumName.trim();
if (!albumName) {
return alert('Album names must contain at least one non-space character.');
}
if (albumName.indexOf('/') !== -1) {
return alert('Album names cannot contain slashes.');
}
var albumKey = encodeURIComponent(albumName) + '/';
s3.headObject({Key: albumKey}, function(err, data) {
if (!err) {
return alert('Album already exists.');
}
if (err.code !== 'NotFound') {
return alert('There was an error creating your album: ' + err.message);
}
s3.putObject({Key: albumKey}, function(err, data) {
if (err) {
return alert('There was an error creating your album: ' + err.message);
}
alert('Successfully created album.');
viewAlbum(albumName);
});
});
}
function listAlbums() {
s3.listObjects({Delimiter: '/'}, function(err, data) {
if (err) {
return alert('There was an error listing your albums: ' + err.message);
} else {
var albums = data.CommonPrefixes.map(function(commonPrefix) {
var prefix = commonPrefix.Prefix;
var albumName = decodeURIComponent(prefix.replace('/', ''));
return getHtml([
'<li>',
'<span onclick="deleteAlbum(\'' + albumName + '\')">X</span>',
'<span onclick="viewAlbum(\'' + albumName + '\')">',
albumName,
'</span>',
'</li>'
]);
});
var message = albums.length ?
getHtml([
'<p>Click on an album name to view it.</p>',
'<p>Click on the X to delete the album.</p>'
]) :
'<p>You do not have any albums. Please Create album.';
var htmlTemplate = [
'<h2>Albums</h2>',
message,
'<ul>',
getHtml(albums),
'</ul>',
'<button onclick="createAlbum(prompt(\'Enter Album Name:\'))">',
'Create New Album',
'</button>'
]
document.getElementById('app').innerHTML = getHtml(htmlTemplate);
}
});
}
function viewAlbum(albumName) {
var albumPhotosKey = encodeURIComponent(albumName) + '/'; // 여기 '//'였는데 '/'로 수정함.
s3.listObjects({Prefix: albumPhotosKey}, function(err, data) {
if (err) {
return alert('There was an error viewing your album: ' + err.message);
}
// `this` references the AWS.Response instance that represents the response
var href = this.request.httpRequest.endpoint.href;
var bucketUrl = href + albumBucketName + '/';
var photos = data.Contents.map(function(photo) {
var photoKey = photo.Key;
var photoUrl = bucketUrl + encodeURIComponent(photoKey);
return getHtml([
'<span>',
'<div>',
'<img style="width:128px;height:128px;" src="' + photoUrl + '"/>',
'</div>',
'<div>',
'<span onclick="deletePhoto(\'' + albumName + "','" + photoKey + '\')">',
'X',
'</span>',
'<span>',
photoKey.replace(albumPhotosKey, ''),
'</span>',
'</div>',
'<span>',
]);
});
var message = photos.length ?
'<p>Click on the X to delete the photo</p>' :
'<p>You do not have any photos in this album. Please add photos.</p>';
var htmlTemplate = [
'<h2>',
'Album: ' + albumName,
'</h2>',
message,
'<div>',
getHtml(photos),
'</div>',
'<input id="photoupload" type="file" accept="image/*">',
'<button id="addphoto" onclick="addPhoto(\'' + albumName +'\')">',
'Add Photo',
'</button>',
'<button onclick="listAlbums()">',
'Back To Albums',
'</button>',
]
document.getElementById('app').innerHTML = getHtml(htmlTemplate);
});
}
function addPhoto(albumName) {
var files = document.getElementById('photoupload').files;
if (!files.length) {
return alert('Please choose a file to upload first.');
}
var file = files[0];
var fileName = file.name;
var albumPhotosKey = encodeURIComponent(albumName) + '/';
var photoKey = albumPhotosKey + fileName;
s3.upload({
Key: photoKey,
Body: file,
ACL: 'public-read'
}, function(err, data) {
if (err) {
return alert('There was an error uploading your photo: ', err.message);
}
alert('Successfully uploaded photo.');
viewAlbum(albumName);
});
}
function deletePhoto(albumName, photoKey) {
s3.deleteObject({Key: photoKey}, function(err, data) {
if (err) {
return alert('There was an error deleting your photo: ', err.message);
}
alert('Successfully deleted photo.');
viewAlbum(albumName);
});
}
function deleteAlbum(albumName) {
var albumKey = encodeURIComponent(albumName) + '/';
s3.listObjects({Prefix: albumKey}, function(err, data) {
if (err) {
return alert('There was an error deleting your album: ', err.message);
}
var objects = data.Contents.map(function(object) {
return {Key: object.Key};
});
s3.deleteObjects({
Delete: {Objects: objects, Quiet: true}
}, function(err, data) {
if (err) {
return alert('There was an error deleting your album: ', err.message);
}
alert('Successfully deleted album.');
listAlbums();
});
});
}
모든 코드를 다 작성하시고, html 페이지를 웹브라우저에서 열으셔서 로그인을 하시면 앨범 목록이 보이고(처음엔 비었겠지만)
앨범을 생성하실 수 있습니다. 사진도 넣으실 수 있구요. 물론 사진 삭제도 됩니다.
다만 저도 아직 이상한 점은... 앨범을 생성하면 자동으로 빈껍데기 파일이 앨범안에 하나가 생성되더라구요..
코드 상에서 putObject가 있는데 일부러 넣은 것 같아서 뺄 수도 없고,,, 일단 좀 살펴봐야할 것 같습니다.
이 부분이 전체적인 기능에 문제가 되지는 않으니 일단 무시해도 될 것 같습니다~
여기까지해서 이번 포스팅을 마치겠습니다.
참고로, 본 포스팅의 코드가 제가 실습한 전체 코드입니다.
혹여 틀린 부분이나 보완되야할 부분을 댓글로 남겨주시면 최대한 빠르게 댓글 달고 수정하도록 하겠습니다~
감사합니다~!
참고 링크