Firebase에 이미지 업로드하기 (feat. Firestorage)

y·2024년 6월 30일
3

💬 TIL

목록 보기
10/10
post-thumbnail
post-custom-banner

Next.js에서 Firebase 사용하기에서 Firebase를 이용했었다. 해당 글에는 기본적으로 Firebase에 서버라우트를 이용해 연결하고, Firestore에 글을 작성하는 등의 기능을 사용했다.

그런데 Firebase에서 이미지나 파일 등은 Firestore가 아닌, Storage에 저장하는 것을 추천한다.

Firestore와 Storage의 차이


  • Firestore: Firestore는 문서 기반의 NoSQL 데이터베이스로, 문서 내에 구조화된 데이터를 저장할 수 있다. 예를 들어, 사용자 정보, 게시물 등과 같은 데이터를 문서 형태로 저장할 수 있다.
  • Storage: 클라우드 기반의 파일 저장소로, 이미지, 비디오, 오디오 등의 binary 데이터를 저장할 수 있다.

앞의 글에서는 Firestore 사용법에 대해 배웠으니, 이제 Storage에 이미지를 저장하는 법을 알아보자.

1. 이미지 먼저 받아오기


이미지를 업로드하기 위해 이미지를 받아와야한다.

export const ImageInput = () => {
  const [images, setImages] = useState<File[]>([]);
  const [, setPost] = useRecoilState(postState);

  const handleUploadFile = (e :React.ChangeEvent) => {
    let files = Array.from((e.target as HTMLInputElement).files as FileList);
    if (images.length + files.length <= 10) {
      let update = [...images, ...files];
      setImages(update);
    }
  };

  useEffect(() => {
    setPost((post) => ({
      ...post,
      image_list: images,
    }));
  }, [images]);
  
  return (
    <Container>
      <div>선택된 이미지 ({images.length}/10)</div>
      <input className="input" id="input" accept="/image/*" type="file" multiple onChange={ handleUploadFile }/>
    </Container>
  );
}
  • input 태그에 tpye="file"로 지정해서 파일을 입력받도록 지정한다

  • 파일이 업로드 되는 것을 onChange에서 확인하고 handleUploadFile 함수가 실행된다

  • 받아온 여러 개의 파일을 images 배열에 저정한다

  • 이미지만 업로드한다면 여기서 바로 Storage에 이미지 업로드를 하면 되고, 나 같은 경우 이미지와 각종 다른 게시물들 내용을 함께 전송할 거라 post라는 변수에 저장했다

2. Storage 연결하기


Firebase 연결

  • 위에서 빌드 - Storage를 누르고 Storage를 생성해준다

  • 나는 생성 후 혹시 몰라 images 폴더를 따로 만들었다 (이미지 뿐만 아니라 다른 파일을 저장할 가능성 때문이다)

import { initializeApp } from "firebase/app";

const firebaseConfig = {
  apiKey: process.env.FIREBASE_API_KEY,
  authDomain: process.env.FIREBASE_AUTH_DOMAIN,
  projectId: process.env.FIREBASE_PROJECT,
  storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.FIREBASE_MESSAGING_SENDERID,
  appId: process.env.FIREBASE_APP_ID,
  measurementId: process.env.FIREBASE_MEASUREMENT_ID
};

const app = initializeApp(firebaseConfig);
export default app;

Storage 연결

import app from "./firebaseDB";
import { getStorage } from "firebase/storage"

const fireStorage = getStorage(app);
export default fireStorage;
  • Firestore 연결할 때 코드와 비슷하다

3. input으로 받은 이미지를 서버 라우트로 처리하기


  • input에서 받아 images 배열에 이미지 파일을 저장했고, 이를 이제 서버 라우트 측으로 전송해주어야 한다

  • 이미지 전송과 함께 게시물 등록을 처리하는 엔드포인트를 api/post/register로 지정했고, 요청할 때 앞에 나의 주소를 적어줘야한다

    일반적으로 로컬에서 개발 중이면 http://localhost:3000/api/post/register로 적어주면 되고, 배포 후에는 배포 후 주소/api/post/register로 바꾸면 된다

export const Submit = () => {
  const [post, setPost] = useRecoilState(postState);

  const { mutate, isPending } = useMutation({
    mutationFn: () => writePost({
      title: post.title,
      content: post.content,
      image_list: post.image_list,
    }), 
    onSuccess: (data) => {
      /* 이미지 업로드가 성공하면 진행할 로직 작성 */
      console.log(data);
    },
    onError: (error) => {
      console.log(error);
    },
  })

  ...

  const registerPost = () => {
    if (post.title !== '' && post.content !== '' && post.image_list.length !== 0) {
      mutate();
    } else {
      console.log("error")
    }
  }

  return (
    <Container onClick={ registerPost }>
      <div className="text">등록하기</div>
    </Container>
  );
}
  • 등록하기 버튼을 누르면 registerPost 함수가 실행되고 저정했던 images가 writePost 함수로 전송된다

writePost() 함수 코드

import axios from "axios";

export const writePost = async(post: SendPost) => {
  const formData = new FormData();
  formData.append('title', post.title);
  formData.append('content', post.content);
  post.image_list.forEach((file, index) => {
    formData.append(`image_${index}`, file);
  })

  const response = await axios.post(`${process.env.NEXT_PUBLIC_BASE_URL}/api/post/register`, formData, {
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  });
  return response;
}
  • 서버 라우트로 요청을 보내는 함수이며, formData로 변경해 보내준다

  • 객체 정보와 같은 경우는 제대로 전송되지 않기 때문인데, 위 예시에서는 객체 데이터가 없어 상관 없지만 FormData를 이용해주는 경우가 안전하다

app/api/post/register/route.tsx

  • writePost에서 요청한 것을 받는 api로, POST 요청이므로 함수 이름도 POST로 작성하면 된다

  • 타입스크립트로 작성했지만, 자바스크립트 이용자는 타입 명시 부분만 제거하면 사용법은 같다

  • 전체 코드는 아래와 같고, 코드에 대한 자세한 설명은 밑에 해두었다

import { NextRequest, NextResponse } from "next/server";
import { addDoc, collection, Timestamp } from "firebase/firestore";
import fireStore from "@/app/_firebase/firestore";
import fireStorage from "@/app/_firebase/firestorage";
import { v4 as uuid } from 'uuid';
import { getDownloadURL, ref, uploadBytes } from "firebase/storage";

export async function POST(request :NextRequest) {
  const formData = await request.formData();

  try {
    /* 이미지 파일 Storage에 업로드하는 로직 */
    const images = [];
    let i = 0;
    while (formData.has(`image_${i}`)) {
      images.push(formData.get(`image_${i}`));
      i++;
    }
    const uploadName = uuid();
    const uploadImage = images.map((image, index) => {
      const imageName = uploadName + "_" + index.toString();
      const storageRef = ref(fireStorage, `images/${imageName}`);

      return uploadBytes(storageRef, image as File).then(async (snapshot) => {
        const downloadURL = await getDownloadURL(snapshot.ref);
        return downloadURL;
      })
    });

    const imageURLs = await Promise.all(uploadImage);

    /* 이미지 업로드 후 생성된 url과 함께 게시물 자체를 Firestore에 등록 */
    const response = await addDoc(collection(fireStore, "Post"), {
      title: formData.get('title'),
      content: formData.get('content'),
      time: Timestamp.now(),
      image_list: imageURLs
    });
    return NextResponse.json(response.id);
  } catch (error) {
    console.error('Error adding document: ', error);
    return NextResponse.json('게시물 등록에 실패하였습니다');
  }
};

이미지를 Storage에 업로드하기

const images = [];
let i = 0;
while (formData.has(`image_${i}`)) {
  images.push(formData.get(`image_${i}`));
  i++;
}
const uploadName = uuid();
const uploadImage = images.map((image, index) => {
  const imageName = uploadName + "_" + index.toString();
  const storageRef = ref(fireStorage, `images/${imageName}`);

  return uploadBytes(storageRef, image as File).then(async (snapshot) => {
    const downloadURL = await getDownloadURL(snapshot.ref);
    return downloadURL;
  })
});
  1. formData로 이미지 이름을 image_{index}로 전달했으니, 받아온 데이터에 has()로 있는지 확인 후, images 배열에 저장해주면 된다

  2. images 배열에 저장한 이미지를 Storage에 올려줄 때, Storage에 중복된 이미지 파일 이름이 있을 수 있다

    • 이를 방지하기 위해 업로드할 이미지 파일 이름을 uuid()를 이용해 생성한다
    • 여러 이미지 업로드 시 uuid()로 생성한 문자열 + _{index}로 하면 이미지 이름 중복을 방지하기 쉽다
  3. 만들어 놓은 Storage에 접근하기 위해 ref()를 이용한다

    • 2번에서 Storage 연결 코드를 작성하고, export default fireStorage해줬기 때문에 fireStorage로 넣어주면 된다
    • 두번째 인자에는 저장할 위치를 지정하는데, images 폴더를 만들었으니 images/저장할 이미지 이름으로 적어주면 된다
  4. 3번에서 Storage와 연결을 했으므로, 이미지 파일을 하나씩 올려준다

    • uploadBytes 함수에 Storage와 연결한 참조와 이미지를 각각 넣어준다
    • callback은 then()에 넣는데, 이미지를 업로드하고나면 해당 이미지에 접근할 수 있는 url을 반환해주기 때문에 이 url을 저장해두면 언제든지 이미지에 접근할 수 있다

Storage에 이미지 업로드 후 이미지 url 저장하기

const imageURLs = await Promise.all(uploadImage);

/* 이미지 업로드 후 생성된 url과 함께 게시물 자체를 Firestore에 등록 */
const response = await addDoc(collection(fireStore, "Post"), {
  title: formData.get('title'),
  content: formData.get('content'),
  time: Timestamp.now(),
  image_list: imageURLs
});
  1. Promise.all로 여러 이미지 업로드를 병렬적으로 업로드한 후, 업로드한 이미지의 url을 imageURLs에 저장한다
  2. 게시물을 등록할 때 image_list 필드에 위의 imageURLs을 넣어서 전송한다

이렇게 업로드하면 나중에 게시물의 image_list에 있는 url을 img 태그의 src에 넣어주면, 이미지가 서버에서 가져와진다

로직 요약하기

  1. Storage를 생성하고, 내 프로젝트와 연결해준다

  2. input으로 이미지를 받아와 이를 배열에 저장하고, 이 정보를 서버 라우트로 전송한다

  3. 이미지 파일 이름 중복을 방지하고자 uuid()로 이미지 파일 이름을 바꾸어주고 Storage에 저장한다

  4. Storage에 저장하고 나면 해당 이미지에 접근할 수 있는 url을 반환하므로, 이를 저장한다

  5. 해당 url로 이미지에 접근하면 된다

    게시물 등록에 이미지가 필요했다면, Storage에 이미지 저장 → 저장 후 생긴 url을 게시물 자체를 저장하는 데이터베이스에 저장해주기 → 게시물 데이터를 불러올 때, 이미지는 url로 접근하기의 순으로 처리하면 된다

profile
hiyunn.contact@gmail.com
post-custom-banner

0개의 댓글