⚛️ Basic 01. TanStack Query의 본질과 철학 (Why React Query?)
📋 개요
useEffect 페칭의 한계를 깨닫고, Server State와 Client State의 명확한 분리를 통해 TanStack Query의 필요성을 이해합니다.
📋 목차
- ☕️ 영철이의 고민: "데이터 가져오는 게 이렇게 힘들 일인가요?"
- 🤔 왜 알아야 하는가: Server State의 독립 선언
- 1.
useEffect페칭의 5가지 재앙 (The Bad Parts) - 2. 해결책: "비동기 상태 관리자" TanStack Query
- 3. Stale-While-Revalidate (SWR) 패러다임
- 📝 배운 내용 점검하기 (Quiz)
"영철 님, 지금 작성하신
useEffect페칭 코드... 그 안에 숨어있는 5개의 버그가 보이시나요?"
☕️ 영철이의 고민: "데이터 가져오는 게 이렇게 힘들 일인가요?"
(월요일 오후, 머리를 벅벅 긁으며 모니터를 뚫어져라 쳐다보는 영철)
🐣 영철: 아... 데이터 하나 가져오는데 코드가 왜 이렇게 길어지지? 로딩 상태(isLoading) 만들고, 에러 상태(error) 만들고... 어랏, 카테고리를 '소설'에서 '만화'로 빠르게 바꿨더니 데이터가 꼬이네?
🦁 영호: 영철 님, 또 useEffect 안에서 fetch 돌리고 계시네요.
🐣 영철: 헉! 리드 님! 네, 카테고리별 도서 목록 가져오는 컴포넌트 만들고 있었어요. 제가 의존성 배열도 완벽하게 넣었는데 왜 데이터가 꼬이는 걸까요?
🦁 영호: 네트워크 요청은 보낸 순서대로 도착한다는 보장이 없으니까요(Race Condition). 영철 님이 짠 10줄짜리 페칭 코드에요, 지금 당장 터질 수 있는 버그가 5개나 숨어있어요. 데이터 페칭은 쉽지만, 비동기 상태 관리(Async State Management)는 결코 만만하지 않습니다.
🤔 왜 알아야 하는가: Server State의 독립 선언
🦁 영호: 영철 님, 우리가 Redux나 Zustand 같은 상태 관리 도구에 넣어두고 전역으로 돌려 쓰는 데이터들을 한 번 생각해 볼까요? 대부분이 서버에서 가져온 데이터(게시글, 유저 정보 등)일 겁니다.
이걸 **Client State(클라이언트 상태)**처럼 취급하니까 문제가 생기는 거예요. 서버 데이터는 우리 프론트엔드 앱이 '소유'한 데이터가 아닙니다. 그저 사용자에게 최신 상태를 보여주기 위해 잠시 빌려온 데이터일 뿐이죠.
TanStack Query(React Query)는 이 "빌려온 데이터(Server State)"를 클라이언트 상태로부터 완벽하게 분리하여 캐싱, 무효화, 백그라운드 업데이트를 자동으로 처리해주는 비동기 상태 관리자입니다. 이걸 이해하지 못하면 평생 로딩 스피너와 싸우게 될 거예요.
1. useEffect 페칭의 5가지 재앙 (The Bad Parts)
"그냥 fetch 한 번 호출하면 끝나는 거 아니야?" 라고 생각하기 쉽습니다. 하지만 실무에서 아래와 같은 코드를 작성했다면 심각한 문제들을 마주하게 됩니다.
// 🐣 영철: "이 정도면 완벽하지 않나요?"
function Bookmarks({ category }) {
const [data, setData] = useState([]);
const [error, setError] = useState();
useEffect(() => {
fetch(`/api/bookmarks/${category}`)
.then(res => res.json())
.then(d => setData(d))
.catch(e => setError(e));
}, [category]); // 카테고리가 바뀔 때마다 다시 가져옴!
// return JSX...
}이 짧은 코드에 숨겨진 5가지 버그는 다음과 같습니다 (TkDodo의 "Why You Want React Query" 참조):
- 레이스 컨디션 (Race Condition) 🏎️: 카테고리를 빠르게 'A' ➔ 'B'로 변경할 때, A의 응답이 B보다 늦게 도착하면 화면에는 엉뚱하게 A의 데이터가 표시됩니다.
- 사라진 로딩 상태 🕐: 두 번째 요청부터는 로딩 스피너를 보여줄 방법이 없습니다. 사용자는 앱이 멈춘 줄 알게 됩니다.
- 위험한 빈 배열 초기화 🗑️:
useState([])로 초기화하면, "진짜 데이터가 없는 빈 상태"와 "아직 로딩 중이라 빈 상태"를 UI에서 구분할 수 없습니다. - 오염된 에러 상태 🔄: A 카테고리에서 에러가 난 뒤 B 카테고리로 성공적으로 넘어갔을 때, 에러 상태를 수동으로 지워주지 않으면 과거의 에러 UI가 계속 떠 있게 됩니다.
- StrictMode 더블 페칭 🔥: 개발 모드에서는 무조건 2번씩 호출됩니다.
2. 해결책: "비동기 상태 관리자" TanStack Query
React Query는 데이터 페칭 라이브러리가 아닙니다(페칭은 여전히 fetch나 axios가 합니다). **비동기 상태 관리자(Async State Manager)**입니다.
앞서 본 재앙 같은 코드가 React Query를 만나면 이렇게 변합니다.
// 🦁 영호: "단 5줄. 레이스 컨디션 방지, 캐싱, 로딩 처리, 오류 관리가 모두 끝납니다."
function Bookmarks({ category }) {
const { isLoading, data, error } = useQuery({
queryKey: ['bookmarks', category], // 의존성 배열 역할을 동시에 수행
queryFn: () => fetch(`/api/bookmarks/${category}`).then(res => res.json())
});
// return JSX...
}Server State vs Client State의 명확한 분리
React Query의 핵심 철학은 관심사의 분리입니다.
- Client State: 다크 모드 토글, 모달 열림 상태, 현재 선택된 탭 등 프론트엔드 앱이 완벽하게 통제하고 소유하는 상태 (Zustand, Context API 등 사용)
- Server State: 서버 DB에 저장되어 있고, 여러 사용자가 언제든 바꿀 수 있으며, 프론트엔드가 그저 화면에 그리기 위해 최신 스냅샷을 빌려온 상태 (React Query 사용)
이 두 가지를 억지로 하나의 전역 스토어(Redux 등)에 구겨 넣지 말고, Server State는 React Query의 우아한 캐싱 시스템에 전면 위임하는 것이 이 라이브러리의 존재 이유입니다.
3. Stale-While-Revalidate (SWR) 패러다임
React Query가 데이터를 다루는 방식을 한 문장으로 요약하면 **"일단 구운 식빵(캐시)을 먼저 주고, 그 사이에 뒤에서 새 식빵(최신 데이터)을 굽는다"**입니다. 이것이 바로 Stale-While-Revalidate 전략입니다.
- 사용자가 페이지에 진입하면 캐시된 데이터(Stale)를 즉시(Instantly) 보여줍니다.
- 동시에 뒤(Background)에서는 서버에 최신 데이터가 있는지 **조용히 확인(Refetch)**합니다.
- 변경사항이 있다면 화면을 슬쩍 최신화합니다.
이로 인해 사용자는 로딩 스피너를 거의 보지 못하며, 앱이 기가 막히게 빠르다고 느끼게 됩니다.
🐣 영철이의 퇴근 일기
오늘 진짜 뼈를 맞았다. useEffect 안에 fetch 하나 넣었다고 5가지 버그 구멍이 뚫려있었을 줄이야...
💡 오늘의 교훈: "데이터 페칭은 쉽지만, 비동기 상태 관리는 결코 쉽지 않다. 서버 데이터는 내 것이 아니라 빌려온 거니까, 똑똑한 대여소(React Query)에 맡기자."
앞으로 무지성 useEffect 페칭은 절대 금지다. 뭔가 직접 상태 코드를 추가할 때마다 버그가 늘어난다는 영호 리드 님의 말이 머리에 계속 맴돈다. 상태를 클라이언트 꺼랑 서버 꺼로 나누니까 생각도 훨씬 깔끔해졌다. 집 가서 치킨 시켜놓고 내 코드 싹 다 useQuery로 갈아엎어 봐야지! 🍗
📝 배운 내용 점검하기 (Quiz)
Q. 다음과 같이 여러 번 컴포넌트 간에 데이터를 공유하기 위해 서버 데이터를 전역 상태 관리 도구(예: Zustand 혹은 Redux)에 복사하여 담으려고 합니다. 이때 발생할 수 있는 잠재적 문제는 무엇인가요?
✅ 정답: Server State의 "구독(Subscription)" 및 "백그라운드 업데이트" 혜택을 잃게 되고, 데이터가 영구적으로 과거의 상태(Outdated)에 머무르게 됩니다.
💡 상세 해설:
- 원리 설명: React Query는 백그라운드에서 지속적으로
Stale-While-Revalidate를 수행하여 최신 데이터를 유지합니다. 하지만useQuery로 받아온 데이터를 다시useState혹은zustand스토어로 "복사(Sync)"하는 순간, 원본 데이터(Query Cache)와의 연결고리가 끊어집니다. - 오답 피드백: "영철 님, 데이터를 전역 스토어에 복사해 버리면 쿼리가 아무리 백그라운드에서 데이터를 최신화해도 컴포넌트는 그걸 모르게 돼요. 서버 데이터는 Query Cache라는 전용 금고에 그대로 두시고, 꺼내 쓰기만 하세요!"
- 📌 핵심 기억법: State Copy = Opt-out of Background Updates. Server State는 캐시(QueryClient)에 온전히 맡겨라!