기록하는 개발자

[React, Firebase] TwitterCloneCoding 3.0 File Upload, Delete file 본문

Web/React

[React, Firebase] TwitterCloneCoding 3.0 File Upload, Delete file

밍맹030 2021. 10. 17. 21:08
728x90

< 구현할 화면 >

 

1. Home 화면에서 Tweet을 생성 할 때 file과 글을 함께 올릴 수 있다.

(글과 사진 중 하나만 올리는 것 또한 가능.)

 

2. 파일 선택 후 Clear Photo 버튼을 통해 선택을 취소할 수 있다.

3. 파일 선택 및 글 작성 후 Tweet 버튼을 클릭하면 글과 파일이 함께 post된다.

4. Delete Tweet 버튼을 클릭하면 Collection에서 tweet 객체가 삭제되고

해당 tweet 객체에 file이 있었다면, Storage에 올라간 file도 함께 삭제된다.

< firebase의 collection > : attachmentUrl의 이름으로 file의 url(경로) 항목이 추가된다.

 

< firebase 의 storage > : user가 upload한 file 관련 정보를 확인할 수 있다.

 

< fbase.js >

import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
import { getStorage } from "firebase/storage";

const firebaseConfig = {
    apiKey: process.env.REACT_APP_API_KEY,
    authDomain: process.env.REACT_APP_API_AUTH_DOMAIN,
    projectId: process.env.REACT_APP_API_PROJECT_ID,
    storageBucket: process.env.REACT_APP_API_STORAGE_BUCKET,
    messagingSenderId: process.env.REACT_APP_API_MESSAGING_SENDER_ID,
    appId: process.env.REACT_APP_API_APP_ID,
};

initializeApp(firebaseConfig);

export const authService = getAuth();
export const dbService = getFirestore();
export const storageService = getStorage();

- firebase의 storage 사용을 위해 import와 export를 해준다.

 

 

 

< Home.js >

import { dbService, storageService } from "fBase";
import React, { useEffect, useState } from "react";
import Tweet from "../components/Tweet";
import { collection, addDoc, query, onSnapshot, orderBy, serverTimestamp } from '@firebase/firestore'
import { ref, uploadString,getDownloadURL } from "@firebase/storage";
import {v4} from 'uuid';

const Home = ({userInfo}) => {
    const [tweet, setTweet] = useState('');
    const [tweets, setTweets] = useState([]);
    const [attachment, setAttachment] = useState("");

    useEffect(() => {
        const q = query(collection(dbService, 'tweets'), orderBy('createdAt', 'desc'))
        const unsubscribe = onSnapshot(q, (querySnapshot) => {
            const nextTweets = querySnapshot.docs.map((document) => {
                return {
                    id: document.id,
                    ...document.data(),
                }
            })
            setTweets(nextTweets);
        })
        return () => {
            unsubscribe();
        }
    }, [])

    const OnSubmit = async (e) => {
        e.preventDefault();
        let attachmentUrl = "";

        if (attachment !== "") {
            const fileRef = ref(storageService, `${userInfo[Object.keys(userInfo)[0]].uid}/${v4()}`);
            const uploadFile = await uploadString(fileRef, attachment, "data_url");
            attachmentUrl = await getDownloadURL(uploadFile.ref);
        }

        const tweetObj = {
            text : tweet,
            createdAt: serverTimestamp(),
            creatorId : userInfo[Object.keys(userInfo)[0]].uid,
            attachmentUrl
        };
        
        await addDoc(collection(dbService, "tweets"),tweetObj);
        setTweet("");
        setAttachment("");
    }

    const OnChange = (e) => {
        const { target: { value } } = e
        setTweet(value);
    }

    const onFileChange = (event) =>{
        const { 
            target : {files},
        } = event;

        const theFile = files[0];
        const reader = new FileReader();
        reader.onloadend = (finishedEvent) =>{
            const {
                currentTarget : {result},
            }=finishedEvent;
            setAttachment(result);
        }
        reader.readAsDataURL(theFile);
    }

    const onClearAttachment=()=>setAttachment("");

    return (
        <div>
            <form onSubmit={OnSubmit}>
                <input type="text" placeholder="What's on your mind?" 
                 maxLength={120} onChange={OnChange} value={tweet} />
                 <input type="file" accept="image/*" onChange={onFileChange}/>
                <input type="submit" value="Tweet" />
                {attachment&&(
                    <div>
                        <img src={attachment} width="100px" height="100px"/>
                        <button onClick={onClearAttachment}>Clear Photo</button>
                    </div>
                )}
            </form>
            <div>
                {tweets.map((tweet) => (
                    <Tweet key={tweet.id} 
                    tweetObj={tweet} 
                    isOwner={tweet.creatorId===userInfo[Object.keys(userInfo)[0]].uid}
                    />
                ))}
            </div>
        </div>
    )
}
export default Home;

 

 

1. 기존 코드에 추가된 import와 state

import { ref, uploadString,getDownloadURL } from "@firebase/storage";
import {v4} from 'uuid';

const Home = ({userInfo}) => {
    const [tweet, setTweet] = useState('');
    const [tweets, setTweets] = useState([]);
    const [attachment, setAttachment] = useState("");

  import {v4} from 'uuid';  →  "npm install uuid"를 통해 설치하며, 특정 식별자를 랜덤으로 생성해준다.

const [attachment, setAttachment] = useState(""); 

   : attachment는 사용자가 tweet 생서 시 file을 선택할 경우 해당 file의 url(경로)를 가진다.

 

 

2. onFileChange

const onFileChange = (event) =>{
        const { // event의 target안으로 가서 files를 받아 변수로 저장
            target : {files},
        } = event;

        const theFile = files[0];
        const reader = new FileReader();
        reader.onloadend = (finishedEvent) =>{
            const { // finishedEvent currentTarget 가서 result 받아 변수로 저장
                currentTarget : {result},
            } = finishedEvent;
            setAttachment(result);
        }
        reader.readAsDataURL(theFile);
    }

- file이 선택된 경우 작용하는 event handler이다.

-  theFile  : user가 선택한 file 객체를 저장한 변수

reader.readAsDataURL(theFile)  theFile  의 data를 Url형식의(base64로 인코딩된) 문자열로 가져온다.

-  reader.onloadend = (finishedEvent) => { ... }  :  file의 읽기 작업이 완료되면 readyState가 DONE으로 변경되고 loadend가 trigger 된다. 이 때 reader의 result 속성은 URL형식의 data를 가진다. 이 result를 사용하여 attachment를 변경한다. 

 

FileReader.readAsDataURL() https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL

 

FileReader.onload : https://developer.mozilla.org/en-US/docs/Web/API/FileReader/onload

 

 

3. onClearAttachment

const onClearAttachment=()=>setAttachment("");

- photo 선택 후, 선택 취소를 위해 clear photo 버튼을 click할 경우 작용하는 event handler로, setAttachment를 이용하여 attachment를 ""로 변경한다.

 

 

4. OnSubmit

    const OnSubmit = async (e) => {
        e.preventDefault();
        let attachmentUrl = "";
        
        if (attachment !== "") { 
            const fileRef = ref(storageService, `${userInfo[Object.keys(userInfo)[0]].uid}/${v4()}`);
            const uploadFile = await uploadString(fileRef, attachment, "data_url");
            attachmentUrl = await getDownloadURL(uploadFile.ref);
        }

        const tweetObj = {
            text : tweet,
            createdAt: serverTimestamp(),
            creatorId : userInfo[Object.keys(userInfo)[0]].uid,
            attachmentUrl
        };
        
        await addDoc(collection(dbService, "tweets"),tweetObj);
        setTweet("");
        setAttachment("");
    }

 

-1) storage에  fileRef  의 이름으로 이미지 폴더를 생성한다.

const fileRef = ref(storageService, `${userInfo[Object.keys(userInfo)[0]].uid}/${v4()}`)

 

-2) 생성한  fileRef  폴더에 이미지를 넣는다.

    fileRef : storage에 생성한 image 폴더

    attachment : user가 선택한 file의 url

    "data_url" : data_url로 인코딩된 문자열을 추가한다는 의미

const uploadFile = await uploadString(fileRef, attachment, "data_url");

 

-3) ref 속성을 쓰면 그 이미지의 Reference에 접근 가능, 이미지가 저장된 stroage 주소를 받을 수 있다.

attachmentUrl = await getDownloadURL(uploadFile.ref);

 

-4) 기존 tweetObj 에 file 경로를 추가한다.

-5) collection에 tweetObj 를 추가 완료하면 Tweet 입력 state와 file 입력 state를 ""로 초기화한다.

 

 

5.

    return (
        <div>
            <form onSubmit={OnSubmit}>
                <input type="text" placeholder="What's on your mind?" 
                 maxLength={120} onChange={OnChange} value={tweet} />
                 <input type="file" accept="image/*" onChange={onFileChange}/>
                <input type="submit" value="Tweet" />
                {attachment&&(
                    <div>
                        <img src={attachment} width="100px" height="100px"/>
                        <button onClick={onClearAttachment}>Clear Photo</button>
                    </div>
                )}
            </form>
            <div>
                {tweets.map((tweet) => (
                    <Tweet key={tweet.id} 
                    tweetObj={tweet} 
                    isOwner={tweet.creatorId===userInfo[Object.keys(userInfo)[0]].uid}
                    />
                ))}
            </div>
        </div>
    )
}
export default Home;

 

- img 태그 추가 : user가 file을 선택하면 onFileChange event handler가 작용한다.

<input type="file" accept="image/*" onChange={onFileChange}/>

 

 

- attachment(file)이 존재하는 경우 미리보기 형식으로 form 태그 내에서 file을 보여준다. 

{attachment&&(
    <div>
        <img src={attachment} width="100px" height="100px"/>
        <button onClick={onClearAttachment}>Clear Photo</button>
    </div>
)}

 

< Tweet.js > : 삭제 부분 및 tweetObj upload부분 수정

import React, {useState} from 'react';
import { deleteDoc, updateDoc, doc } from '@firebase/firestore'
import { dbService, storageService } from "fBase";
import { ref, deleteObject } from "@firebase/storage";


const Tweet = ({ tweetObj, isOwner})=>{
    const TweetTextRef = doc(dbService, "tweets", `${tweetObj.id}`);
    const [editing, setEditing]= useState(false);
    const [newTweet, setNewTweet] = useState(tweetObj.text);
    
    const onDeleteClick = async() =>{
        const ok=window.confirm("Are you sure you wanna delete this tweet?");
        if (ok) {
            await deleteDoc(doc(dbService, `tweets/${tweetObj.id}`));
            if(tweetObj.attachmentUrl)
                await deleteObject(ref(storageService, tweetObj.attachmentUrl));    
        }
    };

    const onSubmit=async(e)=>{
        e.preventDefault();
        await updateDoc(TweetTextRef, { text: newTweet });
        setEditing(false);
    };

    const onChange =(e)=>{
        const{ target : {value} }= e;
        setNewTweet(value);
    };
    
    const toggledEditing = () => setEditing((prev)=>!prev);

    return(
        <div>
            { editing ? 
                <>
                    <form onSubmit={onSubmit}>
                        <input type="text" placeholder="Edit your tweet"
                        value={newTweet} required onChange={onChange}/>
                        <input type="submit" value="Update Tweet"/>
                    </form> 
                    <button onClick={toggledEditing}>Cancel</button>
                </>
                : <> 
                    <h4>{tweetObj.text}</h4>
                    {tweetObj.attachmentUrl && <img src={tweetObj.attachmentUrl} width="100px" height="100px" /> }
                    {isOwner&&(
                    <>
                        <button onClick={onDeleteClick}>Delete Tweet</button>
                        <button onClick={toggledEditing}>Edit Tweet</button>
                    </>
                    )} 
             </>
            }
        </div>
    );
}

export default Tweet;

1.

const onDeleteClick = async() =>{
    const ok=window.confirm("Are you sure you wanna delete this tweet?");
    if (ok) {
        await deleteDoc(doc(dbService, `tweets/${tweetObj.id}`));
        if(tweetObj.attachmentUrl)
            await deleteObject(ref(storageService, tweetObj.attachmentUrl));    
    }
};

- tweetObj에 file이 존재하는 경우 tweetObj.attachmentUrl로부터에서 reference를 받아와, storage로부터 해당 id를 가진 tweet의 file을 삭제한다.

 

2.

    return(
        <div>
            { editing ? 
                <>
                    <form onSubmit={onSubmit}>
                        <input type="text" placeholder="Edit your tweet"
                        value={newTweet} required onChange={onChange}/>
                        <input type="submit" value="Update Tweet"/>
                    </form> 
                    <button onClick={toggledEditing}>Cancel</button>
                </>
                : <> 
                    <h4>{tweetObj.text}</h4>
                    {tweetObj.attachmentUrl && <img src={tweetObj.attachmentUrl} width="100px" height="100px" /> }
                    {isOwner&&(
                    <>
                        <button onClick={onDeleteClick}>Delete Tweet</button>
                        <button onClick={toggledEditing}>Edit Tweet</button>
                    </>
                    )} 
             </>
            }
        </div>
    );
}

 

- tweetObj의 text를 출력하고 attachmentUrl이 존재한다면 해당 url에 대한 img를 함께 보여준다.

<h4>{tweetObj.text}</h4>
    {tweetObj.attachmentUrl && <img src={tweetObj.attachmentUrl} width="100px" height="100px" /> }

 

 

728x90