Redux vs Zustand 상태 관리 비교: 쇼핑몰 & 대시보드 실무 사례

2025. 6. 1. 22:18·Frontend Development
반응형

GitHub에서 학습하기

React로 쇼핑몰 웹 애플리케이션이나 관리자 대시보드를 개발할 때 전역 상태 관리 도구를 선택하는 일은 매우 중요합니다. 대표적인 상태 관리 라이브러리인 Redux와 Zustand는 서로 다른 철학과 장단점을 지니고 있습니다. 본 글에서는 쇼핑몰과 대시보드 같은 현실적인 UI 시나리오를 배경으로 Redux와 Zustand를 비교합니다. 특히 다음과 같은 핵심 항목들을 중심으로 두 라이브러리의 접근 방식을 살펴보고자 합니다.

  • 인증 토큰 관리: Redux에서는 어떻게 관리하며, Zustand에서는 어떤 방식으로 처리할 수 있는지
  • 상태 디버깅 및 추적: Redux DevTools의 강점과, Zustand에서의 대안 및 구현 방법
  • 미들웨어 확장성: Redux의 미들웨어 (thunk, saga 등) 사용 사례와 Zustand에서의 한계
  • Undo/Redo 기능: Redux에서 이 기능을 구현하는 방법과 실제 활용 사례, 그리고 Zustand에서의 가능 여부와 어려움
  • 코드 예제 비교: 동일한 기능을 Redux와 Zustand로 각각 구현한 코드 예시
    프로젝트 규모 확대 시 유지보수성과 협업: 대규모 프로젝트에서 두 접근 방식의 차이점 (구조화, 팀원 간 작업 분배 등)

각 항목별로 쇼핑몰이나 대시보드의 현실적인 기능을 예로 들어 설명하며, Redux와 Zustand 접근 방식의 이점과 단점을 명확히 짚어보겠습니다.

인증 토큰 관리

실무 시나리오: 쇼핑몰 사이트에서 사용자가 로그인하면 서버로부터 JWT 인증 토큰을 받아옵니다. 이 토큰은 이후 상품 목록 조회, 주문 생성 등의 API 호출 시 인증 헤더에 포함되어야 합니다. 관리자 대시보드에서도 로그인 세션을 유지하기 위해 토큰 관리는 필수적입니다. 이제 Redux와 Zustand에서 이 토큰을 어떻게 저장하고 관리할 수 있는지 비교해보겠습니다.

Redux에서의 토큰 관리

Redux에서는 일반적으로 전역 상태 트리에 토큰 값을 저장합니다. 이를 위해 auth와 같은 슬라이스(slice)를 만들어 token을 상태로 포함시키고, 로그인 성공/실패 등의 액션을 정의합니다. 예를 들어 Redux Toolkit을 사용한다면 다음과 같이 구현할 수 있습니다.

// Redux Toolkit을 사용한 authSlice 예시
import { createSlice } from '@reduxjs/toolkit';

const authSlice = createSlice({
  name: 'auth',
  initialState: { 
    token: null,
    user: null
  },
  reducers: {
    loginSuccess: (state, action) => {
      state.token = action.payload.token;
      state.user = action.payload.user;
    },
    logout: (state) => {
      state.token = null;
      state.user = null;
    }
  }
});

// 액션 생성자와 리듀서 추출
export const { loginSuccess, logout } = authSlice.actions;
export default authSlice.reducer;

위 코드에서 로그인 성공 시 토큰과 사용자 정보를 Redux 상태에 저장하고, 로그아웃 시 토큰을 지우도록 했습니다. 컴포넌트에서는 useSelector로 현재 토큰을 읽거나 useDispatch로 loginSuccess 액션을 디스패치(dispatch)하여 토큰을 업데이트합니다.

  • 이 때 장점은 Redux 상태가 한 곳에서 관리되어 모든 컴포넌트가 동일한 토큰 값을 참조한다는 점입니다. 예를 들어, 어떤 페이지에서 로그아웃 액션이 발생하면 다른 모든 페이지에서도 token 값이 일관되게 null로 변경되어, 인증이 필요한 요청을 막을 수 있습니다. 또한 Redux DevTools를 통해 토큰 값이 언제 어떻게 변경됐는지 추적하기 쉽습니다.
  • 토큰은 보통 휘발성 메모리에만 저장하지 않고 영구 저장소(예: localStorage)에도 동기화합니다. Redux에서는 redux-persist와 같은 미들웨어를 사용해 state를 자동으로 localStorage에 저장해두고 앱 재실행 시 불러올 수 있습니다. 이를 통해 사용자가 새로고침해도 로그인 상태를 유지할 수 있습니다.

한 가지 유의할 점은 보안입니다. Redux 상태는 브라우저 JS 메모리에 있으므로 XSS 취약점이 있다면 토큰이 노출될 수 있습니다. 이는 Zustand 등 다른 클라이언트 상태 관리도 마찬가지이므로, 토큰은 HttpOnly 쿠키 등으로 관리하는 것이 가장 안전하지만, 여기서는 두 상태 관리 간 구현상의 차이에 집중하겠습니다.

Zustand에서의 토큰 관리

Zustand에서는 더 간단하게 토큰 상태를 정의할 수 있습니다. Zustand의 스토어는 리액트 훅을 통해 사용되며, 초기 상태와 업데이트 함수를 한 곳에 작성합니다. 예를 들어 동일한 인증 기능을 Zustand로 작성하면 다음과 같습니다.

// Zustand를 사용한 인증 상태 예시
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

const useAuthStore = create(
  persist(
    (set) => ({
      token: null,
      user: null,
      loginSuccess: (token, user) => set({ token, user }),
      logout: () => set({ token: null, user: null })
    }),
    { name: 'auth-storage' } // localStorage에 'auth-storage' 키로 자동 저장
  )
);

위 코드에서는 persist 미들웨어를 사용하여 Zustand 상태를 브라우저 localStorage와 동기화했습니다. useAuthStore 훅을 컴포넌트에서 사용하여 token이나 user 상태를 조회하거나, loginSuccess 함수를 호출해 상태를 업데이트합니다.

  • 장점: Redux 대비 보일러플레이트 코드가 훨씬 적습니다. 별도의 action 타입이나 리듀서를 정의하지 않아도, set 함수를 통해 곧바로 상태를 업데이트합니다. 또한 persist 미들웨어를 활용하여 비교적 쉽게 상태를 영구 저장할 수 있습니다 (Redux의 redux-persist에 대응).

  • 단점: 상태 변경에 대한 중앙 기록이 없기 때문에 토큰 변경 이력 추적이나 액션 단위의 제어가 어렵습니다. 예를 들어, Redux에서는 로그아웃 액션이 디스패치된 시점을 DevTools로 확인하거나 다른 미들웨어에서 가로채어 처리할 수 있지만, Zustand에서는 logout() 함수를 누가 언제 호출했는지 중앙에서 알기 어렵습니다. 또한 토큰 갱신 로직(예: 만료 시 자동 리프레시)을 구현할 때 Redux에선 미들웨어(saga 등)로 전역 관리가 가능한 반면, Zustand에선 이러한 로직을 직접 만들어야 합니다.

요약:

  • Redux는 인증 토큰을 전역 상태로 체계적으로 관리합니다. 액션/리듀서를 통해 업데이트 과정을 명시적으로 다루고, redux-persist 등의 도구로 쉽게 영속성 부여가 가능합니다. 다만 설정에 어느 정도 코드량이 필요합니다.

  • Zustand는 심플한 API로 토큰 상태와 업데이트 함수를 한 곳에 정의할 수 있어 구현이 빠르고 직관적입니다. 하지만 상태 변경 추적이나 고급 제어 측면에서는 내장 지원이 부족하므로, 필요한 경우 추가적인 코드나 미들웨어를 작성해야 합니다.

상태 디버깅 및 추적

쇼핑몰이나 대시보드 앱이 복잡해질수록 "지금 앱 상태가 왜 이렇게 됐지?", "어떤 사용자 액션 때문에 이 상태 변화가 일어났지?"를 파악하는 것이 매우 중요합니다. Redux와 Zustand는 이 부분에서 상당히 다른 디버깅 경험을 제공합니다.

Redux DevTools를 활용한 디버깅

Redux의 가장 큰 강점 중 하나는 공식 DevTools 확장 프로그램을 통한 시간 여행 디버깅과 액션 추적 기능입니다. Redux 애플리케이션은 모든 상태 변화가 액션을 통해 이루어지기 때문에, DevTools에서는 각 액션의 타입, 이전 상태와 이후 상태를 차례대로 기록합니다. 개발자는 DevTools UI를 통해 과거의 특정 액션으로 되돌아가거나 (undo), 앞으로 다시 적용(redo) 해 보는 식으로 상태 변화를 시간순으로 재생할 수 있습니다. 이러한 time-travel debugging 기능은 복잡한 앱에서 버그를 찾거나 상태 변화를 이해하는 데 매우 강력합니다.

Redux DevTools를 사용하면 상태 변경 이력을 시간순으로 탐색하고 액션을 재생(replay)할 수 있으며, 전체 상태 트리를 검사할 수도 있습니다. 이러한 기능은 애플리케이션이 복잡해질수록 특히 유용합니다.

예를 들어, 쇼핑몰 관리자 대시보드에서 매출 차트가 갱신되지 않는 버그가 있다고 가정해봅시다. Redux DevTools를 통해 사용자의 버튼 클릭, API 응답 액션 등의 흐름을 재생해 보면 어느 지점에서 상태 업데이트가 잘못되었는지 추적할 수 있습니다. 마찬가지로 쇼핑몰 웹사이트에서 상품을 장바구니에 추가했는데 UI에 반영이 안 된다면, DevTools 로그를 보고 해당 액션(예: cart/addItem이 디스패치되었는지, 리듀서가 상태를 제대로 바꿨는지 확인할 수 있습니다.

또한 Redux DevTools는 액션별로 상태 diff(차이) 를 보여주고, 성능 모니터링, 액션 디스패치 강제 실행 등의 고급 기능도 제공합니다. 이러한 풍부한 디버깅 생태계는 Redux가 기업용 대형 프로젝트에서 신뢰받는 이유 중 하나입니다.

Zustand에서의 상태 추적 (DevTools 및 기타 방법)

Zustand는 경량 라이브러리이지만, Redux DevTools 연동을 지원합니다. 공식적으로 제공되는 devtools 미들웨어를 사용하면, Redux 없이도 Redux DevTools 브라우저 확장에 Zustand 스토어를 연결할 수 있습니다.설정 방법은 비교적 간단한데, 스토어를 생성할 때 다음과 같이 devtools로 래핑하면 됩니다.

import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

const useStore = create(devtools((set) => ({
  bears: 0,
  increaseBears: () => set((state) => ({ bears: state.bears + 1 }))
})));

위와 같이 설정하면 Redux DevTools에서 Zustand 상태를 볼 수 있고, 상태 변경 검사, 액션 추적, 시간 여행 디버깅까지 기본적으로 가능해집니다. Redux와 달리 별도의 action 객체가 없는데도, DevTools에서는 각 set 호출을 하나의 액션처럼 다룹니다. (참고로, DevTools에 표시되는 액션 이름은 기본적으로 anonymous로 나오지만 set 함수의 세 번째 인자로 커스텀 액션 이름을 전달하여 명명할 수도 있습니다. 예를 들어 set({ bears: 0 }, false, "removeAllBears")와 같이 작성하면 해당 변경이 DevTools에 "removeAllBears" 액션으로 기록됩니다.)

Zustand의 DevTools 통합은 설정이 쉽고 가벼운 반면, Redux DevTools의 풍부한 기능 중 일부는 제한될 수 있습니다. 예를 들어, Redux-saga 같은 복잡한 미들웨어의 흐름까지 DevTools로 모니터링하는 것은 Redux만의 장점입니다. 반면 Zustand는 그런 개념이 없으므로 DevTools에서는 단순한 상태 변경 이력만 확인하는 용도로 보게 됩니다. 또한 여러 개의 Zustand 스토어를 쓰고 있다면 DevTools 인스턴스도 따로 관리해야 하는데, Redux는 보통 하나의 스토어로 전체 상태를 관리하므로 이러한 부분도 Redux 쪽이 일관적입니다.

그럼에도 불구하고 소규모 프로젝트나 비교적 단순한 상태 관리 상황에서는 Zustand + DevTools만으로도 충분한 디버깅이 가능합니다. React 개발자 도구(React DevTools)로는 볼 수 없는 전역 상태의 변화를 추적할 수 있기 때문입니다. 만약 DevTools보다 더 간단한 방법으로 상태 변화를 로깅하고 싶다면, Zustand의 subscribe 함수를 이용해 상태 변화시마다 콘솔에 로그를 남기는 자체 미들웨어를 작성할 수도 있습니다. (Zustand 미들웨어로 set 함수를 감싸 로그를 찍는 예시는 공식 문서에도 나와 있습니다

요약:

  • Redux는 Redux DevTools를 통해 강력한 상태 디버깅 도구를 제공합니다. 액션 단위로 변화 추적, 시간 여행, 상태 비교 등이 가능하여 복잡한 버그를 잡기 수월합니다. 대시보드와 같은 복잡한 UI의 상태 흐름도 DevTools로 한눈에 파악할 수 있습니다.

  • Zustand도 DevTools 연동이 가능하여 기본적인 상태 추적과 시간 여행 디버깅을 지원하지만, 액션 명시성이나 미들웨어 흐름 추적 면에서는 제한적입니다. 작은 앱에서는 경량 솔루션으로 충분하나, 아주 복잡한 상태 변화를 세밀하게 모니터링해야 한다면 Redux 쪽이 유리합니다.

미들웨어 확장성

미들웨어는 상태 변경 전후로 공통 로직을 삽입하거나, 비동기 처리를 제어하는 등 확장 기능을 추가하기 위해 사용됩니다. 쇼핑몰이나 대시보드 애플리케이션에서는 예를 들어 API 호출, 로깅, 에러 처리, 사용자 행위 추적 등의 공통 관심사를 미들웨어로 구현할 수 있습니다. Redux와 Zustand는 이 부분에서 접근 방식이 크게 다릅니다.

Redux의 미들웨어와 확장

Redux는 디스패치(dispatch) 라는 개념을 중심으로 동작하며, 미들웨어는 디스패치된 액션이 리듀서에 도달하기 전에 가로채서 처리할 수 있는 함수들의 파이프라인입니다. Redux 미들웨어를 이용하면 다음과 같은 일들을 할 수 있습니다:

  • 비동기 API 호출 제어: 가장 널리 알려진 redux-thunk 미들웨어는 액션 대신 함수를 디스패치하여, 그 함수 안에서 API를 호출하고 응답을 받은 뒤 일반 액션을 디스패치하는 패턴을 제공합니다. 예를 들어 상품 목록을 가져오는 액션을 thunk로 작성하면, 컴포넌트에서는 dispatch(fetchProducts())만 호출하고, 실제 데이터 fetching 로직과 성공/실패 시 상태 업데이트(dispatch)는 미들웨어 내부에서 일어납니다.

  • 복잡한 비동기 흐름: redux-saga와 같은 미들웨어는 사가(saga) 라는 generator 함수를 통해 복잡한 비동기 시나리오를 관리합니다. 예를 들어 쇼핑몰에서 "사용자가 주문 버튼을 누르면" 결제 API 호출 -> 재고 확인 API 호출 -> 결과에 따라 성공/실패 액션 디스패치 -> 실패 시 사용자에게 알림 등의 일련의 비동기 작업 흐름을 saga로 순차/병렬 제어할 수 있습니다. 이러한 흐름 제어를 Redux 없이 직접 하려면 컴포넌트나 기타 곳에서 promise 체인을 관리해야겠지만, Redux-saga를 쓰면 액션 수준에서 중앙 관리가 가능합니다.

  • 로깅/에러 처리: 미들웨어를 활용하여 모든 액션과 상태 변화 로깅, 예외 발생 시 보고 등의 작업을 전역에서 처리할 수 있습니다. 예컨대 analytics 미들웨어를 추가해 특정 액션 발생 시 서버에 사용자 이벤트를 기록하거나, 에러 처리 미들웨어로 reducer에서 에러 발생 시 추가 액션을 디스패치하는 것이 가능합니다.

이처럼 Redux는 미들웨어를 통한 풍부한 생태계를 갖추고 있습니다. Redux 공식 생태계 및 커뮤니티에는 다양한 미들웨어가 존재하며, 필요하다면 직접 정의할 수도 있습니다. Redux Toolkit을 사용하면 getDefaultMiddleware로 thunk 등이 기본 포함되고, 추가 미들웨어를 손쉽게 연결할 수 있습니다. 또한 RTK Query 같은 고급 데이터 패칭 라이브러리도 Redux 미들웨어의 일종으로 Redux 스토어에 쉽게 통합됩니다.

Redux 미들웨어 사용 예 (쇼핑몰 시나리오)

// Redux Thunk 예시: 상품 목록을 가져오는 비동기 액션
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

// RTK의 createAsyncThunk 사용 (내부적으로 thunk 미들웨어 필요)
export const fetchProducts = createAsyncThunk('products/fetchAll', async () => {
  const response = await fetch('/api/products');
  return response.json();
});

const productsSlice = createSlice({
  name: 'products',
  initialState: { items: [], loading: false },
  reducers: { },
  extraReducers: (builder) => {
    builder
      .addCase(fetchProducts.pending, (state) => {
        state.loading = true;
      })
      .addCase(fetchProducts.fulfilled, (state, action) => {
        state.loading = false;
        state.items = action.payload;
      })
      .addCase(fetchProducts.rejected, (state) => {
        state.loading = false;
      });
  }
});

위 코드에서 createAsyncThunk를 이용해 API 호출 로직을 작성했고, Redux Toolkit이 제공하는 thunk 미들웨어가 이 함수를 처리해줍니다. 이처럼 Redux는 비동기 로직을 액션 단위로 캡슐화하여 관리할 수 있습니다.

Zustand에서의 미들웨어와 한계

Zustand에는 Redux처럼 액션 디스패치를 가로채는 구조는 없습니다. 대신 스토어 생성 시 미들웨어를 조합하여 기능을 추가하는 방식을 취합니다. 사실 미들웨어라기보다 고차 함수로 스토어 설정 함수를 래핑하는 개념에 가깝습니다. Zustand의 공식 미들웨어로는 앞서 사용한 persist (스토어 영구 저장), devtools (Redux DevTools 연동), immer (Immer를 이용한 편리한 불변성 처리) 등이 있습니다. 이러한 미들웨어들은 주로 상태 저장/변경 방식에 관련된 공통 기능을 제공하며, Redux 미들웨어처럼 액션을 가로채어 흐름을 관리하지는 않습니다.

그렇다면 Zustand에서 비동기 로직이나 부가 기능은 어떻게 처리할까요? 크게 두 가지 방식이 있습니다:

  1. 스토어 내부에서 직접 처리: Zustand의 액션은 단순 함수이므로, 그 안에서 async/await을 사용하거나 Promise 체인을 호출할 수 있습니다. 예를 들어, Redux에서 thunk로 구현했던 상품 목록 가져오기를 Zustand에서는 아래처럼 구현할 수 있습니다.

    const useProductStore = create((set) => ({
    items: [],
    loading: false,
    fetchAll: async () => {
     set({ loading: true });
     try {
       const res = await fetch('/api/products');
       const data = await res.json();
       set({ items: data, loading: false });
     } catch (e) {
       set({ loading: false });
     }
    }
    }));

    컴포넌트에서는 useProductStore.getState().fetchAll() 또는 useProductStore((state) => state.fetchAll)() 식으로 호출하여 데이터를 가져오고 상태를 업데이트합니다. 이 접근 방식은 간단한 비동기 작업에 대해서는 Redux보다 코드도 적고 이해하기 쉬우며, 추가 라이브러리 학습이 필요 없다는 장점이 있습니다.

  2. 리액트 훅 또는 기타 외부 라이브러리 활용: Zustand를 사용할 때도 데이터 패칭은 React Query 같은 별도 상태 관리 라이브러리에 맡기고, Zustand에는 필요한 최소한의 전역 상태만 두는 방식이 종종 사용됩니다. 예를 들어 쇼핑몰 상품 목록은 React Query로 불러오고 캐싱하며, 장바구니 상태나 UI 토글 상태만 Zustand로 관리할 수도 있습니다. 이런 식으로 상태 관리의 역할 분리를 할 수 있지만, 이는 Redux와 직접 비교했을 때 Zustand 자체에서 모든 걸 처리하지 않고 다른 도구와 조합하는 예시라 할 수 있습니다.

Zustand에는 Redux-saga에 해당하는 내장 기능은 없으며, 복잡한 비동기 시나리오 제어를 위해 특별히 제공되는 도구는 없습니다. 필요하다면 사용자 정의 subscribe를 활용하거나, 상태 변화를 감지하여 추가 함수를 호출하는 방식으로 구현해야 합니다. 하지만 이는 규모가 커질수록 점점 유지보수가 어렵고 버그가 생기기 쉬운 방향입니다. Reddit의 한 개발자는 "Zustand는 대형 프로젝트에는 그리 적합하지 않고, 복잡한 상태 관리 아키텍처를 직접 설계할 자신이 있는 경우에만 사용하는 것이 좋다"고 조언하기도 했습니다. 이 말은 특히 미들웨어/확장성 측면을 지적한 것으로 볼 수 있습니다. Redux-Toolkit이 제공하는 견고한 가드레일(guard rails) – 예를 들어 미들웨어를 통한 일관된 비동기 처리 흐름 – 이 없는 Zustand에서는 개발자 스스로 그러한 구조를 만들어야 하며, 팀원들의 스킬 편차가 크다면 일이 더 어려워질 수 있습니다.

요약:

  • Redux는 미들웨어를 통한 뛰어난 확장성을 갖추고 있습니다. Redux-thunk, Redux-saga 등의 미들웨어를 활용하면 비동기 로직과 부가 기능을 체계적이고 모듈화된 방식으로 구현할 수 있습니다. 다양한 서드파티 미들웨어와 RTK Query, Redux Persist 등의 풍부한 생태계도 Redux의 강점입니다

  • Zustand는 상태 업데이트 함수 자체를 활용한 직접적인 처리 방식을 취하며, 복잡한 흐름 제어를 도와주는 내장 미들웨어는 없습니다. 간단한 요구사항에서는 오히려 코드량이 적고 이해하기 쉽지만, 요구사항이 복잡해질수록 개발자가 수동으로 패턴을 구축해야 하는 부분이 늘어나고, 이는 팀 규모가 클 때 일관성 유지에 부담이 될 수 있습니다. (물론 Zustand도 persist, immer 등 핵심적인 내장 미들웨어는 제공하므로, 캐싱이나 불변성 처리 같은 공통 기능은 손쉽게 붙일 수 있습니다.)

Undo/Redo 기능 구현

Undo/Redo는 사용자가 수행한 최근의 상태 변경을 취소하거나 다시 적용할 수 있게 해주는 기능입니다. 예를 들어 쇼핑몰 관리자 대시보드에서 실수로 대시보드 위젯 구성을 변경했을 때 Undo 버튼을 눌러 이전 상태로 되돌아갈 수 있다면 유용할 것입니다. 또는 쇼핑몰 웹사이트에서 필터를 적용했다가 Undo로 원래 상태로 돌아가는 시나리오를 생각할 수 있습니다. 이 섹션에서는 Redux와 Zustand에서 Undo/Redo를 구현하는 방법과 현실적인 활용에 대해 살펴보겠습니다.

Redux에서의 Undo/Redo

Redux의 아키텍처는 시간 이동(time-travel)에 유리하기 때문에 Undo/Redo 기능을 비교적 쉽게 접목할 수 있습니다. 사실 Redux DevTools 자체가 이미 개발 단계에서의 Undo/Redo(액션 단위 되돌리기)를 제공하고 있지요. 이를 실제 사용자 기능으로 노출시키려면 애플리케이션 상태에 과거 상태들의 히스토리를 관리하면 됩니다. Redux 공식 문서에서는 상태 히스토리를 관리하는 패턴을 소개하는데, past, present, future 세 가지 리스트를 두고 현재 상태 변경 시마다 이전 상태를 past에 쌓아두는 방식을 권장합니다. 이러한 로직을 수동으로 구현할 수도 있지만, 실무에서는 redux-undo 와 같은 라이브러리를 활용하여 손쉽게 Undo 기능을 도입할 수 있습니다. redux-undo는 기존 리듀서를 감싸는 higher-order reducer 형태로, 자동으로 past / present / future 상태를 관리해줍니다.

현실적인 활용: Undo/Redo는 복잡한 편집 시나리오에서 주로 활용됩니다. 예를 들어, KendoReact 같은 UI 라이브러리도 Redux + Redux-Undo를 활용하여 데이터 편집, 페이징, 정렬 또는 필터링 작업 중 상태를 스위칭(전환)할 수 있도록 지원합니다. 사용자가 그리드에서 필터를 적용했다가 Undo를 누르면 이전 필터 없는 상태로 복원되거나, 폼 편집 중 여러 번의 변경을 Undo/Redo로 왔다 갔다 할 수 있는 것입니다. 이러한 상태 전환 기능은 일반적으로 Redux에서 편리하게 구현되며, 대형 앱에서 복잡한 편집 요구사항이 있을 때 선택적으로 사용됩니다.

물론 Undo/Redo를 남용하면 상태 이력이 기하급수적으로 늘어나 메모리 문제가 될 수 있으므로, 필요한 경우에만 도입하는 것이 좋습니다. Redux에서는 특정 슬라이스에만 undo 역량을 붙이거나 (undoable(reducer)) 일정 개수 이상의 히스토리는 제거하는 등 여러 제어 옵션을 라이브러리 차원에서 제공합니다.

Zustand에서의 Undo/Redo

Zustand는 기본적으로 Undo/Redo 기능을 제공하지 않습니다. 상태 변경 이력을 내부적으로 보존하지도 않기 때문에, 이를 구현하려면 직접 상태 히스토리를 관리하거나 별도의 미들웨어를 사용해야 합니다. 다행히도 커뮤니티에서 만든 zundo 라는 경량 미들웨어가 있어 Zustand에 Undo/Redo를 비교적 쉽게 붙일 수 있습니다. zundo 미들웨어를 적용하면 Zustand 스토어에 pastStates와 futureStates를 자동으로 관리하는 temporal 객체가 추가되고, undo(), redo() 메서드를 통해 이전 상태로 이동하거나 되돌리는 기능을 사용할 수 있습니다.

즉, Redux에서 redux-undo를 쓰듯이, Zustand에서는 zundo를 써서 Undo/Redo를 구현할 수 있습니다. 사용법도 Redux와 비슷하게, 스토어를 만들 때 temporal(시간 관련) 미들웨어로 감싸주면 됩니다.

import { create } from 'zustand';
import { temporal } from 'zundo';

const useStoreWithUndo = create(
  temporal((set) => ({
    items: [],
    addItem: (item) => set((state) => ({ items: [...state.items, item] })),
    removeItem: () => set({ items: [] })
  }))
);

// 이렇게 생성된 store에는 useStoreWithUndo.temporal.getState().undo(), redo() 등이 제공됩니다.

zundo를 사용하면 DevTools처럼 개발용이 아니라 런타임에서 실제 Undo/Redo UI를 제공할 수 있습니다. 다만 공식 기능이 아니므로 큰 커뮤니티 지원을 기대하기는 어렵고, 복잡한 상태를 다룰 때 얼마나 안정적인지는 검증이 필요합니다.

만약 별도 라이브러리를 쓰지 않고 수동으로 Zustand에서 Undo를 구현한다면, subscribe를 통해 이전 상태를 스택에 쌓아두고 적절한 시점에 set으로 복구하는 식의 작업이 필요합니다. 이 경우 상태가 복잡할수록 실수가 생기기 쉽고, 특히 부분 상태만 되돌리기 같은 기능은 관리가 어렵습니다. 반면 Redux는 아키텍처가 엄격하고 상태가 불변 객체로 관리되기 때문에 이러한 시간 여행 기능을 추가하기에 상대적으로 수월한 편입니다.

요약:

  • Redux에서는 Undo/Redo를 구현하기 위한 검증된 패턴과 라이브러리가 있습니다. redux-undo 등을 통해 히스토리를 관리하면 데이터 편집, 설정 변경 등의 복잡한 UI 상호작용을 지원할 수 있으며, 이는 실제 엔터프라이즈 앱에서도 활용되고 있습니다.

  • Zustand에서는 Undo/Redo가 기본 제공되지 않지만, 서드파티 미들웨어(zundo) 를 이용해 비교적 쉽게 도입할 수 있습니다. 다만 이는 추가 라이브러리에 의존하며, Redux만큼 널리 사용되는 패턴은 아닙니다. 규모가 작고 Undo 기능이 국지적으로 필요할 때는 Zustand에서도 적용 가능하겠지만, 앱 전반에 광범위하게 Undo/Redo를 도입해야 하는 상황이라면 Redux를 고려하는 것이 안전합니다.

코드 예제: Redux와 Zustand로 동일 기능 구현

위에서 개념적으로 비교한 내용들을 실제 코드에서는 어떻게 구현하는지 직접 비교해보겠습니다. 예시로 쇼핑몰의 장바구니 상태 관리 기능을 Redux와 Zustand로 각각 구현해볼 것입니다. 장바구니에는 상품 아이템 목록이 있고, 아이템 추가와 아이템 제거 기능을 지원한다고 가정합니다. 이 간단한 기능을 둘 다 구현해 보면, 코드 구조와 사용 방법의 차이가 드러날 것입니다.

Redux 예시 코드 (장바구니 구현)

Redux는 현대적인 사용 방식을 위해 Redux Toolkit을 활용합니다. createSlice로 장바구니 슬라이스를 정의하고, configureStore로 스토어를 설정한 후, React 컴포넌트에서 useSelector와 useDispatch로 상태를 사용합니다.

// Redux Toolkit을 이용한 장바구니 slice 정의
import { configureStore, createSlice } from '@reduxjs/toolkit';

const cartSlice = createSlice({
  name: 'cart',
  initialState: { items: [] },
  reducers: {
    addItem: (state, action) => {
      // Immer 라이브러리 덕분에 아래처럼 직접 push가 가능 (불변성 유지됨)
      state.items.push(action.payload);
    },
    removeItem: (state, action) => {
      state.items = state.items.filter((item, idx) => idx !== action.payload);
    }
  }
});

// 액션과 리듀서 export
export const { addItem, removeItem } = cartSlice.actions;
const store = configureStore({ reducer: { cart: cartSlice.reducer } });

// React 컴포넌트에서 Redux 상태 사용
function CartComponent() {
  const items = useSelector(state => state.cart.items);
  const dispatch = useDispatch();

  const handleAdd = (item) => {
    dispatch(addItem(item));
  };
  const handleRemove = (index) => {
    dispatch(removeItem(index));
  };

  return (
    <div>
      <h2>장바구니</h2>
      <ul>
        {items.map((product, idx) => (
          <li key={idx}>
            {product.name} - {product.price}원 
            <button onClick={() => handleRemove(idx)}>제거</button>
          </li>
        ))}
      </ul>
      <button onClick={() => handleAdd({ name: 'Sample', price: 1000 })}>
        샘플 아이템 추가
      </button>
    </div>
  );
}

Redux Toolkit의 createSlice를 이용하면 액션 타입과 액션 크리에이터를 따로 정의할 필요 없이 reducers 안에 상태 변경 로직을 작성할 수 있습니다. 위 코드에서 addItem은 새로운 아이템을 배열에 추가하고, removeItem은 주어진 인덱스의 아이템을 제거하도록 구현했습니다. React 컴포넌트 (CartComponent)에서는 Redux 상태를 구독하고(useSelector), 필요 시 액션을 디스패치하여 상태를 변경합니다.

Redux를 사용한 구현의 특징을 정리하면 다음과 같습니다.

  • 상태(items)와 이를 변경하는 로직(addItem, removeItem)이 슬라이스 내부에 잘 구조화되어 있습니다. 다른 기능 (예: 사용자 정보, 주문 내역 등)은 각각 별도의 슬라이스/리듀서로 분리되어 관리될 것입니다.

  • 상태 변경은 반드시 액션 디스패치를 통해서만 일어나며, 액션의 내용과 상태 변화가 명확히 연관지어집니다.

  • Redux DevTools에서는 cart/addItem 또는 cart/removeItem 같은 액션 이름으로 상태 변경을 확인할 수 있고, 이전/이후 상태 비교나 시간 여행이 가능합니다.

  • 컴포넌트는 dispatch를 통해 간접적으로 상태를 변경하며, useSelector로 필요한 부분만 구독하므로, 성능상 이점도 있습니다. (예를 들어 CartComponent는 state.cart.items만 구독하므로, 장바구니 외 다른 Redux 상태가 변하면 이 컴포넌트는 리렌더링되지 않습니다.)

Zustand 예시 코드 (장바구니 구현)

동일한 장바구니 기능을 Zustand로 구현하면 코드가 어떻게 달라지는지 살펴보겠습니다.

// Zustand를 이용한 장바구니 상태 정의
import { create } from 'zustand';

const useCartStore = create((set) => ({
  items: [],
  addItem: (product) => set((state) => ({ 
    items: [...state.items, product] 
  })),
  removeItem: (index) => set((state) => ({ 
    items: state.items.filter((_, idx) => idx !== index) 
  }))
}));

// React 컴포넌트에서 Zustand 상태 사용
function CartComponent() {
  const items = useCartStore((state) => state.items);
  const addItem = useCartStore((state) => state.addItem);
  const removeItem = useCartStore((state) => state.removeItem);

  return (
    <div>
      <h2>장바구니</h2>
      <ul>
        {items.map((product, idx) => (
          <li key={idx}>
            {product.name} - {product.price}원 
            <button onClick={() => removeItem(idx)}>제거</button>
          </li>
        ))}
      </ul>
      <button onClick={() => addItem({ name: 'Sample', price: 1000 })}>
        샘플 아이템 추가
      </button>
    </div>
  );
}

Zustand 버전에서는 useCartStore 훅을 통해 상태와 업데이트 함수를 바로 사용할 수 있습니다. set 함수를 호출하여 상태를 변경하며, Redux와 달리 직접 함수 호출로 상태를 업데이트한다는 점이 다릅니다. 위 코드에서는 addItem이 내부적으로 기존 상태의 items 배열을 복사한 새 배열을 만들어 추가하고 있고, removeItem은 filter를 통해 특정 인덱스를 제외한 새 배열로 업데이트하고 있습니다. (기존 상태를 건드리지 않고 새로운 상태 객체를 반환하는 점에서 Immer 없이도 Redux의 불변성 관리와 비슷한 결과를 얻고 있습니다.)

Zustand 구현의 특징을 정리하면 다음과 같습니다.

  • Redux처럼 액션 타입이나 디스패치 개념이 없고, 상태와 업데이트 로직이 한 군데 묶여 있습니다. 코드를 위에서부터 읽으면 초기값과 수정 함수들이 한눈에 들어오므로 이해하기 쉽습니다.

  • Redux에서는 컴포넌트에서 dispatch(addItem(item)) 식으로 호출한 반면, Zustand에서는 addItem(item) 함수를 바로 호출하여 상태를 변경합니다. 이로 인해 Redux처럼 중앙 액션 처리기가 개입할 여지는 없습니다. 즉, 상태 변경이 즉각적이고 (immer를 쓰지 않는 한) 동기적으로 이뤄집니다.

  • 성능 측면에서는, Zustand의 useStore 훅은 인자로 전달한 selector (state => state.items)에 따라 해당 부분이 변경될 때만 컴포넌트를 리렌더합니다. 이는 Redux의 useSelector와 유사한 동작을 합니다. 다만 Redux는 리덕스 자체가 상태 변경을 모두 스케줄링하고 배치 처리하는 반면, Zustand는 React의 렌더링 사이클과 긴밀하게 통합되어 있지 않으므로 업데이트가 일어나면 바로 해당 컴포넌트를 리렌더링합니다 (물론 React의 batched updates로 자동 최적화는 됩니다). 두 접근 모두 성능상 큰 문제 없이 동작하며, 적절히 분리만 한다면 대규모 앱에서도 괜찮은 성능을 보입니다.

코드 비교 관찰:

  • Redux 코드가 좀 더 장황합니다. 슬라이스 정의, store 설정, dispatch/selector 사용 등 구조적인 코드가 존재합니다. 반면 Zustand는 아주 최소한의 코드만으로 동일 로직을 달성했습니다. 작은 기능을 빠르게 만들 때는 Zustand의 간결함이 돋보입니다.

  • Redux는 명시적인 액션을 통해 의도를 드러냅니다. 예를 들어 Redux 코드에서 dispatch(addItem(item))을 보면 "장바구니에 아이템 추가"라는 사건이 발생함을 알 수 있고, DevTools에도 그렇게 기록됩니다. Zustand에서는 addItem(item) 호출이 곧바로 상태 변화로 이어지지만, 그 호출이 어디서 발생했는지 추적하려면 직접 로그를 남기거나 DevTools에 의존해야 합니다.

  • Redux는 모듈화에 유리합니다. 위 예시에서는 작게 보이지만, 만약 장바구니 외에 사용자 관리, 주문 처리 등 여러 슬라이스가 추가된다 해도 각 부분이 state.someFeature로 분리되어 관리되고, 액션 명도 구분됩니다. Zustand도 한 스토어 안에 여러 상태를 객체로 나눠서 관리할 수 있지만(예: { cart: {...}, user: {...} } 형태), 일반적으로 Redux의 구조화/제한된 패턴이 대규모 앱에서 더 견고한 경향이 있습니다.

결론적으로, 둘 다 동일한 기능을 구현할 수 있지만, 개발자가 작성해야 하는 코드의 스타일과 양은 상당히 다릅니다. 다음 섹션에서는 이런 차이가 대규모 프로젝트에서 어떤 영향을 미치는지, 협업 면에서는 어떤 장단점이 있는지 살펴보겠습니다.

대규모 프로젝트에서의 유지보수성과 팀 협업

쇼핑몰 서비스가 성장하여 수많은 상품, 사용자, 주문 관련 기능이 추가되거나, 대시보드 기능이 점점 복잡해져서 여러 개발자가 동시에 작업해야 하는 상황을 가정해 봅시다. 이처럼 프로젝트 규모가 커질 때 Redux와 Zustand는 유지보수성과 협업 측면에서 장단점이 뚜렷이 나타납니다.

Redux: 엄격한 구조의 힘과 팀원 간 명시적 계약

Redux는 규칙과 패턴이 엄격하기 때문에, 큰 팀이 협업할 때 오히려 장점으로 작용합니다. 모두가 일정한 규칙(예: 액션은 무엇을 해야 하고, 리듀서는 순수 함수여야 한다 등)을 따르므로 코드의 일관성이 유지됩니다. 새로운 팀원이 들어와도 Redux 패턴 자체가 학습曲선은 있을지언정 문서와 예제가 풍부하고 널리 쓰이는 표준이기에 프로젝트 구조를 이해하기가 상대적으로 쉽습니다. 특히 Redux Toolkit 등장 이후로 보일러플레이트가 줄고 사용법이 간소화되어 (slice 중심 구조) 초심자도 금방 적응할 수 있습니다. 또한 Toolkit과 TypeScript를 함께 쓰면 액션과 상태의 타입 안정성이 높아져 개발 경험이 좋아집니다.

팀 협업 시 이점:

  • 모듈화된 코드 구조: Redux는 기능별로 슬라이스 혹은 Ducks 패턴으로 코드가 조직화됩니다. 예를 들어 상품 관리 코드는 productsSlice, 사용자 인증 코드는 authSlice 등으로 나누고, 이를 하나로 합쳐 스토어를 구성합니다. 팀원이 각자 맡은 도메인의 슬라이스를 작업하면 충돌을 최소화할 수 있습니다. 반면 Zustand로 전역 상태를 한곳에 모두 정의한다면, 한 파일/모듈에 점점 많은 내용이 쌓여서 여러 사람이 동시에 수정하기 까다로울 수 있습니다. (물론 Zustand도 상태를 여러 파일로 분리한 뒤 combine하거나 여러 store로 나눌 수는 있지만, Redux만큼 강제되지는 않습니다.)

  • 명확한 변경 경로: Redux에서는 모든 변경이 액션으로 기록되므로, 협업 중에도 어느 모듈이 언제 상태를 변경했는지 추적하기 쉽습니다. 버그 트래킹 시에도 DevTools 로그를 공유하거나 재현하여 문제를 찾는 등, 디버깅 노하우를 팀 전체가 공유하기 수월합니다.

  • 풍부한 에코시스템 활용: 앞서 언급했듯, Redux의 중간웨어, 개발 도구, 추가 라이브러리가 많아 문제 해결에 대한 레시피를 찾기 쉽습니다. 예를 들어, 어떤 팀이 Undo 기능을 넣고 싶다면 이미 잘 알려진 방법이나 라이브러리를 적용하면 되므로 시행착오를 줄입니다.

Redux의 단점 (대규모 프로젝트 관련): 초기 설정이나 개념 학습이 필요하고, 작은 상태 변경에도 액션/리듀서 코드를 작성해야 하는 건 사실입니다. 그러나 이러한 초기 비용은 프로젝트 규모가 커질수록 상쇄됩니다. 오히려 코드량이 늘어도 한눈에 흐름을 파악하기 좋다는 면에서, Redux는 "큰 프로젝트일수록 빛을 발한다"는 평가를 받곤 합니다.

Zustand: 유연한 구조의 양날의 검

Zustand는 매우 유연하고 제한이 적은 도구이기 때문에, 개발 팀이 스스로 규칙을 만들어 잘 운용한다면 큰 프로젝트에서도 사용은 가능합니다. 예를 들어 상태를 독립적인 여러 slice로 분리하여 각각 별도 zustand 훅을 만들고, 이를 필요한 곳에서만 사용하는 방식으로 영역을 나누는 전략을 쓸 수 있습니다. 또한 상태 모델링을 어떻게 할지, 어떤 식으로 상태 간 의존성을 관리할지 등을 팀 규약으로 정해서 사용할 수도 있습니다.
그러나 이러한 작업은 팀의 역량과 합의에 전적으로 의존합니다. 구조적 강제가 없기에 누구라도 쉽게 새로운 전역 상태를 추가하고 여기저기서 사용할 수 있지만, 그것이 적절하게 관리되고 있는지 판단하는 역할도 사람에게 달려 있습니다. 앞서 인용한 의견처럼, 규모가 큰 프로젝트에서 Zustand를 쓰는 것은 "스스로 확장 가능한 아키텍처를 설계할 자신이 있을 때" 권장됩니다. 타입스크립트 사용 시에도 Redux Toolkit은 미리 정의된 패턴 덕에 타입 추론이 비교적 자동화되지만, Zustand는 개발자가 상태와 액션 타입을 직접 정의하고 관리해야 하므로 타입 관리가 어려워질 수 있다는 지적도 있습니다.

Zustand의 대규모 프로젝트 상의 장단점:

  • 장점: 보일러플레이트가 적으므로 새로운 기능을 추가하는 속도가 빠를 수 있습니다. 어떤 간단한 전역 상태가 필요하면 금방 zustand 훅 하나 만들어서 쓰면 되니, 초기 개발 페이스는 빠를 수 있습니다. 또한 특정 모듈이 Redux로 관리될ほど 복잡하지 않다면, Zustand를 사용한 모듈은 코드량이 작아 리팩토링이나 변경 범위가 직관적일 수 있습니다.

  • 단점: 프로젝트 전반의 코드 규칙을 잘 지키지 않으면, 상태 관리 코드가 난잡해질 위험이 있습니다. 예를 들어 쇼핑몰 앱에서 장바구니와 상품 목록, 사용자 정보를 모두 하나의 useStore에 넣어두고 여기서 막 업데이트 로직을 작성하면, 시간이 지날수록 이 스토어는 결합도가 높아지고 오류가 발생해도 원인을 찾기 어려워집니다. 또한 Zustand는 미들웨어 생태계가 제한적이고 (앞서 본 몇 가지 내장 미들웨어 외엔 커뮤니티 미들웨어가 많지 않음), Redux처럼 체계적인 개발자 도구들이 부족하므로 (DevTools는 있지만 액션명이 표준화되지 않는 등) 팀원 간 디버깅 협업에 불리할 수 있습니다.

  • 팀원 역량 요구: Redux는 일정 수준 이상 사용법이 표준화되어 있고 자료가 많지만, Zustand는 자유도가 높아 팀원마다 사용방법이 달라질 수 있습니다. 누군가는 Zustand에서 직접 비동기 호출을 할 수도 있고, 누군가는 React Query를 섞어 쓸 수도 있습니다. 프로젝트 리더가 이런 부분을 통일하지 않으면 일관성 없는 코드가 양산될 수 있습니다.

결국 대규모 프로젝트에서 Redux vs Zustand를 선택하는 것은 무엇을 우선시하느냐의 문제입니다. 한 Reddit 사용자는 "대규모 상용 작업에는 여전히 Redux-Toolkit을 사용한다. Redux는 모든 수준의 개발자에게 알맞은 가드레일을 제공한다"고 언급했습니다. 반면, 프로젝트가 비교적 단순하고 팀원들이 경량 솔루션을 더 선호한다면 Zustand도 고려 대상입니다. 다만 Redux를 대체할 정도로 규모가 커진 상황이라면, Redux-Toolkit을 통한 구조화가 주는 안정감을 무시하기 어렵습니다.

결론: 언제 Redux를 쓰고 언제 Zustand를 쓸까?

쇼핑몰이나 대시보드처럼 실제 서비스를 개발할 때, Redux와 Zustand 중 어느 것을 선택할지는 프로젝트의 복잡도, 팀 규모, 요구되는 기능 등에 달려 있습니다.

  • Redux 권장 시나리오: 상태가 아주 복잡하고 예측 가능한 패턴으로 관리되어야 하는 대규모 애플리케이션, 다수의 개발자가 참여하여 명확한 규칙과 도구가 필요한 경우, 또는 Undo/Redo나 복잡한 비동기 흐름 등 고급 기능을 많이 활용해야 하는 경우 Redux가 적합합니다. Redux는 엄격한 단방향 데이터 흐름과 풍부한 미들웨어로 이러한 상황에서 신뢰성을 제공합니다. 엔터프라이즈급 대시보드나 대형 쇼핑몰 프론트엔드라면 Redux-Toolkit 기반의 아키텍처가 장기적으로 유지보수에 유리할 것입니다.

  • Zustand 권장 시나리오: 비교적 간단한 상태 관리가 필요한 중소 규모 프로젝트나, 빠른 프로토타이핑이 중요한 경우 Zustand가 유효한 선택입니다. 초기 러닝커브가 거의 없고 필요한 기능만 골라 적용하면 되므로 개발 생산성이 높습니다. 예컨대 스타트업의 쇼핑몰 MVP나, 기능이 몇 개 없는 단순한 관리자 페이지에서는 Redux의 무거움을 피하고 Zustand로 가볍게 상태를 공유하는 편이 나을 수 있습니다. 또한 React Context 대용으로도 사용할 수 있어, 프로젝트 일부분에만 도입해서 사용하는 혼합 전략도 가능할 것입니다.

두 라이브러리 모두 React 상태 관리를 효율화하는 도구이며, 공통적으로 React 훅 기반의 인터페이스를 제공하므로 학습한 개념은 다소 상호 전이되는 면이 있습니다. 결국 중요하는 것은 프로젝트 요구사항에 맞게 도구를 선택하는 것입니다. 이 글에서 살펴본 것처럼, Redux와 Zustand는 인증 토큰 관리부터 디버깅, 미들웨어, Undo 기능, 코드 구조, 협업 적합성에 이르기까지 장단점이 다르므로, 개발자는 각 특징을 고려하여 최적의 솔루션을 결정해야겠습니다.

반응형
저작자표시 비영리 변경금지 (새창열림)

'Frontend Development' 카테고리의 다른 글

TypeScript 제네릭 완전 정복: 실행 결과로 배우는 실전 가이드  (0) 2025.06.09
TypeScript 제네릭, 이제는 정말 쉽게 이해해보자  (0) 2025.06.09
Streaming SSR: 서버 사이드 렌더링의 새로운 패러다임  (0) 2025.05.30
React 컴포넌트 설계의 핵심: 재사용성과 유지보수성을 높이는 방법  (0) 2025.05.29
브라우저 저장소 API: localStorage vs sessionStorage vs Cookie 완벽 가이드  (0) 2025.05.29
'Frontend Development' 카테고리의 다른 글
  • TypeScript 제네릭 완전 정복: 실행 결과로 배우는 실전 가이드
  • TypeScript 제네릭, 이제는 정말 쉽게 이해해보자
  • Streaming SSR: 서버 사이드 렌더링의 새로운 패러다임
  • React 컴포넌트 설계의 핵심: 재사용성과 유지보수성을 높이는 방법
Kun Woo Kim
Kun Woo Kim
안녕하세요, 김건우입니다! 웹과 앱 개발에 열정적인 전문가로, React, TypeScript, Next.js, Node.js, Express, Flutter 등을 활용한 프로젝트를 다룹니다. 제 블로그에서는 개발 여정, 기술 분석, 실용적 코딩 팁을 공유합니다. 창의적인 솔루션을 실제로 적용하는 과정의 통찰도 나눌 예정이니, 궁금한 점이나 상담은 언제든 환영합니다.
  • Kun Woo Kim
    WhiteMouseDev
    김건우
  • 깃허브
    포트폴리오
    velog
  • 전체
    오늘
    어제
  • 공지사항

    • [인사말] 이제 티스토리에서도 만나요! WhiteMouse⋯
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 분류 전체보기 (100) N
      • Frontend Development (39) N
      • Backend Development (21) N
      • Algorithm (33) N
        • 백준 (11) N
        • 프로그래머스 (17)
        • 알고리즘 (5)
      • Infra (1)
      • 자료구조 (3)
  • 링크

    • Github
    • Portfolio
    • Velog
  • 인기 글

  • 태그

    tailwindcss
    frontend development
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Kun Woo Kim
Redux vs Zustand 상태 관리 비교: 쇼핑몰 & 대시보드 실무 사례
상단으로

티스토리툴바