프로젝트에서는 활성화 되어있는 모집파티를 실시간으로 Update하는 과정이 필요했다. Socket.io를 이용하여 클라이언트와 서버의 실시간 연동을 통해 클라이언트들의 실시간성 유지하였다.
클라이언트에서는 모임을 생성하면 소켓서버에 이벤트를 방출한다. 이벤트를 감지한 소켓서버는 DB를 Update하고, 소켓과 연결된 클라이언트들에게 이벤트를 방출한다.
서버소켓에서 방출한 이벤트를 감지한 클라이언트들은 콜백으로 모집중인 파티, 내가찜한 파티의 전역상태를 최신으로 Update하는 과정을 통해 실시간으로 변하는 데이터를 받아 올 수 있었다.
// partySlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { get } from '../api/API';
import type { Party } from '../pages/MainPage/Type';
const initialState: { myPartyList: Party[]; activePartyList: Party[] } = {
myPartyList: [],
activePartyList: [],
};
export const getMyPartyList = createAsyncThunk('party/host', async (_, { rejectWithValue }) => {
try {
const res = await get('/api/parties/liked-party');
if (!res) {
throw new Error('에러!');
}
return res;
} catch (err) {
rejectWithValue(err);
}
});
export const getActivePartyList = createAsyncThunk(
'party/active',
async (_, { rejectWithValue }) => {
try {
const parties = await get('/api/parties');
if (!parties) {
throw new Error('에러!');
}
return parties;
} catch (err) {
rejectWithValue(err);
}
},
);
const partySlice = createSlice({
name: 'party',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getMyPartyList.fulfilled, (state, action) => {
const lists = [...action.payload];
const newList = lists.map((list: Party) => {
if (list.likedNum !== list.partyLimit) return list;
list.isComplete = 1;
return list;
});
state.myPartyList = [...action.payload];
});
builder.addCase(getMyPartyList.rejected, (state, action) => {
state.myPartyList = [];
});
builder.addCase(getActivePartyList.fulfilled, (state, action) => {
state.activePartyList = [...action.payload];
});
builder.addCase(getActivePartyList.rejected, (state, action) => {
state.activePartyList = [];
});
},
});
export default partySlice;
카카오페이 Tech에서 React Query를 도입한 글을 보게 되었다. 프로젝트에서도 Redux를 이용해 서버상태를 관리하는데에 여러 단점이 존재하였기 때문에 리팩토링 과정에서 React Query를 도입하기로 결정하였다.
현재 Redux를 통해 비동기데이터를 관리하는데 프로젝트에서 발생하는 단점들은 다음과 같다.
- Boilerplate 코드가 너무 많다.
- 클라이언트 전역상태를 관리하는 부분과 서버상태를 관리하는 부분이 하나의 store에 존재한다.
- 불필요한 API호출이 발생한다.
Redux는 상태를 읽기전용으로 취급하고, 하나의 store에서만 action과 Reducer를 통해 데이터가 변경되기 때문에 복잡한 상태를 체계적으로 관리 할 수 있다.
하지만 기본적으로 작성해야 하는 Boilerplate 코드가 너무 많다. 그래서 코드를 보면 한눈에 로직이 들어오지 않는 단점이 있다.
API를 호출하는 과정에서도 Loading, Error, update 같은 상태들을 state로 관리하였는데 데이터가 변경된 후 state를 변경함으로써, useEffect를 통해 api를 재 호출하는 과정으로 새로운 데이터를 받아왔다.
또한 비동기 데이터까지 처리하는 경우 하나의 store에서 서버상태와 클라이언트 상태를 모두 관리해야 한다는 단점이 발생한다. 성격이 다른 데이터를 한곳에서 변경하는 것은 코드적인 관점에서 좋지 않다.
Redux 자체는 클라이언트의 전역상태를 관리하기 위한 라이브러리이지, 서버의 상태를 관리하기 위한 라이브러리가 아니므로, Loading, Error와 같은 서버상태를 개발자가 알아서 지정해줘야 한다. 이는 협업시에도 개발자의 생산성을 떨어트린다.
// foodDetail.tsx
const [isLoading, setLoading] = useState<boolean>(true);
const [update, setUpdated] = useState<boolean>(false);
const [shopState, setShopState] = useState<ShopState>(initialShopState);
const [commentState, setCommentState] = useState<CommentState[]>([]);
const [menuState, setMenuState] = useState<MenuState[]>([]);
const fetchCommentState = async (shopId: number) => {
const commentState = await getComment(shopId);
setCommentState(commentState);
};
const fetchShopState = async (shopId: number) => {
const [shopState, menuState] = await Promise.all([getShop(shopId), getMenu(shopId)]);
setShopState(shopState);
setMenuState(menuState);
};
const fetchInitialData = async () => {
await fetchCommentState(shopId);
await fetchShopState(shopId);
setLoading(false); // 데이터 fetching후 loading상태 변경
};
const updateCommentState = useCallback(() => {
setUpdated((current) => !current);
}, []); // prop으로 전달 > 하위 컴포넌트에서 실행
useEffect(() => {
fetchInitialData();
}, []);
useEffect(() => {
fetchCommentState(shopId);
}, [update]); // update상태가 변경되었다면 다시 data Refetching함.
// CommentList.tsx
const handleDelete = async (e: React.MouseEvent<HTMLButtonElement>, commentId: number) => {
await deleteComment(commentId);
updateCommentState();
}; // 댓글 삭제후 refetch
ref)