현재 테스트 운영중으로 불안정한 상황이 연속될 수 있습니다. 양해 부탁드립니다.

Next.js 15에서 route api를 이용해 아마존 라이트세일 버킷(S3)에 이미지 올리기

아마존 라이트세일에도 S3 서비스를 - 버킷이라는 이름으로 운영하고 있습니다.
가장 저렴한 요금제가 2024년 11월 현재 5GB 용량/ 25GB 전송량을 기준으로 한달 1달러에 제공하고 있습니다. 그런데 12개월간 무료체험이 적용되는 중이라, 사실 무료로 제공되고 있는 상황입니다. 안 써볼 수 없겠죠.
라이트세일 버킷은 기본적으로 S3와 같은 서비스라고 볼 수 있지만, 복잡한 IAM 설정이나 보안 규칙같은게 상당히 간소화 되어있습니다.
그래서 저처럼 복잡한 서버 관리에 대한 부담을 덜고 싶어하는 사람 입장에서 손쉽게 접근할 수 있는 서비스라고 봅니다.
기본적인 소스는 ChatGPT를 통해 생성했습니다만, 케이스가 조금 달라서 그대로 쓸 수는 없어서 라이트세일 버킷에 맞도록 수정한 소스입니다.


먼저 라이트세일 버킷을 하나 만듭니다. 아무 이름이나 편한대로 지어주시면, 그 버킷 이름이 곧 주소에 사용됩니다. 그리고 S3 업로드에 대한 환경설정이 필요합니다. 라이트세일 -> 버킷 -> 권한 -> 액세스 키메뉴를 통해서 발급받은 액세스 ID, 액세스 키, 그리고 라이트세일 버킷을 만들때 사용한 버킷 이름, 그리고 인스턴스가 소속된 지역(Region)을 넣어줍니다. 저는 서울을 기준으로 라이트세일 인스턴스를 만들었기 때문에 ap-northeast-2로 넣어주었습니다.

//.env
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_BUCKET_NAME=
AWS_REGION=

S3와 연결할 aws-sdk를 설치해줍니다. v2는 2025년을 기준으로 종료될 예정이므로, v3로 적용했습니다.

npm i @aws-sdk/client-s3

S3와 연결할 때 사용할 설정을 .env 파일로부터 가져와 S3 라이브러리와 연결해줍니다.

// lib/s3Client.ts
import { S3Client } from "@aws-sdk/client-s3"

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 { s3Client }

클라이언트에서 이미지를 가져와 업로드 처리하는 소스입니다. api/upload/route.ts 파일로 요청을 보내면, S3로 업로드 처리한 후 data.url로 결과를 받아볼 수 있습니다.

// components/ImageUpload.tsx
import React, { useState } from "react"
import { Button } from "@/components/ui/button" // shadcn

export default function ImageUpload() {
  const [file, setFile] = useState<File | null>(null) 

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files) {
      setFile(e.target.files[0]) // API로 전달할 파일 
    }
  }

  const handleUpload = async () => {
    if (!file) return
    const fileData = new FormData()
    fileData.append("file", file)
    // API 요청으로 서명된 URL 가져오기
    try {
      const response = await fetch("/api/upload", {
        method: "POST",
        body: fileData,
      })
      if (response.ok) {
        const data = await response.json()
        console.log("파일 업로드 완료!")
        console.log(data.url) // 서버로부터 전달받은 S3 이미지 주소

      } else {
        console.error("업로드 실패:", response.statusText)
      }
    } catch (error) {
      console.error("업로드 중 오류 발생:", error)
    }
  }

  return (
    <div>
      <input type="file" onChange={handleFileChange} />
      <Button
        variant={"outline"}
        type={"button"}
        onClick={handleUpload}
        disabled={!file}
      >
        업로드
      </Button>
    </div>
  )
}

app/api/upload/route.tsx 파일에 아래와 같이 POST로 들어온 요청을 처리하도록 작성해주었습니다. 단일 파일만 받아서 처리하는 상황입니다. 만약에 라이트세일 버킷을 생성할때 권한에서 모든 객체는 퍼블릭 및 읽기 전용으로 설정했다면 ACL 설정은 하지 않아도 됩니다. 저는 개별 객체를 퍼블릭 및 읽기 전용으로 설정가능 상태로 권한 설정을 해두었기 때문에, 파일 정책을 명시해주는 ACL을 적용해주었습니다.

// api/upload/route.tsx
import { NextRequest, NextResponse } from "next/server"
import { s3Client } from "@/lib/s3Client"
import { ObjectCannedACL, PutObjectCommand } from "@aws-sdk/client-s3"

export async function POST(req: NextRequest) {
  const form = await req.formData()
  const file = form.get("file") as File

  // 파일이 없거나, 파일 사이즈가 0이라면 400 오류 반환
  if (!file || file.size === 0) {
    return new Response("No file uploaded", { status: 400 })
  }
  // form데이터의 파일 버퍼처리
  const buffer = await file.arrayBuffer()

  const fileParams = {
    Bucket: process.env.AWS_BUCKET_NAME!,
    Key: file.name, // formData에 첨부된 파일 이름
    Body: new Uint8Array(buffer), // bucket에 전달할 파일
    ContentType: file.type, // formData에 첨부된 파일 타입(jpg,png 등..)
    ACL: ObjectCannedACL.public_read, // 파일에 대한 정책은 '공개' 상태로 지정
  }
  // S3에 파일 업로드
  await s3Client.send(new PutObjectCommand(fileParams))
  // 클라이언트로 반환할 URL 주소
  const url = `https://${fileParams.Bucket}.s3.amazonaws.com/${fileParams.Key}`
  // 200 결과 반환
  return NextResponse.json({ url }, { status: 200 })
}

테스트 해본 결과 이미지가 잘 업로드 되고, 결과도 잘 반환되고 있습니다.
블로그를 만들면서 ChatGPT를 비롯한 인공지능에 많이 의존하고 있습니다만, 아무래도 Next.js 15가 나온지 얼마 안된 상태여서 그런지 답변이 만족스럽지 못한 상황이 많았습니다. 그래서 개인적으로 겪은 케이스를 공유할 겸 정리해보았습니다.


hhyunki
a month ago