256일차 리액트(React) - 트위터 웹사이트 완성
2021. 9. 11. 19:59ㆍDiary/201~300
https://kwongeneral.github.io/0912_React_Twitter/#/
React App
kwongeneral.github.io
배포 설정
1. package.json 수정
2. 파이어베이스 승인된 도메인 추가
3. 파이어베이스 보안 정책 수정
4. 비밀키 보안 설정
전체 소스코드
1. package.json
2. App.js
import {useEffect, useState} from "react";
import React from "react";
// import AppRouter from "components/Router";
import AppRouter from "./Router";
import { authService } from "../fbase";
function App() {
const [init, set_init] = useState(false);
// const [isLogin, set_isLogin] = useState(false);
const [userObj, set_userObj] = useState(null);
// console.log(authService.currentUser)
// setTimeout(() => console.log(authService.currentUser), 2000);
useEffect(() => {
// authService.onAuthStateChanged((user) => console.log(user));
authService.onAuthStateChanged((user) => {
if(user) {
// set_isLogin(user);
set_userObj({
uid: user.uid,
displayName: user.displayName,
updateProfile: (args) => user.updateProfile(args)
});
} else {
// set_isLogin(false);
set_userObj(false);
}
set_init(true);
});
}, []);
const refreshUser = () => {
// set_userObj(authService.currentUser);
const user = authService.currentUser;
set_userObj({
uid: user.uid,
displayName: user.displayName,
updateProfile: (args) => user.updateProfile(args)
})
};
return (
<>
{ init ? ( <AppRouter isLogin={ Boolean(userObj) } userObj={ userObj } refreshUser={ refreshUser } /> ) : ( "초기화중..." ) }
{/*{ init ? ( <AppRouter isLogin={ isLogin } userObj={ userObj } refreshUser={ refreshUser } /> ) : ( "초기화중..." ) }*/}
{/*<footer>© {new Date().getFullYear()} Twitter</footer>*/}
</>
);
}
// 구조 분해 할당
// function App({ isLogin }) {
// return (
// <>
// <AppRouter isLogin={ isLogin } />
// <footer>© {new Date().getFullYear()} Twitter</footer>
// </>
// );
// }
// 기존 방법
// function App() {
// const [isLogin, set_isLogin] = useState(false);
// return (
// <>
// <AppRouter isLogin={ isLogin } />
// <footer>© {new Date().getFullYear()} Twitter</footer>
// </>
// );
// }
export default App;
3. AuthForm.js
import {useState} from "react";
import {authService} from "../fbase";
const AuthForm = () => {
const [email, set_email] = useState("");
const [password, set_password] = useState("");
const [newAccount, set_newAccount] = useState(true);
const [error, set_error] = useState("");
const toggleAccount = () => set_newAccount((prev) => !prev);
const onChange = (event) => {
// console.log(event.target.name);
const { target : { name, value } } = event;
if(name === "email") {
set_email(value);
} else if(name === "password") {
set_password(value);
}
};
const onSubmit = async (event) => {
event.preventDefault();
try {
let data;
if(newAccount) {
data = await authService.createUserWithEmailAndPassword(email, password)
} else {
data = await authService.signInWithEmailAndPassword(email, password)
}
console.log("데이터 : ", data);
} catch (error) {
// console.log("에러 : ", error)
set_error(error.message)
}
}
return (
<>
<form onSubmit={ onSubmit } className="container">
<input className="authInput" name="email" value={ email } onChange={ onChange } type="email" placeholder="이메일" required/>
<input className="authInput" name="password" value={ password } onChange={ onChange } type="password" placeholder="비밀번호" required/>
<input className="authInput authSubmit" type="submit" value={ newAccount ? "회원가입" : "로그인" } required/>
{ error && <span className="authError">{ error }</span>}
</form>
<span onClick={ toggleAccount } className="authSwitch">
{ newAccount ? "로그인" : "회원가입" }
</span>
</>
)
}
export default AuthForm;
4. Navigation.js
import { Link } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTwitter } from "@fortawesome/free-brands-svg-icons";
import { faUser } from "@fortawesome/free-solid-svg-icons";
const Navigation = ({ userObj }) => {
return (
<nav>
<ul style={{ display: "flex", justifyContent: "center", marginTop: 50 }}>
<li>
<Link to="/" style={{ marginRight: 10 }}>
<FontAwesomeIcon icon={ faTwitter } color={ "#04AAFF" } size="2x" />
</Link>
</li>
{ userObj.displayName !== null ?
<li>
<Link to="/profile" style={{ marginLeft: 10, display: "flex", flexDirection: "column",
alignItems: "center", fontSize: 12 }}>
<FontAwesomeIcon icon={ faUser } color={ "#04AAFF" } size="2x" />
{/*{ userObj.displayName }의 마이페이지*/}
</Link>
</li> :
<li>
<Link to="/profile" style={{ marginLeft: 10, display: "flex", flexDirection: "column",
alignItems: "center", fontSize: 12 }}>
<FontAwesomeIcon icon={ faUser } color={ "#04AAFF" } size="2x" />
{/*{ userObj.displayName }의 마이페이지*/}
</Link>
</li>
}
</ul>
</nav>
)
};
export default Navigation;
5. Router.js
import { HashRouter as Router, Route, Switch, Redirect } from "react-router-dom"
import { useState } from "react";
import Auth from "../routes/Auth";
import Home from "../routes/Home";
import Navigation from "./Navigation";
import Profile from "../routes/Profile";
// Switch를 사용하면 여러가지 라우터 중 하나만 렌더링할 수 있게 해준다.
// 특정 작업 후, 페이지 이동은 Redirect나 history.push를 사용한다.
const AppRouter = ({ isLogin, userObj, refreshUser }) => {
return (
<Router>
{ isLogin && <Navigation userObj={ userObj } /> }
<Switch>
{ isLogin ? (
<div style={{ maxWidth: 890, width: "100%", margin: "0 auto", marginTop: 80, display: "flex",
justifyContent: "center" }}>
<Route exact path="/">
<Home userObj={ userObj } />
</Route>
<Route exact path="/profile">
<Profile userObj={ userObj } refreshUser={ refreshUser } />
</Route>
</div>
) : (
<Route exact path="/">
<Auth />
</Route>
) }
{/*<Redirect from="*" to="/" />*/}
</Switch>
</Router>
)
}
export default AppRouter;
6. Tweet.js
import { dbService, storageService } from "../fbase";
import { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrash, faPencilAlt } from "@fortawesome/free-solid-svg-icons";
const Tweet = ({ tweetObj, isOwner }) => {
const [editing, set_editing] = useState(false);
const [newTweet, set_newTweet] = useState(tweetObj.text);
const toggleEditing = () => set_editing((prev) => !prev);
const onChange = (event) => {
const { target: { value } } = event;
set_newTweet(value);
}
const onDeleteClick = async () => {
const ok = window.confirm("삭제하시겠습니까?");
// console.log("ok : ", ok)
if(ok) {
// console.log("tweetObj.id : ", tweetObj.id);
await dbService.doc(`tweets/${tweetObj.id}`).delete() // 작은 따옴표가 아닌 1 옆의 백틱
// console.log("onDeleteClick data : ", data)
if(tweetObj.attachmentUrl !== "") {
await storageService.refFromURL(tweetObj.attachmentUrl).delete();
}
}
}
const onSubmit = async (event) => {
event.preventDefault();
// console.log("tweetObj.id, newTweet : ", tweetObj.id, newTweet)
await dbService.doc(`tweets/${tweetObj.id}`).update({ text: newTweet });
set_editing(false);
}
return (
<div className="tweet">
{ editing ? (
<>
<form onSubmit={ onSubmit } className="container tweetEdit">
<input onChange={ onChange } value={ newTweet } required placeholder="수정하기" autoFocus
className="formInput"/>
<input type="submit" value="수정하기" className="formBtn"/>
</form>
<button onClick={ toggleEditing } className="formBtn cancelBtn">취소</button>
</>
) : (
<>
<h4>{ tweetObj.text }</h4>
{ tweetObj.attachmentUrl && (
<img src={ tweetObj.attachmentUrl } width="50px" height="50px" />
) }
{ isOwner && (
<div className="tweet__actions">
<span onClick={ onDeleteClick }>
<FontAwesomeIcon icon={ faTrash } />
</span>
<span onClick={ toggleEditing }>
<FontAwesomeIcon icon={ faPencilAlt } />
</span>
</div>
)
}
</>
)
}
</div>
)
};
export default Tweet;
7. TweetFactory.js
import {dbService, storageService} from "../fbase";
import {v4 as uuidv4} from "uuid";
import {useState} from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlus, faTimes } from "@fortawesome/free-solid-svg-icons";
const TweetFactory = ({ userObj }) => {
const [tweet, set_tweet] = useState("");
const [attachment, set_attachment] = useState("");
const onSubmit = async (event) => {
event.preventDefault();
if(tweet === "") {
return
}
/*
await dbService.collection("tweets").add({
text: tweet,
createdAt: Date.now(),
creatorId: userObj.uid,
})
set_tweet("");
*/
let attachmentUrl = "";
if(attachment !== "") {
// storageService.ref().child와 같이 스토리지 레퍼런스의 함수인 child를 사용하면 폴더, 파일 이름을 설정할 수 있다.
const attachmentRef = storageService.ref().child(`${userObj.uid}/${uuidv4()}`)
const response = await attachmentRef.putString(attachment, "data_url");
attachmentUrl = await response.ref.getDownloadURL();
console.log("response 1 : ", response);
console.log("response 2 : ", await response.ref.getDownloadURL());
}
await dbService.collection("tweets").add({
text: tweet,
createdAt: Date.now(),
creatorId: userObj.uid,
attachmentUrl
})
set_tweet("");
set_attachment("");
}
const onChange = (event) => {
event.preventDefault();
const { target: { value } } = event;
set_tweet(value);
}
const onFileChange = (event) => {
// console.log(event.target.files);
const { target: { files } } = event; // files = event.target.files
const theFile = files[0]
const reader = new FileReader();
reader.onloadend = (finishedEvent) => {
// console.log(finishedEvent);
const { currentTarget : { result } } = finishedEvent // result = finishedEvent.currentTarget.result
set_attachment(result);
};
// readAsDataURL 함수는 파일 정보를 인자로 받아서 파일 위치를 URL로 반환해 준다.
// 이ㅏ 함수는 리액트 생명주기 함수처럼 파일 선택 후, '웹 브라우저가 파일을 인식하는 시점', '웹 브라우저 파일 인식이 끝난 시점' 등을
// 포함하고 있어서 시점까지 함께 관리해줘야 URL을 얻을 수 있다.
// reader.readAsDataURL(theFile)
if(Boolean(theFile)) {
reader.readAsDataURL(theFile)
}
};
// 파일 취소
const onClearAttachment = () => set_attachment("");
return (
<form onSubmit={ onSubmit } className="factoryForm">
<div className="factoryInput__container">
<input className="factoryInput__input" type="text" value={ tweet } onChange={ onChange }
placeholder="당신의 생각은?" maxLength={ 120 } />
<input type="submit" value="→" className="factoryInput__arrow" />
</div>
<label htmlFor="attach-file" className="factoryInput__label">
<span>사진 추가</span>
<FontAwesomeIcon icon={ faPlus } />
</label>
<input id="attach-file" type="file" accept="image/*" onChange={ onFileChange } style={{ opacity: 0 }} />
<input type="submit" value="Tweet"/>
{ attachment && (
<div className="factoryForm__attachment">
<img src={ attachment } width="50px" height="50px" style={{ backgroundImage: attachment }}/>
<div className="factoryForm__clear" onClick={ onClearAttachment }>
<span>초기화</span>
<FontAwesomeIcon icon={ faTimes } />
</div>
</div>
)}
</form>
)
};
export default TweetFactory;
8. Auth.js
import { useState } from "react";
import { authService, firebaseInstance } from "../fbase";
import AuthForm from "../components/AuthForm";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTwitter, faGoogle, faGithub } from "@fortawesome/free-brands-svg-icons";
const Auth = () => {
// 소셜 로그인 기능 : 구글, 깃허브 등등 소셜 로그인 목표 확인
const onSocialClick = async (event) => {
// console.log(event.target.name);
let provider;
const { target: { name } } = event;
if(name === "google") {
provider = new firebaseInstance.auth.GoogleAuthProvider();
} else if(name === "github") {
provider = new firebaseInstance.auth.GithubAuthProvider();
}
const data = await authService.signInWithPopup(provider);
console.log("onSocialClick data : ", data)
};
return (
<div className="authContainer">
<FontAwesomeIcon icon={ faTwitter } color={ "#04AAFF" } size="3x" style={{ marginBottom: 30 }} />
<AuthForm />
<div className="authBtns">
<button onClick={ onSocialClick } name="google" className="authBtn">
구글 로그인 <FontAwesomeIcon icon={ faGoogle } />
</button>
<button onClick={ onSocialClick } name="github" className="authBtn">
깃허브 로그인 <FontAwesomeIcon icon={ faGithub } />
</button>
</div>
</div>
)
}
export default Auth;
9. Home.js
import {useEffect, useState} from "react";
import { dbService, storageService } from "../fbase";
import Tweet from "../components/Tweet";
import { v4 as uuidv4 } from "uuid";
import TweetFactory from "../components/TweetFactory";
const Home = ({ userObj }) => {
// console.log("Home userObj : ", userObj)
const [tweets, set_tweets] = useState([]);
// const getTweets = async () => {
// const dbTweets = await dbService.collection("tweets").get();
// // dbTweets.forEach((document) => console.log("document.data() : ", document.data()))
// // dbTweets.forEach((document) => set_tweets((prev) => [document.data(), ...prev]))
// dbTweets.forEach((document) => {
// const tweetObject = { ...document.data(), id: document.id };
// set_tweets((prev) => [tweetObject, ...prev])
// })
// }
useEffect(() => {
// onSnapshot 함수도 get 함수와 마찬가지로 스냅샷을 반환한다.
// 스냅샷에는 문서 스냅샷들이 포함되어 있는데, 문서 스냅샷들은 snapshot.docs와 같이 얻어낼 수 있다.
// 여기에 map 함수를 적용해서 문서 스냅샷에서 원하는 값만 뽑아서 다시 배열화 할 수 있다.
// forEach 함수는 배열 요소를 순회하면서 매 순회마다 set_tweets 함수를 사용해야 하지만,
// map 함수는 순회하며 만든 배열을 반환하므로 반환한 배열을 1번만 set_tweets 함수에 전달하면 되니 훨씬 효율적이다.
dbService
.collection("tweets")
.orderBy("createdAt", "desc")
.onSnapshot((snapshot => {
const newArray = snapshot.docs.map((document) => ({
id: document.id, ...document.data()
}));
set_tweets(newArray)
}))
}, [])
// console.log("tweets : ", tweets);
return (
<div className="container">
<TweetFactory userObj={ userObj } />
<div style={{ marginTop: 30 }}>
{ tweets.map((tweet) => (
// <div key={ tweet.id }>
// <h4>{ tweet.text }</h4>
// </div>
<Tweet key={ tweet.id } tweetObj={ tweet } isOwner={ tweet.creatorId === userObj.uid } />
)) }
</div>
</div>
)
}
export default Home;
10. Profile.js
import {authService, dbService} from "../fbase";
import { useHistory } from "react-router-dom";
import {useEffect, useState} from "react";
const Profile = ({ userObj, refreshUser }) => {
const history = useHistory();
const [newDisplayName, setNewDisplayName] = useState(userObj.displayName != null ? userObj.displayName : "");
const onLogOutClick = () => {
authService.signOut();
history.push("/");
}
const onChange = (event) => {
const { target: { value } } = event;
setNewDisplayName(value);
}
const onSubmit = async (event) => {
event.preventDefault();
if(userObj.displayName !== newDisplayName) {
await userObj.updateProfile({ displayName: newDisplayName });
refreshUser();
}
};
/*
const getMyTweets = async () => {
const tweets = await dbService
.collection("tweets")
.where("creatorId", "==", userObj.uid)
.orderBy("createdAt", "asc")
.get();
console.log(tweets.docs.map((doc) => doc.data()))
};
*/
// Profile 컴포넌트가 렌더링 된 이후, 실행 될 함수 ( useEffect )
useEffect(() => {
// getMyTweets();
}, [])
return (
<div className="container">
<form onSubmit={ onSubmit } className="profileForm">
<input type="text" placeholder="닉네임" onChange={ onChange } value={ newDisplayName } autoFocus
className="formInput"/>
<input type="submit" value="프로필 수정" className="formBtn" style={{ marginTop: 10 }}/>
</form>
<span onClick={ onLogOutClick } className="formBtn cancelBtn logOut">
로그아웃
</span>
</div>
)
}
export default Profile;
11. fabase.js
import firebase from "firebase/compat";
import "firebase/auth"
// 파이어베이스 데이터베이스
import "firebase/firestore"
// 파이어베이스 저장소
import "firebase/storage"
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_MESSAGING_SENDER_ID,
appId: process.env.REACT_APP_APP_ID
};
// export default firebase.initializeApp(firebaseConfig)
firebase.initializeApp(firebaseConfig)
export const authService = firebase.auth();
// 소셜 로그인 기능 : 위의 auth 함수에는 소셜 로그인에 필요한 provider가 없다.
export const firebaseInstance = firebase;
// 파이어베이스 데이터베이스
export const dbService = firebase.firestore();
// 파이어베이스 저장소
export const storageService = firebase.storage();
12. index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
// import firebase from "firebase/compat";
import firebase from "./fbase";
// console.log(firebase)
import "./styles.css"
ReactDOM.render(<React.StrictMode><App /></React.StrictMode>, document.getElementById('root'));
'Diary > 201~300' 카테고리의 다른 글
258일차 SWA - 데이터 수집 계획 (0) | 2021.09.13 |
---|---|
257일차 - SWA 개인 프로젝트 시작 전, 생각 정리 (0) | 2021.09.12 |
255일차 리액트(React) - 트위터 웹사이트 (3) (0) | 2021.09.10 |
254일차 리액트(React) - 트위터 웹사이트 (2) : 본격적인 시작 (0) | 2021.09.09 |
253일차 리액트(React) - 트위터 웹사이트 (1) : 환경 설정 (0) | 2021.09.08 |