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. 결과

react

기존의 코드로는 페이지가 렌더링 될 때 waterfall현상이 일어났지만 react-query의 도입으로 모든 컴포넌트의 비동기 처리가 끝난 뒤 사용자에게 한 번에 보여줄 수 있게 되었다. 또한 다른 페이지로 이동 후 다시 돌아와도 react-query로 인해 서버에서 다시 데이터를 가져오는 것이 아니라서 렌더링 속도 측면에서 굉장히 빨라졌다.


5. 폴더 구조

react