리덕스는 위 사진과 같이 하나의 STORE를 두어 모든 상태를 저장하고 읽으며 비교하는 라이브러리이다.
정의: 자바스크립트 애플리케이션을 위한 예측 가능한 상태(state)컨테이너. 애플리케이션의 전역 상태를 중앙에서 효율적으로 관리하기 위한 라이브러리.
목적: 복잡한 애플리케이션에서 컴포넌트 간의 상태 공유 및 관리를 용이하게 함. 상태 변화를 추적하고 디버깅하는 데 도움.
핵심 원칙:
Single Source of Truth: 애플리케이션의 전체 상태는 하나의 스토어(Store)에 저장됨.
State is Read-Only: 상태는 직접 변경 불가. 오직 액션(Action)에 의해서만 변경 요청 가능.
Changes are Made with Pure Functions: 상태 변경은 순수 함수인 리듀서(Reducer)에 의해서만 이루어짐.
Store (스토어): 애플리케이션의 단 하나뿐인 상태 저장소. 현재 상태 보유, 액션 디스패치, 상태 변화 구독 기능 제공.
(디스패치는 액션을 발생시키는 함수. 상태 변경을 위한 명령을 전달하는 역할을 함)
Action (액션): 상태에 어떤 변화가 필요한지를 나타내는 객체. { type: 'ACTION_TYPE', ...payload }
형태. type
은 필수.
Reducer (리듀서): 현재 상태와 디스패치된 액션을 인자로 받아 새로운 상태를 반환하는 순수 함수. 상태 불변성 유지 필수.
Dispatch (디스패치): 액션을 발생시키는 함수. store.dispatch(action)
호출을 통해 액션을 스토어로 전달.
UI에서 상태 변경 이벤트 발생.
이벤트 핸들러 내에서 dispatch
함수 호출하여 액션 전달.
스토어가 디스패치된 액션을 받아 해당 액션을 처리할 리듀서 실행.
리듀서는 현재 상태와 액션을 기반으로 새로운 상태 생성 및 반환.
스토어의 상태가 새로운 상태로 업데이트.
상태 변화를 구독하고 있는 컴포넌트들 리렌더링.
필요성: Redux의 기본 상태 변경(리듀서)은 동기적으로만 동작하는데 API 호출 등 시간이 걸리는 비동기 작업은 리듀서가 직접 처리하기 어려움 -> 비동기 처리 필요!
해결책: 미들웨어(Middleware) 도입.
주요 비동기 미들웨어: Redux Thunk(간단한 비동기), Redux Saga(복잡한 비동기 흐름 관리) 등.
간단한 비동기 처리 흐름:
설치: Redux Toolkit인 npm install @reduxjs/toolkit react-redux
(Toolkit이 BEST!!)
Store 생성: createStore
(Redux) 또는 configureStore
(Redux Toolkit) 함수 사용. 리듀서 함수 연결.
Provider 설정: react-redux
의 Provider
컴포넌트로 애플리케이션의 최상위 컴포넌트 감싸기. 생성한 스토어를 store
prop으로 전달. 모든 하위 컴포넌트에서 스토어 접근 가능.
컴포넌트에서 상태 조회: react-redux
의 useSelector
훅 사용. 스토어 상태 중 필요한 부분 선택.
컴포넌트에서 액션 디스패치: react-redux
의 useDispatch
훅 사용. 디스패치 함수 가져와 액션 전달.
createSlice
및 Counter 예시 코드createSlice
개념Redux Toolkit 기능으로, 상태(state)의 한 조각(slice)과 관련된 액션 타입, 액션 생성 함수, 리듀서를 한 번에 정의하는 함수.
Redux 로직 작성 시 상용구 코드 감소 및 개발 편의성 향상 목적.
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)만 함수로 써주면 됨
// src/store/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice'; // createSlice로 만든 리듀서 import
export default configureStore({
reducer: {
counter: counterReducer, // 'counter'라는 이름으로 리듀서 등록
// 다른 슬라이스가 있다면 여기에 추가
},
});
// 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로 감싸주면 그 안에 있는 모든 컴포넌트들이 리덕스 스토어 데이터에 접근할 수 있게 됨.
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는 스토어에서 데이터 꺼내올 때 씀.
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/리액트 기본 상태 관리로는 한계가 오고 코드가 꼬이기 시작하기 때문에 리덕스 같은 중앙 집중식 상태 관리 라이브러리가 필요함.
쉽게 말해, 동네 구멍가게는 사장 혼자 다 해도 되는데,
전국 체인점 되려면 본부 만들고, 부서 나누고(슬라이스), 각 부서에서 일 처리 규정(리듀서) 만들고, 지점(컴포넌트)에서 본부에 요청 보내서(디스패치) 처리하는 시스템이 필요한 것이다~!