본문 바로가기
  • 삽질하는 자의 블로그
메인-프로젝트/Next.js - 오늘 뭐먹지? 프로젝트

9. 개인 유저 별 찜 기능을 구현해보자

by 이게뭐당가 2022. 12. 3.

찜 기능은 페이지의 핵심 기능으로, 주변에 있거나, 먹고 싶은 음식들을 찜 해서,

칼로리와, 영양성분을 파악하고,  고민되는 날, 랜덤선택기로 음식 하나를 랜덤으로 선택 할 수 있습니다.

 

음식 아이디를 동적 페이지로 생성했습니다.

 

계획 상으로, 음식의 디테일 페이지 내에 찜 기능을 구현하고,

어떤 음식을 보여줄 것인지, 어떤 음식을 찜 할 것인지, 데이터를 보내야 하기 때문에,

동적 페이지를 만들어, 각 유저별로,  음식의 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 를 넣고 공유하는, "상태 관리 라이브러리" 를 사용하여,

서버사이드 렌더링을 진행할때, 저장된 유저의 정보를 빼와서, 사전 데이터페칭을 진행하여야 할 것 같다.

 

댓글