React.js/SNS Project
Day 6
Jinny96
2022. 8. 25. 18:55
1. Refactoring HomeHeader.jsx
Before
/Home/Header.jsx
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Grid, Typography } from "@mui/material";
import IconButton from '@mui/material/IconButton';
import AddCircleIcon from '@mui/icons-material/AddCircle';
import { getDocs, query, collection, where } from "firebase/firestore";
import {db} from "../../Env/Firebase";
const Header = () => {
const navigate = useNavigate();
const id = sessionStorage.getItem("user_id");
const [userImage, setUserImage] = useState("");
/** 로그인할 때 유저의 id를 세션스토리지에 저장했다.
* 이를 이용해 쿼리문을 만들어 users 테이블에서 id 필드값이 일치하는 것을 찾는다.
* 필드값이 일치하는 문서를 가져와 유저의 대표이미지를 불러온다.
*/
useEffect(() => {
const q = query(collection(db, "users"), where("id", "==", id));
getDocs(q).then((querySnapshot) => {
querySnapshot.forEach((doc) => {
setUserImage(doc.data().image);
});
});
}, []);
/** 왼쪽 사진 클릭시 내 피드로 넘어가기 */
const onClickMyFeed = () => {
navigate("/myFeed");
}
/** 오른쪽 플러스 아이콘 클릭시 피드 올리는 페이지로 이동하기 */
const onClickCreateFeed = () => {
navigate("/createFeed");
}
return (
<Grid container style={{ alignItems: "center", textAlign: "center", width: "80%", margin: "auto", marginTop: 70, top: 0, position: "sticky" }}>
<Grid item xs={4}>
{userImage &&
<IconButton onClick={onClickMyFeed} style={{padding: 0}}>
<img src={userImage} style={{ width: 40, height: 40, borderRadius: "50%" }}/>
</IconButton>
}
</Grid>
<Grid item xs={4}>
<Typography variant="h4">
<Link to="/home" style={{ textDecoration: 'none', color: 'black' }}>
Youngstagram
</Link>
</Typography>
</Grid>
<Grid item xs={4}>
<IconButton onClick={onClickCreateFeed} style={{padding: 0}}>
<AddCircleIcon fontSize="large" color="primary" />
</IconButton>
</Grid>
</Grid>
)
}
export default Header;
After
/Home/HomeHeader.jsx
import { Link, useNavigate } from "react-router-dom";
import { useQuery } from 'react-query';
import { Grid, Typography } from "@mui/material";
import Tooltip from '@mui/material/Tooltip';
import IconButton from '@mui/material/IconButton';
import AddPhotoAlternateIcon from '@mui/icons-material/AddPhotoAlternate';
import ExitToAppIcon from '@mui/icons-material/ExitToApp';
import Avatar from '@mui/material/Avatar';
import { signOut } from "firebase/auth";
import { getDocs, query, collection, where } from "firebase/firestore";
import { db, auth } from "../../Env/Firebase";
/**
* users 컬렉션 중 아이디 필드 값이 로그인한 유저 아이디와 같은 문서를 가져오고
* 문서 내에 image 필드 값 리턴
*/
const fetchUserImage = async () => {
let image = "";
const q = query(collection(db, "users"), where("id", "==", sessionStorage.getItem("user_id")));
await getDocs(q).then((querySnapshot) => {
querySnapshot.forEach((doc) => {
image = doc.data().image;
});
});
return image;
}
const Header = () => {
const navigate = useNavigate();
/** fetchUserImage 함수 실행 -> 세션스토리지에 유저 대표이미지 저장 */
const { data } = useQuery("user_image", fetchUserImage, {
suspense: true,
refetchOnWindowFocus: false,
retry: 0,
onSuccess: data => {
sessionStorage.setItem("user_image", data);
},
});
/** 왼쪽 사진 클릭시 내 피드로 넘어가기 */
const onClickMyFeed = () => {
navigate("/myFeed");
}
/** 오른쪽 플러스 아이콘 클릭시 피드 올리는 페이지로 이동하기 */
const onClickCreateFeed = () => {
navigate("/createFeed");
}
const onClickLogout = () => {
signOut(auth).then(() => {
sessionStorage.clear();
navigate("/");
}).catch(() => {
alert("다시 시도해주세요.");
});
}
return (
<Grid container style={{ alignItems: "center", textAlign: "center", width: "80%", margin: "auto", marginTop: 70 }}>
<Grid item xs={4}>
<Tooltip title="내 피드">
<IconButton onClick={onClickMyFeed} style={{ padding: 0 }}>
<Avatar src={data} />
</IconButton>
</Tooltip>
</Grid>
<Grid item xs={4}>
<Typography variant="h4">
<Link to="/home" style={{ textDecoration: 'none', color: 'black' }}>
Youngstagram
</Link>
</Typography>
</Grid>
<Grid item xs={4}>
<Tooltip title="피드 작성">
<IconButton onClick={onClickCreateFeed} style={{ padding: 0 }}>
<AddPhotoAlternateIcon fontSize="large" color="primary" />
</IconButton>
</Tooltip>
<Tooltip title="로그아웃">
<IconButton onClick={onClickLogout} style={{ padding: 0, marginLeft: 20 }}>
<ExitToAppIcon fontSize="large" color="grey" />
</IconButton>
</Tooltip>
</Grid>
</Grid>
)
}
export default Header;
- 파일 이름 수정 (Header.jsx -> HomeHeader.jsx)
- react-query 적용 : useEffect는 페이지 리 렌더링을 하기 때문에 클라이언트 사이드에서 번쩍거림이나 waterfall 현상을 경험할 수 있다. 이러한 현상을 방지하기 위해 react-query를 사용하여 비동기 과정 시 사용자에게 보여줄 로딩 창을 기존의 명령형이 아닌 선언적으로 관리하고 페이지를 한 번만 렌더링 한다.
- 로그아웃 버튼 생성
- 각 아이콘 버튼에 툴팁 추가
2. Create HomeMain.jsx
/Home/HomeMain.jsx
import { useQuery } from 'react-query';
import { Grid } from "@mui/material";
import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader';
import CardMedia from '@mui/material/CardMedia';
import CardContent from '@mui/material/CardContent';
import CardActions from '@mui/material/CardActions';
import Avatar from '@mui/material/Avatar';
import IconButton from '@mui/material/IconButton';
import FavoriteIcon from '@mui/icons-material/Favorite';
import { getDocs, collection } from "firebase/firestore";
import { db } from "../../Env/Firebase";
const fetchFeedData = async () => {
let dataArr = [];
await getDocs(collection(db, "feeds")).then((querySnapshot) => {
querySnapshot.forEach((doc) => {
dataArr.push(doc.data());
});
})
return dataArr;
}
const HomeMain = () => {
const { data } = useQuery("all_feeds", fetchFeedData, {
suspense: true,
refetchOnWindowFocus: false,
});
return (
<Grid container spacing={2} style={{ alignItems: "center", width: "50%", margin: "auto", marginTop: 50 }}>
{data.map((item, idx) => {
return (
<Grid key={idx} item xs={12}>
<Card sx={{ width: "100%", margin: "auto" }}>
<CardHeader
avatar={
<Avatar src={item.user_image} />
}
title={item.name}
subheader={item.time_stamp.toDate().toDateString()}
/>
<CardMedia
component="img"
height="500"
image={item.image}
alt={item.name}
style={{ width: "100%" }}
/>
<CardContent>
{item.content}
</CardContent>
<CardActions disableSpacing>
<IconButton aria-label="add to favorites">
<FavoriteIcon />
</IconButton>
</CardActions>
</Card>
</Grid>
)
})}
</Grid>
)
}
export default HomeMain;
- 원래 Home.jsx에 있던 내용인데 컴포넌트를 따로 만들었다.
- 기존의 useEffect와 useState를 빼고 react-query로 관리했다.
3. Refactoring Home.jsx
Before
/Home/Home.jsx
import { useEffect, useState } from "react";
import { Grid } from "@mui/material";
import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader';
import CardMedia from '@mui/material/CardMedia';
import CardContent from '@mui/material/CardContent';
import CardActions from '@mui/material/CardActions';
import Avatar from '@mui/material/Avatar';
import IconButton from '@mui/material/IconButton';
import FavoriteIcon from '@mui/icons-material/Favorite';
import { getDocs, collection } from "firebase/firestore";
import { db } from "../../Env/Firebase";
import Header from "./Header";
const Home = () => {
const [data, setData] = useState([]);
useEffect(() => {
const uid = sessionStorage.getItem("user_id");
if (!uid) {
alert("로그인 후 이용해주세요.");
window.location.replace("/");
}
}, []);
useEffect(() => {
let dataArr = [];
getDocs(collection(db, "feeds")).then((querySnapshot) => {
querySnapshot.forEach((doc) => {
dataArr.push(doc.data());
});
}).then(() => {
setData(dataArr);
});
}, []);
return (
<>
<Header />
<Grid container spacing={2} style={{ alignItems: "center", width: "80%", margin: "auto", marginTop: 50 }}>
{data && data.map((item, idx) => {
return (
<Grid key={idx} item xs={12}>
<Card sx={{ width: "80%", margin:"auto" }}>
<CardHeader
avatar={
<Avatar src={item.user_image} />
}
title={item.user_name}
subheader={item.time_stamp.toDate().toDateString()}
/>
<CardMedia
component="img"
height="300"
image={item.image}
alt={item.name}
style={{ width: "100%" }}
/>
<CardContent>
{item.content}
</CardContent>
<CardActions disableSpacing>
<IconButton aria-label="add to favorites">
<FavoriteIcon />
</IconButton>
</CardActions>
</Card>
</Grid>
)
})}
</Grid>
</>
)
}
export default Home;
After
/Home/Home.jsx
import { Suspense, lazy } from "react";
import Loader from "../../Env/Loader";
const HomeHeader = lazy(() => import('./HomeHeader'));
const HomeMain = lazy(() => import('./HomeMain'));
const Home = () => {
return (
<>
<Suspense fallback={<Loader />}>
<HomeHeader />
<HomeMain />
</Suspense>
</>
)
}
export default Home;
- React의 장점을 살려 컴포넌트를 나누어 관리할 수 있게 됐다.
- React v18부터 제공되는 suspense와 lazy를 사용하여 HomeHeader와 HomeMain 컴포넌트를 import 할 때 lazy함수를 사용하여 느리게 불러오고 불러오는 동안 기존에 만들어 두었던 로딩 화면을 유저에게 보여준다. 각 컴포넌트의 비동기 처리가 끝나면 유저에게 페이지가 보일 것이다.
4. 결과
기존의 코드로는 페이지가 렌더링 될 때 waterfall현상이 일어났지만 react-query의 도입으로 모든 컴포넌트의 비동기 처리가 끝난 뒤 사용자에게 한 번에 보여줄 수 있게 되었다. 또한 다른 페이지로 이동 후 다시 돌아와도 react-query로 인해 서버에서 다시 데이터를 가져오는 것이 아니라서 렌더링 속도 측면에서 굉장히 빨라졌다.
5. 폴더 구조