밝을희 클태

[ Next.JS ] AWS S3에 이미지(image)를 업로드, 미리보기 본문

NextJS

[ Next.JS ] AWS S3에 이미지(image)를 업로드, 미리보기

huipark 2024. 6. 15. 14:28

환경

Next.js 14 APP Router

AWS SDK for Javascript v3

 

https://gaebarsaebal.tistory.com/79

 

[ AWS ] S3 버킷(bucket) 생성, 사용자 추가, 액세스 키 만들기

전에 작성한 글이 있는데 너무 대충 설명한 거 같아서 다시 써야겠다.버킷 만들기1. aws에서 s3를 검색해서 들어와 버킷 만들기로 들어간다.2. 버킷 이름을 적는다.3. 나는 현재 계정에서만 사용할

gaebarsaebal.tistory.com

S3 bucket 설정은 이 전 포스팅에서 다룬다.

 

 나는 S3 이미지 업로드를 서버에서 할 것이다. 클라이언트에서 할 수도 있지만 클라이언트에서 하면 AWS key가 노출될 위험이 있고 Presigned URL을 사용해 key의 노출을 막을 수 있지만 더 많은 작업을 해줘야 해서 서버에서 바로 업로드하려고 한다.

 일단 API endpoint를 만들자

# /root/.env.local
AWS_ACCESS_KEY_ID=IAM(보안 자격 증명) user에서 발급받은 access key
AWS_SECRET_ACCESS_KEY=IAM(보안 자격 증명) user에서 발급받은 secret access key
AWS_REGION=S3 bucket 국가 ex) ap-northeast-2
S3_BUCKET_NAME=bucket name
$ npm install @aws-sdk/client-s3
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { NextResponse } from 'next/server';

const s3Client = new S3Client({
  region: process.env.AWS_REGION,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  },
});

export async function POST(req) {
  try {
    const formData = await req.formData();
    const files = formData.getAll('files');

    const uploadPromises = [];
    const uploadedUrls = [];

    for (const file of files) {
      // 각 파일에 대해 루프를 시작 files는 formData.getAll('files')를 통해 가져온 파일 리스트
      const arrayBuffer = await file.arrayBuffer();
      // 파일을 ArrayBuffer로 변환 ArrayBuffer는 바이너리 데이터를 저장할 수 있는 일반적인 버퍼

      const buffer = Buffer.from(arrayBuffer);
      // ArrayBuffer를 Node.js Buffer 객체로 변환, Node.js Buffer 객체는 S3에 업로드할 수 있는 형태의 데이터

      const uploadParams = {
        Bucket: process.env.S3_BUCKET_NAME,
        // 업로드할 S3 버킷의 이름. 환경 변수로 설정된 값을 사용

        Key: `${Date.now()}_${file.name}`,
        // S3에 저장될 객체의 키(파일 이름) 여기서는 파일 이름 앞에 현재 타임스탬프를 붙여서 고유한 파일 이름을 생성

        Body: buffer,
        // 업로드할 파일의 내용 Buffer 객체를 사용

        ContentType: file.type,
        // 클라이언트에서 업로드된 파일의 타입을 그대로 사용
      };

      const command = new PutObjectCommand(uploadParams);
      // S3에 파일을 업로드하는 명령을 생성

      const uploadPromise = s3Client.send(command)
        .then(() => {
          const url = `https://${process.env.S3_BUCKET_NAME}.s3.${process.env.AWS_REGION}.amazonaws.com/${uploadParams.Key}`;
          // 파일이 성공적으로 업로드된 후 해당 파일의 URL을 생성
          // S3 버킷의 URL 형식을 사용 'https://{버킷 이름}.s3.{리전}.amazonaws.com/{파일 키}'

          uploadedUrls.push(url);
          // 생성된 URL을 uploadedUrls 배열에 추가
        })
        .catch(error => {
          console.error(`Error uploading file ${file.name}:`, error);
        });

      uploadPromises.push(uploadPromise);
      // 각 업로드 작업을 Promise 배열에 추가
    }

    await Promise.all(uploadPromises);
    return NextResponse.json({ uploadedUrls }, { status: 200 });
  } catch (error) {
    return NextResponse.json({ error: '업로드중 문제 발생' }, { status: 500 });
  }
}

 

그다음 클라이언트 코드로 와서 이미지를 선택해서 담을 Hook을 만들어준다.

imageUrls는 사용자가 선택한 이미지를 미리 보기 위해 사용되고

imageFilesAWS S3에 업로드하기 위해 사용된다.

const [uploadImages, setUploadImages] = useState({
    imageFiles: [],
    imageUrls: [],
  });

 

그리고 Input을 만들어서 image만 선택할 수 있게 해 주고 이미지 선택을 완료했을 때 handleImageUpload 함수를 실행시켜 준다.

URL.createObjectURL(file)는 각 파일 객체에 대해 브라우저가 해당 파일을 참조할 수 있는 임시 URL을 생성한다.

이제 이미지를 사용자에게 미리 보여주고 싶으면  imageUrls를 src에 주면 된다.

  const handleImageUpload = useCallback(
    e => {
      if (!e.target.files) return;
 
      const files = e.target.files;
      const filesArray = Array.from(files);

      const newArray = filesArray.map(file => URL.createObjectURL(file));
      setUploadImages({
        imageFiles: [...uploadImages.imageFiles, ...filesArray],
        imageUrls: [...uploadImages.imageUrls, ...newArray],
      });
    },
    [uploadImages],
  );

<input type="file" multiple accept="image/*" onChange={handleImageUpload} id="images" hidden />

이제 이미지를 처음에 만든 API endpointformdata를 보내서 서버에서 업로드를 해준다.

이미지 업로드에 성공했으면 S3 bucket에 올라간 이미지 URL들이 반환될 거고

실패했으면 error mag를 반환할 것이다.

 const handleUpload = async () => {
    const formData = new FormData();
    uploadImages.imageFiles.forEach(file => {
      formData.append('files', file);
    });

    try {
      const res = await fetch('/api/upload/product', {
        method: 'POST',
        body: formData,
      });

      const data = await res.json();
      if (res.ok) {
      	...
      } else {
		...
      }
    } catch (error) {
    	...
    }
  };

이미지 업로드에 성공하면 bucket에 이렇게 객체들이 생성이 돼있다.