[Swap(), 그 기록] 4. 이미지 업로드 2편 및 게시글 등록 - Firebase

Soye Park·2023년 1월 29일
1
post-thumbnail

이 포스팅은 사이드 프로젝트를 진행하며 생겼던 여러 일들에 대해서 기록을 겸하여 작성한 글입니다. 주기적으로 작성하고자 노력은 하지만 블로그 포스팅 한번 하려고 하면 머릿속에서 나만의 언어로 정리도 하면서 작성하다가 몇시간은 사용하는 지라 주기적이지 않을 가능성이 농후합니다. 아자아자...

Firebase를 활용한 이미지 업로드

들어가면서...

이것저것 블로그 포스팅 거리가 점점 쌓여가지만... 공부하느라 바빠 포스팅이 늘어졌다. 조금 더 열심히 해보도록 하고..!
사이드 프로젝트를 하면서 게시글 CRUD를 작업하게 됐다. 백엔드 개발자가 없는 팀이므로 서버리스 서비스 중 파이어베이스를 쓰기로 하였다.
지지난번 포스팅에서는 FileReader를 활용해 파일업로드를 구상했었는데, 알아보니 FileReader는 현재 말도 안되게 긴 URL과 이미지 최적화 문제로 쓰이지 않는 편이라는 사실을 알았다. 또한 서버가 없는 팀에서 파일업로드를 위해서는 파이어베이스를 사용해야하는데 이를 활용하기 위해서는 결국 Storage에 파일이 올라가야 하므로 아예 새로 기능을 구현해야했다. 헤헤..ㅠ

Firebase의 어떤 서비스를 활용해야할까?

파이어베이스는 정말 다양한 기능을 지원한다. 데이터를 저장할 수 있는 Firestore, 이미지와 영상 등을 저장해두는 Storage, 채팅 등 실시간 데이터를 위한 Realtime Database, 서비스 배포를 위한 Hosting 서비스 등 백엔드로 구현 가능한 모든 기능이 있다고 해도 무방한데 심지어 공식 문서까지 굉장히 친절하다.

그 중 이번에 사용한 기능은 Firestore와 Storage였는데, 아래의 등록 페이지에서 볼 수 있듯이 정말 평범한 상품 등록페이지였다. 이미지가 없는 회원 가입 같은 서비스도 (파이어베이스에서 인증 및 가입 기능을 제공하긴 하지만) Firestore를 통해 쉽게 데이터를 받을 수 있다. (파이어베이스를 활용한 회원가입과 채팅 기능도 개인 사이드로 포스팅 예정... cooooming soon-😎)

Storage 활용 - 업로드!

기본적인 파이어베이스 세팅 방법은 다른 블로그에도 많고 문서에도 워낙 잘되어있어서 따로 글을 적지않을 것이고, 기능 구현 방법에 대해서만 기록해놓도록 할 것이다.

  1. storage 불러오기
  const storage = getStorage();
  • storage를 불러온다.

  1. 우선 파일을 담아줄 state와 url을 담아줄 state를 생성한다.
  const [imgUpload, setImgUpload] = useState(null);
  const [imgUrl, setImgUrl] = useRecoilState(ImgUrlArrState);
  • 이미지는 기본적으로 비어있는 상태일 것이기 떄문에 기본값으로 null을 지정해주었고,
  • 이미지의 url을 담아줄 배열은 추후 firestore로 post될 객체에 담아주어야하므로 recoil을 활용하여 전역에서 편하게 사용할 수 있도록 해주었다.

  1. input태그를 통해 file의 정보를 받는다. (feat. onChange)
  • input으로 file을 업로드하면 onChange 이벤트를 통해 들어온 event.target.files[0](이미지)를 setImgUpload 를 통해 스테이트에 넣어준다.
  • file타입의 input 이벤트는 파일의 정보가 배열로 들어오는데 (multiple 속성으로 여러장도 받을 수 있다), 나는 한장씩 받을 것이므로 files 배열의 [0]번째 데이터를 state로 넣어줬다.
 onChange={event => {
 	setImgUpload(event.target.files[0]);
 }}

  1. upload 함수 작성 (1) ref를 활용한 데이터가 저장될 참조 위치 지정
  • 업로드 시의 조건은 imgUpload state의 값이 null이 아닌 경우, 즉 파일이 업로드 되었을 때이므로 얼리 리턴문을 활용해 버그 가능성을 줄여주었다.
 if (imgUpload === null) return;
  • 파이어베이스 storage에 파일을 업로드 하기 위해서는 데이터가 저장될 참조 위치가 필요한데, 'firebase/storage' 모듈이 제공하는 ref 메소드를 활용하면 된다. ref 함수는 ref(storage, "참조 위치/저장될 이름") 을 받는데, 여기에서 한가지 고민이자 문제가 생겼다.

  1. 그래서 파일 이름은 어떻게 해야하지..?
  • 파이어베이스 storage는 동일한 이름의 파일의 업로드되면 그대로 덮어씌워지게 되었는데, 불특정 다수가 사용하는 서비스 특성 상 같은 이름의 파일이 업로드되면 서비스의 정상사용이 불가능할 것이라는 생각이 들었다. 그래서 가장 유니크할 수 있는 게 무엇일까 고민했는데, 랜덤값을 제공하는 라이브러리 등을 사용해도 됐겠지만, 나는 시간을 선택했다.
const id = Date.now()

밀리세컨드 단위의 시간이라면 당연히 유니크할 것이라고 생각했다. 하지만 혹시라도 서비스가 커진다면 이라는 가정에서 시간도 중복이 일어날 수 있겠구나 하는 생각이 들었다.

고민을 하던 중----

그렇다면 파일명은 매번 다른 것들이 올라올테니까! 시간을 파일명의 길이로 나누면 굉장히 유니크한 값이 되겠군? +@ 로 기존 이미지 이름까지!

const id = Date.now() / imgUpload.name.length;
const imageRef = ref(storage, `images/${imgUpload.name}_${id}`);

그렇게 업로드된 파일의 이름

이렇게 하면 레어하겠지..?


  1. 본격적으로 이미지 업로드
  • 파이어베이스 스토리지는 파일 업로드를 위한 메소드 몇가지를 지원한다.

    1. input type="file"를 통한 업로드 또는 바이트 배열을 이용한 업로드 메소드, uploadBytes()
    2. 위의 uploadBytes() 메서드를 사용할 수 없을 때 fileReader 등을 활용한 base64, base64url, data_url 를 위한 업로드 메소드 uploadString()
    3. 그 외에도 uploadBytesResumable() 같은 데이터 전송의 진행율 확인 및 일시 정지 기능 등이 있는 메소드들이 있는데, 서비스 특성상 필요없기 때문에 선택하지 않았다.

공식문서

uploadBytes()의 사용법은 간단하다.
uploadBytes(저장할 위치 Ref, 저장할 파일)

그리고 이렇게 받아진 데이터를 .then 을 통해 정보를 받아 URL화 하면된다.

// uploadBytes() 메소드를 통해 저장할 위치와 저장할 파일을 지정해주고 실행되면 파일은 업로드 된다.
// 하지만 파일이 업로드되는 즉시 이미지의 주소를 imgUrl 배열에 담아줘야하므로 추가로 진행해줘야한다.
// (uploadBytes는 프로미스 객체를 포함한다) then을 통해 파일의 정보를 URL로 변경해주는 getDownloadURL 메서드에 정보를 넘겨준다.
uploadBytes(imageRef, imgUpload).then(snapshot => {
// getDownloadURL() 메소드는 ref를 통해 해당 파일의 url을 가지고 올 수 있다.
// 이렇게 받아진 url을 imgUrl 에 담아야하기 때문에 다시 한번 then을 통해 데이터를 넘겨준다.
   getDownloadURL(snapshot.ref).then(url => {
// 배열에 이전 값을 계속해서 더해나가야하므로 prev 인자를 구조분해를 통해 추가해주면 된다.
// 추후 딜리트 기능을 위해 url뿐만 아니라 파일의 이름을 id로 해 넘겨줬다.
     setImgUrl(prev => [
       ...prev,
       { url: url, id: `images/${imgUpload.name}_${id}` },
     ]);
   });
 });
};

  1. 파일이 업로드가 완료될 때마다 실행되게 하기!
  • 업로드 함수는 imgUpload 스테이트에 변동이 일어날 때마다 실행되면 되므로 useEffect 훅을 사용했다.
useEffect(upload, [imgUpload]);

  1. 미리 보기 렌더링
{imgUrl.map(item => {
  return (
    <PreviewComponent key={item.url}>
      <PreviewImgCard url={item.url} />
      <StyledDeleteBtn
        top="-4px"
        right="10px"
        id={item.id}
        onClick={event => deleteImg(event)}
      >
        <CgClose width="1.5rem" id={item.id} />
      </StyledDeleteBtn>
    </PreviewComponent>
  );
})}
  • 렌더링이야 간단하다. 그냥 url이 담긴 배열을 map 메소드를 통해 활용하면 그만-

Storage 활용 - 삭제!!

  1. 렌더링된 미리보기에 업로드 파일 삭제 기능 구현!!

    삭제기능 간단하다. 하지만 id를 뭘로 할까 엄청 고민했던 듯...

  • 파이어베이스에서는 참조할 ref만 있다면 쉽게 storage에서 파일을 삭제할 수 있는 기능을 제공한다
  • deleteObject(참조할 ref)
// 클릭 이벤트가 발생하면
const deleteImg = event => {
// ref(삭제될 storage, 삭제할 파일의 이름)
  const deleteRef = ref(storage, event.target.parentNode.id);
// deleteObject() 참조만 넣어주면 끝... 댕쉽
  deleteObject(deleteRef);
// 하지만 스토리지에서 삭제한다고 imgUrl에도 바로 삭제되는 거 아니니까 id를 기준으로 필터 메소드를 통해 손쉽게 URL삭제!
  setImgUrl(imgUrl.filter(obj => obj.id !== event.target.parentNode.id));
};

끝이 보인다.. 이제 데이터를 Firestore로!

const data = {
  title: event.target[0].value,
  hash_tag: hashArr,
  content: event.target[3].value,
  swap: event.target[4].checked,
  share: event.target[5].checked,
  name: JSON.parse(userInfo)['displayName'],
  uid: JSON.parse(userInfo)['uid'],
  date: postDate,
  convertDate: `${Date.now()}`,
  imgUrl: imgUrlArr,
};
  • Firestore로 전송할 객체의 구성을 먼저 잡았다. imgUrl과 hashtag는 Recoil을 활용한 탓에 쉽게 들고왔다.

파이어베이스는 일단 한가지 기능만 활용할 줄 알아도 다른 기능을 이용하는 것은 쉬운 것 같다. addDoc() 메소드를 활용해 store에 업로드했는데, addDoc(저장할 위치 ref, 저장할 data) 면 실행 시 업로드 끝- 정말 끝... 아 끝이라구요!!!


구현된 기능


마치며

확실히 파이어베이스의 기능은 강력하다. 하지만 데이터베이스를 구성하는 방식이라던가 이름을 어떻게 해야 중복이 없을 수 있는지에 대한 고민이라던가 추후 파일을 검색하게 되었을 때 어떤 것이 검색최적화를 가능하게 할까 등의 고민이 들었다. 이런 것은 결국 백엔드 개발자 없이도 서버를 사용할 수 있는 서버리스 서비스라고 해결해주진 않는 듯하다. 고로 공부가 더 필요하구만!

참고

파이어베이스 공식 문서

profile
응애FE개발자/ 블로그 이전 : https://soyeah-log.vercel.app/

0개의 댓글