문제 상황
프로젝트를 진행하다 보니 북마크 기능에서 불필요한 API 호출이 발생하는 것을 발견했다. 정보 딱 하나만 조회하면 되는데 전체 정보 조회 API가 하나만 존재하는 상황이었다. 그리고 각 페이지마다 중복된 북마크 로직이 존재하는 구조적으로 비효율적인 부분을 발견했다! 나는 단순한 북마크 상태 변경에도 전체 상세 정보를 다시 불러오는 것이 비효율적이라고 판단했고, 이를 개선하기 위해 백엔드 팀과 적극적으로 소통하며 해결책을 찾아갔다. (= 설득해서 단일 조회 API를 만들었다 ㅎㅎ)
기존 코드의 문제점 1
처음에는 북마크 상태 변경 시 전체 상세 정보를 조회하는 API만 있었다. 백엔드 팀의 초기 설계는 단순했지만, 실제 사용에서는 비효율적인 부분이 있었다.
이 구조의 문제점은 API 비효율, 즉 불필요한 데이터를 조회하고 컴포넌트 간 강한 결합도를 만들게 하는 구조였다.
북마크 상태만을 위한 단일 API가 있다면 더 효율적일 것 같다는 생각이 들었다. 따라서 백엔드 팀에 다음과 같은 내용으로 새로운 API 추가를 제안했다.
구조적인 관점에서는
1. 북마크 컴포넌트는 다른 페이지에서도 재사용될 가능성이 높다는 점
2. 상세 정보와 북마크는 독립적으로 관리되는 것이 좋다는 점
확장성 관점에서는
1. 추후 북마크 목록이나 추천 기능 등이 추가될 때 더 유연하게 대응할 수 있다는 점
2. 다른 기능에서도 북마크 상태만 필요한 경우가 생길 수 있다는 점
유지보수 관점에서는
1. 관심사를 분리하면 버그 추적도 더 쉬워질 것이라는 점
2. 각 기능의 책임이 명확해져 코드 관리가 수월해진다는 점을 근거로 들었다.
단일 API 만드는 것이 어려운 건 아니기도 하고 백엔드팀은 쉽게 제안을 받아들여줬당 ㅎㅎㅎ
기존 코드의 문제점 2
각 페이지마다 중복된 북마크 로직이 존재하는 상황이었다. 상태 관리 분산의 문제가 생겼다. 쉽게 말하자면 북마크 기능을 전역 컴포넌트로 빼서 효율성을 높이겠다는 소리!
- 여러 페이지에서 각각 북마크 로직 구현
- 동일한 장소의 북마크 상태가 페이지마다 달라질 수 있는 위험
- 코드 중복과 유지보수 어려움
따라서 북마크 컴포넌트를 완전히 분리하기로 했다.
기능 구현 및 해결 방안
먼저 단일 북마크 API 도입을 하기로 했다. 백엔드팀에게 북마크 상태만을 위한 경량화된 API를 요청했다.
그리고 나서 중앙화된 상태 관리를 위해 독립적인 북마크 컴포넌트를 구현했다. 하나의 북마크 컴포넌트가 전체 북마크 상태 관리하도록 설정했다. React Query의 캐시 기능 활용해 모든 페이지에서 동일한 북마크 상태 공유하도록 했다.
1. 독립적인 북마크 컴포넌트 구현
export const Bookmark = ({ placeId }: BookmarkButtonProps) => {
const queryClient = useQueryClient();
// 북마크 상태만 조회 (0.95KB)
const { data: bookmarkStatus } = useQuery({
queryKey: ['bookmark', placeId],
queryFn: () => getPlaceBookmark(placeId),
});
const { mutate: toggleBookmark } = useMutation({
mutationFn: async () => {
if (bookmarkStatus?.result.isSaved) {
return await deletePlaceBookmark(placeId);
}
return await postPlaceBookmark(placeId);
},
onSuccess: () => {
// 북마크 상태만 갱신
queryClient.invalidateQueries({ queryKey: ['bookmark', placeId] });
},
});
return (
<IconButton onClick={() => toggleBookmark()}>
<img
src={bookmarkStatus?.result.isSaved ? bookmarkActive : bookmarkDefault}
alt="북마크"
/>
</IconButton>
);
};
2. 북마크 관련 API 분리
// getPlaceBookmark.ts
interface BookmarkResponse {
isSuccess: boolean;
code: string;
message: string;
result: {
placeId: number;
isSaved: boolean;
};
}
export const getPlaceBookmark = async (placeId: number): Promise<BookmarkResponse> => {
const { data } = await client.get<BookmarkResponse>(`/places/${placeId}/bookmarks`);
return data;
};
자세한 코드는 밑에서 확인할 수 있다.
https://github.com/ssu-B1G4/Bookmark_web/pull/77
성과 측정
백엔드 팀이 새로운 API를 만들어준 후, 네트워크 패널을 통해 실제 개선 효과를 측정해봤다.
- 왼쪽 기존 API (전체 조회): 1.6KB
- 오른쪽 새로운 API (북마크만): 0.95KB
- 개선율: 54.2% 감소
데이터 전송률만 감소한게 아니라 호출 횟수도 감소했다.
- 기존: 북마크 토글 시 2번의 API 호출 (토글 + 상세정보)
- 개선: 1번의 API 호출 (토글만)
이렇게 API 호출당 54.2%의 데이터 전송량 감소하는 최적화를 진행하였다.
추가로 고려해볼 만한 점은
- Optimistic Updates 적용으로 UX 더욱 개선
- 에러 처리와 재시도 로직 보강
- 오프라인 지원을 위한 전략 수립
이런 것들이 있을 것 같다. 낙관적 업데이트를 리팩토링 하면서 다시 적용해봐야겠다.
이런 사소한 것들이 작은 최적화처럼 보일 수 있지만, 실제로는 상당한 효과가 있다는 점을 실제 수치로 파악해볼 수 있는 좋은 경험이었다. 그리고 또 느낀 점은... 더 효율적인 방법이 있어보인다면 망설이지 말고 다른 팀에게 내 의견을 어필하기!!
'React' 카테고리의 다른 글
[React/리액트] 웹 폰트 로딩 최적화하기: preload와 swap 적용하기 (0) | 2025.01.08 |
---|---|
[React/리액트] React 성능 최적화: Code Splitting과 Lazy Loading 적용기 / 번들 최적화하기 (1) | 2025.01.08 |
[React/리액트] 프로젝트에 Sentry 도입하기 / 모니터링 시스템 구축하기 (0) | 2025.01.08 |
React에서 WebSocket 채팅 구현하기 / SockJS 사용하기 (0) | 2025.01.07 |
React에서 웹과 앱 환경에 따라 하단바 다르게 표시하기 (0) | 2025.01.07 |