Redux는 React 앱에서 데이터를 관리하는 도구입니다.
마치 은행처럼 생각해보세요:
React에서 컴포넌트 간 데이터 공유가 필요할 때:
// 🔴 Redux 없이 - props drilling 문제 발생
function App() {
const [count, setCount] = useState(0);
return (
<div>
<Header count={count} />
<Main count={count} setCount={setCount} />
<Footer count={count} />
</div>
);
}
// ✅ Redux 사용 - 어디서든 쉽게 데이터 접근
function AnyComponent() {
const count = useSelector(state => state.counter.value);
// 데이터를 props로 전달받을 필요 없음!
}
데이터가 변경되는 과정을 순서대로 살펴봅시다:
사용자 동작 발생
// 버튼 클릭 등의 이벤트 발생
<button onClick={() => dispatch(increment())}>
증가
</button>
Action 생성
// increment() 함수가 다음과 같은 액션 객체 생성
{
type: 'counter/increment'
}
Reducer 실행
// 리듀서가 액션을 처리하여 새로운 상태 생성
const counterReducer = (state = { value: 0 }, action) => {
switch (action.type) {
case 'counter/increment':
return { value: state.value + 1 };
default:
return state;
}
}
Store 업데이트
// 새로운 상태가 스토어에 저장됨
store = { counter: { value: 1 } }
컴포넌트 업데이트
// 변경된 상태를 사용하는 컴포넌트가 리렌더링
const count = useSelector(state => state.counter.value);
// count 값이 바뀌어서 화면 갱신
# Next.js 프로젝트 생성
npx create-next-app@latest redux-counter --typescript --tailwind --eslint
# 필요한 패키지 설치
npm install @reduxjs/toolkit react-redux
src/
├── store/ # Redux 관련 폴더
│ ├── slices/ # 리듀서와 액션들
│ │ └── counterSlice.js
│ └── index.js # 스토어 설정
│
├── components/ # 리액트 컴포넌트
│ └── Counter.js
│
└── app/ # Next.js 페이지
└── page.js
// src/store/slices/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0, // 카운터 값
status: 'idle' // 상태 표시 (예: 로딩 상태)
},
reducers: {
// 증가 기능
increment: (state) => {
state.value += 1;
// Redux Toolkit은 직접 수정해도 됩니다!
// (내부적으로 불변성 관리를 해줌)
},
// 감소 기능
decrement: (state) => {
state.value -= 1;
},
// 특정 값만큼 증가
incrementByAmount: (state, action) => {
// action.payload로 전달된 값만큼 증가
state.value += action.payload;
},
// 상태 변경
setStatus: (state, action) => {
state.status = action.payload;
}
}
});
// 액션 생성자들 내보내기
export const {
increment,
decrement,
incrementByAmount,
setStatus
} = counterSlice.actions;
// 리듀서 내보내기
export default counterSlice.reducer;
// src/store/index.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './slices/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer
}
});
// src/app/layout.js
"use client";
import { Provider } from 'react-redux';
import { store } from '@/store';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<Provider store={store}>
{children}
</Provider>
</body>
</html>
);
}
// src/components/Counter.js
"use client";
import { useSelector, useDispatch } from 'react-redux';
import {
increment,
decrement,
incrementByAmount,
setStatus
} from '@/store/slices/counterSlice';
export default function Counter() {
// Redux 상태 가져오기
const count = useSelector((state) => state.counter.value);
const status = useSelector((state) => state.counter.status);
// 디스패치 함수 가져오기
const dispatch = useDispatch();
// 5만큼 증가시키는 함수 (로딩 상태 포함)
const handleIncreaseByFive = () => {
// 로딩 상태로 변경
dispatch(setStatus('loading'));
// 0.5초 후에 값 증가 및 상태 변경
setTimeout(() => {
dispatch(incrementByAmount(5));
dispatch(setStatus('complete'));
}, 500);
};
return (
<div className="p-8 max-w-md mx-auto">
<div className="bg-white rounded-lg shadow-md p-6">
{/* 제목 */}
<h1 className="text-2xl font-bold text-center mb-6">
Redux 카운터
</h1>
{/* 카운터 값 표시 */}
<div className="text-center mb-4">
<div className="text-4xl font-bold text-blue-600">
{count}
</div>
<div className="text-sm text-gray-500 mt-1">
상태: {status}
</div>
</div>
{/* 버튼들 */}
<div className="flex gap-2 justify-center">
<button
onClick={() => dispatch(increment())}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
>
증가 (+1)
</button>
<button
onClick={() => dispatch(decrement())}
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition-colors"
>
감소 (-1)
</button>
<button
onClick={handleIncreaseByFive}
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors"
disabled={status === 'loading'}
>
{status === 'loading' ? '처리 중...' : '5 증가'}
</button>
</div>
</div>
</div>
);
}
// src/app/page.js
import Counter from '@/components/Counter';
export default function Home() {
return (
<main className="min-h-screen bg-gray-100 py-12">
<Counter />
</main>
);
}
// ❌ 잘못된 방법: 직접 상태 변경
const wrongReducer = (state, action) => {
state.value = 123; // 일반 Redux에서는 이렇게 하면 안됨!
}
// ✅ 올바른 방법: 새로운 상태 반환
const correctReducer = (state, action) => {
return {
...state,
value: 123
};
}
// 🎉 Redux Toolkit을 사용하면 직접 수정해도 됨!
const toolkitReducer = (state, action) => {
state.value = 123; // OK!
}
// ❌ 비효율적인 방법
const data = useSelector(state => {
return state.items.filter(item => item.completed);
});
// ✅ 효율적인 방법
const selectCompletedItems = state =>
state.items.filter(item => item.completed);
const data = useSelector(selectCompletedItems);
이제 여러분은 Redux Toolkit을 사용하여 간단한 카운터 앱을 만들 수 있게 되었습니다! 🎉
더 복잡한 앱을 만들 때도 이 기본 개념들이 그대로 적용됩니다.