Studying/React

리액트 공부하기 - 전역 상태 관리 라이브러리 Zustand

creamymood 2025. 5. 23. 13:41

context API는 익혔으니 

뭔가 실무적으로 사용할 만 한 전역 상태관리를 공부 해보려고 하는데

리덕스.. 는 쳐다만 봐도 동기를 잃어서,,, 조교님이 추천해주시기도 하고, 요즘 핫하다는 zustand 공부하기

 

진짜 공부해보니까 정~말 간단하고 좋으네욥. 

 


우선 공식 문서 읽어보기 !

 

 

GitHub - pmndrs/zustand: 🐻 Bear necessities for state management in React

🐻 Bear necessities for state management in React. Contribute to pmndrs/zustand development by creating an account on GitHub.

github.com

똑똑한 곰돌이.


Zustand는 정말 배우기 쉬운 상태 관리 라이브러리
React에서 useState나 useContext 써본 경험이 있다면 아주 금방 익힐 수 있다.

useState와 useContext가 믹스된 느낌이랄까..

 

 1단계: 기본 개념 이해하기

✔️ Zustand란?

  • 독일어로 "상태"라는 뜻
  • 가볍고 빠른 React 상태 관리 라이브러리
  • Redux처럼 전역 상태를 다룰 수 있지만 코드가 훨씬 짧고 직관적

2단계: 기본 사용법 익히기

1. 라이브러리

npm install zustand

2. store 만들기

// store.js
import { create } from 'zustand'

const useStore = create((set) => ({
  count: 0,
  increase: () => set((state) => ({ count: state.count + 1 })),
  reset: () => set({ count: 0 }),
}))

export default useStore

zustand의 store는 거의 context API의 create이랑 provider가 합쳐진 느낌이네..

 

아래는 위 store에 대한 코드 설명이다. ↓

더보기

 

// store.js

 

이건 파일 이름이야. zustand로 만든 전역 상태(store)를 여기에 정의하는 거야.
다른 컴포넌트에서 불러와서 사용할 수 있게 만들어.


import { create } from 'zustand'

zustand에서 가장 중요한 함수인 create()를 불러오는 코드야.
이 함수로 상태 저장소(store)를 만들 수 있어.


const useStore = create((set) => ({
  • create() 함수를 호출하면서 안에 상태 객체를 정의하는 함수를 넣고 있어.
  • 이 함수는 set이라는 함수를 받아.
    👉 이 set은 상태를 업데이트할 때 사용하는 함수야.
  • 결과적으로 useStore는 우리가 만든 커스텀 훅이 되고,
    컴포넌트에서 이걸 쓰면 상태와 상태를 바꾸는 함수들을 쓸 수 있게 돼.

  count: 0,

상태의 기본값이야.
count라는 이름의 숫자 상태를 만들고, 초기값은 0으로 설정한 거야.


  increase: () => set((state) => ({ count: state.count + 1 })),

increase는 함수야.
이걸 실행하면 set()을 이용해서 현재 상태의 count 값을 +1 해줘.

  • (state) => ({ count: state.count + 1 }) 이 부분은 현재 상태를 받아서 count를 증가시킨 새 객체를 반환하는 거야.
  • 즉, count 값을 1 증가시키는 함수!

  reset: () => set({ count: 0 }),

reset도 함수야.
실행하면 set()을 이용해서 count 값을 0으로 리셋해.
상태 객체 전체를 { count: 0 }로 바꾸는 거야.


}))

create 함수 안에 있는 객체 정의가 끝났다는 뜻이야.
이제 이 상태와 상태 변경 함수들이 저장소에 잘 등록됐어.


export default useStore

useStore 훅을 밖에서 쓸 수 있도록 내보내는 코드야.
다른 컴포넌트에서 이걸 import useStore from './store' 이렇게 불러와서 사용할 수 있어.


📦 이 코드를 한 줄로 요약하면?

count 값을 전역에서 관리하면서 증가시키거나 초기화할 수 있는 상태 저장소(store)를 만든 것

 

3. 컴포넌트에서 사용하기

import useStore from './store'

function Counter() {
  const { count, increase, reset } = useStore()
  
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increase}>+1</button>
      <button onClick={reset}>Reset</button>
    </div>
  )
}

 

3단계: 파악해야 할 주요 개념

create() store를 생성하는 함수
set() 상태를 변경할 때 사용
get() 현재 상태 가져올 때 사용
subscribe() 상태 변화 감지
persist 미들웨어 상태를 localStorage에 저장 가능
devtools 미들웨어 Redux DevTools 사용 가능

 


4단계: 미들웨어 적용하기 (옵션)

localStorage 저장 (persist)

import { create } from 'zustand'
import { persist } from 'zustand/middleware'

const useStore = create(persist(
  (set) => ({
    darkMode: false,
    toggle: () => set((state) => ({ darkMode: !state.darkMode }))
  }),
  {
    name: 'settings', // localStorage key 이름
  }
))

상태 변경 함수나, 상태가 여러개 있다면?

여러 상태 사용하는 예시 

// src/stores/useAppStore.js
import { create } from 'zustand';

const useAppStore = create((set) => ({
  // 상태들
  count: 0,
  isLoggedIn: false,
  username: '',

  // 상태 바꾸는 함수들
  increase: () => set((state) => ({ count: state.count + 1 })),
  login: (username) => set(() => ({ isLoggedIn: true, username })),
  logout: () => set(() => ({ isLoggedIn: false, username: '' })),
}));

export default useAppStore;

 

사용 방법 (리액트 컴포넌트에서)

import useAppStore from './stores/useAppStore';

function MyComponent() {
  const { count, increase, isLoggedIn, login, logout } = useAppStore();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increase}>+1</button>

      {!isLoggedIn ? (
        <button onClick={() => login('Jane')}>Login</button>
      ) : (
        <div>
          <p>Welcome, {useAppStore.getState().username}!</p>
          <button onClick={logout}>Logout</button>
        </div>
      )}
    </div>
  );
}

 

store 코드 설명

더보기

구조 먼저 보기

increase: () => set((state) => ({ count: state.count + 1 })),
login: (username) => set(() => ({ isLoggedIn: true, username })),
logout: () => set(() => ({ isLoggedIn: false, username: '' })),

위에 있는 각각은 함수예요.
괄호 () 안에는 그 함수에 전달할 매개변수(파라미터) 를 적어요.


✔️ 하나씩 해석해볼게요:

1. increase: () => set((state) => ({ count: state.count + 1 }))

  • increase는 매개변수가 필요 없어요.
  • 그냥 현재 state.count 값을 1 올리기만 하면 되니까 괄호 안이 비어 있어요.

→ 그래서 ()는 아무것도 받지 않는 함수를 뜻해요.


2. login: (username) => set(() => ({ isLoggedIn: true, username }))

  • login은 사용자의 이름(또는 ID) 같은 정보를 받아서 로그인 상태를 저장해야 해요.
  • 그래서 (username) 이라는 매개변수(parameter) 를 받는 거예요.

→ 즉, 이 함수는 호출할 때 이렇게 써요: login('Jane')


3. logout: () => set(() => ({ isLoggedIn: false, username: '' }))

  • logout은 아무 정보도 필요 없이 로그아웃만 시키면 되니까 매개변수가 없어요.

 정리하자면:

  • () 안에는 그 함수가 필요로 하는 값(매개변수) 을 넣어요.
    • 필요하면 (username)처럼 이름을 정해서 받고,
    • 필요 없으면 그냥 ()만 써요.

갑자기 매개 변수 개념도 헷갈리기 시작..

💡 매개변수란?

함수가 사용할 수 있도록 '외부에서 주는 값'이야.

함수를 만들 때 괄호 ( ) 안에 쓰는 이름이 매개변수(parameter) 예요.

예시:

function sayHello(name) {
  console.log('Hello, ' + name);
}
  • 여기서 name이 매개변수(parameter) 예요.
  • 이 함수는 name이라는 이름으로 어떤 값을 받을 준비를 하고 있는 거예요.

💡 함수 호출할 땐?

sayHello('Jane');
  • 이때 'Jane'은 인자(argument) 라고 해요.
  • name이라는 매개변수 자리에 들어갈 실제 값이죠!

🎨 그림처럼 이해하기

function greet(who) {
  console.log("Hi " + who);
}

greet("Jane");
  • who → 매개변수(parameter) : 함수를 만들 때 정의한 이름
  • "Jane" → 인자(argument) : 함수를 부를 때 전달하는 실제 값

✔️ Zustand 예제로 다시 연결하면:

login: (username) => set(() => ({ isLoggedIn: true, username })),
  • 여기서 username은 매개변수(parameter) 에요.
  • 함수 실행할 때 login("Jane") 이런 식으로 값을 넘기면
  • username 자리에 "Jane"이 들어가요.

 

 

set((state) => ({ count: state.count + 1 })) // 함수형 업데이트
vs
set({ loading: true }) // 직접 값 전달

이 두 코드의 차이가 뭐지? 왜 set 함수 쓰는 법이 다르지?

더보기

결론부터 말하면:

set 함수는 두 가지 방식으로 사용할 수 있기 때문이야!


🔍 두 가지 방식 설명

 1. 객체 직접 전달

set({ loading: true });
  • 그냥 새로운 상태를 통째로 넘기는 방식이야.
  • 이 경우에는 이전 상태는 신경 안 써도 될 때 사용해.
  • 상태를 "단순히 바꿔치기" 할 때 좋아!

 2. 함수형 업데이트

set((state) => ({ count: state.count + 1 }));
  • 현재 상태 state를 기반으로 새로운 상태를 계산할 때 사용해.
  • 상태를 누적하거나 이전 값을 참고할 때 꼭 필요해.
  • 예: count += 1, list.push(...)처럼 이전 상태가 필요할 때!

✨ 예시

// 버튼 클릭 시 로딩 상태 설정
startLoading: () => set({ loading: true })

// 버튼 클릭 시 카운트 증가
increase: () => set((state) => ({ count: state.count + 1 }))

 


zustand는 provider로 감쌀 필요 없어?

더보기

Zustand는 Provider로 감쌀 필요 없어?

✔️ 네! Zustand는 Provider가 필요 없습니다.

이게 zustand의 가장 큰 장점 중 하나예요.

  • React 트리 어딘가에 따로 <Provider>로 감싸는 구조 필요 없음
  • 어떤 컴포넌트든 그냥 useStore() 훅만 쓰면 전역 상태 접근 가능

예:

// 그냥 아무 곳에서나
const count = useStore((state) => state.count)

 

보너스: 왜 이게 좋은가요?

  • 렌더링 최적화: Context는 전역 상태 하나 바뀌면 모든 구독 컴포넌트가 다시 렌더링됨 😵
    → Zustand는 구독 단위로만 리렌더링되기 때문에 성능이 훨씬 좋음
  • 간결한 코드: boilerplate 없이 바로 store 생성 + 사용

주의! zustand 상태 안에.. useEffect를 사용 하면 안됌

더보기

useEffect는 zustand 스토어 안에서는 쓰면 안 돼!

 

useEffect는 React 컴포넌트 안에서만 작동하는 훅이야.
근데 zustand store는 React 컴포넌트가 아니야, 그냥 JavaScript 함수야.

그래서 아래처럼 하면 에러 나:

// ❌ 이렇게 하면 안 돼
const useUserStore = create((set) => {
  useEffect(() => {
    // 뭔가 하려고 해도 작동 안 함
  }, []);

  return {
    user: null,
    ...
  };
});

올바른 패턴: useEffect는 컴포넌트에서 호출하고, zustand의 메서드만 불러!

// useUserStore.js
const useUserStore = create((set) => ({
  user: null,
  fetchUser: async () => {
    const result = await getUserInfo();
    set({ user: result?.user || null });
  },
}));

// SomeComponent.jsx
import { useEffect } from "react";
import useUserStore from "@/stores/useUserStore";

export default function SomeComponent() {
  const fetchUser = useUserStore((state) => state.fetchUser);

  useEffect(() => {
    fetchUser(); // ✅ 여기서 호출!
  }, []);

  return <div>...</div>;
}

출처 : zustand 공식 문서, 챗지피티