본문 바로가기
개발/Astro Framework

Astro Framework에서 Cloudinary로 이미지 업로드

by GreatCoding 2024. 3. 18.

서비스를 만들면서 필수로 필요한 기능들이 몇가지 있는데, 기록을 위해 데이터베이스를 사용하는걸 해봤으니 이번에는 이미지를 업로드하여 공유하는 기능을 만들어볼까요?

이미지를 운영중인 서버에 직접 업로드하는 방법도 있지만 serverless 배포방식이 유행하면서 이미지를 별도 공간에 올리는 경우가 많아졌습니다.

아마존 웹 서비스를 이용하는 경우에는 s3에 presigned 업로드를 많이 하는데, 우리는 25기가의 저장공간과 일정 조건까지는 무료로 쓸 수 있는 cloudinary 서비스를 이용해보도록 하겠습니다.

단순 이미지를 업로드 할 수 있는 저장공간을 제공해주는것 외에도 서비스에서 이미지를 리사이즈 해준다던가, 크롭을 해준다던가, 여러 필요한 기능들을 sdk형태로 많이 제공해줍니다. 이런 부가적인 기능들은 차차 알아보도록 하고 일단 필수 기능인 업로드 부터 해볼까요?

먼저 필요한 패키지를 설치해줍니다.

$ npm install cloudinary
$ npm install @unpic/astro

 

그리고 cloudinary에 업로드 하기 위한 api key를 설정해주셔야 하는데요.

무료로 회원가입을 한 후 cloudinary programmable media에 접근하신 후 DashBoard 메뉴에 접근하면 다음과 같은 api key를 획득할 수 있습니다.

cloudinary api key

(값을 지운 후 스크린샷을 캡쳐하였습니다)

이 키 값을 아스트로 프로젝트 루트의 .env에 기록해줍니다.

CLOUDINARY_URL=
CLOUDINARY_CLOUD_NAME=
CLOUDINARY_API_KEY=
CLOUDINARY_API_SECRET=

그 후 아스트로 프로젝트의 pages 폴더 하위에 upload.astro파일을 만들고 다음과 같이 코드를 작성합니다.

import { v2 as cloudinary } from "cloudinary";
import { Image } from "@unpic/astro";

// (1)
cloudinary.config({
    cloud_name: import.meta.env.CLOUDINARY_CLOUD_NAME,
    api_key: import.meta.env.CLOUDINARY_API_KEY,
    api_secret: import.meta.env.CLOUDINARY_API_SECRET,
});

const uploadStream = async (buffer: Uint8Array, options: any) => {
    return new Promise((resolve, reject) => {
        cloudinary.uploader.upload_stream(options, (error, result) => {
            if (error) return reject(error);
            resolve(result);
        }).end(buffer);
    });
};

let uploadedFile: any | undefined;

// (2)
if (Astro.request.method === "POST") {
    const data = await Astro.request.formData();
    const file = data.get("file") as File;
    const arrayBuffer = await file.arrayBuffer();
    const uit8Array = new Uint8Array(arrayBuffer);

    // (6)
    const response = await uploadStream(uit8Array, {
        folder: "astro",
    });

    uploadedFile = response;
}
---
<!doctype html>
<html lang="en">
<head>
</head>
<body>
<main>
{
    uploadedFile ? 
    // (5)
    (
        <section>
            <h2>업로드 완료</h2>
            <Image
                src={uploadedFile.secure_url}
                layout="constrained"
                placeholder="dominantColor"
                width={300}
                height={300}
                />

            <a href="./images.astro">모든 사진 보러가기</a>
        </section>
    ) : 
    // (3)
    (
        <section>
            <form method="post" enctype="multipart/form-data"> <!--(4)-->
                <label for="file">사진 선택하기:</label>
                <input type="file" name="file" id="file" required />
                <button>업로드</button>
            </form>
        </section>
    )
}
</main>
</body>
</html>

--- 사이에 있는 astro의 front matter 영역은 서버사이드에서 실행이 됩니다.
여기서 먼저 .env에 설정해두었던 key들로 초기화를 해줍니다(1). 브라우저에서 url을 입력하거나 링크를 타고 들어와서 GET 방식으로 호출되었기 때문에 POST 분기는 지나치게 되고(2), POST 분기를 지나침에 따라 uploadFile도 비었기 때문에 파일을 업로드 할 수 있는 form 영역이 서버사이드에서 렌더링이 됩니다(3).
파일을 업로드할것이기 때문에 form에 enctype을 multipart/form-data로 설정한것을 유의깊게 확인하세요. (4)
이후 file input에서 설정된 이미지를 submit button으로 호출을 하면 form에 설정된 action 경로로 POST 전송을 하게 됩니다. 본 예제에서는 같은 파일이기 때문에 action props가 없습니다.
이제 upload.astro 파일이 서버에서 POST method로 다시한번 호출됨에 따라 --- 사이에 있는 front matter부분의 if 분기 내에서 실제 파일 업로드를 수행합니다(6). 파일 업로드가 성공하였다면 uploadFile 변수도 채워지게 되어 form 대신 실제로 업로드한 파일을 보게 되구요.(5)
만약 서비스 자체적으로 database를 운영하고 있다면 (6)에서 결과값으로 받은 uploadFile에서 필요한 값을 추출하여 자신의 서비스의 database에 기록해도 됩니다. 여러 정보가 담겨 있지만 width, height, bytes, url, secure_url, folder, original_filename 정도 참조하면 될것 같습니다. 저는 https 주소를 위해 secure_url만 기록해두었습니다.

이제 업로드된 이미지들을 보게 될 images.astro를 작성해봅니다.

---
import { v2 as cloudinary } from "cloudinary";
import { Image } from "@unpic/astro";

// (7)
cloudinary.config({
    cloud_name: import.meta.env.CLOUDINARY_CLOUD_NAME,
    api_key: import.meta.env.CLOUDINARY_API_KEY,
    api_secret: import.meta.env.CLOUDINARY_API_SECRET,
});

// (8)
let images = [];
try {
    const result = await cloudinary.search
    .expression("folder:astro")
    .sort_by("uploaded_at", "desc")
    .max_results(30)
    .execute();

    images = result.resources;
} catch (error) {
    if (error instanceof Error) {
        console.error(error.message);
    } else {
        console.error(error);
    }
}
---
<!doctype html>
<html lang="en">
<head>
</head>
<body>
<main>
<p><a href="./upload">이미지 업로드하러 가기</a></p>

<section>
{
    // (9)
    images.map((image: { secure_url: string; filename: string }) => (
        <Image
            src={image.secure_url}
            layout="constrained"
            placeholder="dominantColor"
            width={200}
            height={200}
            />
    ))
}
</section>
</main>
</body>
</html>

마찬가지로 (7)에서 .env에 설정된값으로 초기화를 해줍니다. 그리고 (6)에서 업로드 된 데이터중 astro 폴더에 있는 파일들만 갖고오도록 합니다(8). 운영중인 database가 있다면 cloudinary 대신 자신의 database에서 값을 갖고오도록 하면 되겠죠?
(9)에선 실제로 https 주소로 이미지를 표현하게 됩니다. unpic으로 부터 갖고온 Image 컴포넌트의 경우 웹 프론트엔드에서 이미지를 위해 처리해줘야하는 여러가지 작업을 대신 수행해주는데요. 그중에서도 placeholder는 특히 유용합니다. 이미지가 브라우저에 전송되기 전에 base64로 인코딩된 임시 정보로 영역을 채워줄 수 있어요. 이미지에 쓰인 색의 중간값으로 미리 영역을 채워놓는 dominantColor 값이나, 흐릿하게 먼저 보여주는 blur등을 설정할 수 있습니다.

이상으로 Astro Framework에서 cloudinary에 이미지를 업로드하고 보여주는걸 간단히 살펴봤습니다.
해당 코드는 cloudinary-community github으로 부터 참고했으며, 해당 레파지토리에는 위 예제와 같은 서버사이드 업로드 말고도 upload widget을 위한 방법, 다른 프레임워크를 위한 cloudinary 업로드 예제도 많으니 추가정보가 필요하시면 확인해보시기 바랍니다.

댓글