Side컴포넌트에서 tab 메뉴를 만들었다. 나는 그 탭을 클릭하여 선택할 것이고, 선택된 tab의 id를 이용해서 컴포넌트를 조건부 렌더링 하려고 한다. 이때 조건부 렌더링이 Side 컴포넌트의 내부에서 일어난다면 useState 훅을 이용해서 간단하게 구현할 수 있다. 하지만 Side 컴포넌트 내부에 있는 tab을 클릭해, Main 컴포넌트에서 조건부 렌더링을 해야하는 상황(서로 다른 컴포넌트 간 state 공유)이라면 상태를 전역으로 관리해주어야 한다. zustand라는 라이브러리를 이용하면 이를 매우 간단하게 해결해줄 수 있다.
초기 코드를 보자
// Side.tsx
import styles from "./Side.module.scss";
import HomeIcon from "@mui/icons-material/Home";
import ConstructionIcon from "@mui/icons-material/Construction";
import YouTubeIcon from "@mui/icons-material/YouTube";
import AttachMoneyIcon from "@mui/icons-material/AttachMoney";
import LogoutIcon from "@mui/icons-material/Logout";
import { useState } from "react";
// props 타입
interface ISideProps {
signOutHandler: () => void;
}
// tab 메뉴 타입
interface ITabData {
id: number;
content: string;
icon: JSX.Element;
}
// 로그 아웃을 위한 props로 내려받은 signOutHandler
function Side({ signOutHandler }: ISideProps): JSX.Element {
// map 함수로 뿌려줄 tab의 데이터
const tabData: ITabData[] = [
{
id: 1,
content: "메인 화면",
icon: <HomeIcon className={styles.imageIcon} />,
},
{
id: 2,
content: "상품 리스트",
icon: <ConstructionIcon className={styles.imageIcon} />,
},
{
id: 3,
content: "유튜브 리스트",
icon: <YouTubeIcon className={styles.imageIcon} />,
},
{
id: 4,
content: "견적 리스트",
icon: <AttachMoneyIcon className={styles.imageIcon} />,
},
];
// useState훅을 이용한 상태 관리 (초기상태는 inedx 0번(1번 tab) 활성화)
const [activeTab, setActiveTab] = useState(tabData[0].id);
// tab 클릭 handler (tab의 id를 이용해 선택된 tab 활성화)
const tabClickHandler = (tabId: number): void => {
setActiveTab(tabId);
// active 확인용 console
console.log(`${tabId}번 탭이 active`);
};
return (
<>
<div className={styles.sideContainer}>
<ul>
{tabData.map((tab) => {
// 선택된 tab 동적 class 변수
const tabClass = `${styles.iconContainer} ${
activeTab === tab.id ? styles.active : ""
}`;
return (
<li key={tab.id}>
<div
className={tabClass}
onClick={() => {
// 클릭 시 실제 tab.id를 인자로 전달
tabClickHandler(tab.id);
}}
>
<div className={styles.homeContainer}>
{tab.icon}
<p>{tab.content}</p>
</div>
</div>
</li>
);
})}
</ul>
<div
className={styles.iconContainer}
onClick={() => {
// 로그 아웃 handler
signOutHandler();
}}
>
<div className={styles.logOutIconContainer}>
<LogoutIcon className={styles.imageIcon} />
<p>로그 아웃</p>
</div>
</div>
</div>
</>
);
}
export default Side;
선택된 탭의 디자인을 제어해주는 코드를 작성했다. 현재는 useState훅을 이용해서 Side 컴포넌트 내부에서만 상태를 관리해주고 있다. 이 경우에 다른 컴포넌트에서 상태를 공유할 수 없다는 문제가 발생한다. 이를 해결해 주기 위해 터미널 창을 열어 npm i zustand 명령어로 zustand 라이브러리를 설치해 주자.
npm i zustand
이제 src 디렉토리 내부에 stores 디렉토리를 생성해주고, 그 내부에 파일을 하나 만들어줘서 전역으로 상태관리를 해줄 코드를 작성하자. 파일의 이름은 useActiveTabStore로 해주었다.
// useActiveTabStore.ts
import { create } from "zustand";
// store 타입 (안해주면 에러날 수도 있음!! 타입 정의 꼭 해주기!!)
interface IUseActiveStore {
activeTab: number;
setActiveTab: (tabId: number) => void;
}
const useActiveTabStore = create<IUseActiveStore>((set) => ({
// 초기 상태 (1번 탭 active)
activeTab: 1,
// 1번 tab이 active된 초기상태에서, 다른 n번째 tab 클릭 시 해당 탭 active
setActiveTab: (tabId: number) =>
set((state: { activeTab: number }) => ({
activeTab: (state.activeTab = tabId),
})),
}));
export default useActiveTabStore;
export type { IUseActiveStore };
이제 activeTab과 setActiveTab은 어느 컴포넌트에서든 import해서 상태를 가져다 쓸 수 있다.
// useState훅을 이용한 상태 관리
const [activeTab, setActiveTab] = useState(tabData[0].id);
이 부분을
// zustand store를 이용해 상태 관리
const { activeTab, setActiveTab } = useActiveTabStore();
이렇게 바꿔주기만 하면 Side 컴포넌트 내부에서는 더 이상 수정할 부분이 없다. (import 하는 것 잊지 말자)
이제 Main 컴포넌트에서 조건부 렌더링 할 코드를 작성해주자.
import Layout from "../../components/layout/layout/Layout";
import useActiveTabStore from "../../stores/useActiveTabStore";
function Main(): JSX.Element {
const { activeTab } = useActiveTabStore();
return (
<Layout>
{activeTab === 1 && <Component1 />}
{activeTab === 2 && <Component2 />}
{activeTab === 3 && <Component3 />}
{activeTab === 4 && <Component4 />}
</Layout>
);
}
export default Main;
컴포넌트 간 state 공유 끝!