찜 기능은 페이지의 핵심 기능으로, 주변에 있거나, 먹고 싶은 음식들을 찜 해서,
칼로리와, 영양성분을 파악하고, 고민되는 날, 랜덤선택기로 음식 하나를 랜덤으로 선택 할 수 있습니다.
음식 아이디를 동적 페이지로 생성했습니다.
계획 상으로, 음식의 디테일 페이지 내에 찜 기능을 구현하고,
어떤 음식을 보여줄 것인지, 어떤 음식을 찜 할 것인지, 데이터를 보내야 하기 때문에,
동적 페이지를 만들어, 각 유저별로, 음식의 ID 값을 DB 에 저장합니다.
<프로세스>
1. 우선 빠른 UX 를 위해, getStaticProps 로 사전 데이터 페칭을 진행하고 데이터를 페이지 내에 가져옵니다.
2. 만약 로그인을 했다면, 로그인 정보에서, Favorites Array 데이터가 있는지 파악합니다.
3. 로그인을 했고, Favorites Array 가 있는데, 그 안에 현 foodId 와 같은 값이 있다면,
"추가" 가 아니라 "삭제" 라는 버튼을 표기합니다.
4. 추가 를 누르면, DB 에, Array에 추가 후 UPDATE, 삭제를 누르면 Array에 삭제 후 UPDATE 합니다.
* Favorites Array 에는 메모리를 아끼기 위해, fooid의 ID 만 Array 형태로 만들어,
개인의 ID 하나당 하나의 Array 만 가지게 했습니다.
* getSession 으로 DevServer 에서는 서버사이드에서, 유저의 정보 사전 데이터 페칭을 진행하였지만,
build 된 이후에는 사용 불가능하여, 페이지 내에서 fetch 를 진행하였습니다.
그로인해, 로딩이 발생합니다.
* 로딩은 MODAL 을 만들어, 잠시 대기하게 만드는 방법으로 처리했습니다.
* 현재에는, getServerSideProps 안에, unstable_getServerSession(context.req, context.res, authOptions)
를 통해, 클라이언트가 아닌, 서버측에서 session 을 받아와 사용하는 방법을 생각했지만,
이런 무시무시한 문구가 적혀있고, 신버젼v4 를 사용한 것을 약간 후회...
v3 에서는 getSession 쓰면 되었을텐데....
1. 디테일 페이지
import { connectDb, findAllFoods } from "../helper/db-util";
import FoodDetailForm from "../components/food-detail-components/food-detail-form";
import { useSession } from "next-auth/react";
import Head from "next/head";
import { useState, useEffect } from "react";
import LoadingModal from "../components/ui/modal/modal-for-loading";
function FoodDetailPage(props) {
const { selectedFood, foodid } = props;
const { data: session, status } = useSession();
const [isSameArray, setIsSameArray] = useState(false);
const [loading, setLoading] = useState(false);
const {id,name,image,price,taste,category,alt,calorie,nutri,content,} = selectedFood;
// useSession 으로, 만약 authenticated 되어있다면,
// useEffect를 통해, fetch 요청으로 email 과 foodId 를 보내, isSameArray 의 boolean 값을 받아와, Component 에 보낸다.
async function fetchHandler() {
setLoading(true);
const response = await fetch("/api/food-detail", {
method: "POST",
body: JSON.stringify({ userEmail: session.user.email, foodID: foodid }),
headers: {
"Content-Type": "application/json",
},
});
const responseData = await response.json();
setLoading(false);
setIsSameArray(responseData.sameArray);
}
useEffect(() => {
if (status === "authenticated") {
fetchHandler();
}
}, [status]);
return (
<div>
{loading && <LoadingModal />}
<Head>
<title> Food Detail </title>
<meta
name="description"
content="this page let you know about foods detail like calorie or nutrition"
/>
</Head>
<FoodDetailForm
id={id}
name={name}
image={image}
price={price}
taste={taste}
category={category}
alt={alt}
calorie={calorie}
nutri={nutri}
content={content}
isSameArray={isSameArray}
/>
</div>
);
}
export async function getStaticProps(context) {
let { foodid } = context.params;
// 선택된 음식 사전데이터페칭
const client = await connectDb();
const result = await findAllFoods(client);
const issue = JSON.parse(JSON.stringify(result));
const selectedFood = issue.find((data) => data.id === foodid);
return {
props: {
selectedFood,
foodid,
},
};
}
export async function getStaticPaths(context) {
return {
paths: [{ params: { foodid: "1" } }],
fallback: "blocking",
};
}
export default FoodDetailPage;
중요한 것은. isSameArray 를 만들어서 넘겨준다는 것입니다.
page 내에서 fetch를 하지 않고, 컴포넌트 안에서 fetch 를 하게 되면,
여러 군데에서 사용할 컴포넌트를 사용하는데 좋지 않다고 판단하였습니다.
2. 디테일 폼 컴포넌트 만들기
import Image from "next/image";
import styles from "./food-detail-form.module.css";
import Button from "../ui/card/button";
import { useSession } from "next-auth/react";
import { useState, useEffect } from "react";
import { useRouter } from "next/router";
function FoodDetailForm(props) {
const { data: session, status } = useSession();
const router = useRouter();
const {id,name,image,price,taste,category,alt,calorie,nutri,content,isSameArray,} = props;
const [addingButton, setAddingButton] = useState(isSameArray);
useEffect(() => {
setAddingButton(isSameArray);
}, [isSameArray]);
// API 로 보내줄 데이터는, email 과 foodid
// API 에서는, 그것들을 db에서 뽑아 판단해, 적절한 조치를 취한다.
async function saveToCart() {
if (status === "unauthenticated") {
alert("로그인 하셔야 이용가능합니다.");
router.replace("/login");
return;
}
const foodId = id;
const userEmail = session.user.email;
const inputFavoriteData = { foodId, userEmail };
const response = await fetch("/api/userdetail/addfavorite", {
method: "POST",
body: JSON.stringify(inputFavoriteData),
headers: {
"Content-Type": "application/json",
},
});
const responseData = await response.json();
setAddingButton((prev) => !prev);
}
return (
<main className={styles.maindiv}>
<div className={styles.imagediv}>
<h1> {name}</h1>
<Image
src={image}
width={500}
height={500}
alt={alt}
quality={30}
priority
/>
</div>
<div className={styles.foodinfo}>
<div>
<h3> 평균 가격</h3>
<p>
<span>{price}</span> 원
</p>
</div>
<div>
<h3> 맛 태그</h3>
<p>
<span>{taste}</span>
</p>
</div>
<div>
<h3> 카테고리</h3>
<p>
<span>{category}</span>
</p>
</div>
</div>
<div className={styles.foodnutri}>
<h3> 영양성분</h3>
<div className={styles.foodnutriSubdiv}>
<div className={styles.foodnutriSub}>
<p className={styles.infoname}>칼로리</p>{" "}
<span>{calorie} kcal</span>
<p>/ 100g </p>
</div>
<div className={styles.foodnutriSub}>
<p className={styles.infoname}>탄 / 단 / 지</p> <span>{nutri}</span>
<p>/ 100g </p>
</div>
</div>
</div>
<div className={styles.foodcontent}>
<div>
<h3> 설명</h3>
<p> {content}</p>
</div>
</div>
{isSameArray != null && (
<div className={styles.buttondiv}>
<Button onClick={saveToCart}>{addingButton ? "삭제" : "추가"}</Button>
</div>
)}
</main>
);
}
export default FoodDetailForm;
삼항 연산자를 사용하여, addingButton 의 버튼 이름을 변경해주고,
로그인 한 사람과 하지 않은 사람을 비교하여, 다른 작동방식을 보이게 하였습니다.
3. API 로직 만들기
1. useEmail 이 존재하는지 확인
2. "없다면" insertOne 으로 Array에 하나의 id를 넣어 INSERT한다.
3. "있다면" Array의 데이터를 확인한다.
4. 데이터를 확인하여, 현 foodId 가 Array 안에 존재하는지 확인한다.
5. "없다면" , Array 를 꺼내, foodId 를 삽입하고, UPDATE 한다.
6. "있다면" , Array 를 꺼내, 같은 foodId 를 삭제하고, UPDATE 한다.
import { connectDb } from "../../../helper/userdetail-db-util";
async function handler(req, res) {
if (req.method === "POST") {
const client = await connectDb();
const { userEmail, foodId } = req.body;
// userEmail 이 이미 존재하는 데이터인가?
const findResult = await client
.db("eating")
.collection("userFavorite")
.findOne({ userEmail: userEmail });
// "만약 없다면" Array 형태로, foodId 를 집어넣어준다.
if (!findResult) {
const foodArray = [foodId];
const insertResult = await client
.db("eating")
.collection("userFavorite")
.insertOne({ userEmail, foodArray });
res.status(200).json({ message: "success", insertResult });
client.close();
return;
}
// 이미 같은 useEmail 을 사용한 data가 존재하는가?
// 그렇다면, 그 data 안에 있는 foodArray를 꺼낸 후,
// 현재 넣으려고 하는 foodId가 있는지 확인한다..
const oldFoodArray = findResult.foodArray;
const sameInArray = oldFoodArray.find((id) => id === foodId);
// "userEmail 이 이미 존재", "foodArray는 존재하지 않는다면"
// foodArray에 새로운 foodId를 추가하여, newFoodArray를 만들어서 다시 넣어준다.
if (!sameInArray) {
let newFoodArray = oldFoodArray.push(String(foodId));
const updateResult = await client
.db("eating")
.collection("userFavorite")
.updateOne(
{ userEmail: userEmail },
{ $set: { foodArray: oldFoodArray } }
);
res.status(200).json({ message: "수정완료", updateResult });
client.close();
return;
}
// "userEmail 이 이미 존재," "foodArray 도 이미 존재한다면"
// foodArray에서, 이미 존재하는 foodId를 제거하고, newFoodArray를 만들어서 다시 넣어준다.
const newFoodArray = oldFoodArray.filter((id) => id !== String(foodId));
const updateResult = await client
.db("eating")
.collection("userFavorite")
.updateOne(
{ userEmail: userEmail },
{ $set: { foodArray: newFoodArray } }
);
res.status(200).json({ message: "삭제완료", updateResult });
client.close();
}
}
export default handler;
이렇게 작동하게 된다!
[개선할 점]
fetch 를 사용하여 진행하지 말고, "사전 데이터 페칭을 통해, user의 정보를 받아와야한다."
이건 Nextjs 를 v3 로 다운그레이드 하거나,
v4에 안정적으로 유저 데이터를 서버사이드에서 가져올 신기능을 추가해주거나
정확한 작동방식은 모르겠지만, 중앙에 Store 를 넣고 공유하는, "상태 관리 라이브러리" 를 사용하여,
서버사이드 렌더링을 진행할때, 저장된 유저의 정보를 빼와서, 사전 데이터페칭을 진행하여야 할 것 같다.
끝
댓글