DEVELOPMENT/React.js

[React] Redux와 Redux Toolkit

Watnu 2025. 2. 18. 10:22


01

Redux 란

Redux는 애플리케이션에서 여러 컴포넌트들이 공통으로 사용하는 데이터를 전역 상태라고 부르는데,

이 상태를 효율적으로 관리하는 도구입니다.

 

React 같은 라이브러리에서는 컴포넌트들이 서로 독립적으로 동작하는데,

여러 컴포넌트들이 같은 데이터를 필요로 할 때 Redux를 사용해서 상태를 한 곳에서 관리할 수 있습니다.

 

✅ 필요한 모듈: redux와 react-redux 모듈을 사용합니다.

 

📌 왜 Redux를 배워야 할까?

Redux는 많은 개발자들이 사용하는 인기 있는 라이브러리입니다.

예를 들어, npm trends에서 Redux의 다운로드 수를 보면 얼마나 많은 사람들이 이 도구를 사용하고 있는지 알 수 있습니다.

 

📌 FLUX 패턴?

FLUX 패턴은 상태를 효율적이고 예측 가능한 방식으로 관리하기 위해 만들어진 규칙입니다.

데이터가 애플리케이션 내에서 어떻게 흐르고, 상태가 어떻게 변경되는지를 정의해 줍니다.

 

FLUX 패턴은 단방향 데이터 흐름을 따르는데,

이는 데이터를 한 방향으로만 흐르게 해서 상태 변경이 더 직관적이고 관리하기 쉬워지도록 합니다.

리덕스의 흐름

 

1. Redux 구성 요소

Redux는 여러 구성 요소로 이루어져 있습니다. 이들은 전역 상태를 관리하는 데 중요한 역할을 합니다.

 

✅ Action (액션)

액션은 상태를 어떻게 변경할지를 정의하는 객체입니다.

예를 들어, 버튼을 클릭해서 숫자를 증가시키는 액션은 아래와 같이 작성됩니다.

dispatch({
  type: "INCREMENT", // 액션의 타입 (어떤 종류의 변화인지를 나타냄)
  payload: 1, // 데이터 (여기선 증가할 숫자)
});

 

✅ Dispatch (디스패치)

dispatch는 액션 객체를 리듀서라는 함수로 보내주는 역할을 합니다.

dispatch를 호출하면 액션이 처리되도록 전달됩니다.

dispatch({
  type: "ADD_ITEM", // 아이템을 추가하는 액션
  payload: newItem, // 추가할 아이템 데이터
});

 

✅ Reducer (리듀서)

리듀서는 상태를 변경하는 함수입니다.

이 함수는 현재 상태액션을 받아서 새로운 상태를 반환합니다.

const reducer = (state, action) => {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + action.payload };
    default:
      return state;
  }
};

 

✅ Store (스토어)

스토어는 전역 상태를 저장하는 곳입니다.

애플리케이션에서 데이터를 관리하는 중앙 저장소 역할을 합니다.

 

createStore 함수는 리듀서를 전달받아서 스토어를 생성합니다. 리듀서는 상태를 어떻게 변경할지를 정의한 함수입니다.

const store = createStore(reducer);

이 코드에서 reducer는 상태 변경을 처리하는 함수입니다.

createStore는 이 리듀서를 사용하여 상태 저장소를 만들고, 애플리케이션 전체에서 사용할 수 있도록 합니다.

 

📌 스토어는 하나만!

  • 스토어는 하나만 생성할 수 있습니다.
    Redux에서는 애플리케이션의 전역 상태를 관리하는 하나의 스토어만을 사용합니다.

 

📌 Reducer가 여러 개일 때는?

  • 만약 애플리케이션에 여러 개의 리듀서가 필요하다면, 각각의 리듀서를 하나의 스토어로 합쳐야 합니다.
    이를 위해 combineReducers 함수를 사용합니다.

 

combineReducers는 여러 리듀서를 하나로 합쳐서,

각각의 리듀서가 관리하는 상태의 조각들을 하나의 전체 상태로 만들어줍니다.

import { combineReducers, createStore } from 'redux';

// 여러 개의 리듀서 정의
const counterReducer = (state = { count: 0 }, action) => {
  switch(action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    default:
      return state;
  }
};

const userReducer = (state = { name: 'John' }, action) => {
  switch(action.type) {
    case 'SET_USER':
      return { name: action.payload };
    default:
      return state;
  }
};

// 여러 리듀서를 combineReducers로 합침
const rootReducer = combineReducers({
  counter: counterReducer,
  user: userReducer
});

// 하나의 스토어로 생성
const store = createStore(rootReducer);

📂 설명

이 예시에서, counterReducer와 userReducer는 각각 카운터사용자 정보를 관리하는 리듀서입니다.

combineReducers를 사용하여 이 두 리듀서를 합쳐 하나의 rootReducer를 만들고, 이를 사용해 하나의 스토어를 생성했습니다.

 

📌 Reducer가 여러 개라면?

  • 스토어는 하나지만, 여러 리듀서를 사용하여 애플리케이션의 상태를 여러 부분으로 나누어 관리할 수 있습니다.
  • 상태는 store.getState()를 호출하면 전체 상태를 볼 수 있으며, 각 상태는 counter와 user로 나뉘어 있습니다.
console.log(store.getState()); 
// { counter: { count: 0 }, user: { name: 'John' } }

 

 

✅ React-Redux

React와 Redux를 연결하려면 **useSelector()**와 useDispatch() 훅을 사용합니다.

const dispatch = useDispatch();
const state = useSelector((state) => state);

📂 설명

  • useSelector()는 상태를 가져올 때 사용합니다.
  • useDispatch()는 액션을 보내는 데 사용합니다.

 

2. Redux-Thunk (리덕스-Thunk)

Redux-Thunk는 비동기 작업을 할 수 있게 도와주는 미들웨어입니다.

예를 들어, API 요청을 보내고 그 결과를 처리하는 데 사용됩니다.

Redux에서는 단순히 객체를 액션으로 보내는데, Thunk를 사용하면 함수도 액션으로 보낼 수 있어요.

const fetchData = () => {
  return (dispatch) => {
    fetch('/api/data')
      .then(response => response.json())
      .then(data => {
        dispatch({ type: 'SET_DATA', payload: data });
      });
  };
};

02

Redux Toolkit (리덕스 툴킷)

Redux Toolkit(RTK)은 Redux를 쉽게 사용할 수 있도록 도와주는 도구입니다.

Redux는 처음 배우기 어려울 수 있는데, Redux Toolkit은 이 복잡한 부분을 많이 간소화해 줍니다.

따라서 Redux Toolkit을 사용하는 것이 권장됩니다. (공식문서에도 사용을 권장하고 있습니다.)

 

  필요한 모듈: @reduxjs/toolkit과 react-redux 모듈을 사용합니다.

npm install @reduxjs/toolkit react-redux

 

1. 리덕스 상태 관리 시작하기 

리덕스 툴킷에서 상태(state) 슬라이스(slice)로 관리합니다. 여기서 슬라이스(Slice)란?

슬라이스상태(state)와 이를 변경할 액션(actions)을 묶어놓은 객체입니다.
이 슬라이스 안에는 상태(state)와 이를 업데이트할 함수들(액션)이 포함되죠.

 

✅ createSlice()

Redux Toolkit에서는 createSlice() 함수를 사용해 액션과 리듀서를 한 번에 만들 수 있습니다.

import { createSlice } from '@reduxjs/toolkit';

// 슬라이스 정의
const counterSlice = createSlice({
  name: 'counter', // 상태 이름
  initialState: { value: 0 }, // 초기 상태
  reducers: { // 상태를 변경할 함수들
    increment: (state) => {
      state.value += 1; // 상태값을 1 증가시킴
    },
    decrement: (state) => {
      state.value -= 1; // 상태값을 1 감소시킴
    }
  }
});

// 액션과 리듀서를 한 번에 export
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;

📂 설명

  • createSlice에서 name은 상태의 이름을 정의하고,
  • initialState는 상태의 초기 값을 설정하죠
  • reducers는 상태를 어떻게 변경할지 정의하는 부분으로, 여기서 액션을 정의해합니다.

 

configureStore()

리덕스에서 스토어(store)는 상태를 관리하는 핵심입니다!
여기서 만든 슬라이스들을 합쳐서 하나의 스토어로 만들 수 있어요.

 

configureStore()는 Redux Toolkit에서 제공하는 스토어를 설정하는 함수입니다.

createStore()와 비슷한 역할을 하지만, 더 간단하게 설정할 수 있습니다.

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice'; // 위에서 만든 counterSlice 가져오기

const store = configureStore({
  reducer: {
    counter: counterReducer, // counter 상태 관리
  },
});

export default store;

📂 설명

  • configureStore는 리덕스 툴킷에서 제공하는 함수로,
    여러 리듀서들을 하나의 스토어로 묶어서 관리할 수 있게 해 줍니다.

 

📌  여러 개의 Reducer로 Store 만들기

Redux Toolkit에서는 여러 개의 리듀서를 configureStore를 사용하여 간편하게 하나의 store로 결합할 수 있습니다.

리듀서가 여러 개일 때, 객체 형태로 각 리듀서를 reducer 옵션에 전달하면 됩니다.

 

 

  Reducer가 여러 개일 때 store 만드는 방법

  • configureStore 함수를 사용하여 리듀서들을 하나로 묶어 store를 만듭니다.
  • 각각의 리듀서를 reducer 객체 안에 키-값 형식으로 넣어 전달합니다.
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';  // 카운터 리듀서
import userReducer from './userSlice';        // 사용자 리듀서

// 여러 리듀서를 하나로 묶어서 store 생성
const store = configureStore({
  reducer: {
    counter: counterReducer,  // counter 상태 관리
    user: userReducer         // user 상태 관리
  },
});

export default store;

📂 설명

  • configureStore는 store를 간편하게 생성해 주는 함수입니다.
  • reducer 객체 내에 여러 개의 Reducer키-값 형식으로 넣어주면 됩니다.
    • counter: counterReducer: counter라는 상태를 counterReducer가 관리합니다.
    • user: userReducer: user라는 상태를 userReducer가 관리합니다.

 

 액션 사용법

createSlice()에서 정의한 리듀서 이름을 기반으로 액션을 사용할 수 있습니다.

dispatch(increment()); // increment 액션 호출

 

2. Redux Toolkit에서 Thunk 사용하기

Redux Toolkit에서는 createAsyncThunk()를 사용해서 비동기 작업을 처리할 수 있습니다.

예를 들어, API에서 데이터를 가져오는 작업을 처리할 수 있습니다.

import { createAsyncThunk } from '@reduxjs/toolkit';

export const fetchData = createAsyncThunk('data/fetch', async () => {
  const response = await fetch('/api/data');
  return response.json();
});

03

리액트에서 리덕스 사용하기

리덕스를 리액트에서 사용하려면, Provider를 사용해서 스토어(store)를 리액트 앱에 연결해야 합니다.

1. 리액트 앱에 스토어 연결하기

import React from 'react';
import { Provider } from 'react-redux';
import store from './redux/store'; // 위에서 만든 store 가져오기
import Counter from './Counter'; // 카운터 컴포넌트

const App = () => {
  return (
    <Provider store={store}>  {/* 스토어 연결 */}
      <Counter />  {/* 카운터 컴포넌트 사용 */}
    </Provider>
  );
};

export default App;

📂 설명

  • Provider는 리덕스 스토어리액트 컴포넌트에 공급해줍니다.
  • 이제 Counter 컴포넌트에서는 리덕스를 통해 상태를 쉽게 관리할 수 있어.

 

2. 상태값 가져오기 (useSelector)

리액트 컴포넌트에서 리덕스 상태를 가져오려면, useSelector를 사용합니다.

import { useSelector } from 'react-redux';

const Counter = () => {
  const count = useSelector(state => state.counter.value); // 리덕스 상태 가져오기

  return (
    <div>
      <h1>{count}</h1>  {/* 상태 값 표시 */}
    </div>
  );
};

📂 설명

  • useSelector는 리덕스 상태에서 필요한 값을 추출해서 컴포넌트에서 사용할 수 있게 합니다.

3. 액션 디스패치하기 (useDispatch)

리덕스 상태를 변경하려면, 액션을 디스패치(dispatch) 해야 합니다.
리액트 컴포넌트에서 useDispatch를 사용하면 액션을 디스패치할 수 있죠.

import { useDispatch } from 'react-redux';
import { increment, decrement } from './counterSlice'; // 액션 가져오기

const Counter = () => {
  const dispatch = useDispatch();

  return (
    <div>
      <button onClick={() => dispatch(increment())}>Increment</button> {/* 증가 */}
      <button onClick={() => dispatch(decrement())}>Decrement</button> {/* 감소 */}
    </div>
  );
};

📂 설명

  • dispatch()는 액션을 디스패치하는 함수로, 상태를 변경할 수 있습니다.

4. 비동기 작업 처리하기 (createAsyncThunk)

리덕스 툴킷은 비동기 작업을 다루는 createAsyncThunk도 제공합니다.  API 요청 등을 처리할 때 유용하죠.

import { createAsyncThunk } from '@reduxjs/toolkit';

export const fetchData = createAsyncThunk('data/fetch', async () => {
  const response = await fetch('/api/data');
  return response.json();
});

📂 설명

  • createAsyncThunk는 비동기 함수를 처리할 때 사용합니다.
  • API에서 데이터를 가져올 때 비동기 작업을 처리할 수 있습니다.

5. 결론

  • 슬라이스 만들기 (createSlice): 상태와 액션을 한 번에 만들 수 있습니다.
  • 스토어 만들기 (configureStore): 여러 슬라이스를 하나의 스토어에 묶어줍니다.
  • 상태 가져오기 (useSelector): 리덕스 상태를 컴포넌트에서 사용할 수 있습니다.
  • 액션 디스패치하기 (useDispatch): 상태를 변경하려면 액션을 디스패치합니다.

04

Redux Toolkit 한번 더 예제 만들어보기

1. 슬라이스(Slice) 설정

import { createSlice } from '@reduxjs/toolkit';

// 슬라이스 정의
const pokeSlice = createSlice({
  name: 'pokeData',
  initialState: {
    data: [],  // 포켓몬 데이터 초기 상태를 빈 배열로 설정
    wishlist: [], // 찜 목록은 빈 배열로 시작
  },
  reducers: {
    // SET: 외부에서 받아온 데이터를 data에 설정하기
    SET: (state, action) => {
      state.data = action.payload; // 받아온 데이터를 state.data에 할당
    },

    // CREATE: 찜 목록에 추가하기
    CREATE: (state, action) => {
      const itemToAdd = state.data.find((item) => item.id === action.payload.id); // id로 아이템 찾기
      if (itemToAdd && !state.wishlist.find((item) => item.id === action.payload.id)) {
        state.wishlist.push(itemToAdd); // 찜 목록에 아이템 추가
      }
    },

    // REMOVE: 찜 목록에서 삭제하기
    REMOVE: (state, action) => {
      state.wishlist = state.wishlist.filter((item) => item.id !== action.payload.id); // 해당 id 아이템 제거
    },
  },
});

// 액션 내보내기
export const { SET, CREATE, REMOVE } = pokeSlice.actions;
// 리듀서 내보내기
export default pokeSlice.reducer;

📂 설명 : 이 코드는 슬라이스를 정의하고, CREATEREMOVE 액션을 설정합니다.

  • SET 액션을 사용해 외부 데이터를 받아와 state.data에 설정하고, initialState에서 data는 빈 배열로 시작합니다.
  • CREATE 액션은 찜 목록에 아이템을 추가하고,
  • REMOVE 액션은 찜 목록에서 아이템을 삭제하는 역할을 하죠.

📌  action.payload.id 처럼 왜 불러오는 건가요?

payload는 액션에서 전달하고자 하는 데이터를 담는 부분입니다.
payload는 액션 객체의 type과 payload 속성을 포함하는데, payload에는 우리가 액션을 처리할 때 필요한 추가적인 데이터를 넣을 수 있습니다.

 

👉 액션은 기본적으로 다음과 같은 구조를 가지고 있습니다.

{
  type: 'ACTION_TYPE', // 액션 타입 (액션 종류)
  payload: { /* 전달할 데이터 */ } // 실제 데이터
}

 

👉 왜 payload.id를 사용하는가?
예를 들어, 아이템을 찜 목록에 추가하는 CREATE 액션에서는 id를 통해 어떤 item을 찜 목록에 추가할지 지정해야 합니다.
payload.id는 액션을 디스패치할 때 넘겨주는 데이터에서 id를 참조하는 것입니다.

즉, id를 통해 어떤 아이템을 수정하거나 추가할지를 결정하는 방식이죠.

 

👉 payload 사용 예시

// CREATE 액션을 디스패치할 때
const onCreate = (id) => {
  dispatch(CREATE({ id })); // id를 payload로 넘겨서 CREATE 액션 실행
};

📂 설명

  • CREATE({ id })에서 id는 액션의 payload로 전달되는 값입니다.
  • 이 값은 pokeItemSlice에서 action.payload.id로 사용되며,
    그 값을 통해 특정 아이템을 찾아 찜 목록에 추가하게 됩니다.

 

2. 스토어(Store) 설정

import { configureStore } from '@reduxjs/toolkit';
import pokeReducer from './pokeSlice'; // 위에서 만든 슬라이스 가져오기

const store = configureStore({
  reducer: {
    pokeData: pokeReducer, // pokeData 상태 관리
  },
});

export default store;

📂 설명

  • :configureStore는 스토어를 설정하는 함수로, pokeData라는 리듀서를 스토어에 등록해 줍니다.
    이로 인해 리덕스 스토어에서 pokeData를 관리할 수 있게 됩니다.

3. App.js에서 리덕스 스토어 연결하기

import React from 'react';
import { Provider } from 'react-redux';
import store from './redux/store'; // 리덕스 스토어 가져오기
import PokeList from './components/PokeList'; // 찜 목록을 보여줄 컴포넌트

const App = () => {
  return (
    <Provider store={store}> {/* 스토어를 애플리케이션에 공급 */}
      <PokeList />
    </Provider>
  );
};

export default App;

📂 설명

  • Provider는 리덕스 스토어를 리액트 애플리케이션에 공급해 줍니다.
  • 이제 PokeList 컴포넌트에서 리덕스 상태를 사용할 수 있습니다!

4. PokeList.js에서 상태 가져오기 및 액션 디스패치하기

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { CREATE, REMOVE, SET } from '../redux/pokeSlice'; // 액션 가져오기
import PokeItem from './PokeItem'; // 아이템을 표시할 컴포넌트

const PokeList = () => {
  const dispatch = useDispatch();
  const pokeData = useSelector((state) => state.pokeData.data); // 포켓몬 데이터 가져오기
  const wishlist = useSelector((state) => state.pokeData.wishlist); // 찜 목록 가져오기

  // 데이터 가져오기
  const fetchPokemonData = async () => {
    try {
      const response = await fetch('https://pokeapi.co/api/v2/pokemon?limit=151');
      const data = await response.json();
      dispatch(SET(data.results)); // SET 액션으로 받아온 데이터를 store에 설정
    } catch (error) {
      console.error('데이터를 불러오는 중에 오류가 발생했습니다.', error);
    }
  };

  useEffect(() => {
    fetchPokemonData(); // 컴포넌트가 마운트될 때 데이터 불러오기
  }, [dispatch]);

  const onCreate = (pokemon) => {
    dispatch(CREATE(pokemon)); // 전체 아이템을 찜 목록에 추가
  };

  const onRemove = (pokemon) => {
    dispatch(REMOVE(pokemon)); // 찜 목록에서 아이템 삭제
  };

  return (
    <div>
      <h2>포켓몬 리스트</h2>
      <div>
        {pokeData.map((pokemon) => (
          <PokeItem
            key={pokemon.id}
            pokemon={pokemon}
            onCreate={() => onCreate(pokemon)} // pokemon 객체 전체 전달
            onRemove={() => onRemove(pokemon)} // pokemon 객체 전체 전달
            isInWishlist={wishlist.some((item) => item.id === pokemon.id)} // 찜 목록에 있는지 확인
          />
        ))}
      </div>
    </div>
  );
};

export default PokeList;

📂 설명

  • useSelector를 사용해서 리덕스 상태에서 포켓몬 데이터찜 목록을 가져옵니다.
  • onCreate와 onRemove는 각각 찜 목록에 아이템을 추가하거나 삭제하는 함수.
  • isInWishlist는 해당 아이템이 찜 목록에 있는지 확인하고, 버튼"찜하기" 또는 "찜 취소"로 바꿉니다.

 

5. PokeItem.js에서 찜하기/찜 취소 버튼 추가

import React from 'react';

const PokeItem = ({ pokemon, onCreate, onRemove, isInWishlist }) => {
  return (
    <div>
      <h3>{pokemon.name}</h3>
      <img src={pokemon.sprites.front_default} alt={pokemon.name} />
      <div>
        {isInWishlist ? (
          <button onClick={() => onRemove(pokemon.id)}>찜 취소</button>  {/* 찜 목록에서 제거 */}
        ) : (
          <button onClick={() => onCreate(pokemon.id)}>찜하기</button>  {/* 찜 목록에 추가 */}
        )}
      </div>
    </div>
  );
};

export default PokeItem;

📂 설명

  • SET 액션 사용: SET 액션을 디스패치하여 API로부터 데이터를 받아 data에 설정합니다.
  • 버튼을 클릭하면 onCreate 또는 onRemove가 호출되어 상태가 변경됩니다.

05

번외: Context API와 Redux의 차이

Context APIRedux는 둘 다 애플리케이션 상태를 관리하는 도구지만, 사용 방법과 적합한 상황이 다릅니다.

둘의 차이점을 간단하게 비교해 볼게요.

 

Context API

  • 간단한 상태 관리에 사용.
  • 설정이 간단하고 사용하기 쉬움.
  • 작은 앱이나 간단한 상태 공유에 적합.
  • 상태 변경 시 모든 컴포넌트가 리렌더링 될 수 있음.

Redux

  • 복잡한 상태 관리에 적합.
  • 비동기 처리와 상태 추적이 강력.
  • 설정과 사용법이 복잡하지만 대규모 앱에 유리.
  • 상태 변화가 중앙에서 관리되며, 필요한 컴포넌트만 리렌더링.

 

👉 간단 비교 요약

  • Context API: 작은 앱에서 간단한 상태 공유 → 빠르고 쉬움
  • Redux: 큰 앱에서 복잡한 상태 관리 → 확장성과 비동기 처리에 유리

06

마무리

Redux와 Redux Toolkit은 애플리케이션에서 효율적인 상태 관리를 가능하게 해주는 강력한 도구들입니다.

  • Redux는 처음에는 설정이 다소 복잡할 수 있지만, Redux Toolkit을 사용하면 그 복잡함을 크게 줄일 수 있습니다.
  • Redux Toolkit은 비동기 처리와 상태 관리의 흐름을 간소화하여, 특히 대규모 애플리케이션에서 더 빠르고 직관적으로 상태를 관리할 수 있게 도와줍니다.

 

따라서 상태 관리가 복잡한 애플리케이션에서는 Redux Toolkit을 사용하는 것이 효율적입니다.

또한, Redux단방향 데이터 흐름 덕분에 상태 변화가 예측 가능하고 관리하기 용이합니다.

 

이제 ReduxRedux Toolkit을 잘 활용하면, 보다 깔끔하고 유지보수하기 쉬운 애플리케이션을 만들 수 있습니다!!

 

Redux의 기본적인 개념을 확실히 이해한 후,

Redux Toolkit을 활용해 비동기 작업과 복잡한 상태 관리를 효율적으로 다뤄보세요!