기억의 실마리
2022. 11. 17. 00:04

# App.js

index.js를 제외하면 최상위 컴포넌트 파일이다.

App.js에서부터 속성들을 가져와야 한다.

필요한 속성은 userObj이다.

 

userObj는 파이어베이스에서 getFirestore.currentUser.[ 필요한키 ]

이렇게 가져올 수 있는 데이터이지만 userObj라는 변수를 만들어 속성에 넣고

여기저기서 전달받아 사용하는 이유는 소스를 통합하여 확장성 있게

사용하고 싶기때문이다. userObj 하나만 변경해도 통합되어 변경되기 때문

더 직관적으로 변경,저장하기 쉬워진다.

function App() {
  const [init, setInit] = useState(false);  //false = 홈화면대기중...
  const [userObj, setUserObj] = useState(null);  //유저데이터를 위한 스테이트
  useEffect(()=>{
    authService.onAuthStateChanged((user)=>{
    //firestore에서 auth 변화리스너, 고로 user = usedata이다.
    
      if (user) {
        setIsloggedIn(true);
        setUserObj({
          displayName: user.displayName,
          uid: user.uid,
          updateProfile: { displayName: user.displayName },
      /* user에는 아주 많은 유저데이터가 있는데 그 중 필요한 key만 가져와서
        값을 적용시켜주고 있다. 방대한 데이터는 re-render에 불리하기때문이다. */
          
        });
      } else {
        setIsloggedIn(false);  //flase = 로그인실패.
      }
      setInit(true); //true = 홈화면이 된다.
    });
      },[]);
  const refreshUser = () => {
    const user = authService.currentUser;
    //refreshUser에는 user가 존재하지 않아, 새로 선언해준다.
    
    setUserObj({
      displayName: user.displayName,
      uid: user.uid,
      updateProfile: { displayName: user.displayName },
    });
  }
  //refreshUser함수를 만든 이유는 프로필 업데이트 기능을 위해서다.
  
  return (
      <>
        { init ? (
            <AppRouter
                isLoggedIn={isLoggedIn}
                userObj={userObj}
                refreshUser={refreshUser}
            />
        ) : (
            "Initializing...."
        ) }
      </>
  );
   //AppRouter에 속성들을 전달시켜준다.
}

 

# Router.js

두번째 컴포넌트파일이며, Profile 컴포넌트로 속성들을 전달해준다.

import {
    BrowserRouter as Router, Route, Routes,
} from "react-router-dom";
import {Auth} from "routes/Auth";
import {Home} from "routes/Home";
import {Profile} from "routes/Profile";
import {Navigation} from "components/Navigation";

export const AppRouter = ( {refreshUser, isLoggedIn, userObj} )=> {
    return (
        <Router>
            {isLoggedIn && <Navigation userObj={userObj}/>}
            <Routes>
                {isLoggedIn ? (
                    <>
                        <Route path="/" element={<Home userObj={userObj}/>}/>
                        <Route path="/Profile"
                               element={ <Profile userObj={userObj}
                                refreshUser={refreshUser} /> }
                        />
                    </>
                ): (
                    <>
                        <Route path="/" element={<Auth/>}/>
                    </>
                )}
            </Routes>
        </Router>
    );
};

AppRouter 인자로 오브젝트 리터럴로 속성이름을 넣어서 전달받고

컴포넌트요소 안에도 동일명속성으로 중괄호 안도 넣어준다.

 

# Navigation.js

고정 되어있는 네비게이션 컴포넌트파일이다.

import React from "react";
import {Link} from "react-router-dom";

export const Navigation = ({ userObj })=> {
    return (
    <nav>
        <ul>
            <li>
                <Link to="/">Home</Link>
            </li>
            <li>
                <Link to="/profile">{userObj.displayName}의 Profile</Link>
            </li>
        </ul>
    </nav>
    );
};

Home 메뉴와 userObj.displayName + "의 Profile" 메뉴가 있다.

userObj.displayName는 현재 접속되어있는 유저데이터에서 이름을 가져온다.

 

# Profile.js

import React, {useEffect, useState} from "react";
import {authService, dbService} from "Fbase";
import { useNavigate } from "react-router-dom";
import {collection, getDocs, query, where} from "@firebase/firestore";
import {updateProfile} from "@firebase/auth";

export const Profile = ( { refreshUser, userObj } )=> {
         //상위 컴포넌트에서 가져온 속성들

    const navigate = useNavigate();
    //로그아웃 처리 후 홈화면으로 이동시킬때 사용
    
    const [newDisplayName, setNewDisplayName] = useState(userObj.displayName);
    //가져온 유저데이터에서 displayName
    
    const onLogOutClick = () => {
        authService.signOut();
        navigate("/",{replace:true});
    };
    
    // eslint-disable-next-line react-hooks/exhaustive-deps
    const getMyZweets = async ()=> {
        try {
            const q = query(
                collection(dbService,"zweets"),
                where("creatorId", "==", userObj.uid),
            );
            const querySnapshot = await getDocs(q);
            querySnapshot.forEach((doc)=>{
                console.log(doc.id,"=>",doc.data());
            });
        } catch (e) {
            console.log(e);
        }
    }
    useEffect(()=> {
        getMyZweets();
    },[getMyZweets])
    const onChange = ( {target: {value} }) => {
        setNewDisplayName(value);
    }
    const onSubmit = async (e)=> {
        e.preventDefault();
        if(userObj.displayName !== newDisplayName){
            await updateProfile(authService.currentUser,{displayName: newDisplayName});
             /*여기서 userObj가 아닌 authService.currentUser를 사용한 이유는
             파이어베이스 v9부터는 메소드의 영역이 다르기 때문에 위와같이 해결했다. */
        }
        refreshUser();
        //서브밋되면 리프레쉬유저함수를 실행시켜 프로필도 re-render되게 만든다.
    }
    return (
        <>
            <form onSubmit={onSubmit}>
                <input
                    onChange={onChange}
                    type="text"
                    placeholder="Display name"
                    value={newDisplayName}
                />
                <input type="submit" value="Update Profile"/>
            </form>
            <button onClick={onLogOutClick}>Log Out</button>
        </>
    )
};

프로필 화면에서 텍스트인풋내에 글을 쓴 후 Update Profile 버튼이나

엔터를 눌러 서브밋하면 refreshUser함수가 실행되어 네비게이션의 프로필이름이 바뀐다.

 

# refreshUser 함수 다시보기

  const refreshUser = () => {
    const user = authService.currentUser;
    setUserObj({
      displayName: user.displayName,
      uid: user.uid,
      updateProfile: { displayName: user.displayName },
    });
  }

setUserObj로 인하여 userObj가 변경되고

변화를 감지한 react가 re-render시킨 것이다.

2022. 11. 15. 23:22

3-1. 이미지업로드 게시글과 이어지는 내용이다.

3-1게시글에선 업로드가 가능할 수 있는 기능만 추가해준 것이고

 

3-2에선 확실하게 마운트 시켜주고 삭제시키는 기능을 구현했다.

 

# 메소드 영역

import {storageService} from "Fbase";
//Fbase.js에서 getStorage from "firebase/storage"를 가져온 것

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

export const Home = ({ userObj }) { 

//...

const [attachment, setAttachment] = useState("");
//업로드할 사진을 위한 스테이트

const onSubmit = async (event)=> {
    event.preventDefault();
    let attachmentUrl = "";    //서로 영역에서 사용하기 위해 let으로 선언하여 업데이트함.
    try{
        if(attachment !== "") {
            const attachmentRef = ref(storageService, `${userObj.uid}/${uuidv4()}`);
            //uuidv4는 랜덤으로 스트링을 만들어준다.
            
            const response = await uploadString(attachmentRef, attachment, "data_url");
            //"data_url"은 readAsDataURL에서 전달받는다.
            
            attachmentUrl = await getDownloadURL(response.ref);
        }
        const zweetObj = {
            text: zweet,
            createdAt: Date.now(),
            creatorId: userObj.uid,
            attachmentUrl               //zweetObj에서 attachmentUrl이 추가되었다.
        };
        await addDoc(collection(dbService, "zweets"),zweetObj);
    } catch (error) {
        console.error("Error adding document: ", error);
    }
    setZweet("");
    setAttachment("");   //서브밋 후 초기화
};

 

Home.js에서 onSubmit함수의 변경사항

 

const onDeleteClick = async () => {
    const ok = window.confirm("Are you sure you want to delete this zweet?");
    if(ok){
        await deleteDoc(zweetTextRef);
        
        await deleteObject(ref(storageService, zweetObj.attachmentUrl));
        //이미지가 있다면 이미지도 지워줘야 하기때문에 추가
    }
};

Zweet.js에서 onDeleteClick함수의 변경사항

 

# 컴포넌트 마운트 영역

<div>
    <h4>{zweetObj.text}</h4>
    {zweetObj.attachmentUrl && <img src={zweetObj.attachmentUrl} width="50px" height="50px"/>}
 //zweetObj에 attachmentUrl(이미지파일url)리터럴이 생겼기 때문에 zweetObj.attachmentUrl로 변경
    
  ...
   
</div>

지우거나 업로드 했을때 사진도 함께 업로드 되거나 삭제되어야 하기때문에 함수들에 변경사항이 생겼다.

2022. 11. 15. 21:25

2. 업로드된 컴포넌트 삭제/수정 기능과 이어지는 내용이다.

Home.js 파일내부에 내용이다.

 

Home.js는 Zwitter앱의 홈화면(메인화면)이 될 컴포넌트이다.

 

 

# 메소드 영역

const [attachment, setAttachment] = useState();
//파일을 위한 스테이트이다.

const onFileChange = ({target:{files}}) => {
    const theFile = files[0];
    //파일을 가져오고
    
    const reader = new FileReader();
    //reader를 만든 후
    
    reader.onloadend = ({currentTarget:{result}}) => {
        setAttachment(result);
    };
    //파일로드가 완료되면 파일을 스테이트에 넣어줌.
    
    reader.readAsDataURL(theFile);
    //readAsDataURL을 사용해서 파일을 읽는다.
}
const onClearAttachment = ()=> setAttachment(null);
//클리어 버튼을 누를경우 적용시켜줄 함수

 

# 컴포넌트 마운트 영역

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

이미지를 선택하여 업로드하면 attachment가 존재하게 되고 위와 같이 요소들이 마운트된다.

가로세로 50px의 미리보기 이미지가 나오게 되고 Clear버튼을 누르면

onClearAttachment함수에 의해서 이미지가 사라지게된다.

( setAttachment(null) => attachment = null )

2022. 11. 14. 23:20

1. Real Time 기능 게시글과 이어지는 내용이다.

글을 입력하고 업로드하면 마운트되는 컴포넌트의 이름은 Zweet이며

 

Zweet.js내부에 단독으로 존재한다. ( 아래 내용들 모두 Zweet.js의 내용 )

 

# 메소드 영역

import React, {useState} from "react";
import {dbService} from "../Fbase";  //Fbase.js에서 가져온 변수이고 getFirestore메소드이다.
import {deleteDoc, doc, updateDoc } from "firebase/firestore";


export const Zweet = ({ zweetObj, isOwner })=> {   
//zweetObj = 최신화된 유저오브젝트 , isOwner = 글쓴이가 맞는지 여부

    const [editing, setEditing] = useState(false);            //에디트 활성화 여부
    const [newZweet, setNewZweet] = useState(zweetObj.text);  //에디트input의 text
    
    const zweetTextRef = doc(dbService, "zweets", `${zweetObj.id}`);
    //중복코드를 변수로 선언
    
    const onDeleteClick = async () => {
        const ok = window.confirm("Are you sure you want to delete this zweet?");
        if(ok){
            await deleteDoc(zweetTextRef);
        }
    };
    //confirm메소드를 사용하여 boolean값을 받고 true면 deleteDoc메소드를 실행한다
    
    const toggleEditing = () => setEditing((prev)=> !prev);   //에디트를 활성화/비활성화 시킨다
    const onChange = ({target:{value}})=> setNewZweet(value); //벨류가 바뀌면 re-render시킨다
    
    const onSubmit = async (e) => {
        e.preventDefault();
        await updateDoc(zweetTextRef,{
            text: newZweet,
        });
        setEditing(false);
    };
    //text를 newZweet의 value로 바꿔서 업데이트하고 다시 에디트를 비활성화 시켜준다.

 

# 컴포넌트 마운트 영역

    return (
        <div>
            {
                editing ? (
                    <>
                        <form onSubmit={onSubmit}>
                            <input type="text"
                                   placeholder="Edit your zweet"
                                   value={newZweet}
                                   required
                                   onChange={onChange}
                            />
                            <input
                                type="submit"
                                value="Update Zweet"
                            />
                        </form>
                        <button onClick={toggleEditing}>Cancel</button>
                    </>
                ) : (
                    <>
                        <div>
                            <h4>{zweetObj.text}</h4>
                            {isOwner && (
                                <>
                                    <button onClick={onDeleteClick}>Delete Zweet</button>
                                    <button onClick={toggleEditing}>Edit Zweet</button>
                                </>
                            )}
                        </div>
                    </>
                )
            }
        </div>
    );

에디트컴포넌트의 Default 값은 false이기 때문에 unmount이다. 에디트 버튼을 클릭할 경우 toggleEditing함수가 실행되어서 editing = true가 되고 에디트컴포넌트가 mount 된다. 이 후 input에 글을 수정하여 submit시키면(Enter or Edit Zweet 버튼) onSubmit함수에 의해 updateDoc메소드가 실행되어 업데이트되고 onSubmit의 로직처리중 마지막, setEditing(false)로 인해 다시 에디트컴포넌트는 unmount되고 홈컴포넌트가 mount된다.

2022. 11. 14. 20:09
import React, {useEffect, useState} from "react";
import { dbService } from "Fbase";
import { addDoc, collection, onSnapshot, query, orderBy } from "firebase/firestore";
import {Zweet} from "components/Zweet";

export const Home = ({ userObj })=> {
    const [zweet, setZweet] = useState("");
    const [zweets, setZweets] = useState([]);
    useEffect(()=>{
        const q = query(
            collection(dbService, "zweets"),
            orderBy("createdAt","desc")

        );
        onSnapshot(q, (snapshot)=>{
            const zweetArr = snapshot.docs.map((doc)=>({
                id: doc.id,
            ...doc.data(),
            }));
            setZweets(zweetArr);
        });
    },[]);

( dbService = getFirestore , Zweet = "서브밋시 마운트 될 컴포넌트"

userObj = "firebase.auth().onAuthStateChanged 에서 받아온 유저오브젝트" )

 

리액트라우터를 이용할 Router.js 컴포넌트에

 

메인화면 컴포넌트(Home)를 글을 업로드시 실시간기능을 넣는다고 가정했을때

 

위와 같이 유스이팩트를 통해서 쿼리를 만들고 업로드 될 요소의 순서를 지정해준다.

 

createdAt는 시간별로 나올 것이고 desc를 설정하였기 때문에 현재와 가까운

 

시간일수록 위로 업로드 된다.

 

onSnapshot을 사용하여서 본격적으로 실시간 기능을 구현시킨다.

 

onSnapshot은 데이터베이스에 변화가 생겼을 경우 알려주는 리스너 역할이다.

 

map을 통해서 각각 id와 data()의 리터럴을 가진 요소가 실시간으로 업로드 된다.

 

 

const onSubmit = async (event)=> {
    event.preventDefault();
    try{
        const docRef = await addDoc(collection(dbService, "zweets"),{
            text: zweet,
            createdAt: Date.now(),
            creatorId: userObj.uid,
        });
        console.log("Document written with ID: ",docRef);
    } catch (error) {
        console.error("Error adding document: ", error);
    }
    setZweet("");
};

글을 업로드 했을때(submit 했을때) 실행되는 함수이다.

 

서브밋 됐을때 새로고침기능을 없앤 후 async를 사용하여 try와 catch로 성공/실패 했을때

 

지정해둔 함수를 반환하도록 만든다.

 

addDoc기능을 임포트해서 사용했으므로 새로운 Document(문서)를만들어서

 

text, createdAt, creatorId 의 리터럴을 만들어 준다.

 

마지막으로 setZweet을 빈문자열로 만들어줘서 글쓰는 인풋을 초기화 시켜준다.

 

 

const onChange = ({ target: {value} })=> {
    setZweet(value);
}

업로드할 input 내의 리스너를 만들어줘서 즉각 랜더링될 수 있도록 만든다.

 

 

        return (
            <div>
                <form onSubmit={onSubmit}>
                    <input
                        value={zweet}
                        onChange={onChange}
                        type="text"
                        placeholder="What's on your mind?"
                        maxLength={120}
                    />
                    <input type="submit" value="zweet"/>
                </form>
                <div>
                    {zweets.map(zweet => (
                        <Zweet key={zweet.id} zweetObj={zweet}
                        isOwner={zweet.creatorId === userObj.uid }/>
                    ))}
                </div>
            </div>
        );
};

isOwner 속성은 댓글작성자만 글을 삭제할 수 있도록 하기 위해 가져온 속성이다.

2022. 11. 12. 22:07

Firebase 설치시 npm i firebase 를 하면 최신버전이 인스톨된다.

 

npm i firebase@9.14.0 이와 같이 입력하면 버전 9.14.0으로 사용가능 하다.

 

create-react-app 을 사용했을 때 의 기준으로 작성되었다.

 

 

# 사용예시

import firebase from "firebase/compat/app";
import "firebase/compat/auth";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
    apiKey: process.env.REACT_APP_API_KEY,
    authDomain: process.env.REACT_APP_AUTH_DOMAIN,
    projectId: process.env.REACT_APP_PROJECT_ID,
    storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
    messagingSenderId: process.env.REACT_APP_MESSAGIN_ID,
    appId: process.env.REACT_APP_APP_ID,
};

firebase.initializeApp(firebaseConfig);

export const authService = firebase.auth();
export const dbService = getFirestore();

나의 경우 Fbase.js 라는 js파일을 만들어 주고 사용하고자하는 기능들을 임포트 시킨 후 따로 변수를 지정해주어서

export 해서 필요한 컴포넌트에서 임포트 시켜서 사용하고 있다.

 

 

# Absolute Imports 경로 src고정하는 기능 ( 부가기능 )

import 경로의 경우 Absolute Imports를 사용하고 있기 때문에 초기경로가 모두 src폴더에서 시작한다.

장점으로는 가독성이 좋고, 경로를 직관적으로 볼 수 있어 경로오류를 줄여주므로 작업 속도를 늘려 줄 수 있다.

적용 방법은 src폴더 위치와 동일한 위치에 jsconfig.json 파일을 만들어서 https://create-react-app.dev/docs/importing-a-component/

링크를 통해 Absolute Imports의 코드블럭 내용을 복사하여 해당 파일에 붙여넣기하면 된다.

 

# Fire Base 프로젝트 만들기

파이어베이스 웹사이트에 접속하여 새로운 프로젝트를 만들고자 하는 앱 혹은 웹을 선택하여 생성한다.

이 후 코드블럭에 Firebase configuration 부분을 복사해서 Fbase.js에 붙여넣기 한다.

const firebaseConfig = {
    apiKey: This0is0the0key,
    authDomain: project-111.firebaseapp.com,
    projectId: project-111,
    storageBucket: project-111.appspot.com,
    messagingSenderId: 112114119000,
    appId: 10:3.1415926535:web:a1b1c2d615s6s1s5,
};

위와 같이 오브젝트 리터럴을 가지게 되는데 이 config key들이 노출되면 누구나 해당 키를 연결하여 데이터 베이스를 만질 수 있게 된다.

깃허브에 푸쉬하여 공유할 경우 암호화를 시킬 필요가 있어보인다.

 

# config key 깃허브 푸쉬로부터 암호화 시키기

.gitignore 의 위치와 같은 위치에 .env 파일을 만든 후 아래와 같이 변수를 만들어 준 후 Firebase에서 받은 config key들을 할당해준다.

REACT_APP_API_KEY=This0is0the0key
REACT_APP_AUTH_DOMAIN=project-111.firebaseapp.com
REACT_APP_PROJECT_ID=project-111
REACT_APP_STORAGE_BUCKET=project-111.appspot.com
REACT_APP_MESSAGIN_ID=112114119000
REACT_APP_APP_ID=10:3.1415926535:web:a1b1c2d615s6s1s5

위와 같이 create-react-app에서는 환경변수를 사용할때 REACT_APP_***_*** 이런식으로 변수명을 지정해주어야 적용된다.

지키지 않을경우 절대로 할당값은 적용되지 않는다.

 

이 후에 .gitignore 파일을 열어서 # misc 주석이 달려있는 가장 밑 부분에 .env를 추가해주면 .env는 푸쉬에서 제외된다.

( 웹스톰의 경우 리베이스를 해주어야 된다. )

 

const firebaseConfig = {
    apiKey: process.env.REACT_APP_API_KEY,
    authDomain: process.env.REACT_APP_AUTH_DOMAIN,
    projectId: process.env.REACT_APP_PROJECT_ID,
    storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
    messagingSenderId: process.env.REACT_APP_MESSAGIN_ID,
    appId: process.env.REACT_APP_APP_ID,
};

이 후 process.env.변수명 을 입력해주어서 암호화 한다.