Redux
Redux Toolkit
JavaScript 앱의 예측 가능한 상태 관리 라이브러리. 단방향 데이터 흐름과 불변성 원칙.
Redux Toolkit
JavaScript 앱의 예측 가능한 상태 관리 라이브러리. 단방향 데이터 흐름과 불변성 원칙.
Redux는 2015년 Dan Abramov가 개발한 JavaScript 상태 관리 라이브러리로, Flux 아키텍처를 단순화한 구현체입니다. 세 가지 핵심 원칙이 있습니다. 단일 진실의 원천(Single Source of Truth) - 전체 앱 상태가 하나의 Store에 저장됩니다. 상태는 읽기 전용(State is Read-Only) - Action을 통해서만 변경 가능합니다. 순수 함수(Pure Functions) - Reducer는 이전 상태와 Action을 받아 새 상태를 반환하는 순수 함수입니다.
Redux의 데이터 흐름은 단방향입니다. 사용자 이벤트가 발생하면 dispatch(action)을 호출하고, Store가 Reducer를 실행하여 새 상태를 계산하며, 구독된 컴포넌트가 리렌더링됩니다. 이 예측 가능한 흐름은 디버깅을 쉽게 하고, 상태 변화를 추적할 수 있게 합니다. Redux DevTools로 시간 여행 디버깅(Time Travel Debugging)이 가능합니다.
Redux Toolkit(RTK)은 2019년에 공식 출시된 Redux의 현대적 구현체로, 보일러플레이트를 대폭 줄였습니다. createSlice로 Action과 Reducer를 한 번에 정의하고, Immer를 내장하여 불변성을 자동 처리합니다. configureStore가 DevTools, middleware 설정을 자동화합니다. RTK는 이제 Redux 사용의 표준 방식입니다.
RTK Query는 Redux Toolkit 1.6에서 추가된 데이터 페칭 라이브러리로, React Query와 유사한 캐싱, 자동 리페칭, 낙관적 업데이트를 제공합니다. createApi로 엔드포인트를 정의하면 자동으로 hooks(useGetUsersQuery, useCreateUserMutation 등)가 생성됩니다. 서버 상태 관리에 Redux와 별도 라이브러리를 조합할 필요가 없어졌습니다.
// features/cart/cartSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartState {
items: CartItem[];
isOpen: boolean;
}
const initialState: CartState = {
items: [],
isOpen: false,
};
const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
// Immer로 "변경"처럼 작성해도 불변성 자동 처리
addItem: (state, action: PayloadAction<Omit<CartItem, 'quantity'>>) => {
const existingItem = state.items.find(item => item.id === action.payload.id);
if (existingItem) {
existingItem.quantity += 1;
} else {
state.items.push({ ...action.payload, quantity: 1 });
}
},
removeItem: (state, action: PayloadAction<string>) => {
state.items = state.items.filter(item => item.id !== action.payload);
},
updateQuantity: (state, action: PayloadAction<{ id: string; quantity: number }>) => {
const item = state.items.find(item => item.id === action.payload.id);
if (item) {
item.quantity = action.payload.quantity;
}
},
clearCart: (state) => {
state.items = [];
},
toggleCart: (state) => {
state.isOpen = !state.isOpen;
},
},
});
// Action creators 자동 생성
export const { addItem, removeItem, updateQuantity, clearCart, toggleCart } = cartSlice.actions;
// Selectors
export const selectCartItems = (state: RootState) => state.cart.items;
export const selectCartTotal = (state: RootState) =>
state.cart.items.reduce((total, item) => total + item.price * item.quantity, 0);
export const selectCartCount = (state: RootState) =>
state.cart.items.reduce((count, item) => count + item.quantity, 0);
export default cartSlice.reducer;
// services/api.ts - RTK Query
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
interface User {
id: number;
name: string;
email: string;
}
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers) => {
const token = localStorage.getItem('token');
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
return headers;
},
}),
tagTypes: ['User', 'Post'],
endpoints: (builder) => ({
// Query - GET 요청
getUsers: builder.query<User[], void>({
query: () => '/users',
providesTags: ['User'],
}),
getUser: builder.query<User, number>({
query: (id) => `/users/${id}`,
providesTags: (result, error, id) => [{ type: 'User', id }],
}),
// Mutation - POST/PUT/DELETE 요청
createUser: builder.mutation<User, Omit<User, 'id'>>({
query: (body) => ({
url: '/users',
method: 'POST',
body,
}),
invalidatesTags: ['User'], // 목록 자동 리페치
}),
updateUser: builder.mutation<User, Partial<User> & Pick<User, 'id'>>({
query: ({ id, ...body }) => ({
url: `/users/${id}`,
method: 'PATCH',
body,
}),
invalidatesTags: (result, error, { id }) => [{ type: 'User', id }],
}),
deleteUser: builder.mutation<void, number>({
query: (id) => ({
url: `/users/${id}`,
method: 'DELETE',
}),
invalidatesTags: ['User'],
}),
}),
});
// Hooks 자동 생성
export const {
useGetUsersQuery,
useGetUserQuery,
useCreateUserMutation,
useUpdateUserMutation,
useDeleteUserMutation,
} = api;
// -------------------------------------------------
// 컴포넌트에서 사용
function UserList() {
const { data: users, isLoading, error } = useGetUsersQuery();
const [deleteUser, { isLoading: isDeleting }] = useDeleteUserMutation();
if (isLoading) return <div>로딩 중...</div>;
if (error) return <div>에러 발생</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>
{user.name}
<button
onClick={() => deleteUser(user.id)}
disabled={isDeleting}
>
삭제
</button>
</li>
))}
</ul>
);
}