이 가이드를 통해 다음을 학습합니다:
1. Redux의 기본 개념과 사용법
2. Redux Toolkit을 활용한 상태 관리
3. 비동기 작업 처리
4. TypeScript와 함께 사용하기
# Next.js 프로젝트 생성
npx create-next-app@latest redux-practice --typescript --tailwind --eslint
# 필요한 패키지 설치
npm install @reduxjs/toolkit react-redux @tanstack/react-query axios
먼저 가장 기본적인 로그인/로그아웃 기능을 구현해봅니다.
// src/types/auth.ts
export interface User {
id: string;
username: string;
role: 'admin' | 'user';
}
export interface AuthState {
user: User | null;
token: string | null;
isAuthenticated: boolean;
}
// src/store/slices/authSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { AuthState, User } from '@/types/auth';
const initialState: AuthState = {
user: null,
token: null,
isAuthenticated: false
};
export const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
setCredentials: (
state,
action: PayloadAction<{ user: User; token: string }>
) => {
state.user = action.payload.user;
state.token = action.payload.token;
state.isAuthenticated = true;
},
logout: (state) => {
state.user = null;
state.token = null;
state.isAuthenticated = false;
}
}
});
export const { setCredentials, logout } = authSlice.actions;
export default authSlice.reducer;
// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import authReducer from './slices/authSlice';
export const store = configureStore({
reducer: {
auth: authReducer
}
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// src/components/LoginForm.tsx
import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { setCredentials } from '@/store/slices/authSlice';
export const LoginForm = () => {
const dispatch = useDispatch();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// 실제로는 API 호출을 하겠지만, 예시를 위해 직접 데이터를 생성
dispatch(
setCredentials({
user: {
id: '1',
username,
role: 'user'
},
token: 'dummy-token'
})
);
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="username">Username</label>
<input
type="text"
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="border p-2 w-full"
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="border p-2 w-full"
/>
</div>
<button
type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Login
</button>
</form>
);
};
// src/types/order.ts
export interface Order {
id: string;
productName: string;
quantity: number;
status: 'pending' | 'approved' | 'rejected';
createdAt: string;
}
export interface OrderState {
orders: Order[];
selectedOrder: Order | null;
loading: boolean;
error: string | null;
}
// src/store/slices/orderSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { Order, OrderState } from '@/types/order';
// 비동기 작업을 위한 thunk
export const fetchOrders = createAsyncThunk(
'orders/fetchOrders',
async () => {
// 실제로는 API 호출
const mockOrders: Order[] = [
{
id: '1',
productName: 'Product 1',
quantity: 2,
status: 'pending',
createdAt: new Date().toISOString()
}
];
return mockOrders;
}
);
const initialState: OrderState = {
orders: [],
selectedOrder: null,
loading: false,
error: null
};
export const orderSlice = createSlice({
name: 'orders',
initialState,
reducers: {
selectOrder: (state, action: PayloadAction<string>) => {
state.selectedOrder = state.orders.find(
order => order.id === action.payload
) || null;
},
updateOrderStatus: (
state,
action: PayloadAction<{ orderId: string; status: Order['status'] }>
) => {
const order = state.orders.find(o => o.id === action.payload.orderId);
if (order) {
order.status = action.payload.status;
}
}
},
extraReducers: (builder) => {
builder
.addCase(fetchOrders.pending, (state) => {
state.loading = true;
})
.addCase(fetchOrders.fulfilled, (state, action) => {
state.loading = false;
state.orders = action.payload;
})
.addCase(fetchOrders.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || 'Failed to fetch orders';
});
}
});
export const { selectOrder, updateOrderStatus } = orderSlice.actions;
export default orderSlice.reducer;
// src/components/OrderList.tsx
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from '@/store';
import { fetchOrders, selectOrder } from '@/store/slices/orderSlice';
export const OrderList = () => {
const dispatch = useDispatch();
const { orders, loading, error } = useSelector(
(state: RootState) => state.orders
);
useEffect(() => {
dispatch(fetchOrders());
}, [dispatch]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div className="space-y-4">
{orders.map((order) => (
<div
key={order.id}
className="border p-4 rounded"
onClick={() => dispatch(selectOrder(order.id))}
>
<h3 className="font-bold">{order.productName}</h3>
<p>Quantity: {order.quantity}</p>
<p>Status: {order.status}</p>
</div>
))}
</div>
);
};
// src/types/document.ts
export interface Document {
id: string;
title: string;
type: 'manual' | 'ecn';
url: string;
approvalStatus: 'pending' | 'approved' | 'rejected';
}
export interface DocumentState {
documents: Document[];
filters: {
type: Document['type'] | 'all';
searchTerm: string;
};
loading: boolean;
}
// src/store/slices/documentSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { Document, DocumentState } from '@/types/document';
const initialState: DocumentState = {
documents: [],
filters: {
type: 'all',
searchTerm: ''
},
loading: false
};
export const documentSlice = createSlice({
name: 'documents',
initialState,
reducers: {
setDocuments: (state, action: PayloadAction<Document[]>) => {
state.documents = action.payload;
},
setFilter: (
state,
action: PayloadAction<{
type: DocumentState['filters']['type'];
searchTerm: string;
}>
) => {
state.filters = action.payload;
},
updateApprovalStatus: (
state,
action: PayloadAction<{
documentId: string;
status: Document['approvalStatus'];
}>
) => {
const document = state.documents.find(
doc => doc.id === action.payload.documentId
);
if (document) {
document.approvalStatus = action.payload.status;
}
}
}
});
export const { setDocuments, setFilter, updateApprovalStatus } =
documentSlice.actions;
export default documentSlice.reducer;
위 코드를 기반으로 다음 기능들을 직접 구현해보세요:
인증 기능 확장
주문 관리 기능 확장
문서 관리 기능 확장
성능 최적화
테스트 작성
미들웨어 활용