1. 라이브러리 설치
# 여러 추가 hook을 사용할 수 있다.
npm install usehooks-ts 
  1. 작성 코드
"use client";

import Link from "next/link";
import { Plus } from "lucide-react";
import { useLocalStorage } from "usehooks-ts";
import { useOrganization, useOrganizationList } from "@clerk/nextjs";

import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { Accordion } from "@/components/ui/accordion";

interface SidebarProps {
  storageKey?: string;
}

export const Sidebar = ({ storageKey = "t-sidebar-state" }: SidebarProps) => { // 초기값 설정
    // Localstorage에 사용되는 Key와 value의 type정의
    const [expanded, setExpanded] = useLocalStorage<Record<string, any>>( 
    storageKey,
    {}
  );
  // 사용할 value만 재정의 (회사정보, 로드 정보)
  const { organization: activeOrganization, isLoaded: isLoadedOrg } =
    useOrganization();
  // 맴버정보, 로드 정보
  const { userMemberships, isLoaded: isLoadedOrgList } = useOrganizationList({
    userMemberships: {
      infinite: true, //무한리스트 사용 설정
    },
  });
  // 로컬스토리지(메뉴목록)을 돌며, key가 true인 경우 해당 key를 defaultAccordionValue array에 추가
  // {"myorg-123": true} => ["myorg-123"]
  // ex) ['org_2cUZMP7J7xibJlMXPP1NPdurJro', 'org_2cUDkHhAtBXtfzlCmB8WhFT3M5l']
  const defaultAccordionValue: string[] = Object.keys(expanded).reduce(acc: string[], key: string) => {
    if (expanded[key]) {
      acc.push(key);
    }
    return acc;
  }, []);

  // 펼처진 행의 id를 받아서 펼치고 받은 아이디에 대해서 value를 반대로 지정
  // 현재 값 {"key" : boolean} 형태의 object를 그대로 복사해서, id를 key로가지는 boolean을 반대로 만들어주는 역할을 한다.
  // 참고 
  // const onExpand = (id: string) => {
  // console.log("start on Expand");
  // console.log("id : ", id); // org_xxxxx
  //  setExpanded((curr) => {
  //  console.log("curr : ", curr); //{ org_xxxxx: false }
  //  let a = [...curr];
  //  console.log("...curr : ", a); /// { org_xxxx: false }
  //  return {
  //    ...curr,
  //    [id]: !expanded[id],
  //  };
    });
    console.log(expande
const onExpand = (id: string) => {
    setExpanded((curr) => ({ // object를 return하는데
      ...curr, //current값을 그대로 복사해와서
      [id]: !expanded[id], // id에 할당된 value값만 뒤집어서
    })); // localstorage에 저장한다.
  };

  //로딩이 완료되지 않았을때 스켈레톤 노출
  if (!isLoadedOrg || !isLoadedOrgList || userMemberships.isLoading) {
    return (
      <>
        <Skeleton />
      </>
    );
  }

  return (
    <>
      <div className="font-medium text-xs flex items-center mb-1">
        <span className="pl-4">Workspace</span>
        <Button
          asChild
          type="button"
          size="icon"
          variant="ghost"
          className="ml-auto"
        >
          <Link href="/select-org">
            <Plus className="h-4 w-4" />
          </Link>
        </Button>
      </div>
      <Accordion
        type="multiple" // 여러개의 아코디언을 동시에 열 수 있음
        defaultValue={defaultAccordionValue} // 아코디언의 열린상태를 설정
        className="space-y-2"
      >
        {userMemberships.data.map(({ organization }) => (
          <NavItem
            key={organization.id} // 고유키 할당
            isActive={activeOrganization?.id === organization.id} // 현재 활성화된 조직이 아코디언의 조직이 맞는지
            isExpanded={expanded[organization.id]} // 해당 아코디언이 열려있는지
            organization={organization as Organization} // 해당 조직 정보 전달 (organization 객체를 Organization 타입으로 단언)
            onExpand={onExpand} // 열림상태변환 토글함수 전달
          />
        ))}
      </Accordion>
    </>
  );
};


비고. reduce의 기본 구조

array.reduce((accumulator, currentValue) => {

// 로직

}, initialValue);

// 누적값과, 현재 값을 인자로 받으며 초기값을 시작으로 각 값을 돌며 로직을 실행한 후 하나의 return 객체를 반환해주는 함수이다.

//nav-item component
//app/(dashboard)/_compoenets/nav-item.tsx

"use client"; //CSR 컴포넌트로 지정
import { useRouter, usePathname } from "next/navigation"; // AppRouter을 사용하려면 next/navigation을 사용해야한다.

import Image from "next/image";
import { Activity, CreditCard, Layout, Settings } from "lucide-react";

import { cn } from "@/lib/utils";
import {
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";

export type Organization = {
  id: string;
  slug: string;
  imageUrl: string;
  name: string;
};

// 인터페이스 설정 type과 비슷하지만 몇가지 다른점이있다.
// 1. interface는 확장이 가능하며, interface는 다른 interface나 type과 결합이 가능하다
// 3. 중복 선언시 선언이 결합된다. type을 중복해서 정의할경우 오류를 발생시킨다.

interface NavItemProps { 
  isExpanded: boolean;
  isActive: boolean;
  organization: any;
  onExpand: (id: string) => void; //반환값이 없는 보이드 함수
}

export const NavItem = ({
  isExpanded,
  isActive,
  organization,
  onExpand,
}: NavItemProps) => {
  const router = useRouter();
  const pathname = usePathname();

  const routes = [ // 아코디언 목록 (상세 메뉴 목록)
    {
      label: "Boards",
      icon: <Layout className="h-4 w-4 mr-2" />,
      href: `/organization/${organization.id}`,
    },
    {
      label: "Activity",
      icon: <Activity className="h-4 w-4 mr-2" />,
      href: `/organization/${organization.id}/activity`,
    },
    {
      label: "Settings",
      icon: <Settings className="h-4 w-4 mr-2" />,
      href: `/organization/${organization.id}/settings`,
    },
    {
      label: "Billing",
      icon: <CreditCard className="h-4 w-4 mr-2" />,
      href: `/organization/${organization.id}/billing`,
    },
  ];

  const onClick = (href: string) => {
    router.push(href);
  };

  return (
    <AccordionItem value={organization.id} className="border-none">
      <AccordionTrigger
        onClick={() => onExpand(organization.id)} // 열림 상태 업데이트
        className={cn(
          "flex items-center gap-x-2 p-1.5 text-neutral-700 rounded-md hover:bg-neutral-500/10 transition text-start no-underline hover:no-underline",
          isActive && !isExpanded && "bg-sky-500/10 text-sky-700" //현재 활성화된 그룹 + 접혀져 있을때
        )}
      >
        <div className="flex items-center gap-x-2">
          <div className="w-7 h-7 relative">
            {/* 외부 이미지를 사용하기 위해서는 next.config.js에 허용설정을 해야한다. *코드 3 참조*/}
            <Image
              fill
              src={organization.imageUrl}
              alt="Organization"
              className="rounded-sm object-cover"
            />
          </div>
          <span className="font-medium text-sm">{organization.name}</span>
        </div>
      </AccordionTrigger>
      <AccordionContent className="pt-1 text-neutral-700">
        {routes.map((route) => (
          <Button
            key={route.href}
            size="sm"
            onClick={() => onClick(route.href)}
            className={cn(
              "w-full font-normal justify-start pl-10 mb-1",
              pathname === route.href && "bg-sky-500/10 text-sky-700"
            )}
            variant="ghost"
          >
            {route.icon}
            {route.label}
          </Button>
        ))}
      </AccordionContent>
    </AccordionItem>
  );
};

// /next.config.mjs

/** @type {import('next').NextConfig} */
//const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "img.clerk.com",
      },
    ],
  },
//};

//export default nextConfig;