반응형

React의 상태관리를 위한 방법들이 몇가지가 있는데요.

그중에서 zustand를 소개하고자 합니다.

 

상태관리

주요한 상태관리 방식은 다음과 같습니다.

 

방식사용 도구 / 패턴추천 상황

React Context + useReducer 내장 상태 관리 소규모 앱, 글로벌 상태 많지 않을 때
Zustand / Jotai / Recoil 경량 상태관리 중소 규모 앱, 간단하게 쓰고 싶을 때
Redux Toolkit 표준 Redux 중대형 앱, 상태 변화 복잡할 때
BLoC 패턴 (구현형) 커스텀 Hook + 클래스/Stream 구조 Flutter에서 익숙한 개발자, 로직-UI 분리를 철저히 하고 싶을 때
❌ 단순 useState 남발 없음 상태가 많아지면 혼돈 💥

 

상태관리 및 모듈화 (또는 구조화) 하는 것은 소프트웨어 유지보수 및 제사용성을 높이고, 품질 향상을 위해서 진행합니다.

 

DI (Dependancy Injection) 구조는  테스트를 위해서 필요한 요소이기도 합니다.

때문에 DI framework을 고려하기도 하죠.

 -  Factory + DI Framework (ex: Inversify, NestJS 등)

 

 

상태관리 방법 상황별 추천 

소규모 프로젝트 Context + useReducer, Zustand
중/대형 앱, 많은 비즈니스 로직 Redux Toolkit
Flutter BLoC 경험자 커스텀 BLoC 구조 or MobX-State-Tree
단순한 상태만 있다 useState, useContext로 충분

 

 

상태 관리시 고려할 사항이 한가지 더있습니다.

 

전역 상태에 두는 것이 적절한 조건 설명

조건  설명
🔁 여러 컴포넌트에서 공유됨 예: 리스트 → 상세 → 수정
🔒 로그인 유저 전용 데이터 로그인 후 고정된 데이터
📦 비교적 작거나, 페이지 단위 데이터 수십~수백 개 수준

❌ 다만, 데이터 양이 너무 크거나 자주 바뀌면, 메모리/리렌더 성능 문제가 생길 수 있음.

 

 

 

전역 상태로 인한 메모리/성능 이슈

이슈 유형 설명 대응 방법
데이터가 수천~수만 개 예: works가 10,000건 이상 페이징, 무한스크롤, 서버 캐시
모든 상태 변경이 전체 리렌더 유발 Redux/Context에서 너무 많은 컴포넌트가 구독 Zustand 같은 부분 구독 상태관리 추천
works 자체가 큰 객체 파일 포함, 이미지 base64 등 저장 대신 ID만 들고 있고, 세부 정보는 lazy fetch

 

 

✅ 성능 좋게 전역 상태를 유지하는 팁

1. 필요한 데이터만 저장하기

예: 전체 works 배열 대신 worksMap: { [id]: Work } + workIds: string[] 같이 분리 저장

2. 컴포넌트별로 부분 구독

Redux Toolkit + useSelector (메모이제이션)

Zustand는 기본적으로 부분 구독이라 성능 좋음

3. 서버 캐싱 도구 활용

React Query / SWR로 데이터를 “전역처럼” 쓰고 자동 캐싱/패칭 활용

4. LocalStorage / IndexedDB 연계

너무 큰 데이터는 브라우저 저장소에 offload 가능

 

 

React쪽에서 많이 사용하는 상태관리 방법중에 

거의 설정 필요 없이 바로 사용 가능. 작지만 유연한 구조에 딱! 인 녀석이 Zustand입니다.

 

 

Zustand

 

 

 

 

 

Zustand + 서버 캐싱 조합

// useWorkStore.ts
import { create } from 'zustand';

interface Work {
  id: string;
  title: string;
}

interface WorkStore {
  works: Record<string, Work>;
  setWorks: (works: Work[]) => void;
}

export const useWorkStore = create<WorkStore>((set) => ({
  works: {},
  setWorks: (list) =>
    set({
      works: Object.fromEntries(list.map((w) => [w.id, w])),
    }),
}));

 

이러면:

전역 상태 관리

필요한 데이터만 접근

컴포넌트 리렌더 최소화

 

 

✨ 결론

Works 데이터를 전역으로 관리해도 메모리 문제는 거의 없음,

데이터가 너무 크거나, 빈번하게 바뀌거나, 전체 리렌더를 유발하는 구조라면 성능 최적화 필요.

Zustand, Redux Toolkit, React Query 조합을 잘 쓰면 아주 효율적으로 관리 가능

 

 

 Zustand가 인기 많은 이유

 

장점 설명
심플한 API create() 하나로 상태 만들고 끝. boilerplate 거의 없음
Redux보다 가볍고 직관적 action, reducer, slice 다 필요 없음
부분 상태 구독 가능 리렌더 최적화가 자동으로 됨 (성능 굿)
TypeScript 친화적 타입 추론 잘 되고, 구조도 깔끔
서버 상태와도 궁합 좋음 React Query, SWR 등과 같이 쓰기 쉬움
중간 규모 프로젝트에 딱 복잡하지 않으면서 강력함

 

 

실제 사용하는 곳들

Vercel (Zustand 만든 팀)

T3 Stack (Next.js + TypeScript + Tailwind + Zustand + Prisma)

커뮤니티 기반 SaaS, 사이드 프로젝트 등에서 널리 사용됨

 

 

 

프로젝트 규모 Zustand 적합성
소규모/사이드 프로젝트 👍 아주 적합
중간 규모 SaaS 👍 훌륭함
대규모 기업 프로젝트 👍 가능하지만, 팀의 스타일에 따라 Redux 선호할 수도
글로벌 상태가 많지 않은 앱 ✅ 가볍게 도입 가능

 

비교

항목 Zustand Redux Toolkit Context + useReducer
코드 간결함 ✅ 매우 심플 ❌ 비교적 장황함 ⚠️ 구조화 필요
전역 상태 관리 ✅ 가능 ✅ 아주 강력 ⚠️ 퍼포먼스 이슈 가능
리렌더 최적화 ✅ 기본 내장 ⚠️ 직접 memoization 필요 ❌ 전체 리렌더 발생 가능
타입스크립트 ✅ 굿 ✅ 굿 ⚠️ 직접 타입 정의 많음
학습 난이도 ⭐ 쉬움 ❌ 중간 이상 ⭐ 쉬움

 

정리

Zustand는 2024~2025 기준으로 가장 많이 쓰이는 경량 상태 관리 라이브러리 중 하나임

복잡한 Redux 셋업 없이도 전역 상태를 깔끔하게 다룰 수 있고, 성능도 아주 좋음.

 

 

 

설치하기

npm install zustand

 

 

적용하기

Folder 구조

src/
  features/
    works/
      repositories/
        WorkRepository.ts
      stores/
        useWorkStore.ts
      model/
        work.ts

 

 

WorkRepository.ts (함수형)

// features/works/repositories/WorkRepository.ts
import { supabase } from '../../../common/supabase';
import { table } from '../../../common/config/configstring';
import { Work, WorkView } from '../model/work';

export const WorkRepository = {
  async getList(): Promise<WorkView[]> {
    const { data, error } = await supabase.from(table.WORK_VIEW).select('*');
    if (error) console.error('getList error:', error);
    return data ?? [];
  },

  async getListByAssigneeId(assignee: string): Promise<WorkView[]> {
    const { data, error } = await supabase
      .from(table.WORK_VIEW)
      .select()
      .eq('assignee', assignee);
    if (error) console.error('getListByAssigneeId error:', error);
    return data ?? [];
  },

  async getCountByAssigneeId(assignee: string): Promise<number> {
    const { count, error } = await supabase
      .from(table.WORK_VIEW)
      .select('*', { count: 'exact' })
      .eq('assignee', assignee)
      .limit(0);
    if (error) console.error('getCountByAssigneeId error:', error);
    return count ?? 0;
  },

  async getListByFilter(filter: string, offset = 0, cnt = 10): Promise<{ v: WorkView[]; total: number }> {
    const { data, count } = await supabase
      .from(table.WORK_VIEW)
      .select('*', { count: 'estimated' })
      .or(filter)
      .range(offset, offset + cnt);
    return { v: data ?? [], total: count ?? 0 };
  },

  async getById(id: number): Promise<WorkView | null> {
    const { data, error } = await supabase
      .from(table.WORK_VIEW)
      .select()
      .eq('id', id);
    if (error) console.error('getById error:', error);
    return data?.[0] ?? null;
  },

  async insert(work: Work): Promise<Work | null> {
    const { data, error } = await supabase
      .from(table.WORKS)
      .insert(work)
      .select();
    if (error) console.error('insert error:', error);
    return data?.[0] ?? null;
  },

  async update(work: Work): Promise<Work | null> {
    const { data, error } = await supabase
      .from(table.WORKS)
      .update(work)
      .eq('id', work.id)
      .select();
    if (error) console.error('update error:', error);
    return data?.[0] ?? null;
  },

  async delete(id: number): Promise<void> {
    const { error } = await supabase.from(table.WORKS).delete().eq('id', id);
    if (error) console.error('delete error:', error);
  },
};

 

 

useWorkStore.ts (Zustand)

// features/works/stores/useWorkStore.ts
import { create } from 'zustand';
import { Work, WorkView } from '../model/work';
import { WorkRepository } from '../repositories/WorkRepository';

interface WorkState {
  works: WorkView[];
  selectedWork: WorkView | null;
  totalCount: number;
  loading: boolean;

  fetchWorks: () => Promise<void>;
  fetchWorksByAssignee: (assignee: string) => Promise<void>;
  fetchWorksByFilter: (filter: string, offset?: number, cnt?: number) => Promise<void>;
  fetchWork: (id: number) => Promise<WorkView | null>;

  createWork: (work: Work) => Promise<Work | null>;
  updateWork: (work: Work) => Promise<Work | null>;
  deleteWork: (id: number) => Promise<void>;
}

export const useWorkStore = create<WorkState>((set) => ({
  works: [],
  selectedWork: null,
  totalCount: 0,
  loading: false,

  fetchWorks: async () => {
    set({ loading: true });
    const data = await WorkRepository.getList();
    set({ works: data, loading: false });
  },

  fetchWorksByAssignee: async (assignee: string) => {
    set({ loading: true });
    const data = await WorkRepository.getListByAssigneeId(assignee);
    set({ works: data, loading: false });
  },

  fetchWorksByFilter: async (filter, offset = 0, cnt = 10) => {
    set({ loading: true });
    const { v, total } = await WorkRepository.getListByFilter(filter, offset, cnt);
    set({ works: v, totalCount: total, loading: false });
  },

  fetchWork: async (id: number) => {
    const work = await WorkRepository.getById(id);
    set({ selectedWork: work });
    return work;
  },

  createWork: async (work: Work) => {
    const newWork = await WorkRepository.insert(work);
    return newWork;
  },

  updateWork: async (work: Work) => {
    const updated = await WorkRepository.update(work);
    return updated;
  },

  deleteWork: async (id: number) => {
    await WorkRepository.delete(id);
  },
}));

 

 

 

사용 예시

const { works, fetchWorks, loading } = useWorkStore();

useEffect(() => {
  fetchWorks();
}, []);

return loading ? <p>Loading...</p> : <ul>{works.map(w => <li key={w.id}>{w.title}</li>)}</ul>;

 

 

 

# 해피코딩 !!

반응형

 

Nivo 

https://nivo.rocks/components/

 

Components | nivo

 

nivo.rocks

 

 

 

 

Google Fonts

https://fonts.google.com/

 

Browse Fonts - Google Fonts

Making the web more beautiful, fast, and open through great typography

fonts.google.com

 

'Node.js , React, Docker' 카테고리의 다른 글

[React] OSM :MAP Tile Server  (0) 2025.03.24
[Docker] NginX docker 환경 만들기  (0) 2024.12.24
[AWS] 여러가지 알아야 할것들  (3) 2024.07.12
Docker 간단히 이해하기  (0) 2024.07.11
[Docker] proxy 문제 해결  (0) 2021.11.11

+ Recent posts