icon안동민 개발노트

클라이언트 사이드 상태 관리 도구 통합 (예 Redux, Zustand)


 Next.js 프로젝트에서 복잡한 상태 관리가 필요한 경우, 외부 상태 관리 라이브러리를 사용할 수 있습니다.

 이 절에서는 Redux와 Zustand를 Next.js 프로젝트에 통합하는 방법과 그 장단점에 대해 알아보겠습니다.

Redux 기본 개념 및 통합

 Redux는 예측 가능한 상태 컨테이너로, 복잡한 애플리케이션의 상태 관리에 널리 사용됩니다.

 기본 개념

  • Store : 애플리케이션의 전체 상태를 저장
  • Actions : 상태 변경을 위한 이벤트
  • Reducers : 액션에 따라 상태를 변경하는 순수 함수

 Next.js에 Redux 통합하기

  1. 패키지 설치
npm install @reduxjs/toolkit react-redux
  1. Redux store 생성
app/store.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'
 
export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
})
  1. Provider 설정
app/layout.js
'use client'
import { Provider } from 'react-redux'
import { store } from './store'
 
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Provider store={store}>
          {children}
        </Provider>
      </body>
    </html>
  )
}
  1. Redux 사용 예
'use client'
import { useSelector, useDispatch } from 'react-redux'
import { increment } from './counterSlice'
 
export default function Counter() {
  const count = useSelector((state) => state.counter.value)
  const dispatch = useDispatch()
 
  return (
    <div>
      <span>{count}</span>
      <button onClick={() => dispatch(increment())}>Increment</button>
    </div>
  )
}

Zustand 기본 개념 및 통합

 Zustand는 간단하고 가벼운 상태 관리 라이브러리로, 최소한의 보일러플레이트로 상태를 관리할 수 있습니다.

 기본 개념

  • Store : 상태와 액션을 포함하는 훅
  • Actions : 상태를 변경하는 함수

 Next.js에 Zustand 통합하기

  1. 패키지 설치
npm install zustand
  1. Store 생성
app/useStore.js
import { create } from 'zustand'
 
const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}))
 
export default useStore
  1. Zustand 사용 예
app/Counter.js
'use client'
import useStore from './useStore'
 
export default function Counter() {
  const { count, increment } = useStore()
 
  return (
    <div>
      <span>{count}</span>
      <button onClick={increment}>Increment</button>
    </div>
  )
}

서버 컴포넌트와의 조화로운 사용

 서버 컴포넌트에서는 직접적으로 클라이언트 사이드 상태 관리 도구를 사용할 수 없습니다.

 대신, 다음과 같은 패턴을 사용할 수 있습니다.

  1. 서버 컴포넌트에서 데이터 페칭
  2. 클라이언트 컴포넌트로 데이터 전달
  3. 클라이언트 컴포넌트에서 상태 관리 도구 사용

 예시

app/ServerComponent.js
import ClientComponent from './ClientComponent'
 
async function fetchData() {
  // 서버에서 데이터 페칭
}
 
export default async function ServerComponent() {
  const data = await fetchData()
  return <ClientComponent initialData={data} />
}
app/ClientComponent.js
'use client'
import { useEffect } from 'react'
import useStore from './useStore'
 
export default function ClientComponent({ initialData }) {
  const setData = useStore((state) => state.setData)
  
  useEffect(() => {
    setData(initialData)
  }, [initialData])
 
  // 나머지 컴포넌트 로직
}

Redux vs Zustand

 Redux

  • 장점 : 강력한 개발자 도구, 미들웨어 지원, 큰 커뮤니티
  • 단점 : 상대적으로 많은 보일러플레이트, 학습 곡선이 가파름

 Zustand

  • 장점 : 간단한 API, 적은 보일러플레이트, 작은 번들 크기
  • 단점 : Redux에 비해 적은 생태계, 복잡한 상태 관리에는 부족할 수 있음

12장 성능 최적화와의 연관성

 8장의 클라이언트 사이드 상태 관리는 12장의 성능 최적화와 밀접하게 연관됩니다.

 효율적인 상태 관리는 불필요한 리렌더링을 줄이고 애플리케이션의 전반적인 성능을 향상시킵니다.

 12장에서는 이러한 상태 관리 도구를 사용할 때의 성능 최적화 기법 (예 : 선택적 렌더링, 메모이제이션)에 대해 더 자세히 다루게 됩니다.

실습 : Zustand 기반 장바구니 기능 구현

 다음 요구사항을 만족하는 간단한 장바구니 기능을 구현해보세요.

  1. 상품 목록 표시
  2. 장바구니에 상품 추가/제거 기능
  3. 장바구니 내용 표시 및 총액 계산

 구현 예시

app/useStore.js
import { create } from 'zustand'
 
const useStore = create((set) => ({
  cart: [],
  addToCart: (product) => set((state) => ({
    cart: [...state.cart, product]
  })),
  removeFromCart: (productId) => set((state) => ({
    cart: state.cart.filter(item => item.id !== productId)
  })),
  clearCart: () => set({ cart: [] }),
}))
 
export default useStore
app/ProductList.js
'use client'
import useStore from './useStore'
 
const products = [
  { id: 1, name: 'Product 1', price: 10 },
  { id: 2, name: 'Product 2', price: 20 },
  { id: 3, name: 'Product 3', price: 30 },
]
 
export default function ProductList() {
  const addToCart = useStore((state) => state.addToCart)
 
  return (
    <div>
      <h2>Products</h2>
      {products.map(product => (
        <div key={product.id}>
          <span>{product.name} - ${product.price}</span>
          <button onClick={() => addToCart(product)}>Add to Cart</button>
        </div>
      ))}
    </div>
  )
}
app/Cart.js
'use client'
import useStore from './useStore'
 
export default function Cart() {
  const cart = useStore((state) => state.cart)
  const removeFromCart = useStore((state) => state.removeFromCart)
  const clearCart = useStore((state) => state.clearCart)
 
  const total = cart.reduce((sum, item) => sum + item.price, 0)
 
  return (
    <div>
      <h2>Cart</h2>
      {cart.map(item => (
        <div key={item.id}>
          <span>{item.name} - ${item.price}</span>
          <button onClick={() => removeFromCart(item.id)}>Remove</button>
        </div>
      ))}
      <p>Total: ${total}</p>
      <button onClick={clearCart}>Clear Cart</button>
    </div>
  )
}
app/page.js
import ProductList from './ProductList'
import Cart from './Cart'
 
export default function Home() {
  return (
    <div>
      <ProductList />
      <Cart />
    </div>
  )
}

 이 실습을 통해 Zustand를 사용하여 전역 상태를 관리하고, 이를 Next.js 애플리케이션의 여러 컴포넌트에서 활용하는 방법을 경험할 수 있습니다.

 이는 실제 e-commerce 애플리케이션에서 자주 사용되는 패턴으로, 클라이언트 사이드 상태 관리의 실제적인 적용을 이해하는 데 도움이 됩니다.