본문 바로가기

트위터(React, TypeScript, Firebase, Vite)

15. TWEETING - Uploading Images

최종 완료된걸 정리한 글은 제일 아래에 있습니다 ^.^

 

Firebase Storage도 Firebase Database처럼

"Collection/Document/Collection/..."와 같은 폴더 구조와 비슷하다.

 

post-tweet-form.tsx

import { addDoc, collection } from "firebase/firestore";
import { useState } from "react";
import styled from "styled-components"
import { auth, db, storeage } from "../routes/firebase";
import { ref, uploadBytes } from "firebase/storage";

const Form = styled.form`
    display: flex;
    flex-direction: column;
    gap: 10px;
`;

const TextArea = styled.textarea`
   border: 2px solid white;
   padding: 20px;
   border-radius: 20px;
   font-size: 16px;
   color: white;
   background-color: black;
   width: 100%;
   resize: none;
   font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
     Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
   &::placeholder {
     font-size: 16px;
   }
   &:focus {
     outline: none;
     border-color: #1d9bf0;
   }
 `;

const AttachFileButton = styled.label`
   padding: 10px 0px;
   color: #1d9bf0;
   text-align: center;
   border-radius: 20px;
   border: 1px solid #1d9bf0;
   font-size: 14px;
   font-weight: 600;
   cursor: pointer;
 `;
 
 const AttachFileInput = styled.input`
   display: none;
 `;
 
 const SubmitBtn = styled.input`
   background-color: #1d9bf0;
   color: white;
   border: none;
   padding: 10px 0px;
   border-radius: 20px;
   font-size: 16px;
   cursor: pointer;
   &:hover,
   &:active {
     opacity: 0.9;
   }
 `;

export default function PostTweetForm(){
    const [isLoading, setLoading] = useState(false);
    const [tweet, setTweet] = useState("");
    const [file, setFile] = useState<File|null>(null);
    const onChange = (e : React.ChangeEvent<HTMLTextAreaElement>) => {
        setTweet(e.target.value);
    };
    const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const {files} = e.target;
        if(files && files.length === 1){
            setFile(files[0]);
        }
    };

    const onSubmit = async (e:React.FormEvent<HTMLFormElement>) => {
      e.preventDefault();
      const user = auth.currentUser;
      if(!user || tweet === "" || tweet.length > 180){
        return;
      }
        
      try {
        setLoading(true);
        const doc = await addDoc(collection(db, "tweets"), {
          tweet,
          createAt : Date.now(),
          username : user.displayName || "Anonymous",
          userId : user.uid
        });
        if(file){
          const locationRef = ref(storeage, `tweets/${user.uid}-${user.displayName}/${doc.id}`)
                              //import { ref } from "firebase/storage";
                              //tweets 폴더 안에 트윗을 보내는 유저들 저마다의 폴더를 하나씩 생성한다.
          await uploadBytes(locationRef, file);
        }
      } catch (e) {
        console.log(e);
      }finally{
        setLoading(false);
      }

    };

    return <Form onSubmit={onSubmit}>
        <TextArea onChange={onChange} value={tweet} placeholder="What is happening?"/>
        <AttachFileButton htmlFor="file">{file ? "Photo added" : "Add photo"}</AttachFileButton>
        <AttachFileInput onChange={onFileChange} type="file" id="file" accept="image/*"/>
        <SubmitBtn type="submit" value={isLoading ? "Posting..." : "Post Tweet"}/>
    </Form>
};

 

firebase.ts

import { initializeApp } from "firebase/app";
import { getAnalytics } from "firebase/analytics";

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

/* 
  도메인, api ket 등 여러가지 키 값들이 포함된 config 개체를 
  firebase app 생성 시에 firebaseConfig 객체가 주어졌다.
*/
const firebaseConfig = {
  apiKey: "AIzaSyDRHBD9TNQFRsFTV8nf8N2SRJ7C65c4r08",
  authDomain: "nwitter-reloaded-68b8c.firebaseapp.com",
  projectId: "nwitter-reloaded-68b8c",
  storageBucket: "nwitter-reloaded-68b8c.firebasestorage.app",
  messagingSenderId: "464085535303",
  appId: "1:464085535303:web:ba1b5d7558448f3dc07f62",
  measurementId: "G-HF9RX7ZX6F"
};

/*
  firebaseConfig 옵션을 통해서 app을 생성한다.
*/
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);

/*
  app에 대한 인증 서비스를 사용한다.
*/
export const auth = getAuth(app);


/*
  데이터베이스와 스토리지에 대한 엑세스 권한을 얻는다.
*/
export const storeage = getStorage(app);

export const db = getFirestore(app);

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Firebase 사이트 Console 내 Storage에 tweets 폴더가 생성되었고 tweets 폴더 내에 유저ID와 이름으로 된 폴더가 생성된걸 확인할 수 있다.그리고 해당 폴더 내에는 이미지가 생성된걸 확인할 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Firebase 사이트 Console 내 Storage에서 tweets 폴더 내의 폴더를 삭제한다.

* 삭제한 이유는 이미지를 업로드하고 나서 그 이미지의 URL를 받기 위해서이다.

 

Firebase 사이트 Console 내 Firestore Database 내 Collection을 삭제한다.

* 삭제한 이유는 최종으로 이미지를 업로드하고 나서 그 이미지의 URL를 받는 것을 테스트 하기 위해서이다.

 

 

 

 

 

 

 

 

 

 

 

<최총 완료>

데이터를 클라우드 데이터서비스와 스토리지에 업로드하고

다운로드 URL을 사용하여 둘을 연결하였다.

 

post-tweet-form.tsx

import { addDoc, collection, updateDoc } from "firebase/firestore";
import { useState } from "react";
import styled from "styled-components"
import { auth, db, storeage } from "../routes/firebase";
import { getDownloadURL, ref, uploadBytes } from "firebase/storage";

const Form = styled.form`
    display: flex;
    flex-direction: column;
    gap: 10px;
`;

const TextArea = styled.textarea`
   border: 2px solid white;
   padding: 20px;
   border-radius: 20px;
   font-size: 16px;
   color: white;
   background-color: black;
   width: 100%;
   resize: none;
   font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
     Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
   &::placeholder {
     font-size: 16px;
   }
   &:focus {
     outline: none;
     border-color: #1d9bf0;
   }
 `;

const AttachFileButton = styled.label`
   padding: 10px 0px;
   color: #1d9bf0;
   text-align: center;
   border-radius: 20px;
   border: 1px solid #1d9bf0;
   font-size: 14px;
   font-weight: 600;
   cursor: pointer;
 `;
 
 const AttachFileInput = styled.input`
   display: none;
 `;
 
 const SubmitBtn = styled.input`
   background-color: #1d9bf0;
   color: white;
   border: none;
   padding: 10px 0px;
   border-radius: 20px;
   font-size: 16px;
   cursor: pointer;
   &:hover,
   &:active {
     opacity: 0.9;
   }
 `;

export default function PostTweetForm(){
    const [isLoading, setLoading] = useState(false);
    const [tweet, setTweet] = useState("");
    const [file, setFile] = useState<File|null>(null);
    const onChange = (e : React.ChangeEvent<HTMLTextAreaElement>) => {
        setTweet(e.target.value);
    };
    const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const {files} = e.target;
        if(files && files.length === 1){
            setFile(files[0]);
        }
    };

    const onSubmit = async (e:React.FormEvent<HTMLFormElement>) => {
      e.preventDefault();
      const user = auth.currentUser;
      if(!user || tweet === "" || tweet.length > 180){
        return;
      }
        
      try {
        setLoading(true);
        const doc = await addDoc(collection(db, "tweets"), {
          tweet,
          createAt : Date.now(),
          username : user.displayName || "Anonymous",
          userId : user.uid
        });
        if(file){
          const locationRef = ref(storeage, `tweets/${user.uid}-${user.displayName}/${doc.id}`);
          const result = await uploadBytes(locationRef, file);
          const url = await getDownloadURL(result.ref);
          await updateDoc(doc, {photo : url});
        }
        setTweet("");
        setFile(null);
        
      } catch (e) {
        console.log(e);
      }finally{
        setLoading(false);
      }

    };

    return <Form onSubmit={onSubmit}>
        <TextArea onChange={onChange} value={tweet} placeholder="What is happening?"/>
        <AttachFileButton htmlFor="file">{file ? "Photo added" : "Add photo"}</AttachFileButton>
        <AttachFileInput onChange={onFileChange} type="file" id="file" accept="image/*"/>
        <SubmitBtn type="submit" value={isLoading ? "Posting..." : "Post Tweet"}/>
    </Form>
};

firebase.ts

import { initializeApp } from "firebase/app";
import { getAnalytics } from "firebase/analytics";

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

/* 
  도메인, api ket 등 여러가지 키 값들이 포함된 config 개체를 
  firebase app 생성 시에 firebaseConfig 객체가 주어졌다.
*/
const firebaseConfig = {
  apiKey: "AIzaSyDRHBD9TNQFRsFTV8nf8N2SRJ7C65c4r08",
  authDomain: "nwitter-reloaded-68b8c.firebaseapp.com",
  projectId: "nwitter-reloaded-68b8c",
  storageBucket: "nwitter-reloaded-68b8c.firebasestorage.app",
  messagingSenderId: "464085535303",
  appId: "1:464085535303:web:ba1b5d7558448f3dc07f62",
  measurementId: "G-HF9RX7ZX6F"
};

/*
  firebaseConfig 옵션을 통해서 app을 생성한다.
*/
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);

/*
  app에 대한 인증 서비스를 사용한다.
*/
export const auth = getAuth(app);


/*
  데이터베이스와 스토리지에 대한 엑세스 권한을 얻는다.
*/
export const storeage = getStorage(app);

export const db = getFirestore(app);

 

 

 

 

 

nwitter-reloaded.z01
19.53MB
nwitter-reloaded.z02
19.53MB
nwitter-reloaded.z03
19.53MB
nwitter-reloaded.z04
19.53MB
nwitter-reloaded.z05
19.53MB
nwitter-reloaded.zip
15.59MB

'트위터(React, TypeScript, Firebase, Vite)' 카테고리의 다른 글

17. TWEETING - Realtime [1]  (0) 2025.04.17
16. TWEETING - Fetching Timeline  (0) 2025.04.17
14. Tweeting to Firestore  (0) 2025.04.16
13. TWEEING - Post Tweet Form  (0) 2025.04.15
12. TWEETING - Navigation Bar  (0) 2025.04.15