728x90
1. Redux 환경 설정
1-1. Redux Toolkit 설치
npm install @reduxjs/toolkit react-redux
1-2. Redux Store 생성
: 프로젝트 루트에 store 폴더를 생성하고, store.ts 파일을 만듭니다.
// store/store.ts
import { configureStore } from "@reduxjs/toolkit";
import friendsReducer from "./friendsSlice"
import chatReducer from "./chatSlice"
const store = configureStore({ //Redux Toolkit에서 제공하는 스토어 생성 함수
reducer: { //configureStore에 객체 형태로 전달하여, 스토어에서 사용할 리듀서를 등록합니다.
friends: friendsReducer,
chat: chatReducer,
},
});
export type RootState = ReturnType<typeof store.getState>; //스토어의 전체 상태에 대한 타입
//ReturnType<typeof store.getState>를 통해 스토어의 getState()가 반환하는 타입(전체 상태)을 추론하여 가져옵니다.
//컴포넌트에서 useSelector((state: RootState) => state.friends ...)처럼 사용 가능합니다.
export type AppDispatch = typeof store.dispatch;
//store.dispatch 함수의 타입입니다.
//비동기 Thunk 등을 작성할 때 타입 안정성을 높이기 위해 사용합니다.
export default store;
//생성한 스토어 인스턴스를 모듈의 기본값으로 내보냅니다.
//React에서 Provider 컴포넌트로 이 스토어를 감싸주면 전역에서 Redux 상태 사용이 가능해집니다.
1-3. friendsSlice.ts 생성
// store/friendsSlice.ts
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
/**
* createAsyncThunk
* 비동기 로직(예: API 요청)을 처리하기 위해 Redux Toolkit에서 제공하는 편의 함수입니다.
* 첫 번째 인자로 액션 타입 문자열, 두 번째 인자로 비동기 함수를 받습니다.
* 내부에서 fetchFriends.pending, fetchFriends.fulfilled, fetchFriends.rejected 형태의 액션이 자동으로 생성됩니다.
*/
interface Friend {
friendId: string;
email: string;
name: string;
profileImage?: string;
}
interface FriendRequest {
fromUserEmail: string;
fromUserName: string;
fromUserProfileImage: string;
status: string;
}
interface FriendsState {
list: Friend[];
receivedRequests: FriendRequest[];
sentRequests: FriendRequest[];
loading: boolean;
error: string | null;
}
const initialState: FriendsState = {
list: [],
receivedRequests: [],
sentRequests: [],
loading: false,
error: null,
};
// Async actions for API calls
// 실제 API 엔드포인트(/api/friends)를 호출해 친구 정보를 가져옵니다.
// 응답이 정상(ok)이 아니면 에러를 던집니다.
// 성공 시 JSON을 파싱해 Friend[] 형태로 반환합니다.
export const fetchFriends = createAsyncThunk("friends/fetchFriends", async () => {
const response = await fetch("/api/friends");
if (!response.ok) throw new Error("Failed to fetch friends");
return (await response.json()) as Friend[];
});
export const fetchReceivedRequests = createAsyncThunk("friends/fetchReceivedRequests", async () => {
const response = await fetch("/api/friends-requests");
if (!response.ok) throw new Error("Failed to fetch received requests");
return (await response.json()) as FriendRequest[];
});
export const fetchSentRequests = createAsyncThunk("friends/fetchSentRequests", async () => {
const response = await fetch("/api/sent-friend-requests");
if (!response.ok) throw new Error("Failed to fetch sent requests");
return (await response.json()) as FriendRequest[];
});
/**
* createSlice
* Redux Toolkit에서 제공하는 슬라이스 생성 함수입니다.
• name: 슬라이스 이름. 예) "friends".
• initialState: 이 슬라이스가 관리할 상태의 초기 값.
• reducers: 동기 액션 리듀서를 정의할 수 있는 공간. 현재는 빈 객체 {}.
• extraReducers: createAsyncThunk를 사용한 비동기 액션에 대한 리듀서를 정의하는 공간.
*/
const friendsSlice = createSlice({
name: "friends",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
// Fetch Friends
.addCase(fetchFriends.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchFriends.fulfilled, (state, action) => {
state.loading = false;
state.list = action.payload;
})
.addCase(fetchFriends.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || "Failed to fetch friends";
})
// Fetch Received Requests
.addCase(fetchReceivedRequests.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchReceivedRequests.fulfilled, (state, action) => {
state.loading = false;
state.receivedRequests = action.payload;
})
.addCase(fetchReceivedRequests.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || "Failed to fetch received requests";
})
// Fetch Sent Requests
.addCase(fetchSentRequests.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchSentRequests.fulfilled, (state, action) => {
state.loading = false;
state.sentRequests = action.payload;
})
.addCase(fetchSentRequests.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || "Failed to fetch sent requests";
});
},
});
export default friendsSlice.reducer;
2. 전역 스토어 Provider 설정
components / ClientProvider.tsx
'use client';
import { Provider } from "react-redux";
import store from "../../../store/store";
import { SessionProvider } from 'next-auth/react';
export default function ClientProvider({
children,
}: {
children: React.ReactNode;
}) {
return <SessionProvider> <Provider store={store}>{children} </Provider></SessionProvider>;
}
3. FriendsContent.tsx에서 Redux 훅 사용하기
핵심 변경 사항
1. useDispatch, useSelector (또는 useAppDispatch, useAppSelector)
• React Redux에서 제공하는 훅으로,
• useDispatch() : Redux 액션(Thunk 등)을 디스패치할 때 사용
• useSelector() : Redux 스토어의 상태를 읽을 때 사용
2. 비동기 요청
• 기존에는 fetchData()를 통해 직접 API를 호출하고 useState에 담았지만,
• 이제는 fetchFriends, fetchReceivedRequests, fetchSentRequests Thunk 액션을 dispatch 한 뒤, Redux에서 관리하는 상태(friends.list, friends.receivedRequests, friends.sentRequests)를 useSelector로 가져오면 됩니다.
3. 에러 및 로딩 상태
• 마찬가지로 friends.loading, friends.error를 참조하면 됩니다.
import { useDispatch, useSelector } from 'react-redux';
import { fetchFriends, fetchReceivedRequests, fetchSentRequests } from '../../../../store/friendsSlice';
import { RootState } from '../../../../store/store';
import { useAppDispatch, useAppSelector } from '../../../../store/hooks';
const dispatch = useAppDispatch();
// Redux store에서 필요한 상태를 꺼내옵니다.
const {
list: friends,
receivedRequests,
sentRequests,
loading,
error,
} = useSelector((state: RootState) => state.friends);
// 로컬 상태 (새로운 친구 추가용)
const [newFriendEmail, setNewFriendEmail] = useState<string>("");
useEffect(() => {
console.log("friends changed:", friends);
}, [friends]);
useEffect(() => {
// 컴포넌트가 마운트될 때, Thunk 액션을 디스패치해서 데이터 로드
dispatch(fetchFriends());
dispatch(fetchReceivedRequests());
dispatch(fetchSentRequests());
console.log(`friends`,friends)
}, [dispatch]);
728x90