문제 발견
사실 이 문제점을 처음 발견한 것은 지난 7월 말이었습니다. 당시 프로젝트에 투입되어 대부분의 API를 구현하는 역할을 맡다보니 Zustand로 API 통신하기에만 급급했습니다. 이렇다보니 같은 API 통신이 4번이나 발생한다는 것을 알고도, 프로젝트 기한 때문에 React의 Strict.Mode겠지 하며 넘어갔습니다.
문제가 되었던 부분
당시 구현했던 코드입니다.
section: [
{
component: 컴포넌트 A,
},
{
component: 컴포넌트 B,
},
{
component: 컴포넌트 C,
},
{
component: 컴포넌트 D,
},
{
component: 컴포넌트 E,
},
{
component: 컴포넌트 F,
},
],
위 컴포넌트 중 총 4군데에서 동일한 API를 호출하고 있던 것이 문제였습니다.
이를 파악한 동시에 저의 요구사항을 충족시켜줄 방법들을 탐색했습니다.
요구사항
1. 캐싱
2. 동일한 API 호출 방지
3. 렌더링 속도 증가
제가 생각해본 방법들 그리고 왜 선택했는지
1. Tanstack Query
2. Zustand + 커스텀 캐싱 레이어 - 기존 스택 활용
3. SWR
결론부터 말하자면, 당연히(?)라고 말할 수 있지만 Tanstack query를 선택했습니다.
왜 제가 이 방법을 선택했고, 다른 방법들을 선택했는지에 대해 기술해보겠습니다.
우선 2번, Zustand의 경우 대부분의 팀원들이 사용할 수 있는 점 + 제가 대부분의 코드를 작성했기 때문에 손쉽게 사용할 수 있었습니다.
하지만, 캐싱/중복 방지 로직을 직접 구현이 필요했고 Tanstack query만큼 자동화되지 않는다는 점이 치명적이었습니다.
추가로, 저 이후에 프로젝트를 다룰 사람이 보기 편해야 했습니다.
3번, SWR 같은 경우엔 간단합니다. 제가 사용해 본 경험이 없고, Tanstack query보다 기능이 적었습니다.
마지막으로 제가 선택한 Tanstack query은 팀원들은 사실 사용해 본 경험이 없습니다. 하지만, 많지 않은 코드로 간단하게 캐싱 및 API 중복 호출을 방지할 수 있었습니다. 또 함께 성장하기 위해서는 사용해보지 않은, 대부분의 개발자들이 사용하는 라이브러리를 도입해야겠다고 생각했습니다.
hook 리팩토링
우선 이전에 사용되었던 코드부터 살펴봅시다.
이전에 사용되었던 코드
useEffect(() => {
const api = new Api();
const id = Number(params.get('id')); // id값을 기준으로 게시글의 상세 페이지로 이동
/////////// api 호출 부분////////////
}, []); // ❌ 빈 배열 - id가 바뀌어도 재요청 안 됨!
리팩토링 된 코드
const { data: viewData } = useQuery({
queryKey: ['쿼리 키 1', '쿼리 키 2', id], // ✅ id가 바뀌면 자동으로 재요청!
queryFn: async () => { /* ... */ },
});
기존 코드에서 useEffect를 제거하고 쿼리 키를 사용함으로써 id가 바뀐다면, 쿼리 키가 변경되어 자동으로 재요청하는 이득까지 생겼습니다.
추가로, useEffect의 의존성 배열 관리 역시 불필요하게 되었습니다.
리팩토링하면서 생긴 이득에 대해 더 알아봅시다.
자동 클린업 및 요청 취소
- 컴포넌트 언마운트 시 자동으로 요청을 취소합니다.
- id 변경 시 이전 요청을 자동으로 취소합니다.
Race Condition 자동 방지
- 이전 요청을 자동으로 취소합니다.
- 항상 최신 요청 결과만 사용합니다.
동작 원리
1. 같은 쿼리 키 = 같은 데이터
- ['쿼리 키 1', '쿼리 키 2', 123] → id가 123인 게시글 데이터
- ['쿼리 키 1', '쿼리 키 2', 456] → id가 456인 게시글 데이터
2. 중복 요청 자동 방지
- 첫 번째 컴포넌트: API 호출 실행
- 나머지 컴포넌트들: 같은 쿼리 키 감지 → 첫 요청 결과 대기
3. 자동 캐싱
- 한 번 받은 데이터는 5분간 캐시에 저장
- 같은 페이지를 다시 방문하면 즉시 표시
실제 동작 시나리오
URL: ?id=123인 경우
1\. 컴포넌트 A 호출
→ 쿼리 키: ['쿼리 키 1', '쿼리 키 2', id]
→ 캐시 없음 → API 호출 실행 ✅
2\. 컴포넌트 B 호출
→ 쿼리 키: ['쿼리 키 1', '쿼리 키 2', id]
→ 같은 키 감지 → 첫 요청 결과 대기 ⏳
3\. 컴포넌트 C 호출
→ 쿼리 키: ['쿼리 키 1', '쿼리 키 2', id]
→ 같은 키 감지 → 첫 요청 결과 대기 ⏳
4\. 컴포넌트 D 호출
→ 쿼리 키: ['쿼리 키 1', '쿼리 키 2', id]
→ 같은 키 감지 → 첫 요청 결과 대기 ⏳
결과: API 1번만 호출! 🎉
📊 성능 개선 결과
Before (기존 방식)
- ❌ API 호출: 4번
- ❌ 네트워크 트래픽: 4배
- ❌ 서버 부하: 4배
- ❌ 초기 로딩: 느림
After (TanStack Query 적용)
- ✅ API 호출: 1번
- ✅ 네트워크 트래픽: 75% 감소
- ✅ 서버 부하: 75% 감소
- ✅ 초기 로딩: 빠름
- ✅ 재방문 시: 즉시 표시 (캐시 활용)
리팩토링 전

리팩토링 후

결론
작은 실수 하나가 성능에 큰 영향을 미칠 수 있습니다. 개발자 도구를 자주 확인하고, 중복 요청이 발생하지 않는지 체크하는 습관이 중요합니다.
TanStack Query를 도입하면서 코드도 간결해지고, 성능도 개선되고, 사용자 경험도 향상되었습니다.
**참고 자료**