이전 다크모드를 구현할 때 사용했던 전역상태 관리 라이브러리 Zustand에 대해 알아보자.
React의 상태관리 라이브러리는 다양하게 존재한다. 대표적으로 Context API, Redux, Recoil, MobX 등 많기도 하다.
나 역시 첫 번째로 Redux를 사용해 볼까 했었지만 문법이나 사용하는 방법이 익히는데 어렵고 난해하다는 말을 많이 들어 비교적 최소한의 코드로 상태를 관리하는 Recoil과 Zustand 중 고민을 하다 Zustand를 사용하기로 했다.
Zustand란?
Zustand란 독일어로 '상태'라는 뜻을 가진 라이브러리로 React의 전역상태 관리 라이브러리 중 하나로써 Redux와 같이 flux 패턴을 활용하는 기술 중 하나이다. Zustand는 아래와 같은 특징을 가진다.
- 핵심 로직의 코드 줄 수가 적다. (약 42줄) 즉, 진입장벽이 낮기에 동작을 이해하기 쉽고 알아야 할 코드의 양이 작다.
- 중앙 하나에 집중된 형식의 스토어 구조를 활용하면서 상태를 정의하고 사용 방법이 단순하다.
- Context API를 사용할 때와 달리 상태 변경 시 불필요한 리렌더링을 일으키지 않도록 제어가 쉽다.
특징에서도 알다시피 쉽고 간단하다. 이 부분이 Zustand의 제일 큰 장점이 아닐까 싶다. 머리 안 좋은 나조차도 금방 이해하고 여러 부분에서 적용하여 사용을 하니 말이다. 또한, 불필요한 리렌더링을 막아주고 간단하게 Store 파일을 만들어 여기서만 정해진 함수로 상태를 바꿀 수 있도록 관리를 하기에 전역적으로 상태관리를 가능하게 해 준다.
Zustand 사용법
# with npm
npm i zustand
# with yarn
yarn add zustand
우선 라이브러리를 설치한 후 Store 파일을 생성해 주자.
// useThemeToggleStore.ts
import {create} from "zustand";
interface themeToggleStore {
themeMode: boolean;
setThemeMode: (toggle: boolean) => void;
}
const useThemeToggleStore = create<themeToggleStore>((set) => ({
themeMode: false,
setThemeMode: (toggle: boolean) =>
set((state: {themeMode: boolean}) => ({
themeMode: (state.themeMode = toggle),
})),
}));
export default useThemeToggleStore;
다크모드 구현 할 때 작성했던 토글 버튼 Store이다.
create 함수로 Store를 생성해 주고 나는 Ts를 사용하기에 interface를 사용하여 타입을 정의해 준다. Ts가 아닐경우 이 부분은 건너뛰어도 된다. 타입을 정의해준 뒤 Store 내부에는 상태 변수와 해당 상태를 업데이트해 주는 함수를 간단하게 작성해 준다. 이제 생성한 Store를 호출하여 사용해 보자.
// ThemeModeToggle.tsx
import React from "react";
import useThemeToggleStore from "../stores/useThemeToggleStore";
const ThemeModeToggle = () => {
const {themeMode, setThemeMode} = useThemeToggleStore();
return (
<div>
<button onClick={() => setThemeMode(!themeMode)}>
다크모드
</button>
</div>
);
};
export default ThemeModeToggle;
// App.tsx
import React from 'react';
import {Route, Routes} from "react-router-dom";
import {ThemeProvider} from "styled-components";
import MainHome from './home/MainHome';
import SignIn from "./member/SignIn";
import SignUp from "./member/SignUp";
import {darkTheme, lightTheme} from "./styles/theme";
import useThemeToggleStore from "./stores/useThemeToggleStore";
function App() {
const {themeMode, setThemeMode} = useThemeToggleStore();
return (
<>
<ThemeProvider theme={themeMode ? darkTheme : lightTheme } >
<Routes>
<Route path="/" element={<MainHome />} />
<Route path="/signIn" element={<SignIn />} />
<Route path="/signUp" element={<SignUp />} />
</Routes>
</ThemeProvider>
</>
);
}
export default App;
import를 통해 생성한 Store를 가져오고 Store 내부에 작성했던 변수들을 호출하여 사용한다.
보시다시피 서로 다른 컴포넌트지만 Zustand를 통해 전역적으로 상태를 관리하므로 상태값 읽기, 변경이 아주 쉽다.
그리고 자세히 보면 자주 사용하던 useState와 유사한 구조로 생겼기에 활용하기에 어렵지도 않다!
상태값을 배열 구조로 정의해야 한다면 어떻게 작성해야 할까?
그다지 어렵지 않다. Spread와 filter를 사용하면 쉽게 배열에 상태값을 추가하고 삭제하는 등 관리를 할 수 있다. 나는 다중 검색 관련해서 기능을 구현할 때 배열로 된 상태를 전역으로 관리하는 Store를 작성해야 했기에 아래와 같이 구현하였다.
// useLectureSearchDataStore
import { create } from "zustand";
interface lectureSearchDataStore {
searchText:string;
setSearchText: (text:string) => void;
ltDivisionArr: {idx:number; dvItem:string}[];
setLtDivisionArr: (idx:number, dvItem:string) => void;
removeLtDivisionArr: (rmItem:string) => void;
removeAllLtDivisionArr: () => void;
ltDowArr: {idx:number; dwItem:number; dwName:string}[];
setLtDowArr: (idx:number, dwItem:number, dwName:string) => void;
removeLtDowArr: (rmItem:number) => void;
removeAllLtDowArr: () => void;
}
const useLectureSearchDataStore = create<lectureSearchDataStore>((set) => ({
searchText: "",
setSearchText: (text: string) =>
set((state: {searchText: string}) => ({
searchText: (state.searchText = text),
})),
ltDivisionArr: [],
setLtDivisionArr: (idx: number, dvItem: string) =>
set((state) => ({
ltDivisionArr: [...state.ltDivisionArr, {idx, dvItem}],
})),
removeLtDivisionArr: (rmItem: string) =>
set((state) => ({
ltDivisionArr: state.ltDivisionArr.filter((dvArr) => dvArr.dvItem !== rmItem),
})),
removeAllLtDivisionArr: () =>
set(() => ({
ltDivisionArr: [],
})),
ltDowArr: [],
setLtDowArr: (idx: number, dwItem: number, dwName: string) =>
set((state) => ({
ltDowArr: [...state.ltDowArr, {idx, dwItem, dwName}],
})),
removeLtDowArr: (rmItem: number) =>
set((state) => ({
ltDowArr: state.ltDowArr.filter((dwArr) => dwArr.dwItem !== rmItem),
})),
removeAllLtDowArr: () =>
set(() => ({
ltDowArr: [],
})),
}));
export default useLectureSearchDataStore;
export type { lectureSearchDataStore };
상태값을 배열에 추가해 주는 함수는 Spread를 사용하였고 제거를 해야 할 땐 선택한 index를 제외하고 다시 배열을 정의하는 filter를 사용하였다. 또한, 배열을 초기화를 해야할 땐 빈 배열로 다시 정의하는 방식으로 구현하였다.
위와 같이 상태관리를 한다면 전역적으로 많은 상태값을 쉽고 편하게 관리할 수 있을 것이다.
직접 사용해 보며 느낀 점
이전 개인 프로젝트를 진행했을 때는 상태관리 라이브러리를 사용하지 않고 진행을 했었다. 당시에는 굳이 사용하지 않아도 props로 전달하면 그만이었기에 필요성을 못 느꼈었지만 그건 그 프로젝트가 규모가 작고 코드가 단출했기에 가능했던 것 같다..
컴포넌트의 수가 많아지고 규모가 커짐에 따라 props drilling이 많아져 props 추적하기도 어려워질 것이고 무엇보다 궁극적으로 기업에 들어가 개발을 하려면 상태관리 라이브러리는 선택이 아닌 필수라는 것을 느꼈다.
또한, 혼자 프로젝트를 진행하면서 느꼈던 건데 각 컴포넌트 별로 상태값을 정의하고 사용한다면 뒤로 가기에 의해 그 상태값을 잃어버리는 경우가 있었다. 나는 데이터를 불러올 때 지점별로 불러오게 구현을 하였는데 다른 지점을 선택하고 다시 또 다른 컴포넌트로 이동한 후 뒤로 가기를 누르면 첫 번째 지점으로 자동 이동이 되는 현상이 있었다.
알고 보니 해당 리스트를 불러오는 컴포넌트에서 기본 값으로 첫 번째 지점으로 설정이 되어있었기에 이런 일이 일어났던 것이다. 그래서 나는 지점 상태값을 Zustand를 통해 다시 정의하여 사용하였고 이 문제를 해결했었다.
최종적으로 상태관리 라이브러리는 과도한 props drilling에 대한 대처를 위해 사용해야 하기도 하지만 어떤 상태를 하나의 컴포넌트에만 국한되지 않게 사용하기 위해서도 사용하는 것이 좋다 느꼈었다.
'[개인프로젝트] 개발 공부' 카테고리의 다른 글
[프로젝트] 6. Spring Security + JWT로 로그인 구현하기 (2) (3) | 2024.03.06 |
---|---|
[프로젝트] 5. Spring Security + JWT로 로그인 구현하기 (1) (0) | 2024.03.01 |
[프로젝트] 3. React 다크모드 구현하기 (0) | 2024.02.15 |
[프로젝트] 2. SPRINGBOOT 3.0 + Query DSL (0) | 2024.01.06 |
[프로젝트] 1. SPRINGBOOT + REACT 연동하기 (0) | 2023.12.13 |