리액트 - Redux 상태 관리

심영민·2025년 5월 2일
0

유레카

목록 보기
30/33

Redux 개념

리덕스는 위 사진과 같이 하나의 STORE를 두어 모든 상태를 저장하고 읽으며 비교하는 라이브러리이다.

  • 정의: 자바스크립트 애플리케이션을 위한 예측 가능한 상태(state)컨테이너. 애플리케이션의 전역 상태를 중앙에서 효율적으로 관리하기 위한 라이브러리.

  • 목적: 복잡한 애플리케이션에서 컴포넌트 간의 상태 공유 및 관리를 용이하게 함. 상태 변화를 추적하고 디버깅하는 데 도움.

  • 핵심 원칙:

    • Single Source of Truth: 애플리케이션의 전체 상태는 하나의 스토어(Store)에 저장됨.

    • State is Read-Only: 상태는 직접 변경 불가. 오직 액션(Action)에 의해서만 변경 요청 가능.

    • Changes are Made with Pure Functions: 상태 변경은 순수 함수인 리듀서(Reducer)에 의해서만 이루어짐.

Redux 주요 구성 요소

  • Store (스토어): 애플리케이션의 단 하나뿐인 상태 저장소. 현재 상태 보유, 액션 디스패치, 상태 변화 구독 기능 제공.
    (디스패치는 액션을 발생시키는 함수. 상태 변경을 위한 명령을 전달하는 역할을 함)

  • Action (액션): 상태에 어떤 변화가 필요한지를 나타내는 객체. { type: 'ACTION_TYPE', ...payload } 형태. type은 필수.

  • Reducer (리듀서): 현재 상태와 디스패치된 액션을 인자로 받아 새로운 상태를 반환하는 순수 함수. 상태 불변성 유지 필수.

  • Dispatch (디스패치): 액션을 발생시키는 함수. store.dispatch(action) 호출을 통해 액션을 스토어로 전달.

Redux 데이터 흐름

  • UI에서 상태 변경 이벤트 발생.

  • 이벤트 핸들러 내에서 dispatch 함수 호출하여 액션 전달.

  • 스토어가 디스패치된 액션을 받아 해당 액션을 처리할 리듀서 실행.

  • 리듀서는 현재 상태와 액션을 기반으로 새로운 상태 생성 및 반환.

  • 스토어의 상태가 새로운 상태로 업데이트.

  • 상태 변화를 구독하고 있는 컴포넌트들 리렌더링.

Redux 비동기 작업 처리

  • 필요성: Redux의 기본 상태 변경(리듀서)은 동기적으로만 동작하는데 API 호출 등 시간이 걸리는 비동기 작업은 리듀서가 직접 처리하기 어려움 -> 비동기 처리 필요!

  • 해결책: 미들웨어(Middleware) 도입.

    • 미들웨어는 디스패치된 액션이 리듀서에 도달하기 전에 그 사이에서 추가 작업 수행.
    • 비동기 작업이 완료될 때까지 기다린 후, 결과에 따라 다른 액션을 디스패치하는 역할.
  • 주요 비동기 미들웨어: Redux Thunk(간단한 비동기), Redux Saga(복잡한 비동기 흐름 관리) 등.

  • 간단한 비동기 처리 흐름:

    • 비동기 시작 액션 디스패치 (예: 데이터 요청 액션).
    • 미들웨어가 이 액션을 받아 비동기 작업 실행.
    • 작업 완료(성공/실패) 시, 미들웨어가 결과와 함께 새로운 액션 디스패치.
    • 새로운 액션이 리듀서에 전달되어 최종 상태 업데이트.

Redux 설정 및 코드 흐름 (feat. React-Redux)

  • 설치: Redux Toolkit인 npm install @reduxjs/toolkit react-redux (Toolkit이 BEST!!)

  • Store 생성: createStore (Redux) 또는 configureStore (Redux Toolkit) 함수 사용. 리듀서 함수 연결.

  • Provider 설정: react-reduxProvider 컴포넌트로 애플리케이션의 최상위 컴포넌트 감싸기. 생성한 스토어를 store prop으로 전달. 모든 하위 컴포넌트에서 스토어 접근 가능.

  • 컴포넌트에서 상태 조회: react-reduxuseSelector 훅 사용. 스토어 상태 중 필요한 부분 선택.

  • 컴포넌트에서 액션 디스패치: react-reduxuseDispatch 훅 사용. 디스패치 함수 가져와 액션 전달.

Redux Toolkit createSlice 및 Counter 예시 코드

createSlice 개념

  • Redux Toolkit 기능으로, 상태(state)의 한 조각(slice)과 관련된 액션 타입, 액션 생성 함수, 리듀서를 한 번에 정의하는 함수.

  • Redux 로직 작성 시 상용구 코드 감소 및 개발 편의성 향상 목적.

Counter 예시 코드

1. createSlice를 사용한 카운터 슬라이스 정의

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

export const counterSlice = createSlice({
  name: 'counter', // 슬라이스 이름
  initialState: { // 초기 상태
    count: 0,
    label: '카운터',
  },
  reducers: { // 상태 변경 함수들 (immer.js 자동 적용)
    increment: state => {
      state.count += 1; // 불변성 걱정 없이 직접 수정하는 것처럼 작성
    },
    // payload를 받는 increment 예시 추가
    incrementByAmount: (state, action) => {
      state.count += action.payload || 1; // action.payload로 전달된 값 사용
    },
    decrement: state => {
      state.count -= 1;
    },
    resetCount: state => {
      state.count = 0;
    },
  },
});

// 액션 생성자들 자동 생성, export
export const { increment, incrementByAmount, decrement, resetCount } = counterSlice.actions;

// 리듀서 자동 생성, export default
export default counterSlice.reducer;

createSlice는 아주 편한 도구다.

옛날엔 액션 타입 따로, 액션 만드는 함수 따로, 리듀서 따로 만들었는데, 얘는 이거 하나로 다 끝내는거 가능.

데이터 초기값(initialState) 정하고, 이 데이터 바꾸는 방법들(reducers)만 함수로 써주면 됨

2. 스토어 설정에 슬라이스 리듀서 추가

// src/store/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice'; // createSlice로 만든 리듀서 import

export default configureStore({
  reducer: {
    counter: counterReducer, // 'counter'라는 이름으로 리듀서 등록
    // 다른 슬라이스가 있다면 여기에 추가
  },
});

3. 애플리케이션에 Redux 스토어 연결 (main.jsx 또는 App.js)

// src/main.jsx 또는 App.js 파일의 일부
import React from 'react';
import { createRoot } from 'react-dom/client'; // 또는 'react-dom'
import { Provider } from 'react-redux';
import store from './store/store';
import App from './App'; // 또는 라우터 설정 컴포넌트

createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <Provider store={store}>
      {/* App 컴포넌트 또는 라우터 */}
      <App />
    </Provider>
  </React.StrictMode>
);

Provider로 감싸주면 그 안에 있는 모든 컴포넌트들이 리덕스 스토어 데이터에 접근할 수 있게 됨.

4. 컴포넌트에서 상태 읽기 (useSelector)

// src/components/Counter.jsx (상태 표시 전용 컴포넌트 예시)
import React from 'react';
import { useSelector } from 'react-redux';

const Counter = () => {
  // 스토어의 'counter' 슬라이스 전체 상태 또는 특정 값 선택
  const counterState = useSelector(state => state.counter);
  const { count, label } = counterState; // 또는 const { count, label } = useSelector(state => state.counter);

  return (
    <p className="m-1 p-3 border">
      {label}: {count}
    </p>
  );
};

export default Counter;

useSelector는 스토어에서 데이터 꺼내올 때 씀.

5. 컴포넌트에서 액션 디스패치 (useDispatch)

// src/pages/BlogPage.jsx (액션 디스패치 컴포넌트 예시)
import React from 'react';
import { useDispatch } from 'react-redux';
// counterSlice에서 내보낸 액션 생성자 import
import { increment, incrementByAmount, decrement, resetCount } from '../store/counterSlice';
// Counter 컴포넌트 import (상태 표시)
import Counter from '../components/Counter';

const BlogPage = () => {
  // dispatch 함수 가져오기
  const dispatch = useDispatch();

  // 각 버튼 클릭 시 디스패치할 함수들
  const handleIncrement = () => {
    console.log('카운터 1 증가');
    dispatch(increment()); // 인자 없이 디스패치 (payload 없음)
  };

  const handleIncrementBy10 = () => {
    console.log('카운터 10 증가');
    dispatch(incrementByAmount(10)); // payload 10과 함께 디스패치
  };

  const handleDecrement = () => {
    console.log('카운터 1 감소');
    dispatch(decrement()); // 인자 없이 디스패치
  };

   const handleReset = () => {
    console.log('카운터 초기화');
    dispatch(resetCount()); // 인자 없이 디스패치
  };


  return (
    <main>
      <h2>BlogPage</h2>
      <div>
        <h3>redux 연습</h3>
        {/* 여러 개의 Counter 컴포넌트가 동일 상태 공유 */}
        <Counter />
        <Counter />
        <Counter />
        <button onClick={handleIncrement}>카운터 증가 (1)</button>
        <button onClick={handleIncrementBy10}>카운터 증가 (10)</button>
        <button onClick={handleDecrement}>카운터 감소 (1)</button>
        <button onClick={handleReset}>카운터 초기화</button>
      </div>
    </main>
  );
};

export default BlogPage;

useDispatch는 데이터 바꿔달라고 스토어에 명령(액션 디스패치) 보낼 때 씀.

리덕스 어려운데 굳이 써야함?

우선 내가 만드는 앱이 간단해서 컴포넌트 몇 개 없고 공유하는 데이터도 몇 개 없으면 솔직히 리덕스 오버스펙이 맞다. 괜히 복잡하게 느껴지기만 하고 ㅋㅋ

근데 앱 규모가 좀 커진다? 여러 컴포넌트가 같은 데이터를 많이 써야 한다? 데이터 바꾸는 로직이 좀 복잡하다? 그럼 얘기가 달라짐.

-> 그때부터는 바닐라 JS/리액트 기본 상태 관리로는 한계가 오고 코드가 꼬이기 시작하기 때문에 리덕스 같은 중앙 집중식 상태 관리 라이브러리가 필요함.

쉽게 말해, 동네 구멍가게는 사장 혼자 다 해도 되는데,

전국 체인점 되려면 본부 만들고, 부서 나누고(슬라이스), 각 부서에서 일 처리 규정(리듀서) 만들고, 지점(컴포넌트)에서 본부에 요청 보내서(디스패치) 처리하는 시스템이 필요한 것이다~!

profile
코딩너무어려운대 어떡할과 재학중

0개의 댓글