BLOG
SaaS 서비스를 위한 모바일 앱을 개발하기 위한 이미지를 활용 할 경우 디바이스에 맞추어 이미지 크기를 조정하는 것이 중요합니다. 이를 위해, 적절한 크기 (너비 x 높이)와 해상도 (dpi) 가 있어야 합니다 .
이미지 리사이징을 진행 해야 하는 이유는 다음과 같습니다.
- 고해상도 디스플레이의 저해상도 이미지는 잘못된 UX를 만듭니다.
- 저해상도 디스플레이의 고해상도 이미지는 대역폭 과 장치 및 서버 리소스를 모두 낭비 합니다 특히 프레임 워크를 사용하는 경우 인앱 에서 이미지를 채택하고 이미지를 자르거나 크기를 조정 하면 앱이 무거울 수 있습니다.
- 웹 응용 프로그램에서도 이미지 크기를 즉시 조정할 수 있습니다. CDN을 통해보다 효율적으로 배포 할 수 있습니다.
- 웹 사이트의 작동 방식은 서버에 저장된 원본 이미지의 사본을 보유하는 것입니다. 일부 컨텐츠를 요청하면 이미지는 URL을 통해 그 일부입니다.
- 이미지의 URL 다음에 서버의 파일에 직접 액세스합니다. 이것은 웹 사이트에 큰 부담을 줄 것입니다. 요즘 널리 사용되는 첫 번째 단계는 CDN을 사용하는 것입니다.
- CDN 은 불필요한 HTTP 헤더의 대부분 이 생략되어 내용이 더 가벼워지므로 더 빠릅니다 . 또한 컨텐츠는 네트워크의 다른 엔드 포인트로 복제되어 가까운 거리에서 클라이언트로 전달됩니다.
Associating Cloudfront and Lambda@Edge
다음과 같이 AWS 내 CloudFront (CDN) + Lamda@Edge(On The Fly Resizing) 을 진행 해 보겠습니다.
- Cloudfront Viewer request
- CloudFront Origin request
- Cloudfront Origin response
- Cloudfront Viewer response
- 쿼리 매개 변수로 크기를 지정하여 이미지를 그 자리에서 리사이즈 한다
- 뷰어에 따라 최적화 된 이미지 포맷을 제공한다
- 이미지 크기의 화이트리스트를 정의하여 생성 및 배포를 허용한다
- 요청한 이미지 크기, 포맷이 존재하지 않는 경우에만 크기 조정 작업을 수행한다
- CloudFront 배포에 연결된 2개의 Lambda@Edge 트리거. 뷰어 요청 및 원서 응답.
- 오리진으로 사용하는 Amazon S3.
예상 시나리오
- 요청된 이미지는 URI는 Lambda@Edge의 뷰어 요청 함수에서 조작된 적절한 크기와 포맷이다. 이것은 요구가 캐시에 히트하기 전에 발생한다.
- CloudFront는 오리진에서 이미지 개체를 읽는다.
- 필요한 이미지가 이미 S3 버킷에 존재하는 경우, 또는 5단계에서 생성된 저장되어있는 경우 CloudFront는 뷰어 이미지 오브젝트를 돌려준다. 이때 이미지가 캐시된다.
- 캐시된 이미지 객체가 뷰어에 반환된다.
- 리사이즈 작업은 오리진에 이미지가 존재하지 않는 경우에만 실행된다. S3 버킷(오리진) 네트워크 호출을 원래 이미지를 가져 크기를 조정한다. 생성된 이미지는 CloudFront에 전송하기 전에 S3 버킷에 유지된다.
Lambda@Edge 함수
- Viewer-Request 함수 :: Request URI 조작
‘use strict’; const querystring = require(‘querystring’); // defines the allowed dimensions, default dimensions and how much variance from allowed // dimension is allowed. const variables = { allowedDimension : [ {w:100,h:100}, {w:200,h:200}, {w:300,h:300},{w:400,h:400} ], defaultDimension : {w:200,h:200}, variance: 20, webpExtension: ‘webp’ };exports.handler = (event, context, callback) => { const request = event.Records[0].cf.request; const headers = request.headers; // parse the querystrings key-value pairs. In our case it would be d=100×100 const params = querystring.parse(request.querystring); // fetch the uri of original image let fwdUri = request.uri; // if there is no dimension attribute, just pass the request if(!params.d){ callback(null, request); return; } // read the dimension parameter value = width x height and split it by ‘x’ const dimensionMatch = params.d.split(“x”); // set the width and height parameters let width = dimensionMatch[1]; let height = dimensionMatch[2]; // parse the prefix, image name and extension from the uri. // In our case /images/image.jpg const match = fwdUri.match(/(.*)\/(.*)\.(.*)/); let prefix = match[1]; let imageName = match[2]; let extension = match[3]; // define variable to be set to true if requested dimension is allowed. let matchFound = false; // calculate the acceptable variance. If image dimension is 105 and is within acceptable // range, then in our case, the dimension would be corrected to 100. let variancePercent = (variables.variance/100); for (let dimension of variables.allowedDimension) { let minWidth = dimension.w – (dimension.w * variancePercent); let maxWidth = dimension.w + (dimension.w * variancePercent); if(width >= minWidth && width <= maxWidth){ width = dimension.w; height = dimension.h; matchFound = true; break; } } // if no match is found from allowed dimension with variance then set to default //dimensions. if(!matchFound){ width = variables.defaultDimension.w; height = variables.defaultDimension.h; } // read the accept header to determine if webP is supported. let accept = headers[‘accept’]?headers[‘accept’][0].value:””; let url = []; // build the new uri to be forwarded upstream url.push(prefix); url.push(width+”x”+height);// check support for webp if (accept.includes(variables.webpExtension)) { url.push(variables.webpExtension); } else{ url.push(extension); } url.push(imageName+”.”+extension); fwdUri = url.join(“/”); // final modified url is of format /images/200×200/webp/image.jpg request.uri = fwdUri; callback(null, request); }; |
위 코드에서는 뷰어의 ‘Accept’헤더에 기초하여 다른 이미지 포맷을 제공하기 위해 입력된 URI를 조작합니다.
입력된 화이트리스트와 비교하여 가장 가까운, 조작 가능한 크기로 변환합니다.
따라서 비 표준 크기(미리 입력 해 두지 않은) 요청이 온 경우에 그와 가장 비슷한 값을 제공하며, 캐싱되는 이미지를 효율적으로 적중 시켜주는 매커니즘입니다.
또한 불필요한 이미지 크기 생성을 방지할 수 있습니다
- Origin-Response 함수 : : 이미지 개체가 있는지 확인하고 필요에 따라 이미지 크기 조정 수행
‘use strict’; const http = require(‘http’); const https = require(‘https’); const querystring = require(‘querystring’); const AWS = require(‘aws-sdk’); const S3 = new AWS.S3({ signatureVersion: ‘v4’, }); const Sharp = require(‘sharp’); // set the S3 endpoints const BUCKET = ‘image-resize-${AWS::AccountId}-us-east-1’; exports.handler = (event, context, callback) => { let response = event.Records[0].cf.response; console.log(“Response status code :%s”, response.status); //check if image is not present if (response.status == 404) { let request = event.Records[0].cf.request; let params = querystring.parse(request.querystring); // if there is no dimension attribute, just pass the response if (!params.d) { callback(null, response); return; } // read the dimension parameter value = width x height and split it by ‘x’ let dimensionMatch = params.d.split(“x”); // read the required path. Ex: uri /images/100×100/webp/image.jpg let path = request.uri; // read the S3 key from the path variable. // Ex: path variable /images/100×100/webp/image.jpg let key = path.substring(1); // parse the prefix, width, height and image name // Ex: key=images/200×200/webp/image.jpg let prefix, originalKey, match, width, height, requiredFormat,imageName; let startIndex; try { match = key.match(/(.*)\/(\d+)x(\d+)\/(.*)\/(.*)/); prefix = match[1]; width = parseInt(match[2], 10); height = parseInt(match[3], 10); // correction for jpg required for ‘Sharp’ requiredFormat = match[4] == “jpg” ? “jpeg” : match[4]; imageName = match[5]; originalKey = prefix + “/” + imageName; } catch (err) { // no prefix exist for image.. console.log(“no prefix present..”); match = key.match(/(\d+)x(\d+)\/(.*)\/(.*)/); width = parseInt(match[1], 10); height = parseInt(match[2], 10); // correction for jpg required for ‘Sharp’ requiredFormat = match[3] == “jpg” ? “jpeg” : match[3]; imageName = match[4]; originalKey = imageName; } // get the source image file S3.getObject({ Bucket: BUCKET, Key: originalKey }).promise() // perform the resize operation .then(data => Sharp(data.Body) .resize(width, height) .toFormat(requiredFormat) .toBuffer() ) .then(buffer => { // save the resized object to S3 bucket with appropriate object key. S3.putObject({ Body: buffer, Bucket: BUCKET, ContentType: ‘image/’ + requiredFormat, CacheControl: ‘max-age=31536000’, Key: key, StorageClass: ‘STANDARD’ }).promise() // even if there is exception in saving the object we send back the generated // image back to viewer below .catch(() => { console.log(“Exception while writing resized image to bucket”)}); // generate a binary response with resized image response.status = 200; response.body = buffer.toString(‘base64’); response.bodyEncoding = ‘base64’; response.headers[‘content-type’] = [{ key: ‘Content-Type’, value: ‘image/’ + requiredFormat }]; callback(null, response); }) .catch( err => { console.log(“Exception while reading source image :%j”,err); }); } // end of if block checking response statusCode else { // allow the response to pass through callback(null, response); } }; |
위 함수는 CloudFront가 오리진에서 응답을 받은 후 캐시에 저장하기 전에 실행 됩니다.
- 오리진 응답 상태 코드를 확인하고 Amazon S3 버킷에 이미지 개체가 있는지 확인
- 이미지가 존재하는 경우는 그대로 CloudFront 응답 사이클 계속
- 이미지가 S3 버킷에 존재하지 않는 경우, 원본 이미지를 얻고 크기 조정을 수행 버퍼에 입력하여 리사이즈 이미지에 올바른 접두사와 메타 데이터를 부여하여 S3 버킷에 유지
- 이미지의 크기가 바뀔 경우 메모리 내의 리사이즈 이미지를 오리진으로 바이너리 응답을 생성하여, 적절한 상태코드를 헤더에 반환
Note : S3 버킷과 Lambda 함수를 실행하는 Edge 위치가 다른 지역의 경우 Amazon S3에서 Lambda 함수에 지역 간 데이터 전송 (out) 요금이 발생하므로 주의하시기 바랍니다. 이러한 비용은 이미지 생성 마다 1 회 발생합니다.
컨텐츠 관리 시스템으로부터 적절한 URL을 호출하여 필요한 이미지 포맷 및 크기를 미리 생성할 수 있습니다.
또한 워터마크를 부여하도록 코드를 수정하시면 확장도 가능합니다
기능 테스트
- 작성한 S3 버킷에 고해상도의 이미지 파일 (image.jpg)를 ‘images’디렉토리에 업로드합니다.
- 좋아하는 브라우저에서 다음 URL을 엽니다.
https://{cloudfront-domain}/images/image.jpg?d=100×100
** 메가존 클라우드의 ‘Lab to Scale’ 프로그램은 AWS SaaS Factory 베스트 프랙티스 기반의 확장성 높은 SaaS 서비스 설계/구축/운영 서비스를 제공 합니다. 메가존 클라우드의 클라우드 전문가들과 함께 신규 SaaS 서비스 설계부터 국내/외 비즈니스 협업까지 SaaS 서비스의 성공적인 사업화를 지원 합니다