상태 관리 라이브러리

DU·2025년 4월 19일
0

🌟 React 상태 관리 완벽 가이드

리액트(React)는 컴포넌트 기반 UI 라이브러리로, 컴포넌트 내부 상태(state)를 다루는 기능(useState, useReducer 등)을 기본 제공합니다. 하지만 애플리케이션 규모가 커질수록, 컴포넌트 간 데이터를 효율적으로 공유하고 복잡도를 관리하기 위해 별도의 상태 관리 라이브러리를 도입하는 것이 일반적입니다.
이 글에서는 상태 관리의 기초 개념부터 언제, , 어떻게 라이브러리를 쓰는지, 주요 원리종류, 마지막으로 Redux를 활용한 실전 예제까지 하나씩 살펴보겠습니다.


목차

  1. 상태(state)란 무엇인가?
  2. 상태 관리가 필요한 이유 & 언제 사용하는가?
  3. 상태 관리 라이브러리를 왜 사용하는가?
  4. 주요 상태 관리 라이브러리 종류 비교
  5. 상태 관리의 기본 원리
  6. Redux 심화: 구조와 사용법
    • 6.1. 단방향 데이터 흐름(Flux 아키텍처)
    • 6.2. store: 중앙 저장소
    • 6.3. slice/reducer: 상태 모듈화
    • 6.4. Provider: React와 연결
    • 6.5. Hooks(useSelector, useDispatch)
    • 6.6. 비동기 처리(createAsyncThunk)
  7. 마무리 및 추천 학습 자료

1️⃣ 상태(state)란 무엇인가?

  • 정의: 컴포넌트 내부에서 변할 수 있는 데이터, 즉 UI와 로직을 연결하는 값
  • 예시
    const [count, setCount] = useState(0);
    const [text, setText] = useState("");
    • 버튼 클릭 시 숫자(count)가 오르내리고
    • 사용자 입력(input) 값을 관리

2️⃣ 상태 관리가 필요한 이유 & 언제 사용하는가?

  1. 컴포넌트 간 데이터 공유가 필요할 때

    • 로그인 상태, 장바구니 아이템, 테마 설정 등
    • props로 매번 전달하면 깊이가 깊어질수록 코드 복잡도↑ (prop drilling 문제)
  2. 전역 또는 공통으로 사용되어야 할 데이터가 있을 때

    • 사이트 전체에서 사용하는 유저 정보, 알림 토큰, 다크 모드, 언어 설정 등
  3. 복잡한 상태 로직을 모듈화/테스트하기 위해

    • 단순 useState만으로는 액션 흐름을 추적하기 어려움
    • 상태 업데이트 로직을 분리해 유지보수성↑
  4. 비동기 작업 관리

    • 서버 API 호출, 로딩/에러 상태 관리
    • 복잡한 비동기 로직을 체계적으로 다루고 싶을 때

3️⃣ 상태 관리 라이브러리를 왜 사용하는가?

  • 규모 확장성: 프로젝트가 커질수록 상태와 액션이 늘어나고, 이를 통합 관리하는 구조가 필요
  • 예측 가능성: 상태 변경이 모두 “액션 → 리듀서” 흐름을 거치므로, 로그나 디버깅이 쉬움
  • 공동 작업 편의: 팀원 간 역할 분리(프론트엔드 작업, 상태 로직 작성 등)가 수월
  • 클린 아키텍처: UI, 로직, 데이터 계층을 명확히 분리

4️⃣ 주요 상태 관리 라이브러리 종류 비교

라이브러리특징장단점
ReduxFlux 패턴 기반, 큰 커뮤니티, 강력한 에코시스템✅ 예측 가능성, DevTools
❌ 보일러플레이트 코드
Zustand간결한 API, 작은 번들 크기✅ 직관적, 러닝 커브 낮음
❌ 커뮤니티·에코시스템 상대적 작음
RecoilFacebook 개발, atom/selector 개념✅ React 전용, 성능 최적화
❌ 아직 발전 중
Jotai단일 원자(atom) 모델, 동시성 지원✅ 간단, 모던 API
❌ 러닝 커브 약간 있음
MobX관찰(observable) 기반, 자동 리렌더링✅ 코드 간결, 반응형
❌ 복잡한 디버깅

5️⃣ 상태 관리의 기본 원리

  1. 단방향 데이터 흐름
    • UI → Action → Reducer → New State → UI
  2. 불변성(Immutable State)
    • 상태 객체를 직접 수정하지 않고, 항상 새 복사본을 반환
  3. 순수 함수로서의 리듀서
    • 같은 입력(state, action)이면 항상 같은 출력 반환
  4. 모듈화
    • 기능별로 slice/reducer 분리 → 유지보수 용이

6️⃣ Redux 심화: 구조와 사용법

6.1 단방향 데이터 흐름 (Flux 아키텍처)

UI ──▶ Action ──▶ Reducer ──▶ Store ──▶ UI
  • Action: 상태 변경 의도를 나타내는 객체
  • Reducer: action에 따라 state를 반환하는 순수 함수
  • Store: 애플리케이션 전체 상태를 보관하는 중앙 저장소

6.2 store: 중앙 저장소

// src/app/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    // user: userReducer,
    // todo: todoReducer,
  },
});
  • 왜? 모든 상태를 한 곳에 모아두면, 어디서나 일관되게 꺼내 쓰고, 변경 이력을 추적할 수 있음

6.3 slice/reducer: 상태 조각 모듈화

// src/features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const initialState = { value: 0 };

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => { state.value += 1; },  // Immer 덕분에 직접 수정처럼 보이지만, 불변성 유지
    decrement: (state) => { state.value -= 1; },
    incrementByAmount: (state, action) => { state.value += action.payload; },
  },
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
  • 왜? 액션 생성자와 리듀서를 한 곳에 묶어 가독성과 유지보수성을 높임

6.4 Provider: React와 Redux 연결

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux';
import { store } from './app/store';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);
  • 왜? React 트리 전체에 store를 “주입”하여, 하위 컴포넌트 어디서든 접근 가능하게 함

6.5 Hooks: useSelector & useDispatch

  • useSelector: store에서 원하는 상태를 추출
    const count = useSelector((state) => state.counter.value);
  • useDispatch: 액션을 디스패치(dispatch)하여 상태 변경
    const dispatch = useDispatch();
    dispatch(increment());
    dispatch(incrementByAmount(5));

6.6 비동기 처리: createAsyncThunk

// features/todo/todoSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

export const fetchTodos = createAsyncThunk(
  'todos/fetchTodos',
  async () => {
    const res = await fetch('/api/todos');
    return res.json();
  }
);

const todoSlice = createSlice({
  name: 'todos',
  initialState: { items: [], status: 'idle', error: null },
  reducers: { /* 동기 액션 */ },
  extraReducers: (builder) => {
    builder
      .addCase(fetchTodos.pending, (state) => { state.status = 'loading'; })
      .addCase(fetchTodos.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.items = action.payload;
      })
      .addCase(fetchTodos.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message;
      });
  },
});
export default todoSlice.reducer;
  • 왜? API 호출 로직과 상태(loading, success, error)를 간결하게 관리

7️⃣ 마무리 및 추천 학습 자료

0개의 댓글