상세 컨텐츠

본문 제목

Next.js기반 교회 웹사이트 제작 [15] - 전역상태Zudstand 적용

카테고리 없음

by helpilsang 2025. 3. 20. 19:48

본문

하... 뭔.. Recoil ... 내 잘못이다. 더 찾아봤어야되는데 

대안품 Zustand 이름도 주스같고 다시보니 맘에 드네   let's go

출처 : 코딩애플

자 원래 목적이 뭐였어?
위의 짤처럼 단방향으로 던져지는 props와 state의 변화에 의한 리렌더링이 너무 많은곳에서 일어나니까 그걸 방지하기 위해 전역상태관리를 통해서 내가 쓰고싶은 곳만 쓰자는게 우리의 목표였지?

그전에 zustand를 한번 보고가자

문법은 굉장히 쉬운편
뭐 다 같겠지만 Store를 하나 만들어야댐

자 zustand의 공식홈페이지에 나온 사용법이야 간단하지?

그냥 간단하게 설명할게 
create로 store를 만들고 변수와 setter를 작성하여 사용하면돼

그리고 바인딩할때는 그자리에서 사용하는거면 그냥 바로 사용하고
export 된것은 import해서 사용하면됨

 

1. stores/MenuStore.js

"use client";

import { create } from "zustand";
import { persist } from "zustand/middleware";

/**  전역 상태 관리
 * Navbar에서 선택된 데이터를 Sidebar와 공유하기 위한 store
 * zustand에서 지원하는 persist 미들웨어를 사용하여 localStorage에 저장
 * middleware를 사용한 이유 : 새로고침을 하더라도 navbar에서 선택된 메뉴가 유지되도록 하기 위함
 */
const useMenuStore = create(
  persist(
    (set) => ({
      menuData: [
        {
          우리교회: {
            교회소개: "/about",
            담임목사: "/pastor",
            "섬기는 사람들": "/leadership",
            예배안내: "/worshipInfo",
          },
        },
        {
          예배: {
            주일예배: "/sunSermons",
            수요예배: "/wedSermons",
            새벽예배: "/dawnSermons",
          },
        },
        {
          교회게시판: {
            자유게시판: "/freeBoard",
            선교자소식: "/missionaryNews",
            주일만나: "/slunch",
            기도제목: "/prayerTopic",
            주보: "/weeklyNews",
          },
        },
        {
          교회소식: {
            공지사항: "/notices",
            교회일정: "/schedule",
          },
        },
        {
          목회달력: {
            "": "/calendar",
          },
        },
        {
          오시는길: {
            "": "/location",
          },
        },
      ],
      selectedParentMenu: null, //선택된 상위 메뉴
      selectedSubMenu: null, //선택된 하위 메뉴
      setSelectedMenu: (parentMenu, subMenu) =>
        set({
          selectedParentMenu: parentMenu,
          selectedSubMenu: subMenu,
        }),
    }),
    { name: "menu-storage", getStorage: () => localStorage },
  ),
);

export default useMenuStore;

나는 3개의 변수와 1개의 액션을 통해서 store를 구성했고 
zustand에서 지원하는 middleware > localstorage에 저장하여 새로고침을 하더라도 선택된 메뉴가 유지되게 했어 

이제 이거로 Nav에서 선택한것을 sidebar에서도 고대로 선택되게 동기화 해보자잉?

2. components/Navbar.js

"use client";
import useMenuStore from "@/stores/MenuStore";
import { useState } from "react";

function NavItem({ text, href = "#", children }) {
  const [isHovering, setIsHovering] = useState(false);
  const { setSelectedMenu, selectedParentMenu } = useMenuStore();

  return (
    <div
      className="relative"
      onMouseEnter={() => setIsHovering(true)}
      onMouseLeave={() => setIsHovering(false)}
    >
      <a
        href={href}
        className="text-gray-600 px-3 py-2 font-bold hover:text-blue-500 transition-colors"
        onClick={() => {
          setSelectedMenu(text, null);
        }}
      >
        {text}
      </a>

      {children && (
        <div
          className={`absolute left-0 mt-2 w-48 bg-white shadow-lg rounded-md py-2 z-50 transition-all duration-200 ${
            isHovering ? "opacity-100 visible" : "opacity-0 invisible"
          }`}
        >
          {children}
        </div>
      )}
    </div>
  );
}

function SubNavItem({ text, href = "#", parentMenu }) {
  const { setSelectedMenu, selectedParentMenu, selectedSubMenu } =
    useMenuStore();

  return (
    <a
      href={href}
      className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-blue-500"
      onClick={() => {
        setSelectedMenu(parentMenu, text);
      }}
    >
      {text}
    </a>
  );
}

export default function Navbar() {
  const { menuData } = useMenuStore(); // 구조 분해 할당으로 menuData만 가져옴

  return (
    <nav className="flex-1 flex justify-center">
      <div className="hidden md:flex md:space-x-8">
        {menuData.map((menuItem) => {
          const [[parentMenu, subMenu]] = Object.entries(menuItem); // Object.entries로 key, value를 배열로 반환

          return (
            <NavItem key={parentMenu} text={parentMenu}>
              {/**key값은 parentMenu로 설정*/}
              {/** value값은 subMenu로 설정후 다시 key,value로 나누어 값 할당*/}
              {Object.entries(subMenu).map(([subKey, subValue]) => (
                <SubNavItem
                  key={subKey}
                  text={subKey}
                  href={subValue}
                  parentMenu={parentMenu}
                />
              ))}
            </NavItem>
          );
        })}
      </div>
    </nav>
  );
}

이렇게 가져와서 사용했고

3. components/Sidebar.js

"use client";
import useMenuStore from "@/stores/MenuStore";
import Link from "next/link";
import { usePathname } from "next/navigation";

export default function Sidebar() {
  const pathname = usePathname();
  const hideSidebarPaths = ["/", "/login"];
  const { menuData, setSelectedMenu, selectedParentMenu, selectedSubMenu } =
    useMenuStore();

  if (hideSidebarPaths.includes(pathname)) {
    return null;
  }

  const getSubMenuItems = () => {
    if (!selectedParentMenu) return [];

    const parentMenuItem = menuData.find((item) => {
      /** menu들의 key값을 반환
       * Object.keys(item) => ["우리교회", "예배", "교회게시판", "교회소식", "목회달력", "오시는길"]
       * 이렇게 값을 반환하는데 이것을 []로 감싸는 변수로 받으면
       * 값 자체를 변수에 담을 수 있다.
       */
      const [key] = Object.keys(item);
      return key === selectedParentMenu;
    });

    if (!parentMenuItem) return [];

    const subMenuItems = parentMenuItem[selectedParentMenu];
    /**
      [
        ["교회소개", "/about"],
        ["담임목사", "/pastor"],
        ["섬기는 사람들", "/leadership"],
        ["예배안내", "/worshipInfo"]
      ]
      값이 뭐 이런식으로 반환될거임
    */
    return Object.entries(subMenuItems);
  };

  const subMenuItems = getSubMenuItems();

  return (
    <div className="w-72 mt-18 mr-20 p-4 min-h-screen">
      <h2 className="text-xl font-bold text-blue-700 mb-6 text-center">
        {selectedParentMenu || "메뉴"}
      </h2>

      <div className="flex flex-col space-y-2">
        {subMenuItems?.map(([subKey, subValue]) => (
          <Link
            key={subKey}
            href={subValue}
            className="p-3 border border-gray-200 hover:bg-gray-50 transition-colors"
            onClick={() => setSelectedMenu(selectedParentMenu, subKey)}
          >
            <div
              className={`${
                selectedSubMenu === subKey
                  ? "border-l-4 border-blue-700 pl-2 font-medium text-blue-800"
                  : ""
              }`}
            >
              {subKey}
            </div>
          </Link>
        ))}
      </div>
    </div>
  );
}

주석한번 읽어보고 

나 지금 대장내시경약 먹어가지고 화장실이 급해서 더는 못쓰겠다 영상으로 첨부할게

지금 404때문에 화면 깜빡이는건 신경쓰지마봐

화면 녹화 중 2025-03-20 193611.mp4
2.14MB

나머지 공부는 그냥 취업하고 해야겠다... 할게많어~