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>;
# 해피코딩 !!
'Node.js , React, Docker' 카테고리의 다른 글
[React] OSM : 지형 정보 가져오기(export) (3) | 2025.03.24 |
---|---|
[React] OSM :MAP Tile Server (0) | 2025.03.24 |
[Docker] NginX docker 환경 만들기 (0) | 2024.12.24 |
[React] Components (0) | 2024.09.07 |
[AWS] 여러가지 알아야 할것들 (3) | 2024.07.12 |