Next.js에서 Firebase 사용하기에서 Firebase를 이용했었다. 해당 글에는 기본적으로 Firebase에 서버라우트를 이용해 연결하고, Firestore에 글을 작성하는 등의 기능을 사용했다.
그런데 Firebase에서 이미지나 파일 등은 Firestore
가 아닌, Storage
에 저장하는 것을 추천한다.
Firestore
: Firestore는 문서 기반의 NoSQL 데이터베이스로, 문서 내에 구조화된 데이터를 저장할 수 있다. 예를 들어, 사용자 정보, 게시물 등과 같은 데이터를 문서 형태로 저장할 수 있다.Storage
: 클라우드 기반의 파일 저장소로, 이미지, 비디오, 오디오 등의 binary 데이터를 저장할 수 있다.앞의 글에서는 Firestore 사용법에 대해 배웠으니, 이제 Storage에 이미지를 저장하는 법을 알아보자.
이미지를 업로드하기 위해 이미지를 받아와야한다.
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라는 변수에 저장했다
위에서 빌드 - 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;
import app from "./firebaseDB";
import { getStorage } from "firebase/storage"
const fireStorage = getStorage(app);
export default fireStorage;
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>
);
}
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를 이용해주는 경우가 안전하다
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('게시물 등록에 실패하였습니다');
}
};
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;
})
});
formData로 이미지 이름을 image_{index}로 전달했으니, 받아온 데이터에 has()
로 있는지 확인 후, images 배열에 저장해주면 된다
images 배열에 저장한 이미지를 Storage에 올려줄 때, Storage에 중복된 이미지 파일 이름이 있을 수 있다
uuid()
를 이용해 생성한다만들어 놓은 Storage에 접근하기 위해 ref()
를 이용한다
export default fireStorage
해줬기 때문에 fireStorage로 넣어주면 된다images/저장할 이미지 이름
으로 적어주면 된다3번에서 Storage와 연결을 했으므로, 이미지 파일을 하나씩 올려준다
uploadBytes
함수에 Storage와 연결한 참조와 이미지를 각각 넣어준다then()
에 넣는데, 이미지를 업로드하고나면 해당 이미지에 접근할 수 있는 url을 반환해주기 때문에 이 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
});
Promise.all
로 여러 이미지 업로드를 병렬적으로 업로드한 후, 업로드한 이미지의 url을 imageURLs에 저장한다imageURLs
을 넣어서 전송한다이렇게 업로드하면 나중에 게시물의 image_list에 있는 url을 img 태그의 src에 넣어주면, 이미지가 서버에서 가져와진다
Storage
를 생성하고, 내 프로젝트와 연결해준다
input으로 이미지를 받아와 이를 배열에 저장하고, 이 정보를 서버 라우트로 전송한다
이미지 파일 이름 중복을 방지하고자 uuid()로 이미지 파일 이름을 바꾸어주고 Storage에 저장한다
Storage에 저장하고 나면 해당 이미지에 접근할 수 있는 url을 반환하므로, 이를 저장한다
해당 url로 이미지에 접근하면 된다
게시물 등록에 이미지가 필요했다면, Storage에 이미지 저장 → 저장 후 생긴 url을 게시물 자체를 저장하는 데이터베이스에 저장해주기 → 게시물 데이터를 불러올 때, 이미지는 url로 접근하기의 순으로 처리하면 된다