개요
SnapSpot을 개발하면서 '사진'을 어떻게 처리할지에 대해 난관에 봉착했다. 우선 이미 서버는 모듈만 6개로 사진 처리까지 감당할 수 있는 용량이 아닌데다, 사진을 처리하기에 이미 기능이 정신없이 많아서 속도도 느려질 것 같았다. 따라서 S3에 직접 프론트엔드가 올리게 하는 방법을 고안했고, AWS의 Lambda를 사용하기로 결정했다.
Lambda는?
FaaS, Function as a Service의 일종으로, '함수'를 Serverless하게 제공하는 개념을 가진다. 간단한 Rest API를 단일 함수(혹은 메소드)로서 제공한다.
유연한데다 사용한 만큼만 지불할 수 있는(On-Demand) 서비스이기 때문에, 단일 기능을 제공할 때 자주 사용한다.
Go 코드
Go를 사용한 이유는 단순하다. 빠르기 때문이다.
전체 코드부터 보자.
package main
import (
"context"
"encoding/json"
"log"
"time"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go/aws"
)
type Request struct {
FileName string
}
type Response struct {
PresignedUrl string `json:"PresignedUrl"`
}
func HandleRequest(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
var requestObj = Request{}
err := json.Unmarshal([]byte(req.Body), &requestObj)
if err != nil {
log.Fatal(err)
}
creds := credentials.NewStaticCredentialsProvider("access key", "secret key", "")
cfg, err := config.LoadDefaultConfig(
context.TODO(), config.WithCredentialsProvider(creds), config.WithRegion("ap-northeast-2"),
)
if err != nil {
log.Fatal(err)
}
svc := s3.NewFromConfig(cfg)
presignClient := s3.NewPresignClient(svc)
presignedUrl, err := presignClient.PresignPutObject(context.Background(),
&s3.PutObjectInput{
Bucket: aws.String(""),
Key: aws.String(requestObj.FileName),
},
s3.WithPresignExpires(time.Minute*15))
if err != nil {
log.Fatal(err)
return events.APIGatewayProxyResponse{Body: req.Body, StatusCode: 500}, err;
}
url := presignedUrl.URL
/*
var responseObj = Response{
PresignedUrl: url,
}
jsonBytes, _ := json.Marshal(responseObj)
responseString := string(jsonBytes)
*/
return events.APIGatewayProxyResponse{Body: url, StatusCode: 200}, nil
}
func main() {
lambda.Start(HandleRequest)
}
request: http 요청을 담은 객체다. APIGateWayEvent에서 request를 가져온다. 더 다양한 AWS Service의 요청 객체는 다음 레포지토리를 참고하면 확인할 수 있다.
https://github.com/aws/aws-lambda-go/blob/main/events/README_ApiGatewayEvent.md
AWS Session과 연결
creds := credentials.NewStaticCredentialsProvider("access key", "secret key", "")
cfg, err := config.LoadDefaultConfig(
context.TODO(), config.WithCredentialsProvider(creds), config.WithRegion("ap-northeast-2"),
)
if err != nil {
log.Fatal(err)
}
svc := s3.NewFromConfig(cfg)
presignClient := s3.NewPresignClient(svc)
AWS IAM에서 발급받은 access key와 secret key를 넣고 인증 정보(credential) 객체를 생성한다. 객체를 통해 ap-northeast-2 Region(서울)에 연결하고, s3와의 연결을 담당하는 객체도 생성한다.
**주의점: secret key 발급 시 특수문자가 있으면 에러가 난다. 노가다를 통해 특수문자가 없는 secret key를 발급받도록 하자
Presigned Url 발급
presignedUrl, err := presignClient.PresignPutObject(context.Background(),
&s3.PutObjectInput{
Bucket: aws.String("snapspotbucket"),
Key: aws.String(requestObj.FileName),
},
s3.WithPresignExpires(time.Minute*15))
if err != nil {
log.Fatal(err)
return events.APIGatewayProxyResponse{Body: req.Body, StatusCode: 500}, err;
}
Put으로 사진을 저장하는 권한을 가진 presigned url을 생성해 발급한다.
이후 바디로 전송한다.
**주의점: 바디는 text로 전송된다(json 이 아니라는 뜻). Header에 하나하나 설정하는 방법도 시도했으나 실패했다.
React 코드
원래 서버가 하는 일을 프론트한테 요청하는 것이기 때문에 예제 코드가 필수였다. 보자.
import logo from "./logo.svg";
import "./App.css";
import { useEffect, useRef, useCallback, useState } from "react";
import axios from "axios";
function App() {
const inputRef = useRef(null);
const [img, setImg] = useState("");
const [imgMeta, setImgMeta] = useState();
const [presignedUrl, setPresignedUrl] = useState("");
const getPresignedUrl = () => {
console.log(img);
console.log(imgMeta);
console.log(inputRef);
const a = axios
.post(
"",
{
fileName: imgMeta.name,
}
)
.then((res) => {
axios.put(res.data, img, {
"Content-Type": imgMeta.type,
});
});
};
const uploadImageInput = (event) => {
setImgMeta(event.target.files[0]);
var reader = new FileReader();
/*
reader.onload = function (event) {
setImg(event.target.result);
};
*/
reader.readAsDataURL(imgMeta);
reader.onload = () => {
setImg(reader.result);
};
};
return (
<div className="App">
<h2>Hello world</h2>
<input
type="file"
accept="image/*"
ref={inputRef}
onChange={uploadImageInput}
/>
<button label="이미지 업로드" onClick={getPresignedUrl} />
<img width={"100%"} src={img} />
</div>
);
}
export default App;
완벽한 코드는 절대결코아니지만 어떻게 사용하는지에 대한 최소한의 가이드는 제공하고 있다.
결론
이 예제의 핵심은 사진을 저장하는 작업에 있어 백엔드(스프링부트 서버)의 부담을 최소화하는 것이다.
무료로 사용가능한 AWS의 서비스를 활용하였고, 속도가 가장 빠른 Go를 활용하였다. 프론트에서도(정확히는 사용자 입장에서 새로운 사진 전송 시) 속도에 대한 부담이 최소화되도록 설계했다는 점에서 지금 개발중인 서비스에 가장 적합한 방법이라고 생각한다.
api 나와야 하는 게 아직도 작업이 남아있어서 그쪽 마저 개발하고 스프링부트에서 부렸던 여러가지 요행을 다음 글에 적으려고 한다(18초 걸리는 걸 1초로 줄였다던가...)
'Cloud > AWS' 카테고리의 다른 글
RDS 프리티어 한도 내로 생성하기 / AWS RDS 과금 방지 (0) | 2023.07.27 |
---|