이번에 프로젝트를 시작하게 되면서, 항상 그렇듯이 어떤 기능을 사용하고 사용하지 않을 것인지에 대한 회의를 진행했다.
그러다 캐시에 대한 이야기를 나누며 Tanstack query에 대한 이야기가 나왔다.
Next.js를 사용해서 프로젝트를 진행하는데 Next.js의 fetch기능과 Tanstack query 두 가지중 선택하는 과정이 있었다.
결론적으로는 Tanstack query를 사용하지만, 정확히 두 기능의 차이가 뭔지,
프로젝트에서 기술을 사용하는 이유를 정확히 알고 시작을 하는 게 좋지 않을까?라는 생각에 해당 주제로 글을 작성하게 되었다.

"Tanstack query랑 Next.js의 fetch.. 둘 다 캐싱하는 거 아닌가?"
처음 프로젝트에 뭘 사용할 지 의견을 나누는 과정에서 이 두 가지가 논의 선상에 올랐을 때 가장 처음 든 생각은 두 기능의 차이가 뭐지..? 였습니다.
또 react 프로젝트에서는 "그냥 더 편하니까"를 이유로 tanstack query를 썼던 것 같은데, 왜 Next.js에서는 바로 tanstack query를 쓰는 게 아니라 fetch이야기가 나왔을까 또 궁금하더라고요🤔
그래서 오늘은 react에서의 fetch와 Next.js에서의 fetch. Tanstack query의 차이점을 보고 제가 적용했던(또는 하고 있는) 프로젝트들에 적절하게 사용한 건지 한번 생각해 보면서 글을 작성해 보겠습니다.
React에서의 fetch()
일단 React에서의 fetch는 자바스크립트에 내장되어 있는 객체입니다.
브라우저에 내장된 Web API이기 때문에 React, Vue, Vanilla JS든 그냥 JS 환경이라면 어디서든 쓸 수 있는 기능이라는 뜻이죠.
fetch() 사용법
fetch(url, options)
.then((response) => console.log("response:", response))
.catch((error) => console.log("error:", error));
아마 프론트엔드를 개발해 본 분이라면 익숙하실 겁니다.
- 첫 번째 인자로 URL(API), 두 번째 인자로 옵션 객체를 받습니다.
- options : HTTP method, HTTP headers, HTTP body 설정 가능
- Response(응답) 객체로부터는 HTTP 상태(status), HTTP header, HTTP body 등을 읽어올 수 있습니다.
- 반환으로는 Promise 타입의 객체를 반환합니다.
- API 성공 시(then) 응답(response) 객체를 resolve 하고,
- 실패 시(catch) 예외(error) 객체를 reject 합니다.
여기서 HTTP method에는 GET, POST, PUT, DELETE 등이 있습니다.
각각 데이터 가져오기/생성하기/수정하기/삭제하기 등에 사용됩니다만... 아마 다들 알고 계시겠죠..?
다들 알고 계시는 상태라고 간주하고, 이에 대한 설명은 짧게 끝내도록 하겠습니다.
결론적으로는 React가 fetch 기능을 제공한다기보다는,
브라우저(JS)의 fetch 기능을 단지 React 안에서 사용하고 있다는 것에 가깝겠네요.
그럼 코드를 살짝 봐보겠습니다.
import { useEffect, useState } from 'react';
const ProductList = () => {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/products')
.then((res) => res.json())
.then((data) => {
setProducts(data);
setLoading(false);
})
.catch(() => {
// 에러 처리 생략...
setLoading(false);
});
}, []);
if (loading) return <p>로딩 중...</p>;
return (
<ul>
{products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
};
그냥 상품 리스트를 가져오고, 보여주는 간단한 코드입니다.
사실 돌아가는데 문제는 없는 코드지만, 코드를 쭉 읽어보면 굉장히 비효율적인 걸 찾을 수 있습니다.
일단
- 데이터가 매번 새로 요청됨
→ 데이터가 바뀌지 않고 동일한 리스트여도, 페이지 이동 후에 돌아오면 다시 로딩됩니다. 비효율의 끝판왕.. - 로딩/에러 수동 처리
→ 저도 저렇게 처리해 본 경험이 있는 사람으로서(..ㅎ) 코드 복붙의 반복입니다. - UX가 무거워짐
→ 당연한 이야기지만, 캐싱이 없기 때문에 상세 페이지를 확인하고 리스트로 돌아오면 또 로딩해야 합니다.
이런 효율 떨어지고, 불편한 점을 위해 저희가 많이 사용하는 react-query. 현재로서는 tanstack-query로 이름이 바뀐 이 녀석을 많이 사용하게 됩니다.
Tanstack-query
Tanstack-query는 fetch를 대신해 준다기보다는, 이후 과정을 도와주는 라이브러리라고 생각하시면 좋을 것 같습니다.
fetch처럼 데이터를 가져오는 함수를 직접 실행하지는 않지만, 위에서 문제가 되었던 로딩, 에러, 캐싱 등을 도와주기 때문이죠!
자, 백문이 불여일코드라고 백번 말하는 것보단 한번 코드를 보면 이해가 되실 겁니다.
import { useQuery } from '@tanstack/react-query';
const fetchProducts = async () => {
const res = await fetch('/api/products');
if (!res.ok) throw new Error('Network response was not ok');
return res.json();
};
const ProductList = () => {
const { data, isLoading, error } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
});
if (isLoading) return <p>로딩 중...</p>;
if (error) return <p>에러가 발생했어요.</p>;
return (
<ul>
{data.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
};
일단 이 녀석은
- 캐싱이 자동으로 됨
→ 캐싱이 자동으로 되어 있어 같은 요청을 반복할 필요도 없어지고, 이동해도 다시 로딩되지 않고 빠르게 렌더링 됩니다. - 로딩/에러 처리
→ 위 fetch 코드에서는 useState를 사용해서 별도로 변수를 선언하고, 값을 바꿔주고 하면서 수동으로 처리했던 로딩 처리가 여기서는 자동으로 진행됩니다.
따로 useState로 처리할 필요가 사라지면서 깔끔하게 관리가 가능해집니다. - 캐시 그룹핑
→ 캐시를 그룹핑하고 관리가 가능해져서 필터나 무한스크롤, 페이징 같은 기능에 잘 사용됩니다.
한 마디로 정리해 보자면
fetch()를 대신하는 기능이 아닌, fetch()을 통해 가져온 데이터의 흐름을 관리해 주는 도구 가 좀 더 명확한 정의라고 생각합니다.
Next.js에서의 fetch()
자, 그럼 next에서의 fetch는 어떤 점이 다른지 한 번 알아보도록 하겠습니다.

Next.js도 Page Router 기반이 많았고, CSR 환경이 주였던 상황에서
Next.js 13에서 App Router가 추가되고, React Server Component를 본격적으로 도입하게 되면서 변화가 시작됩니다.
fetch()를 서버에서 사용할 때 next.revalidate, cache 옵션 등을 붙여 ISR/SSR 기반의 서버 캐싱을 쉽게 구현할 수 있는 방식이 나오게 됩니다.
Next.js에서의 fetch는 브라우저 기본 fetch를 래핑 한 버전입니다.
App Router 환경에서 서버 컴포넌트 안 fetch가 자동으로 캐싱과 최적화를 해줍니다.
// app/products/page.tsx (서버 컴포넌트)
const getProducts = async () => {
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 60 }, // 60초 동안 캐시 유지
});
return res.json();
};
export default async function ProductPage() {
const products = await getProducts();
return (
<ul>
{products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
여기서 눈에 띄는 코드는 next: { recalidate: 60 }입니다. (저만 눈에 띄지는.... 않겠죠...)
이 녀석은 한 번 가져온 데이터를 60초 동안 서버에서 캐싱해 주는 기능입니다.
동일한 요청이 다시 들어와도 백엔드에 재요청하지 않고, 서버 캐시에서 응답을 주게 됩니다.
어떤 녀석을 사용해야 할까
정리를 마치고 나니 이젠 무슨 차이인지 확실하게 느꼈습니다.
이번에 진행하는 프로젝트에서 Tanstack query와 Next.js의 fetch를 고민하면서 작성한 회의록이 있는데..
tanstack-query
1. 클라이언트 상태와 서버 상태를 명확하게 분리할 수 있다. (관심사 분리)
2. 서버 요청에 필요한 기능 (cache, statetime, refetch) 등을 쉽게 사용할 수 있다.
3. (vs useSWR) mutation 관련 기능 및 더 정교한 cache 컨트롤이 가능하다.
4. (vs Next fetch) - Next는 서버에서의 cache를 다루고 tanstack-query는 클라이언트의 캐시를 다룬다. - 자동 패칭 및 로딩 및 오류 관리를 직접 관리해주어야 함
이렇게 작성되어 있습니다.
당시에는 사실 정확히 이해하지 못하고 아 그렇구낭..😶 하면서 넘어갔었는데 이제는 완전히 이해가 되었습니다.
문제점을 정확히 파악하고 선택했네요.
저희 프로젝트는 아무래도 스터디 관련 플랫폼이다 보니 사용자 중심의 상호작용이 굉장히 많습니다.
- 로그인된 사용자에 따라 서로 다른 데이터를 불러와야 하는 부분도 있고 (면접자 vs 피면접자 또는 스터디 회원 vs 비회원등..)
- 게시판이나 댓글, 스터디 현황 등 데이터가 자주 바뀌고 비동기 요청도 많으며
- 기능이 많지만 아무튼 동적이고 유저 중심적인 데이터 흐름이 좀 많습니다.
아무튼 이런 내용으로 Next.js의 fetch만 쓰기에는
- 클라이언트 상호작용 기반 실시간 갱신에 어울리지 않고
- 일단 로딩 상태 관리, 오류 관리 같은걸 수동으로 해줘야 한다는 점이... 마이너스였습니다.
이런 점에서 현재 프로젝트에 Tanstack-query를 사용하는 건 좋은 선택지였다고 생각합니다.
다만 기능이 가볍거나 다중 사용자와의 연결이 없거나.. 대부분의 데이터가 한 번 요청하면 거의 변하지 않는, 좀 정적에 가깝고 흐름이 예측 가능한 서비스라면 Next.js의 fetch()를 사용하는 게 좀 더 효율적일 수 있습니다.
내 프로젝트가 어떤 유형인지, 어떤 장단점이 있는지 확인하고 항상 고민하며 프로젝트에 도입하는 자세를 가져보도록 노력합시다💪
출처
'Frontend' 카테고리의 다른 글
| Headless UI. shadcn UI를 사용하며 느낀 것들 (0) | 2025.05.14 |
|---|---|
| [디자인 패턴] MVC / MVP / MVVM 패턴 (0) | 2025.04.16 |
| 주소창에 google.com 입력시 일어나는 일 (2) | 2025.04.10 |
| SSE(Server-Sent Event) : 서버야 네가 알려줘 (1) | 2025.02.06 |
| 프론트엔드를 위한 Presigned URL 업로드 (1) | 2025.02.04 |