Post

[AWS] 웹에서 S3 파일 제어(CRUD) - 2. aws-sdk for javascript 사용하기

이전 포스트

웹에서 S3 파일 제어(CRUD) - 1.API Gateway 사용하기

 

내용

AWS에서 제공하는 패키지인 aws-sdk for javascript v3로 Next.js 프로젝트에서 S3 파일 제어(CRUD)

  • AWS와 Next.js의 기본 사용법을 알고 있다는 가정하에 작성
  • Vercel, GitHub Pages 등 호스팅 플랫폼 설정은 다루지 않음
  • API 통신은 Axios를 사용, 다른 라이브러리는 다루지 않음

 

테스트를 위해 제작한 프로젝트

https://nextjs-aws-test.vercel.app

https://github.com/taedonn/nextjs-aws-test

 

설치 패키지 및 버전

1
2
3
4
5
6
7
8
9
10
"dependencies": {
    "@aws-sdk/client-s3": "^3.414.0",
    "@aws-sdk/s3-request-presigner": "^3.414.0",
    "eslint": "8.40.0",
    "eslint-config-next": "13.4.1",
    "next": "13.4.1",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "axios": "^1.4.0"
}

 

.env 설정

.env란?

environment variable의 약자로 웹이나 앱 개발을 할 때, 외부에 공개하면 안 되는 값을 모아두는 파일입니다. Vercel 같은 호스팅 플랫폼에는 대부분 environment variables를 추가하는 페이지가 따로 있기 때문에 배포할 때 같이 올리지 않도록 주의해야 합니다.

1
2
3
4
# IAM 유저 만든 후 생성한 엑세스 키
MY_AWS_ACCESS_KEY='IAM 유저 엑세스 키'
MY_AWS_SECRET_KEY='IAM 유저 시크릿 키'
MY_AWS_S3_BUCKET='버킷 이름'

Vercel에 배포할 계획이라면 AWS_ACCESS_KEY와 AWS_SECRET_KEY 등은 이미 지정되어 있다고 하기 때문에, 중복 사용하지 않고 예시처럼 피해서 사용하면 될 것 같습니다.

 

AWS 설정

이번 포스트는 노드 패키지인 aws-sdk for javascript v3를 사용해 S3를 제어하는 방법에 대해 다룰 예정입니다. 먼저 AWS에서 어떤 설정을 해야 하는지에 대해 알아보겠습니다. AWS의 기본적인 사용법은 이전 포스트에서 자세히 다뤘으니 참고 바랍니다.

IAM User 추가하고 Access key 생성

기본적으로 User는 Role이랑 동일하게 Policy를 생성해 AWS 서비스에 대한 접근 권한을 획득하는 개념입니다만, User를 추가하는 이유는 Next.js 환경에서 AWS에 접근할 때 사용하는 패키지인 aws-sdk for javascript v3에서 User별로 생성할 수 있는 access key와 secret access key를 필요로 하기 때문입니다.

access key와 secret access key란?

유저별로 생성할 수 있는 고유 식별자입니다. 아이디/비밀번호라고 생각하면 쉬울 것 같은데, arn과 마찬가지로 남들에게 노출되지 않게 주의해야 합니다.

 

User를 추가하기 위해 먼저 IAM 콘솔 사이드 메뉴의 Users로 이동한 후, Create user 버튼을 클릭합니다.

IAM - Create user

 

이름을 정한 후 Next 버튼을 눌러 다음 페이지로 이동합니다.

IAM - User name

 

Attach policies directly를 선택하면 나오는 검색창에 AmazonS3FullAccess를 검색한 후 체크해 준 다음, Next 버튼을 눌러 다음 페이지로 이동합니다. 다음 페이지로 이동하면 내가 입력한 정보를 확인하는 창이 나오는데, 이상이 없으면 Create user 버튼을 눌러 User를 생성합니다.

IAM - Set permissions

 

다시 IAM > Users로 들어가서 방금 내가 추가한 User를 클릭해 상세 페이지로 이동한 후 Create access key를 누릅니다.

IAM - User

 

Access key를 어떤 방식으로 사용할지를 선택하는 창인데, 저는 Application running outside AWS를 선택했습니다. 선택을 완료하면 Next 버튼을 눌러 다음 페이지로 이동합니다.

IAM - Access key best practices & alternatives

 

다음 페이지로 이동하면 description을 입력하는 페이지가 나오는데, 필수 사항이 아닌 선택 사항이기 때문에 굳이 입력할 필요는 없습니다. Next 버튼을 눌러 다음 페이지로 이동합니다. 다음 페이지에는 생성된 access key와 secret access key를 볼 수 있는 화면이 나옵니다. secret access key는 이 화면을 나가면 숨김 처리돼서 다시 볼 수 없기 때문에 꼭 따로 저장해 두기 바랍니다. 그렇다고 실수로 나갔다고 해서 큰일이 난 건 아니고, access key를 처음부터 다시 생성하면 됩니다.

IAM - Create access key

 

프론트 설계

  1. <input> 태그를 통해 이미지를 S3로 업로드
  2. S3에 업로드된 이미지의 URL 리턴
  3. 리턴된 이미지의 URL을 화면에 표시

레이아웃

레이아웃은 최대한 단순하게 구성했습니다. 불러온 이미지를 보여줄 이미지 태그와, 이미지를 S3에 업로드할 input 태그를 넣어주고, API 호출을 위해 Input 태그에는 onChange 이벤트를 넣어주어서 파일 추가 및 변경을 체크합니다.

1
2
3
4
<div>
    <img src={imgUrl} onerror="this.style.display='none';"/>
    <input onChange={uploadImg} id="imgUpload" type="file" accept="image/*"/>
</div>

API 호출

API 호출 순서는 다음과 같습니다.

  1. getSignedUrl의 PutObjectCommand를 통해 이미지 파일을 업로드할 임시 API 경로를 받아온다.
  2. 임시 API 경로로 파일을 업로드한다.
  3. 파일이 업로드되면 다시 getSignedUrl의 GetObjectCommand를 통해 이미지를 임시로 읽을 수 있는 URL을 받아온다.
  4. URL을 받아오면 img 태그의 src 속성값에 해당 URL을 넣어준다.
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
// 이미지 URL을 저장할 state
const [imgUrl, setImgUrl] = useState<string>('');

// Input에 파일 업로드 시 실행할 함수
const uploadImg = async (e: ChangeEvent<HTMLInputElement>) => {
	if (e.target.files && e.target.files[0]) {
        // 업로드 된 파일 변수로 저장
        const file = e.target.files[0];
        const fileType = file.name.split('.').pop();
        
        // API 호출
        await axios.post('/api/uploadimg', {
            fileName: 'test.' + fileType // test. + 확장자
            fileType: file.type
        })
        .then(async (res) => {
            // getSignedUrl의 PutObjectCommand로 받아온 url에 파일 업로드
            await axios.put(res.data.url, file, {
                headers: { 'Content-Type': file.type }
            })
            .then(async () => {
                // getSignedUrl의 GetObjectCommand로 받아온 url을 state에 저장
                await axios.get('/api/uploadimg', {
                    params: {
                        fileName: 'test.' + fileType
                    }
                })
                .then((res) => {
                    // state값을 받아온 URL로 변경
                    setImgUrl(res.data.url);
                })
            })
        })
        .catch(err => console.log(err));
    }
}

프론트 전체 코드

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
// react hooks
import { useState } from 'react';

// api
import axios from 'axios';

const Index = () => {
    // 이미지 URL을 저장할 state
    const [imgUrl, setImgUrl] = useState<string>('');

    // Input에 파일 업로드 시 실행할 함수
    const uploadImg = async (e: React.ChangeEvent<HTMLInputElement>) => {
        if (e.target.files && e.target.files[0]) {
            // 업로드 된 파일 변수로 저장
            const file = e.target.files[0];
            const fileType = file.name.split('.').pop();

            // API 호출
            await axios.post('/api/uploadimg', {
                fileName: 'test.' + fileType, // test. + 확장자
                fileType: file.type
            })
            .then(async (res) => {
                // getSignedUrl의 PutObjectCommand로 받아온 url에 파일 업로드
                await axios.put(res.data.url, file, {
                    headers: { 'Content-Type': file.type }
                })
                .then(async () => {
                    // getSignedUrl의 GetObjectCommand로 받아온 url을 state에 저장
                    await axios.get('/api/uploadimg', {
                        params: {
                            fileName: 'test.' + fileType
                        }
                    })
                    .then((res) => {
                        // state값을 받아온 URL로 변경
                        setImgUrl(res.data.url);
                    })
                })
            })
            .catch(err => console.log(err));
        }
    }
    
    return (
    	<div>
            <img src={imgUrl} onerror="this.style.display='none';"/>
            <input onChange={uploadImg} id="imgUpload" type="file" accept="image/*"/>
        </div>
    )
}

export default Index;

 

API 설계

  • POST로 요청이 왔을 때, getSignedUrl의 PutObjectCommand로 받은 URL을 JSON으로 리턴
  • GET으로 요청이 왔을 때, getSignedUrl의 GetObjectCommand로 받은 URL을 JSON으로 리턴

S3 클라이언트 연결

aws-sdk를 사용하려면 먼저 S3 클라이언트에 연결해야 합니다. 이전에 추가한 access key와 secret access key를 사용해 아래와 같이 클라이언트에 연결합니다.

1
2
3
4
5
6
7
8
const s3 = new S3Client({
    credentials: {
        accessKeyId: process.env.MY_AWS_ACCESS_KEY as string,
        secretAccessKey: process.env.MY_AWS_SECRET_KEY as string
    },
    region: 'ap-northeast-2'
});
const s3Bucket = process.env.MY_AWS_S3_BUCKET as string;

POST 요청 시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (req.method === 'POST') {
    // 변수 저장
    const fileName = req.body.fileName;
    const fileType = req.body.fileType;
    const putParams = {
        Bucket: s3Bucket,
        Key: fileName,
        ContentType: fileType,
    }
    
    // getSignedUrl의 PutObjectCommand로 이미지를 업로드할 URL 경로 받기
    const url = await getSignedUrl(s3, new PutObjectCommand(putParams), {expiresIn: 3600});
    
    // JSON으로 URL 전달
    return res.status(200).json({
        url: url,
        msg: "POST 요청 성공"
    });
}

GET 요청 시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (req.method === "GET") {
    // 변수 저장
    const fileName = req.query.fileName as string;
    const getParams = {
        Bucket: s3Bucket,
        Key: fileName,
    }
    
    // getSignedUrl의 GetObjectCommand로 이미지의 URL 경로 받기
    const url = await getSignedUrl(s3, new GetObjectCommand(getParams), {expiresIn: 3600})
    
    // JSON으로 URL 전달
    res.status(200).json({
        url: url,
        msg: "GET 요청 성공"
    });
}

API 전체 코드

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
import type { NextApiRequest, NextApiResponse } from 'next';
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
    // S3 클라이언트 연결
    const s3 = new S3Client({
    	credentials: {
            accessKeyId: process.env.MY_AWS_ACCESS_KEY as string,
            secretAccessKey: process.env.MY_AWS_SECRET_KEY as string,
        },
        region: 'ap-northeast-2'
    });
    const s3Bucket = process.env.MY_AWS_S3_BUCKET as string;
    
    // POST 요청 시
    if (req.method === "POST") {
    	// 변수 저장
        const fileName = req.body.fileName;
        const fileType = req.body.fileType;
        const putParams = {
            Bucket: s3Bucket,
            Key: fileName,
            ContentType: fileType,
        }

        // getSignedUrl의 PutObjectCommand로 이미지를 업로드할 URL 경로 받기
        const url = await getSignedUrl(s3, new PutObjectCommand(putParams), {expiresIn: 3600});

        // JSON으로 URL 전달
        return res.status(200).json({
            url: url,
            msg: "POST 요청 성공"
        });
    }
    
    // GET 요청 시
    if (req.method === "GET") {
        // 변수 저장
        const fileName = req.query.fileName as string;
        const getParams = {
            Bucket: s3Bucket,
            Key: fileName,
        }

        // getSignedUrl의 GetObjectCommand로 이미지의 URL 경로 받기
        const url = await getSignedUrl(s3, new GetObjectCommand(getParams), {expiresIn: 3600})

        // JSON으로 URL 전달
        res.status(200).json({
            url: url,
            msg: "GET 요청 성공"
        });
    }
}

 

테스트

테스트를 위해 Next.js 프로젝트를 새로 만들어 Vercel에 배포했는데, 문제없이 S3에 이미지가 업로드되는 걸 확인할 수 있었습니다. 테스트용 프로젝트에는 제 나름대로의 스타일을 입혀놔서, 위에 단순화한 코드보다는 좀 더 복잡하게 코드가 구성되어 있습니다. 위에 코드랑 똑같이 짰는데도 실행이 안된다면 여기 눌러서 제가 작성한 코드랑 비교해 보세요! 긴 글 읽어주셔서 감사합니다. 🙂

테스트

 

레퍼런스

This post is licensed under CC BY 4.0 by the author.