개발

프론트 필수 라이브러리 모음 : 7화 Zustand & Jotai & Redux - 상태 관리 비교 🧩✨

D-Project 2025. 4. 23. 17:22

7화. Zustand & Jotai & Redux — 상태 관리 삼국지⚔️

안녕하세요, 프론티어들! 🌟 오늘은 프론트엔드 필수 라이브러리 시리즈의 일곱 번째 이야기로, React 애플리케이션의 핵심 요소인 상태 관리(State Management) 라이브러리들을 비교해볼게요! 2025년 현재, 3대 강자로 자리 잡은 Zustand, Jotai, Redux의 특징과 장단점을 알아보고, 어떤 상황에 어떤 라이브러리가 적합한지 살펴봅시다! 🚀

상태 관리, 왜 중요할까요? 🤔

React 애플리케이션이 커질수록 컴포넌트 간에 데이터를 공유하고 관리하는 것이 복잡해져요. 이런 문제를 해결하기 위해 상태 관리 라이브러리가 등장했죠!

상태 관리 라이브러리의 필요성:

  • Props Drilling 방지: 깊은 컴포넌트 트리에서 데이터 전달 간소화
  • 전역 상태 관리: 앱 전체에서 공통으로 사용하는 데이터 관리
  • 예측 가능한 상태 변화: 명확한 패턴으로 상태 업데이트
  • 디버깅 용이성: 상태 변화 추적 및 문제 해결 지원
  • 성능 최적화: 불필요한 리렌더링 방지

오늘 살펴볼 세 가지 라이브러리는 각자 독특한 방식으로 이러한 문제를 해결하고 있어요. 함께 알아볼까요?

1. Zustand: 간결함의 미학 🐻

Zustand(독일어로 "상태"라는 뜻)는 2025년 현재 가장 인기 있는 상태 관리 라이브러리 중 하나로, 미니멀리즘과 직관적인 API로 많은 개발자들의 사랑을 받고 있어요.

Zustand의 주요 특징:

  1. 극도로 단순한 API 📝
    • 직관적인 hook 기반 API로 빠르게 습득 가능
    • 보일러플레이트 코드 최소화
  2. 가벼운 번들 크기 🪶
    • 약 0.6KB(gzip 압축 시)의 초경량 사이즈
    • 성능에 미치는 영향 최소화
  3. Redux DevTools 지원 🔍
    • 강력한 디버깅 도구 활용 가능
    • 상태 변화 추적 및 시간 여행 디버깅
  4. 미들웨어 시스템 🧩
    • persist, immer 등 유용한 미들웨어 지원
    • 필요에 따라 확장 가능한 구조
// Zustand 기본 사용 예시
import { create } from 'zustand';

// 스토어 생성
const useStore = create((set) => ({
  // 초기 상태
  bears: 0,
  fishes: 0,

  // 액션
  increaseBears: () => set((state) => ({ bears: state.bears + 1 })),
  increaseFishes: () => set((state) => ({ fishes: state.fishes + 1 })),
  reset: () => set({ bears: 0, fishes: 0 }),
}));

// 컴포넌트에서 사용
function BearCounter() {
  // 필요한 상태와 액션만 선택적으로 구독
  const bears = useStore((state) => state.bears);
  const increaseBears = useStore((state) => state.increaseBears);

  return (
    <div>
      <h1>{bears} 마리의 곰이 있습니다</h1>
      <button onClick={increaseBears}>곰 추가</button>
    </div>
  );
}

Zustand를 더 강력하게 사용하는 방법:

  1. TypeScript와 함께 사용하기
// TypeScript와 함께 사용하는 Zustand
interface StoreState {
  bears: number;
  fishes: number;
  increaseBears: () => void;
  increaseFishes: () => void;
  reset: () => void;
}

const useStore = create<StoreState>((set) => ({
  bears: 0,
  fishes: 0,
  increaseBears: () => set((state) => ({ bears: state.bears + 1 })),
  increaseFishes: () => set((state) => ({ fishes: state.fishes + 1 })),
  reset: () => set({ bears: 0, fishes: 0 }),
}));
2. **Immer 미들웨어로 불변성 관리 간소화**
```jsx
// Immer 미들웨어 사용 예시
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';

const useStore = create(
  immer((set) => ({
    todos: [{ id: 1, text: '할 일 1', done: false }],
    addTodo: (text) => set((state) => {
      // immer 덕분에 push와 같은 변형 메서드 직접 사용 가능
      state.todos.push({ 
        id: Date.now(), 
        text, 
        done: false 
      });
    }),
    toggleTodo: (id) => set((state) => {
      const todo = state.todos.find(todo => todo.id === id);
      if (todo) todo.done = !todo.done;
    }),
  }))
);
  1. Persist 미들웨어로 상태 유지하기
// Persist 미들웨어 사용 예시
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

const useStore = create(
  persist(
    (set) => ({
      user: null,
      setUser: (user) => set({ user }),
      logout: () => set({ user: null }),
    }),
    { 
      name: 'user-storage', // 로컬 스토리지 키 이름
      getStorage: () => localStorage, // 사용할 스토리지 (localStorage, sessionStorage 등)
    }
  )
);

2025년 Zustand의 최신 기능:

  • 자동 타입 추론 강화: 더 정교한 TypeScript 지원
  • 비동기 작업 유틸리티: 로딩 상태 관리 헬퍼 함수 내장
  • 컨텍스트 기반 분리: 앱의 다른 부분에서 독립적인 스토어 인스턴스 사용 가능
  • 렌더링 최적화: 구독 시스템 개선으로 불필요한 리렌더링 더욱 감소

2. Jotai: 원자적 접근의 혁신 ⚛️

Jotai는 일본어로 "상태"를 뜻하며, 원자(atom) 기반의 접근 방식으로 상태를 작은 단위로 쪼개 관리하는 혁신적인 라이브러리예요. Recoil에서 영감을 받았지만 더 간결하고 유연한 API를 제공합니다.

Jotai의 주요 특징:

  1. 원자적 상태 관리 ⚛️
    • 작고 독립적인 atom 단위로 상태 관리
    • React의 useState와 유사한 직관적인 API
  2. 파생된 상태 (Derived State) 🔄
    • 다른 atom을 기반으로 새로운 atom 생성 가능
    • 복잡한 상태 로직을 선언적으로 표현
  3. 컴포넌트 트리와 독립적 🌳
    • Context Provider 없이도 작동 가능
    • 코드 분할(Code Splitting)에 친화적
  4. 매우 가벼운 사이즈 📦
    • 약 3.5KB(gzip 압축 시)의 작은 번들 크기
    • 필요한 기능만 선택적으로 임포트 가능
// Jotai 기본 사용 예시
import { atom, useAtom } from 'jotai';

// 기본 atom 생성
const countAtom = atom(0);

// 파생된 atom 생성
const doubleCountAtom = atom(
  (get) => get(countAtom) * 2 // 읽기 함수
);

// 읽기/쓰기 atom 생성
const countryAtom = atom(
  (get) => get(countAtom), // 읽기 함수
  (get, set, newCountry) => { // 쓰기 함수
    set(countAtom, get(countAtom) + 1);
    console.log(`새로운 국가: ${newCountry}`);
  }
);

function Counter() {
  // useState와 비슷한 API
  const [count, setCount] = useAtom(countAtom);
  const [doubleCount] = useAtom(doubleCountAtom);

  return (
    <div>
      <h1>카운트: {count}</h1>
      <h2>2배 카운트: {doubleCount}</h2>
      <button onClick={() => setCount(c => c + 1)}>증가</button>
    </div>
  );
}

Jotai를 더 강력하게 사용하는 방법:

  1. 비동기 atom으로 API 데이터 관리
// 비동기 atom 사용 예시
import { atom, useAtom } from 'jotai';

// 비동기 데이터를 위한 atom
const userAtom = atom(async () => {
  const response = await fetch('https://api.example.com/user');
  return response.json();
});

// 로딩 상태를 자동으로 처리하는 컴포넌트
function User() {
  const [user] = useAtom(userAtom);
  
  return (
    <div>
      {user.loading ? (
        <p>로딩 중...</p>
      ) : user.error ? (
        <p>에러: {user.error.message}</p>
      ) : (
        <div>
          <h1>{user.name}</h1>
          <p>{user.email}</p>
        </div>
      )}
    </div>
  );
}
  1. 유틸리티 함수로 복잡한 상태 관리
// 유틸리티 함수 사용 예시
import { atom, useAtom } from 'jotai';
import { atomWithStorage, atomWithReset, atomWithReducer } from 'jotai/utils';

// 로컬 스토리지와 연동
const themeAtom = atomWithStorage('theme', 'light');

// 리셋 가능한 atom
const formAtom = atomWithReset({ name: '', email: '' });

// 리듀서 패턴 사용
const todosAtom = atomWithReducer([], (prev, action) => {
  switch (action.type) {
    case 'add':
      return [...prev, { id: Date.now(), text: action.text, done: false }];
    case 'toggle':
      return prev.map(todo => 
        todo.id === action.id ? { ...todo, done: !todo.done } : todo
      );
    case 'delete':
      return prev.filter(todo => todo.id !== action.id);
    default:
      return prev;
  }
});
  1. atom 그룹으로 관련 상태 함께 관리
// atom 그룹 사용 예시
import { atom, useAtom } from 'jotai';
import { atomFamily } from 'jotai/utils';

// atom 패밀리: ID로 atom 생성
const userAtomFamily = atomFamily(
  (userId) => atom(async () => {
    const res = await fetch(`https://api.example.com/users/${userId}`);
    return res.json();
  }),
);

function UserProfile({ userId }) {
  const [user] = useAtom(userAtomFamily(userId));

  if (!user) return <p>로딩 중...</p>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

2025년 Jotai의 최신 기능:

  • 더 강력한 선택적 구독: 상태의 특정 부분만 구독 가능
  • React 서버 컴포넌트 지원: 서버 컴포넌트와의 완벽한 호환성
  • 변환 추상화(Transform Abstraction): 원자적 상태 변환을 위한 새로운 패턴
  • 향상된 DevTools: 상태 디버깅을 위한 개선된 개발자 도구

3. Redux: 베테랑의 안정성 🏆

Redux는 2015년부터 React 생태계의 표준 상태 관리 솔루션으로 자리매김했으며, 특히 Redux Toolkit(RTK)의 등장으로 현대적인 개발 경험을 제공하고 있어요.

Redux의 주요 특징:

  1. 예측 가능한 상태 관리 🔮
    • 단방향 데이터 흐름과 불변성 원칙
    • 액션, 리듀서, 스토어의 명확한 구조
  2. 강력한 미들웨어 시스템 🔄
    • Thunk, Saga 등 비동기 로직 처리를 위한 미들웨어
    • 로깅, 크래시 리포팅 등 다양한 확장 가능성
  3. 뛰어난 개발자 도구 🛠️
    • 시간 여행 디버깅(Time-travel debugging)
    • 상태 변화 추적 및 문제 해결 용이성
  4. 대규모 커뮤니티와 생태계 👨‍👩‍👧‍👦
    • 방대한 학습 자료와 예제
    • 다양한 확장 라이브러리 지원
// Redux Toolkit 기본 사용 예시
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useSelector, useDispatch } from 'react-redux';

// 슬라이스 생성 (리듀서 + 액션 생성자)
const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => {
      // Redux Toolkit은 내부적으로 Immer를 사용하여 불변성 보장
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
  },
});

// 액션 생성자 추출
export const { increment, decrement, incrementByAmount } = counterSlice.actions;

// 스토어 생성
const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
  },
});

// 앱 컴포넌트
function App() {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  );
}

// 카운터 컴포넌트
function Counter() {
  // useSelector로 상태 선택
  const count = useSelector((state) => state.counter.value);
  // useDispatch로 액션 디스패치
  const dispatch = useDispatch();

  return (
    <div>
      <h1>카운트: {count}</h1>
      <button onClick={() => dispatch(increment())}>증가</button>
      <button onClick={() => dispatch(decrement())}>감소</button>
      <button onClick={() => dispatch(incrementByAmount(5))}>5 증가</button>
    </div>
  );
}

더 강력한 Redux Toolkit 활용법:

  1. RTK Query로 데이터 페칭 관리
// RTK Query 사용 예시
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { configureStore } from '@reduxjs/toolkit';
import { Provider, useSelector } from 'react-redux';

// API 슬라이스 생성
const apiSlice = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }),
  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => '/users',
    }),
    getUserById: builder.query({
      query: (id) => `/users/${id}`,
    }),
    addUser: builder.mutation({
      query: (newUser) => ({
        url: '/users',
        method: 'POST',
        body: newUser,
      }),
    }),
  }),
});

// 생성된 훅 추출
export const { 
  useGetUsersQuery, 
  useGetUserByIdQuery,
  useAddUserMutation 
} = apiSlice;

// 스토어 설정
const store = configureStore({
  reducer: {
    [apiSlice.reducerPath]: apiSlice.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(apiSlice.middleware),
});

// 사용 예시
function UsersList() {
  // 데이터 페칭, 캐싱, 로딩 상태, 에러 처리가 모두 자동으로 관리됨
  const { data: users, isLoading, error } = useGetUsersQuery();
  const [addUser] = useAddUserMutation();

  if (isLoading) return <p>로딩 중...</p>;
  if (error) return <p>에러: {error.message}</p>;

  return (
    <div>
      <h1>사용자 목록</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
      <button onClick={() => addUser({ name: '새 사용자' })}>
        사용자 추가
      </button>
    </div>
  );
}
  1. 비동기 로직 관리를 위한 createAsyncThunk
// createAsyncThunk 사용 예시
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

// 비동기 액션 생성
export const fetchTodos = createAsyncThunk(
  'todos/fetchTodos',
  async (_, { rejectWithValue }) => {
    try {
      const response = await fetch('https://api.example.com/todos');
      return await response.json();
    } catch (err) {
      return rejectWithValue(err.message);
    }
  }
);

// 슬라이스 내에서 비동기 상태 처리
const todosSlice = createSlice({
  name: 'todos',
  initialState: {
    items: [],
    loading: false,
    error: null,
  },
  reducers: {
    // 일반 리듀서...
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchTodos.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchTodos.fulfilled, (state, action) => {
        state.loading = false;
        state.items = action.payload;
      })
      .addCase(fetchTodos.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload;
      });
  },
});
  1. Listener 미들웨어로 반응형 로직 구현
// Listener 미들웨어 사용 예시
import { createListenerMiddleware, isAnyOf } from '@reduxjs/toolkit';
import { increment, decrement } from './counterSlice';
import { fetchAnalytics } from './analyticsSlice';

// 리스너 미들웨어 생성
const listenerMiddleware = createListenerMiddleware();

// 카운터 변경 시 분석 데이터 가져오기
listenerMiddleware.startListening({
  matcher: isAnyOf(increment, decrement),
  effect: async (action, listenerApi) => {
    // 특정 액션에 반응해 다른 액션 디스패치
    listenerApi.dispatch(fetchAnalytics());

    // 취소 처리 가능
    listenerApi.cancelActiveListeners();

    // 스토어 상태 접근
    const state = listenerApi.getState();
    console.log('현재 카운트:', state.counter.value);
  },
});

2025년 Redux Toolkit의 최신 기능:

  • 스토어 모듈화 개선: 대규모 앱을 위한 런타임 스토어 분할
  • 메모리 최적화: 대용량 상태 처리를 위한 성능 최적화
  • React 서버 컴포넌트 지원: 서버 렌더링 환경에서의 최적화
  • RTK Query 향상: 더 강력한 데이터 모델링 및 유효성 검사

삼국지: 어떤 상황에 어떤 라이브러리가 좋을까? 🤔

각 라이브러리는 고유한 장단점이 있어 프로젝트 특성에 따라 선택이 달라질 수 있어요. 상황별 추천을 살펴볼게요!

Zustand를 선택해야 할 때:

  • 중소 규모 프로젝트: 간결한 코드와 빠른 개발 속도가 중요할 때
  • 빠른 학습 곡선이 필요할 때: 팀이 빠르게 적응해야 하는 상황
  • 최소한의 보일러플레이트: 간결한 코드베이스를 유지하고 싶을 때
  • 독립적인 여러 스토어: 기능별로 분리된 여러 스토어가 필요할 때

Jotai를 선택해야 할 때:

  • 세밀한 상태 관리: 작은 단위의 독립적인 상태가 많을 때
  • 선언적 파생 상태: 다른 상태에서 파생된 상태가 많을 때
  • 동적 상태 생성: 런타임에 상태를 동적으로 생성해야 할 때
  • 상태 공유 최소화: 컴포넌트 간 최소한의 상태만 공유하고 싶을 때

Redux를 선택해야 할 때:

  • 대규모 엔터프라이즈 애플리케이션: 복잡한 상태 로직과 워크플로우
  • 엄격한 아키텍처 필요: 명확한 패턴과 구조가 필요한 대규모 팀
  • 복잡한 데이터 페칭/캐싱: RTK Query의 강력한 기능 활용 필요
  • 풍부한 미들웨어 생태계: 로깅, 분석 등 다양한 미들웨어 필요

실전 적용 사례 💼

각 라이브러리의 실제 사용 사례를 통해 더 깊이 이해해 볼게요!

1. 쇼핑몰 장바구니 관리 (Zustand)

// Zustand로 장바구니 관리하기
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

const useCartStore = create(
  persist(
    (set, get) => ({
      items: [],
      totalItems: 0,
      totalPrice: 0,

      // 상품 추가
      addItem: (product, quantity = 1) => {
        const items = get().items;
        const existingItem = items.find(item => item.id === product.id);

        if (existingItem) {
          // 이미 있는 상품이면 수량만 증가
          set(state => ({
            items: state.items.map(item => 
              item.id === product.id 
                ? { ...item, quantity: item.quantity + quantity }
                : item
            ),
            totalItems: state.totalItems + quantity,
            totalPrice: state.totalPrice + (product.price * quantity)
          }));
        } else {
          // 새 상품 추가
          set(state => ({
            items: [...state.items, { ...product, quantity }],
            totalItems: state.totalItems + quantity,
            totalPrice: state.totalPrice + (product.price * quantity)
          }));
        }
      },

      // 상품 제거
      removeItem: (productId) => {
        const item = get().items.find(item => item.id === productId);
        if (!item) return;

        set(state => ({
          items: state.items.filter(item => item.id !== productId),
          totalItems: state.totalItems - item.quantity,
          totalPrice: state.totalPrice - (item.price * item.quantity)
        }));
      },

      // 장바구니 비우기
      clearCart: () => set({ items: [], totalItems: 0, totalPrice: 0 }),
    }),
    { 
      name: 'shopping-cart', // 로컬 스토리지 키
    }
  )
);

2. 사용자 인증 관리 (Jotai)

// Jotai로 사용자 인증 관리하기
import { atom, useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';

// 토큰 저장 (로컬 스토리지)
const tokenAtom = atomWithStorage('auth-token', null);

// 현재 사용자 정보 (API에서 가져옴)
const currentUserAtom = atom(async (get) => {
  const token = get(tokenAtom);
  if (!token) return null;

  try {
    const response = await fetch('https://api.example.com/me', {
      headers: { 'Authorization': `Bearer ${token}` }
    });

    if (!response.ok) {
      throw new Error('인증 실패');
    }

    return response.json();
  } catch (error) {
    console.error('사용자 정보 가져오기 실패:', error);
    return null;
  }
});

// 로그인 상태 atom
const isLoggedInAtom = atom((get) => !!get(tokenAtom));

// 로그인 함수
const loginAtom = atom(
  null, // 읽기 함수 (사용 안 함)
  async (get, set, credentials) => {
    try {
      const response = await fetch('https://api.example.com/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials)
      });

      if (!response.ok) {
        throw new Error('로그인 실패');
      }

      const data = await response.json();
      set(tokenAtom, data.token);
      return true;
    } catch (error) {
      console.error('로그인 실패:', error);
      return false;
    }
  }
);

// 로그아웃 함수
const logoutAtom = atom(
  null, // 읽기 함수 (사용 안 함)
  (get, set) => {
    set(tokenAtom, null);
  }
);

// 컴포넌트에서 사용
function AuthComponent() {
  const [isLoggedIn] = useAtom(isLoggedInAtom);
  const [user] = useAtom(currentUserAtom);
  const [, login] = useAtom(loginAtom);
  const [, logout] = useAtom(logoutAtom);

  // 로그인 폼 제출 처리
  const handleLogin = async (e) => {
    e.preventDefault();
    const email = e.target.email.value;
    const password = e.target.password.value;

    const success = await login({ email, password });
    if (success) {
      console.log('로그인 성공!');
    }
  };

  return (
    <div>
      {isLoggedIn ? (
        <div>
          <h1>안녕하세요, {user?.name}님!</h1>
          <button onClick={logout}>로그아웃</button>
        </div>
      ) : (
        <form onSubmit={handleLogin}>
          <input name="email" type="email" placeholder="이메일" />
          <input name="password" type="password" placeholder="비밀번호" />
          <button type="submit">로그인</button>
        </form>
      )}
    </div>
  );
}

3. 블로그 포스트와 댓글 관리 (Redux)

// Redux Toolkit으로 블로그 관리
import { createSlice, createAsyncThunk, configureStore } from '@reduxjs/toolkit';

// 포스트 가져오기
export const fetchPosts = createAsyncThunk(
  'posts/fetchPosts',
  async (_, { rejectWithValue }) => {
    try {
      const response = await fetch('https://api.example.com/posts');
      return await response.json();
    } catch (err) {
      return rejectWithValue(err.message);
    }
  }
);

// 댓글 가져오기
export const fetchComments = createAsyncThunk(
  'posts/fetchComments',
  async (postId, { rejectWithValue }) => {
    try {
      const response = await fetch(`https://api.example.com/posts/${postId}/comments`);
      const comments = await response.json();
      return { postId, comments };
    } catch (err) {
      return rejectWithValue(err.message);
    }
  }
);

// 포스트 슬라이스
const postsSlice = createSlice({
  name: 'posts',
  initialState: {
    items: [],
    comments: {},
    loading: false,
    error: null,
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      // 포스트 로딩 상태
      .addCase(fetchPosts.pending, (state) => {
        state.loading = true;
      })
      .addCase(fetchPosts.fulfilled, (state, action) => {
        state.loading = false;
        state.items = action.payload;
      })
      .addCase(fetchPosts.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload;
      })
      // 댓글 처리
      .addCase(fetchComments.fulfilled, (state, action) => {
        const { postId, comments } = action.payload;
        state.comments[postId] = comments;
      });
  },
});

// 스토어 설정
const store = configureStore({
  reducer: {
    posts: postsSlice.reducer,
  },
});

// 컴포넌트에서 사용
function BlogPostsList() {
  const dispatch = useDispatch();
  const { items, loading, error } = useSelector((state) => state.posts);

  useEffect(() => {
    dispatch(fetchPosts());
  }, [dispatch]);

  if (loading) return <p>포스트 로딩 중...</p>;
  if (error) return <p>에러: {error}</p>;

  return (
    <div>
      <h1>블로그 포스트</h1>
      <ul>
        {items.map(post => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.body}</p>
            <button onClick={() => dispatch(fetchComments(post.id))}>
              댓글 보기
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

패턴 및 베스트 프랙티스 🌟

어떤 라이브러리를 선택하든 효과적인 상태 관리를 위한 몇 가지 권장 사항이 있어요!

1. 상태 정규화 (State Normalization)

데이터베이스처럼 엔티티를 ID로 참조하고 정규화된 형태로 저장하세요:

// ❌ 중첩된 상태 (피해야 함)
const badState = {
  posts: [
    {
      id: 1,
      title: "첫 번째 포스트",
      comments: [
        { id: 1, text: "첫 번째 댓글" },
        { id: 2, text: "두 번째 댓글" }
      ]
    }
  ]
};

// ✅ 정규화된 상태 (권장)
const goodState = {
  posts: {
    byId: {
      1: { id: 1, title: "첫 번째 포스트", commentIds: [1, 2] }
    },
    allIds: [1]
  },
  comments: {
    byId: {
      1: { id: 1, text: "첫 번째 댓글", postId: 1 },
      2: { id: 2, text: "두 번째 댓글", postId: 1 }
    },
    allIds: [1, 2]
  }
};

2. 선택적 구독 (Selective Subscription)

컴포넌트는 필요한 상태만 구독하세요:

// ❌ 전체 상태 구독 (피해야 함)
function BadComponent() {
  // 모든 상태 변경에 리렌더링됨
  const state = useStore(state => state);
  return <div>{state.user.name}</div>;
}

// ✅ 선택적 구독 (권장)
function GoodComponent() {
  // user.name이 변경될 때만 리렌더링됨
  const userName = useStore(state => state.user.name);
  return <div>{userName}</div>;
}

3. 상태와 UI 분리 (Separation of Concerns)

비즈니스 로직과 UI를 명확히 분리하세요:

// ❌ UI 컴포넌트에 비즈니스 로직 (피해야 함)
function BadCounter() {
  const [count, setCount] = useState(0);

  // 비즈니스 로직이 UI 컴포넌트에 있음
  const increment = () => {
    if (count < 10) {
      setCount(count + 1);
      if (count + 1 === 10) {
        alert('최대치 도달!');
      }
    }
  };

  return (
    <button onClick={increment}>
      증가 ({count})
    </button>
  );
}

// ✅ 상태 관리와 UI 분리 (권장)
// 상태 로직
const useCounter = create((set, get) => ({
  count: 0,
  increment: () => {
    const currentCount = get().count;
    if (currentCount < 10) {
      set({ count: currentCount + 1 });
      if (currentCount + 1 === 10) {
        alert('최대치 도달!');
      }
    }
  }
}));

// UI 컴포넌트
function GoodCounter() {
  const { count, increment } = useCounter();
  return (
    <button onClick={increment}>
      증가 ({count})
    </button>
  );
}

마무리 🎁

상태 관리는 프론트엔드 개발의 핵심이며, 올바른 라이브러리 선택은 프로젝트의 성공에 큰 영향을 미칠 수 있어요! 2025년 현재, Zustand, Jotai, Redux(RTK)는 각자의 방식으로 효과적인 상태 관리 솔루션을 제공하고 있답니다.

  • Zustand: 간결함과 직관성이 필요할 때
  • Jotai: 원자적, 세밀한 상태 관리가 필요할 때
  • Redux: 엄격한 패턴과 풍부한 생태계가 필요할 때

여러분의 프로젝트 특성과 팀의 선호도를 고려해 가장 적합한 라이브러리를 선택하세요! 상태 관리 라이브러리는 도구일 뿐, 중요한 것은 일관된 패턴과 구조를 유지하는 것임을 기억해주세요. 😊

다음 시간에는 서버 상태 관리의 강자, TanStack Query & SWR에 대해 알아볼 예정이니 기대해주세요! 🌐

여러분의 상태 관리가 더욱 효과적이고 즐거워지길 바랍니다! 😄👋