상태 관리, 왜 그리고 어떻게 하고 있나요?

55555-Jyeon·2024년 8월 25일
21

Let's

목록 보기
12/12
post-thumbnail
post-custom-banner

프로젝트를 진행하면 초반에 어떤 상태 관리 라이브러리를 사용할지에 대해 얘기를 나눠보게 됩니다. 아직 배우고 있는 입장에서 새로운 것을 사용해보고 싶은 마음 반, 익숙한 것을 사용하고 싶은 마음이 반 드는 것 같습니다.
그러던 중 이런 식으로 새로운 라이브러리들을 어느 정도 찍먹해보고 나면 어떤 걸 사용하게 될까하는 생각이 들었습니다.

내 프로젝트의 성격과 라이브러리의 특징이 잘 맞아떨어지는 것이 최선의 선택일테니 이번 기회에 라이브러리들의 특징을 간단하게 살펴보면 좋을 것 같아 끄적이게 되었습니다...👀



💡 상태

상태(State)의 사전적 의미

사전에 상태(state)에 대해서 검색을 해보면 아래와 같이 나옵니다 :


CS의 상태(State)

일반적으로 CS(컴퓨터 공학)에서 말하는 상태(state)는 어떤 시스템이나 프로그램이 특정 시점에 가지고 있는 모든 정보나 조건을 말하며 시스템의 동작은 이 상태에 따라 결정됩니다.

🔎 wikipedia에서 더 자세한 내용 읽어보기


리액트의 상태(State)

리액트에서의 상태(State)는 컴퓨터 과학의 기본 개념에서 유래되었으며 특정 시점에 컴포넌트가 가지고 있는 데이터나 정보를 의미합니다.
이 상태는 컴포넌트의 렌더링 결과를 결정하며, 사용자 입력, 이벤트에 따라 값이 변할 수 있습니다.

💡 기본적으로 웹 애플리케이션에서 상태는 상호 작용이 가능한 모든 요소의 현재 값을 의미


상태의 종류

① 전역상태 (Global State)

  • 프로젝트 전체에 영향을 끼치는 상태
  • 여러 컴포넌트에서 쉽게 접근 O
  • ex) 사용자 인증 정보, 테마 설정 등

② 컴포넌트 간 상태 (Cross Component State)

  • 여러 컴포넌트에서 공유되지만, 전역 상태로 관리될 필요는 없는 상태
  • 주로 부모-자식 간 또는 형제 컴포넌트 간에 공유
  • Props로 전달되기 때문에 props drilling 주의 필요
  • ex) 모달의 열림/닫힘 여부 등

③ 지역상태 (Local State)

  • 특정 컴포넌트 안에서만 관리되는 상태
  • 다른 컴포넌트들과 데이터를 공유 X
  • ex) 사용자가 입력한 텍스트, form field의 값 등

상태의 분류

웹 애플리케이션에서 상태로 분류가 가능한 것들은 크게 아래 네 가지 항목으로 분류할 수 있습니다.

① UI

  • 사용자 인터페이스(UI)의 시각적 요소와 관련된 상태
  • UI 요소의 가시성, 활성화 상태, 스타일 등과 관련된 데이터

② URL

  • 브라우저에서 관리되고 있는 상태 값
  • 사용자의 라우팅에 따라 변경되며, URL의 쿼리 파라미터나 해시가 이 상태를 포함 가능
  • ex) 페이지의 경로, 검색 쿼리, 필터 조건 등

③ form

  • 폼과 관련된 상태 값
  • ex) 로딩 중인지(loading), 현재 제출됐는지(submitted), 접근이 불가능한지(disabled), 값이 유효한지(validity), 필드의 값 등

④ server data

  • 클라이언트에서 서버로 요청해 가져온 값/데이터
  • ex) API 요청


🤷🏻‍♀️ 상태 관리의 필요성

상태 관리는 리액트 애플리케이션에서 중요한 개념으로,
애플리케이션의 동작과 사용자 인터페이스를 동적으로 업데이트하는 데 필수적입니다.
또한 복잡한 상태 관리 요구 사항을 충족하기 위해 적절한 상태 관리 전략을 선택하는 것이 중요합니다.


1️⃣ 일관된 데이터 공유

상태 관리를 통해 애플리케이션의 컴포넌트들이 일관된/동일한 데이터를 공유할 수 있습니다. 이를 통해 사용자의 상호작용에 즉시 반응할 수 있으며, 전체 애플리케이션의 일관성을 유지할 수 있습니다.

2️⃣ 애플리케이션 동작 결정

상태는 애플리케이션의 동작을 결정하는 핵심 요소입니다. 컴포넌트의 상태가 변경될 때, 리액트는 해당 컴포넌트를 리렌더링하여 사용자에게 최신 정보를 제공하고, 애플리케이션의 동작을 최신 상태로 유지합니다.

3️⃣ 성능과 유지보수성 향상

효과적인 상태 관리는 애플리케이션의 성능과 유지보수성을 향상시킬 수 있습니다. 잘 설계된 상태 관리 시스템은 코드의 복잡성을 줄여주기 때문에 오류의 예방 및 상태의 쉬운 추적과 관리를 용이하게 해줍니다.

4️⃣ 규모 확장에 대응

애플리케이션의 규모가 커지면서 여러 컴포넌트 간에 상태를 공유하고 관리해야 할 필요가 커집니다. 기본적인 상태 관리 방법만으로는 이러한 복잡한 상황을 처리하기 어려울 수 있으며, 이를 해결하기 위해 아래에 설명할 상태 관리 라이브러리와 패턴이 추가적으로 필요할 수 있습니다.



🗂️ 상태 관리

리액트에서 상태 관리는 컴포넌트의 state와 props를 통해 이루어집니다.

state는 컴포넌트 내부에서 관리되는 데이터이며, props는 부모 컴포넌트로부터 전달받은 데이터입니다.

state는 setState 함수를 통해 업데이트되며, 이 과정에서 리액트는 컴포넌트를 자동으로 리렌더링하여 변경된 데이터를 사용자에게 보여줍니다.

효율적인 상태 관리를 위해서는 다음과 같은 사항을 고려해야 합니다:


고려 사항 및 방법


Q1. 상태를 어디에 두는 것이 좋을까?
A1. 로컬 상태
상태가 특정 컴포넌트에만 필요한 경우, 해당 컴포넌트 내부에서 로컬 상태로 관리


A2. 전역 상태
여러 컴포넌트에서 접근하거나 수정해야 하는 상태는 전역 상태로 관리 

A3. URL 상태
라우팅에 따라 변경되는 상태는 URL 쿼리 파라미터나 해시를 통해 관리 가능
사용자의 페이지 탐색 상태를 저장하고 복원하는 데 유용

Q2. 상태가 유효한 범위를 어떻게 제한할까?
A1. 컴포넌트의 책임 분리
각 컴포넌트는 자신의 상태와 관련된 데이터만 관리하도록 설계
컴포넌트 간의 의존성 ↓ 
상태가 필요한 컴포넌트에서만 상태를 관리

A2. 상태 분할
큰 상태 객체를 여러 개의 작은 상태로 나누어 관리
상태의 범위 ↓, 상태 변경의 영향을 최소화

A3. 상위 컴포넌트에서 상태 관리
상태가 여러 하위 컴포넌트에서 필요할 경우 상위 컴포넌트에서 상태를 관리
하위 컴포넌트에 필요한 데이터만 전달하는 방식으로 유효 범위 제한 

Q3. 상태의 변화에 따라 변경돼야 하는 자식 요소들은 어떻게 이 상태의 변화를 감지할 수 있을까?
A1. Props 전달
상위 컴포넌트에서 상태를 관리, 자식 컴포넌트에 props를 통해 상태 값을 전달
자식 컴포넌트는 전달받은 props가 변경될 때마다 리렌더링

A2. Context API
전역 상태를 Context API를 사용하여 제공
하위 컴포넌트는 Context를 구독하여 상태 변화에 반응

A3. 상태 관리 라이브러리
상태 변화가 발생할 때 자동으로 관련 컴포넌트를 업데이트

Q4. 상태 변화가 일어남에 따라 즉각적으로 모든 요소들이 변경되는 현상은 어떻게 방지할 수 있을까?
A1. 메모이제이션
React.memo, useMemo, useCallback 훅으로 컴포넌트와 함수의 결과를 메모이제이션
상태가 변경될 때만 렌더링 발생 → 불필요한 리렌더링 방지

A2. 상태 분리
컴포넌트의 상태를 세분화
상태가 변경될 때 관련된 컴포넌트만 리렌더링하도록 관리

A3. 성능 최적화
컴포넌트의 shouldComponentUpdate 메서드로 특정 조건에서만 리렌더링되도록 설정
useEffect 훅의 의존성 배열을 설정


📚 전역 상태 관리 라이브러리

효율적인 상태 관리는 애플리케이션의 다양한 레벨에서 상태를 쉽게 접근하고 업데이트할 수 있도록 하는 데 필수적입니다. 리액트 애플리케이션이 커지고 복잡해짐에 따라, 기본적인 state와 props만으로는 상태 관리가 어려울 수 있습니다.

이러한 문제를 해결하기 위해 리액트 생태계에서는 여러 가지 상태 관리 라이브러리와 패턴이 제안되었습니다. 이들 라이브러리는 복잡한 상태를 효율적으로 관리하고, 애플리케이션의 상태를 쉽게 유지보수할 수 있도록 도와줍니다.

✌🏻 상태 관리 라이브러리의 조건 2가지

① 어떠한 상태를 기반으로 다른 상태를 만들어낼 수 있는가
② 필요에 따라 이런 상태 변화를 최적화할 수 있는가

아래에는 리액트에서 널리 사용되는 상태 관리 라이브러리들을 소개합니다 :

Redux

리덕스(redux)는 flux 구조를 구현하기 위해 만들어진 라이브러리 중 하나로 Elm 아키텍처를 도입했다는 특징이 있습니다.

Flux

2014년 즈음 리액트의 등장과 비슷한 시기에 Flux 패턴과 함께 이를 기반으로 한 Flux 라이브러리가 등장하게 됩니다.

리액트는 단방향 데이터 바인딩을 기반으로 한 라이브러리입니다.
flux 패턴 역시 리액트와 마찬가지로 단방향 데이터 흐름을 정의하기 때문에 잘 맞았습니다.


단방향 데이터 바인딩
🟢 PROS
- 데이터의 추적이 쉬움
- 코드의 가독성 용이
🔴 CONS
- 코드의 양이 많아짐

Elm 아키텍처

Elm는 flux와 마찬가지로 데이터 흐름을 세 가지로 분류하고 단방향으로 강제해 웹 애플리케이션의 상태를 안정적으로 관리하고자 한 아키텍처입니다.


기본 원리

1️⃣ 하나의 상태 객체를 스토어에 저장

리덕스는 애플리케이션의 모든 상태를 하나의 중앙 저장소(Store)에 보관

2️⃣ 디스패치(dispatch)로 reducer 함수 호출

상태를 변경하려면, 액션(action)을 디스패치하여 리듀서(reducer) 함수를 호출
리듀서는 현재 상태와 액션을 받아 새로운 상태 객체를 반환

3️⃣ 새로운 상태를 전파

리듀서에서 반환된 새로운 상태 객체는 스토어에 저장
애플리케이션의 모든 구독된 컴포넌트에 전파

사용해보자

먼저 액션(action)은 상태를 변경하는 요청을 정의합니다.
setName이라는 액션을 사용하여 이름을 변경할 겁니다.

// actions.js
export const SET_NAME = "SET_NAME";

export const setName = (name) => ({
  type: SET_NAME,
  payload: name,
});

리듀서(reducer)는 액션을 처리하고 상태를 업데이트하는 함수입니다.
SET_NAME 액션이 호출되면 이름이 변경된 새로운 상태를 반환할 겁니다.

// reducer.js
import { SET_NAME } from "./actions";

const initialState = {
  name: "Jane Doe",
  age: 20,
};

export const userReducer = (state = initialState, action) => {
  switch (action.type) {
    case SET_NAME:
      return {
        ...state,
        name: action.payload,
      };
    default:
      return state;
  }
};

스토어(store)에서는 애플리케이션의 전역 상태를 관리합니다.
createStore 함수를 사용하여 리듀서를 기반으로 스토어를 생성합니다.

// store.js
import { createStore } from "redux";
import { userReducer } from "./reducer";

const store = createStore(userReducer);

export default store;

상위 컴포넌트에서 Provider를 통해 리덕스 스토어를 하위 컴포넌트에 제공합니다.

function App() {
  return (
    <div className="container">
      <Provider store={store}><UserProfile />
      </Provider>
    </div>
  );
}

현재 사용자 이름과 나이를 화면에 표시해주고 아래 버튼 클릭 시 각각 "Jane Doe" 또는 "John Doe"로 이름을 변경되도록 합니다.

useSelector를 통해 전역 상태에서 현재 이름과 나이를 가져오고,
useDispatch를 사용해 이름을 변경하는 액션을 디스패치하고 있습니다.

const UserProfile = () => {
  const user = useSelector((state) => state);
  const dispatch = useDispatch();

  return (
    <div className="user-profile">
      <h1>User Profile</h1>
      <p>Name: {user.name}</p>
      <p>Age: {user.age}</p>
      <button onClick={() => dispatch(setName("Jane Doe"))}>
        Set Female name
      </button>
      <button onClick={() => dispatch(setName("John Doe"))}>
        Set Male name
      </button>
    </div>
  );
};

export default UserProfile;

위 코드의 실행 결과는 아래에서 확인 가능합니다 :


🟢 pros
  • 하나의 전역 상태로 props drilling 문제를 해결 가능
  • 스토어(store)가 필요한 컴포넌트는 connect만으로 접근 가능
🔴 cons
  • 하나의 상태를 바꾸는데 많은 로직 필요 (투머치 보일러플레이트)

🤔 provider가 여러 개라면?

가장 가까운 Provider의 값을 가져오게 됩니다.

Redux를 더욱 쉽게 사용하기 위한 RTK에 대한 게시글 보러 가기


적절한 상태 주입에 대한 고민

context API, useContext

props drilling 및 too much boilerplate 두 가지 단점을 잡기 위해 등장한 것이 바로 리액트의 context API 입니다.

props drilling을 해결하는데에는 리덕스(redux)도 있지만 리덕스의 투머치 보일러플레이트는 부담이 될 뿐만 아니라 컴포넌트 설계 시에도 큰 제약이 있기 때문입니다.
🤷🏻‍♀️ 어떤 제약?

① 작은 컴포넌트 및 간단한 상태 관리에도 과도한 구조적 복잡함
② 애플리케이션이 커질수록 유지보수가 어려움
③ 컴포넌트가 리덕스 스토어에 의존하게 되어 재사용성과 독립성이 저하
④ 상태를 전부 전역으로 관리하므로 특정 컴포넌트 테스트가 어려움


조금 더 자세히 알아보자

context

컨텍스트(context)는 props drilling을 극복하기 위해 등장한 개념입니다.

컨텍스트를 사용할 경우, props를 명시적으로 사용하지 않아도 선언한 모든 하위 컴포넌트에서 값을 사용할 수 있다는 특징이 있습니다.

useContext

useContext는 상위 컴포넌트에서 만들어진 컨텍스트를 함수 컴포넌트에서 사용할 수 있도록 만들어진 훅입니다.


사용해보자
// 01. create context
export const UserContext = createContext();

function App() {
  const [sampleUser, setSampleUser] = useState({ name: "Jane Doe", age: 20 });

  return (
    // 02. provide state to child components by Provider
    <div className="container">
      <UserContext.Provider value={{ sampleUser, setSampleUser }}><UserProfile />
        <UpdateName />
      </UserContext.Provider>
    </div>
  );
}

export default App;

UserContext의 Provider 안에 포함되는 자식 컴포넌트들은 value 안의 상태를 useContext 훅으로 가져다 쓸 수 있습니다.

// component 1
const UserProfile = () => {
  const { sampleUser } = useContext(UserContext);

  return (
    <div>
      <h1>User List</h1>
      <p>Name : {sampleUser.name}</p>
      <p>Age : {sampleUser.age}</p>
    </div>
  );
};

export default UserProfile;

// component 2
const UpdateName = () => {
  const { sampleUser, setSampleUser } = useContext(UserContext);

  const updateName = () => {
    setSampleUser({ ...sampleUser, name: "John Doe" });
  };

  return (
    <div>
      <button onClick={updateName}>Change Name</button>
    </div>
  );
};
export default UpdateName;

위 코드의 실행 결과는 아래에서 확인 가능합니다 :


🟢 pros
  • 전역 상태를 손쉽게 관리 가능
  • 깊이 있는 컴포넌트 트리에서도 직접적으로 상태 사용이 가능
  • 리덕스와 달리 간단한 API를 통해 상태를 관리하므로 설정 및 유지보수의 복잡성 ↓
🔴 cons
  • 컴포넌트의 구조가 복잡해질수록 컨텍스트 사용도 복잡해질 가능성 높음
  • 컴포넌트 내부에서 useContext 사용 시 재사용 불가
  • 해당 컨텍스트 사용 시 모든 컴포넌트가 리렌더링 될 수 있음 (성능 이슈)

🤔 context 사용 시 주의해야할 점이 있다면?

① context API는 상태 관리가 아닌 주입을 도와주는 기능이란 점
렌더링을 막아주는 기능이 없다는 점


2020, created by facebook company

recoil

리코일(recoil)은 페이스북에서 만든 리액트를 위한 상태 관리 라이브러리입니다.
훅의 개념으로 상태 관리를 시작한 라이브러리 중 하나이기도 합니다.

2020년에 처음 만들어졌으며, github 주소로 가보면 아직 정식으로 출시가 되지 않은 것을 확인할 수 있습니다.

리코일 팀에서는 리액트 18에서 제공될 기능들이 지원되기 전까지는 1.0.0을 릴리즈(release)하지 않을 것이라고 밝혔습니다.

따라서 실제 프로젝트에 리코일을 채택해 사용하기엔 안정성, 성능, 사용성 등이 보장되지 않을 수 있습니다.


조금 더 자세히 알아보자

RecoilRoot

리코일을 사용하기 위해 RecoilRoot를 애플리케이션의 최상단에 선언해야 합니다.

RecoilRoot는 리코일에서 생성되는 상태값을 저장하기 위한 스토어를 생성하기 때문에 최상단에 선언을 해야 합니다.

function App() {
	return <RecoilRoot>
      {/*other components*/}
      </RecoilRoot>
}
export default App;

리코일의 상태값은 RecoilRoot로 생성된 context의 스토어에 저장됩니다.
스토어의 상태값에 접근할 수 있는 함수들을 활용해 상태값을 변경할 수 있으며
값의 변경될 경우 해당 상태를 참조하고 있는 모든 하위 컴포넌트에게 알려줍니다.

atom

atom은 리코일에서 최소 상태 단위를 나타내는 개념입니다.
key와 default 값을 필수로 가지며 이 key 값은 다른 atom과 구별되는 식별자가 됩니다.

selector

selector(선택자)는 파생 상태(derived state)를 만들기 위한 도구입니다. 이는 여러 개의 atom을 조합하거나, 하나의 atom에서 특정 데이터를 가공하여 새로운 상태를 계산하는 데 사용됩니다.

즉, selector는 기존 상태를 기반으로 계산된 값을 반환하는 함수입니다.
이를 통해 컴포넌트에서 필요한 데이터만 선택적으로 사용할 수 있게 됩니다.


사용해보자

atom을 만들어 줍니다.
저는 파일을 분리했기 때문에 export를 했지만 한 파일에 전부 작성할 예정이라면 그냥 선언만 해주어도 무방합니다.

export const sampleNameState = atom({
  key: "nameState",
  default: "John",
});

리액트의 useState 대신 useRecoilState를 사용하면 됩니다.
안에는 atom을 생성했을 때 작성했던 식별자 key의 값을 넣어주면 됩니다.

const UserProfile = () => {
  const [name, setName] = useRecoilState(sampleNameState);
  const toggleName = () => {
    setName(name === "John" ? "Jane" : "John");
  };

  return (
    <div>
      <h1>{name}</h1>
      <button onClick={toggleName}>Toggle Name</button>
    </div>
  );
};
export default UserProfile;

최상단 컴포넌트에서 RecoilRoot로 recoil이 사용될 컴포넌트들을 감싸주면 됩니다.

function App() {
  return (
    <div className="container">
      <RecoilRoot>
        <UserProfile />
      </RecoilRoot>
    </div>
  );
}

export default App;

위 코드의 실행 결과는 아래에서 확인 가능합니다 :


🟢 pros
  • selector를 통해 복잡한 상태 구조를 쉽게 관리 가능
  • 각 atom이 독립적으로 동작하므로 성능 최적화에 유리
  • 리액트의 개념과 매우 잘 통합되어 있어 쉽게 사용 가능
🔴 cons
  • 정식 버전 미출시로 인한 안정성 확보의 어려움

recoil 공식 홈페이지에서 더 자세한 내용 보기


recoil보다 유연한

jotai

조타이(jotai)는 리코일의 atom 모델에 영감을 받아 만들어진 라이브러리입니다.

공식 문서에도 언급되어 있지만,
Jotai는 리액트의 Context API에서 발생하는 불필요한 리렌더링 문제를 해결하기 위해 개발되었으며, 메모이제이션이나 추가적인 최적화 없이도 불필요한 리렌더링을 방지하도록 설계되었습니다.

조타이는 상향식(bottom-up) 접근법을 취하고 있기 때문에 작은 단위의 상태(atom)를 상위로 전파할 수 있는 구조를 갖고 있습니다.


더 자세히 알아보자

atom

리코일에서 atom 모델에 영감을 받았기 때문에 조타이에도 동일하게 atom이 존재합니다.
차이가 있다면 조타이에서는 atom 하나로 파생된 상태도 만들 수 있다는 것입니다.

또한 조타이에서는 atom을 생성할 때 고유한 key 값을 전달해주지 않아도 됩니다.

const counterAtom = atom(0)

console.log("atom", counterAtom); 
/* 
"atom",
{
	init: 0,
    read: (get) => get(config),
    write: (get, set, update) =>
    	set(config, typeof update === "function" ? update(get(config)) : update
}
*/

조타이의 useAtomValue 함수에는 rerenderIfChanged라는 로직이 있습니다.
이 로직으로 인해 조타이에서는 atom의 값이 어디서 변경되든지 useAtomValue로 값을 사용하는 쪽에서는 항상 최신의 atom을 사용해 렌더링할 수 있습니다.

rerenderIfChanged가 일어나는 경우는 크게 두 가지가 있습니다 :

① 넘겨받은 atom이 reducer를 통해 스토어에 있는 atom과 달라졌을 때
② subscribe를 수행하고 있던 중 어디서 값이 변경되었을 때


사용해보자

atom으로 상태를 만들 수 있으며 리코일과 다르게 key 값을 넘겨주지 않아도 됩니다.

export const sampleNameAtom = atom("John");

생성된 상태는 useAtom으로 불러와 사용 가능합니다.

const UserProfile = () => {
  const [name, setName] = useAtom(sampleNameAtom);

  const toggleName = () => {
    setName(name === "John" ? "Jane" : "John");
  };

  return (
    <div>
      <h1>{name}</h1>
      <button onClick={toggleName}>Jotai Test</button>
    </div>
  );
};
export default UserProfile;

다른 라이브러리들과 달리 최상단을 provider라던가 context로 감쌀 필요가 없습니다.

function App() {
  return (
    <div className="container">
      <UserProfile />
    </div>
  );
}

export default App;

위 코드가 실행된 결과는 아래와 같습니다 :


🟢 pros
  • 간결한 API 및 사용
  • 타입 지원이 잘 되어 있음 (d.ts 제공)
  • 리액트 18의 변경된 API를 원활히 지원하며 현재 v2 버전 출시
  • recoil의 대안으로 많은 개발자들의 선택을 받음
🔴 cons
  • 상태 관리가 복잡해질 경우 추가적인 도구나 패턴이 필요할 수 있음
  • 상대적으로 작은 커뮤니티와 생태계

jotai 공식 홈페이지에서 더 자세한 내용 보기


redux보다 유연한

zustand

zustand는 리덕스(redux)에 영감을 받아 만들어진 라이브러리입니다.
따라서 zustand는 하나의 스토어(store)를 중앙 집중형으로 활용해 스토어 내부에서 상태를 관리하는 방식입니다.


더 자세히 알아보자

store

zustand의 깃허브를 살짝 훔쳐보면 관련된 코드를 볼 수 있습니다.

zustand의 store와 관련된 코드는 ./src/vanilla.ts에서, store를 리액트에서 사용할 수 있도록 도와주는 함수들은 ./src/react.ts에서 관리되고 있습니다.

바닐라 자바스크립트의 함수와 객체를 사용하여 상태를 관리하기 때문에 리덕스의 복잡한 구조를 갖추지 않아도 된다는 점에서 리덕스보다 유연하다고 볼 수 있습니다.
이러한 구조 덕분에 zustand는 리덕스에 비해 상태 관리 로직이 더욱 간단하고 직관적으로 작성됩니다.

또한 리액트의 useState와 유사한 setState 메커니즘을 사용하여 상태를 업데이트합니다. 이러한 구조는 상태 업데이트가 매우 직관적이며, 부분적인 상태 변경(partial state update)도 쉽게 수행할 수 있도록 도와줍니다.

// 64번째 줄 ~
  const setState: StoreApi<TState>['setState'] = (partial, replace) => {
    const nextState =
      typeof partial === 'function' 
        ? (partial as (state: TState) => TState)(state)
        : partial
    if (!Object.is(nextState, state)) {
      const previousState = state
      state =
        (replace ?? (typeof nextState !== 'object' || nextState === null))
          ? (nextState as TState)
          : Object.assign({}, state, nextState)
      listeners.forEach((listener) => listener(state, previousState))
    }
  }

partial은 state의 일부를 변경할 때, replace는 state를 완전히 새로운 값으로 대체할 때 사용됩니다. 이를 통해 state가 객체일 때 필요에 따라 유연하게 사용할 수 있습니다.

zustand github 방문하기


사용해보자

Zustand의 create 함수를 사용해 스토어를 생성합니다.

export const sampleUserStore = create((set) => ({
  name: "John",
  toggleName: () =>
    set((state) => ({
      name: state.name === "John" ? "Jane" : "John",
    })),
}));

생성한 스토어의 상태와 함수를 그대로 가져와 사용할 수 있습니다.

const UserProfile = () => {
  const { name, toggleName } = sampleUserStore();

  return (
    <div>
      <h1>{name}</h1>
      <button onClick={toggleName}>zustand sample</button>
    </div>
  );
};
export default UserProfile;

마찬가지로 별도의 provider나 context로 감쌀 필요가 없습니다.

function App() {
  return (
    <div className="container">
      <UserProfile />
    </div>
  );
}

위 코드의 실행 결과는 아래와 같습니다:


🟢 pros
  • 간결하고 사용하기 쉬운 API
  • 불필요한 리렌더링 방지
  • 작은 크기와 가벼운 라이브러리
  • React와의 원활한 통합 및 유연성
🔴 cons
  • 상대적으로 작은 생태계와 커뮤니티 지원
  • 대규모 프로젝트에서 상태 관리의 구조화 부족 가능성 존재

zustand 공식 홈페이지에서 더 자세한 내용 보기



전체적인 내용 요약

🤔 언제 무엇을 사용할까

① Context API
  • 특징
    상태를 공유하기 위해 별도의 상태 관리 라이브러리를 사용하지 않아도 됨
  • 한계
    상태 변화가 자주 발생할 경우, Context를 사용하는 컴포넌트들이 과도하게 렌더링될 수 있음
  • 추천 상황
    • 전역 상태를 컴포넌트 트리 전체에 전달할 때
    • 작고 단순한 애플리케이션일 때
    • 애플리케이션 전역에서 필요한 상태를 관리할 때
      ex) 테마 설정, 사용자 인증 정보 등
② Redux
  • 특징
    상태를 한 곳에서 관리하고, 액션과 리듀서를 통해 상태를 변경
  • 한계
    보일러플레이트 코드가 많고 설정이 복잡할 수 있음
  • 추천 상황
    • 대규모 애플리케이션일 때
    • 복잡한 상태와 액션 흐름을 체계적으로 관리해야 할 때
    • 데이터가 여러 컴포넌트 간에 공유될 때
    • 복잡한 비즈니스 로직이 있을 때
③ Recoil
  • 특징
    상태를 원자(atom)와 파생 상태(selector)로 나누어 관리
  • 한계
    상태 관리가 간단하지만, 아직 상대적으로 새로운 라이브러리로 사용 사례가 제한적일 수 있음
  • 추천 상황
    • 중간 규모의 애플리케이션일 때
    • 상태 관리가 비교적 간단하면서도 유연하게 동작해야할 때
④ Zustand
  • 특징
    훅을 사용하여 상태를 관리하며, 보일러플레이트 코드가 적음
  • 한계
    기능이 간단하여 복잡한 상태 관리에는 한계가 있을 수 있음
  • 추천 상황
    • 애플리케이션의 규모가 작고 간단할 때
    • 상태 관리가 간단할 때
⑤ Jotai
  • 특징
    상태를 원자(atom) 단위로 관리하며, 상태 변화에 따른 렌더링을 효율적으로 처리
  • 한계
    다양한 기능을 제공하지만, 개발자에게 새로운 패러다임을 요구할 수 있음
  • 추천 상황
    • 상태를 최소 단위로 관리하고 싶을 때
    • 상태의 변경 사항을 세밀하게 제어하고 싶을 때
    • 높은 성능을 요구할 때

표로 보기

라이브러리 복잡도 사용성 커뮤니티 크기 애플리케이션 규모 러닝 커브 타입스크립트 지원 미들웨어 지원
Context API 낮음 간편 O 작음 낮음 O X
Redux 높음 복잡 O 대규모 높음 O O
Recoil 중간 간편 중간 중간 O X
Zustand 낮음 간편 작음 낮음 O X
Jotai 중간 간편 중간 중간 O X


상태 관리에 대해 블로그들을 찾아보다 발견한

👍 좋은 상태 관리


1️⃣ 상태를 불변 객체로!

상태를 불변 객체로 관리하면 상태 업데이트의 복잡성을 줄이고 성능 최적화에 도움이 됩니다.

2️⃣ 상태를 최소화하고 필요한 곳에서만!

불필요한 상태는 애플리케이션의 복잡성을 증가시키고 버그의 원인이 될 수 있습니다.
필요한 상태만을 관리하여 코드의 간결함을 유지하는 것이 좋습니다.

3️⃣ 상태 업데이트 로직을 컴포넌트 외부에!

상태 업데이트 로직을 컴포넌트 외부로 분리하면 코드의 재사용성을 높이고 테스트를 용이하게 할 수 있습니다. 이렇게 하면 컴포넌트는 UI를 표현하는 역할에 집중할 수 있습니다.




References.

[🌎 Officials]

[📚 Books]

  • 모던 리액트 Deep Dive

[👩🏻‍💻 Blogs]

[🎥 Videos]

profile
🥞 Stack of Thoughts
post-custom-banner

0개의 댓글