AWS-S3와 presignedURL
포스트
취소

AWS-S3와 presignedURL

해당 포스트는 TypeScriptAWS-S3presignedURL를 사용하는 방법에 대한 포스트입니다.

TODO: .env, 엑세스 키 생성 방법, 제거, 복사, 이동

🔑 ACCESS_KEY, ACCESS_SECRET_KEY 구하기

두 개의 키들은 JS(TS)로 S3에 접근할 때 식별하기 위해 사용하는 Key입니다.

0️⃣ root 계정으로 생성 ( 비추천 )

관리자 계정으로 키를 생성했다가 키를 탈취당하면 모든 곳의 접근 권한이 생기므로 위험하기 때문에 IAM을 통해서 키를 생성하는 것이 더 좋습니다.

  1. 관리자 계정으로 로그인 후 여기 접근
  2. 액세스 키(엑세스 키 ID 및 비밀 액세스 키) 클릭
  3. 새 액세스 키 만들기 클릭

바로 새로운 액세스 키가 생성되며 두 가지의 키를 얻을 수 있습니다.
ACCESS_KEY는 이후에도 확인할 수 있으나 ACCESS_SECRET_KEY는 생성한 시점에만 확인할 수 있습니다.

1️⃣ IAM 생성

  1. 관리자 계정 혹은 IAM 생성 권한이 있는 계정으로 여기 접근
  2. 사용자 클릭
  3. 사용자 추가 클릭
  4. 상황에 맞게 권한을 부여한 사용자 생성 ( 저의 경우에는 S3에 대한 모든 권한 부여 )

사용자를 생성하면 즉시 ACCESS_KEY, ACCESS_SECRET_KEY를 얻을 수 있습니다.
물론 ACCESS_SECRET_KEY는 생성한 시점 이후에 확인이 불가능합니다.
또한 콘솔 로그인 권한을 부여했다면 바로 로그인 페이지로 이동하면서 id를 기입해주는 링크를 제공받습니다.
해당 링크로 접근하면 편하게 생성한 IAM계정으로 로그인할 수 있습니다.

✍️ S3 버킷 생성 및 권한 부여

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 버킷 정책 수정
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Statement1",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::버킷명/*"
        }
    ]
}

// CORS 수정
[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "HEAD",
            "GET",
            "PUT",
            "POST",
            "DELETE"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": []
    }
]

🔗 Node.js와 AWS-S3 연결 및 사용 예시

0️⃣ 설치

1
2
# aws와 node.js를 연결시키기 위한 패키지
npm i aws-sdk

1️⃣ 연결 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import AWS from "aws-sdk";

AWS.config.update({
  // 현재 사용중인 region ( "ap-northeast-2" )
  region: process.env.AWS_REGION,
  // IAM에서 얻은 ACCESS_KEY
  accessKeyId: process.env.AWS_ACCESS_KEY,
  // IAM에서 얻은 ACCESS_SECRET_KEY
  secretAccessKey: process.env.AWS_ACCESS_SECRET_KEY,
});

const S3 = new AWS.S3({
  apiVersion: "2012-10-17",
  signatureVersion: "v4",
});

2️⃣ presignedURL 생성 헬퍼 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// S3에 presignedURL api 요청 수신 타입 ( Server -> S3 )
type ApiFetchPresignedURLRequest = {
  name: string;
};
// S3에 presignedURL api 요청 송신 타입 ( S3 -> Server  )
type ApiFetchPresignedURLResponse = {
  preSignedURL: string;
};
// S3에 presignedURL API 요청 함수 시그니처 ( "S3" )
export type ApiFetchPresignedURLHandler = (body: ApiFetchPresignedURLRequest) => ApiFetchPresignedURLResponse;

/**
 * "이미지.확장자"를 받아서 "경로/이미지_시간.확장자"으로 변경해주는 함수
 * 현재 사용하고 있는 s3 폴더 구조는 `"개발모드"/images/이미지파일.확장자` 형태입니다.
 *
 * @param name "이미지.확장자" 형태로 전송
 * @returns "경로/이미지_시간.확장자" 형태로 반환
 */
const convertS3ImagePath = (name: string) => {
  const [filename, ext] = name.split(".");

  return `${process.env.NODE_ENV}/images/${filename}_${Date.now()}.${ext}`;
};

/**
 * S3의 "preSignedURL"을 생성하는 함수
 * @param name 이미지 이름  ("이미지.확장자" 형태 )
 * @returns "preSignedURL" 반환
 */
export const getPresignedURL: ApiFetchPresignedURLHandler = ({ name }) => {
  const photoURL = convertS3ImagePath(name);

  // 20초동안 이미지를 업로드할 수 있는 미리 서명된 URL 생성
  const preSignedURL = S3.getSignedUrl("putObject", {
    // 버킷을 생성할 때 지정한 유니크한 이름
    Bucket: process.env.AWS_BUCKET,
    // 경로 + 파일명
    // ( "image"라는 폴더에 "cat1.jpg"로 저장하고 싶다면 -> "images/cat1.jpg" )
    Key: photoURL,
    // presigendURL 유지 시간 ( 초 )
    Expires: 20,
  });

  // 서명된 URL 반환
  return { preSignedURL };
};

3️⃣ presignedURL 사용 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
type CreateImageRequest = {
  preSignedURL: string;
  file: File;
}
type CreateImageResponse = {}
type CreateImageHandler = (body: CreateImageRequest) => CreateImageResponse;

/**
 * S3에 이미지 생성 요청
 * @param preSignedURL AWS-S3의 presignedURL
 * @param file 입력받은 파일 객체
 * @returns S3에 이미지 생성 요청 Promise ( 네트워크 요청의 응답 데이터는 사용하지 않음 )
 */
const apiCreateImage: CreateImageHandler = async ({ preSignedURL, file }) =>
  axios.put(preSignedURL, file, { headers: { "Content-Type": file.type } });


// S3 이미지 제거 요청 수신 타입
type DeleteImageRequest = {
  name: string;
};

// S3 이미지 제거 요청 송신 타입
type DeleteImageResponse = ApiResponse<{}>;

// S3 이미지 제거 요청 API 함수 시그니처
export type DeleteImageHandler = (
  body: DeleteImageRequest
) => Promise<AxiosResponse<DeleteImageResponse, any>>;

/**
 * S3에 이미지 제거 요청
 * @param name 삭제할 이미지의 이름
 * @returns S3에 이미지 제거 요청 Promise ( 네트워크 요청의 응답 데이터는 사용하지 않음 )
 */
const apiDeleteImage: DeleteImageHandler = async ({ name }) =>
  serverInstance.delete(`/api/image`, { params: { name } });

4️⃣ Express + React 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// *** Node.js ***
// "http://localhost:3050"이라고 가정
// 해당 라우터 코드

import express from "express";

// util
import { getPresignedURL } from "../utils";

// type
import type { Request, Response, NextFunction } from "express";

/**
 * S3에 presignedURL 요청 수신 타입 ( B -> S3 )
 */
type ApiFetchPresignedURLRequest = {
  name: string;
};
/**
 * S3에 presignedURL 요청 송신 타입 ( S3 -> B )
 */
type ApiFetchPresignedURLResponse = {
  preSignedURL: string;
};

const imageRouter = express.Router();

// S3에 presignedURL 요청
imageRouter.get(
  // 실제로 여기는 "/"만 사용하지만 명시적으로 보여주기 위해 사용
  // 원래는 진입점 파일(app.ts)에서 "app.use("/api/image", imageRouter);" 사용
  "/api/image",
  async (
    req: Request<{}, {}, {}, FetchPresignedURLRequest>,
    res: Response<FetchPresignedURLResponse>,
    next: NextFunction
  ) => {
    try {
      const { name } = req.query;

      // S3에서 선언한 헬퍼 함수
      const { preSignedURL } = getPresignedURL({ name });

      res.json({ preSignedURL });
    } catch (error) {
      next(error);
    }
  }
);

export default imageRouter;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// *** React ***
// "http://localhost:3000"이라고 가정

import { useRef, useCallback } from "react";
import axios from "axios";

/**
 * presignedURL 요청 송신 타입 ( B -> F )
 */
type GetPresignedURLResponse = {
  preSignedURL: string;
};

const MyComponent = () => {
  const inputRef = useRef<null | HTMLInputElement>(null);

  const uploadImage = useCallback(
    async (e: React.FormEvent<HTMLFormElement>) => {
      e.preventDefault();

      if (!inputRef || !inputRef.current || !inputRef.current.files)
        return alert("이미지를 등록해주세요!");

      try {
        const file = inputRef.current.files[0];

        // 백엔드로 요청을 보내서 "presignedURL" 받기
        const {
          data: { preSignedURL },
        } = await axios.get<GetPresignedURLResponse>("http://localhost:3050/api/image", {
          params: { name: file.name },
        });

        // S3로 이미지 업로드 요청
        await axios.put(preSignedURL, file, {
          headers: { "Content-Type": file.type },
        });

        // 중간에 에러가 없다면 즉, catch로 들어가지 않고 여기까지 왔다면 업로드 성공
      } catch (error) {
        console.error(error);
      }
    },
    []
  );

  return (
    <form onSubmit={uploadImage}>
      <label htmlFor="image">이미지 입력</label>
      <input id="image" type="file" ref={inputRef} />
      <button type="submit">submit</button>
    </form>
  );
};

export default MyComponent;

🤔 presignedURL을 사용하는 이유

presignedURL이란 미리 서명된 URL으로 일정 시간동안 유효한 URL을 만들어서 이미지를 업로드할 수 있도록 하는 방법입니다.
이 방법을 사용하면 Key가 없이 이미지를 등록할 수 있기 때문에 브라우저에서 직접 AWS-S3로 이미지 등록 요청을 보낼 수 있습니다.

따라서 presignedURL을 사용하는 이유는 보안 키를 숨기고 네트워크 비용을 최소화하면서 이미지를 업로드하기 위해서 입니다.

아래는 이미지를 등록하는 3가지 방법입니다.
결론적으로 presignedURL을 사용하는 방법이 가장 안전하고 효율적이라고 생각합니다.

0️⃣ 이미지 등록 방법 1

브라우저 -> S3 -> 브라우저의 순서로 이미지를 등록하는 방법입니다.
가장 간단하고 이미지를 이미지를 한 번 전송하기 때문에 네트워크 비용이 비교적 적습니다.
하지만 브라우저에서 AWSKey를 가지고 요청해야 하기 때문에 사용자에게 Key를 노출당할 수 있습니다.

서버를 거치지 않는 이미지 등록 방식 이미지 브라우저 -> S3 -> 브라우저

1️⃣ 이미지 등록 방법 2

브라우저 -> 서버 -> S3 -> 서버 -> 브라우저의 순서로 이미지를 등록하는 방법입니다.
이미지를 두 번 전송해야 하기 때문에 네트워크 비용이 비교적 큽니다.
하지만 서버를 통해서 요청하기 때문에 Key를 숨길 수 있습니다.

서버를 거치는 이미지 등록 방식 이미지 브라우저 -> 서버 -> S3 -> 서버 -> 브라우저

2️⃣ 이미지 등록 방법 3 ( presignedURL )

브라우저 -> 서버 -> S3 -> 서버 -> 브라우저 -> S3 -> 브라우저의 순서로 이미지를 등록하는 방법입니다.
가장 복잡하고 네트워크의 낭비도 심해보이지만 Key를 숨기면서 이미지 전송도 최소화하는 가장 좋은 방법입니다.

presignedURL 동작 방식 이미지 브라우저 -> 서버 -> S3 -> 서버 -> 브라우저 -> S3 -> 브라우저

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.