티스토리 뷰

반응형

클라이언트에서 API 호출할때 사용하는 도구를 알아보자. (fetch, axios, TanStack Query, SWR) +  graphql, firebase, supabase

 

fetch

브라우저 내장 API로 별도의 설치 없이 사용이 가능하다.

Promise 기반으로 HTTP 요청을 보낸다.

사용 시 JSON 파싱 등 부가 처리를 직접해야한다.

내장 API로 가볍고 기본적인 기능을 제공하며, 별도의 의존성이 없다는 장점이 있다.

단점으로는 요청/응답 처리 로직을 매번 수동으로 작성해줘야하며 인터셉터나 요청/응답 변환 기능이 없다.

에러처리나 타임아웃, 리트라이 같은 부분도 직접 작성해줘야한다.

const res = await fetch('/api/data');
const data = await res.json();

 

프로젝트가 단순하고, 별도 네트워크 추상화가 필요없을때 도입을 고려해볼만하다. 

큰 프로젝트에서는 직접 개발해야하는 부분들이 있기 때문에 커스텀 훅이나 wrapper등을 직접 관리할 여력이 있을때 사용하면 좋다.

 


 

 

Q. fetch를 사용하게된다면 퍼포먼스가 좋을까?

A. 좋을수도 있고, 아닐 수도 있다.

 

퍼포먼스는 '속도 + 효율 + 개발 생산성'을 말한다.

즉, 퍼포먼스란 단순히 빠른 응답 시간만이 아니라 API 호출 속도, 메모리 사용량, 중복 요청 방지, 리페치 타이밍, 사용자 체감 속도(UI/UX), 코드 유지보수성과 확장성을 포함하는 개념이다.

이걸 기준으로 직접 커스터마이징했을 때의 이점은 아래와 같다.

  • 외부 라이브러리 없이 네이티브 API이기 때문에 초기 번들 사이즈가 작아 빠르다.
  • 요청/응답 구조, 에러처리, 리트라이 로직을 원하는 방식으로 작성할 수 있다는 유연함이 있다.
    • TanStack Query나 SWR에서 기본으로 포함된 기능을 직접 조절할 수 있다. (= 불필요한 기능을 배제할 수 있다.)
    • 프로젝트 특화 로직을 공통 훅에 넣어 직접 개발할 수 있기에 최적화된 로직을 구성할 수 있다.

 

 

예시

커스텀 wrapper

const customFetch = async (url: string, options?: RequestInit) => {
  const res = await fetch(url, {
    ...options,
    headers: {
      Authorization: `Bearer ${getToken()}`,
      ...options?.headers,
    },
  });

  if (!res.ok) {
    throw new Error(await res.text());
  }

  return res.json();
};

 

 

허나 방금 말한 장점들은 반대로 단점으로 작용하기도 한다.

직접 모든 기능을 개발해야하기에 개발자 능력이 중요하다.

  • 중복 로직 발생을 유의해야한다.
  • 기능이 많아지고 복잡해지면 결국 라이브러리 수준이 될 수 있으므로 오히려 유지보수에 안좋을 수 있다.
  • SSR, CSR, suspense등의 연동도 구현해야하므로 난이도가 올라간다.

 

결론은 구성원이 커스터마이징하고 싶은 니즈가 크거나 TanStack Query와 같은 무거운 라이브러리를 피하고 싶을때, 혹은 성능 최적화를 직접 관리할 역량이 될 때 사용하면 좋다.

특히 SSR, Streaming등의 최적화를 세밀하게 하고 싶다면 좋은 옵션이다.

하지만 반대로 빠른 기능 도입, 실무 팀원의 경험치가 다양하다면 TanStack Query나 SWR과 같은 고수준 도구가 더 안정적일 것이다.

 

 


 

 

Q. fetch가 Next.js에서 유리한 점이 뭐야?

A. Next.js 내부에서 '트래킹 가능한 네이티브 API' (특히 Server Components)

하지만 Server Component 기반의 SSR, ISR, SSG, Streaming 렌더링에서만 가능하며, CSR에서는 아무 영향이 없다.

 

Next.js의 App Router는 fetch() 호출을 정적으로 분석(static analysis)할 수 있는데, 그 외 도구는 (axios, TanStack Query, SWR) '자동 감지' 하지 못해 분석이 어렵다.

분석이 어려운 경우 SSR 성능 최적화나 자동 캐싱 전략에 영향을 준다.

CSR에서 영향이 없는 이유는 Next.js는 브라우저 안에서 일어나는 요청을 감지할 수는 없기 때문이다.

CSR의 데이터 요청은 클라이언트에서 동적으로 이루어지며, 렌더링도 브라우저에서 일어나기 때문에 fetch든, axios든 성능도 비슷하고 동일한 대우를 받는다.

 

 

예시

export default async function Page() {
  const res = await fetch('https://api.example.com/data');
  const data = await res.json();

  return <div>{data.title}</div>;
}

 

풀이

Next.js는 해당코드를 아래와 같이 처리한다.

  • 요청 분석: fetch() 호출을 분석해서 URL, 옵션 등을 확인
  • 렌더링 전략 결정: cache, revalidate, next 옵션에 따라 ISR/SSG/SSR 등을 판단
    • next.revalidate: ISR
    • cache: 'force-cache': SSG
    • cache: 'no-store': SSR 강제
  • 자동 프리패치: 링크 타고 들어올때 미리 데이터 불러올지 결정 가능
  • 캐싱 계층 자동 연동: Cache-Control 헤더 설정 없이도 서버 캐싱 처리 가능

 

즉, 

데이터를 서버에서 먼저 불러올지, 클라이언트에서만 불러올지, ISR 에서는 재검증 주기 설정, Streaming  처리 시점등을 Next.js가 알아서 처리할 수 있다는 것이다.

 

반대로 axios, TanStack Query, SWR 같은 경우는 감지가 불가능한데, 이 라이브러리들은 Next.js가 내부적으로 '무슨 요청이 언제 일어나는지' 알 수 없다.

 '비-네이티브 함수' 호출을 분석하거나 추적하지 못해서 페이지 렌더링에 영향을 준다고 판단하지 않는다. 

  • 내부적으로 fetch보다 한단계 더 감싼 구조이기 때문에 Next.js가 요청을 인식하지 못한다.
// axios 예시
const res = await axios.get('/api/data');

 

Next.js는 이를 단순 '렌더링에 영향 없는 일반 코드'로 간주한다.

단순히 Promise일 뿐이고, Next.js 입장에서는 이게 '데이터 요청'인지 모르기 때문에 revalidate, cache, streaming 같은 최적화 대상이 아니다.

 

 

렌더링 방식별 영향

렌더링 방식 fetch() 감지됨 axios 감지됨 설명
✅ SSR (Server Component) ✔️ 예 ❌ 아니요 서버에서 HTML 만들기 전 fetch 감지, 자동 await, 캐싱 최적화 가능
✅ ISR (Incremental Static Regeneration) ✔️ 예 ❌ 아니요 fetch의 revalidate 옵션을 통해 Next.js가 페이지 재생성 시점을 제어
✅ SSG (Static Generation) ✔️ 예 ❌ 아니요 빌드 시점 fetch 분석하여 정적 페이지 생성
✅ Streaming / RSC ✔️ 예 ❌ 아니요 React Server Component 내부 fetch만 분석, streaming 타이밍 결정 가능
❌ CSR (Client Side Rendering) ❌ 아니요 ❌ 아니요 모두 일반 JS 코드일 뿐, Next.js 관여 없음

 

 

예시

fetch 기반 SSR + ISR 설정

// ISR: 60초마다 백그라운드 재검증
await fetch('https://api.example.com/data', {
  next: { revalidate: 60 },
});

 

Next.js는 이 코드를 보고 '60초 마다 이 페이지의 데이터를 다시 불러와야한다'고 인식하고 자동으로 Static Generation + Background Revalidation을 해준다.

 

 

axios

await axios.get('https://api.example.com/data');
// Next.js는 이게 SSR에 필요하단 걸 모름

 

백엔드 캐싱도, ISR도 안 걸리고 기본적으로 클라이언트에서만 요청되는 CSR 요청처럼 처리될 수 있다.

자동 프리패치, 캐싱, streaming과의 연동도 안된다.

  • Streaming Rendering은 서버에서 페이지 전체가 준비되기를 기다리지 않고 준비된 부분부터 먼저 클라이언트로 전송하는 방식이다. 
  • 기존 방식(SSR)은 모든 데이터 준비 후에 HTML을 통째로 보냈다면, Streaming 방식은 준비된 컴포넌트부터 점진적으로 HTML을 보낸다.

 

 

요약

기능 fetch asiox, TanStack Query, SWR
SSR에서 감지됨? ✅ 예 ❌ 아니요
자동 렌더링 전략 결정 ✅ (ISR, SSG, SSR) ❌ 수동 처리
Next.js 캐싱 최적화 ✅ 내부 연동 ❌ 직접 처리해야 함
Streaming / Suspense 연동 ✅ 자연스럽게 작동 ❌ 클라이언트에서만 유효

 

그래서 실무에서는 서버 컴포넌트에서는 fetch 사용이 거의 표준이라고 한다.

클라이언트 컴포넌트나 CSR 전용 로직에는 axios, TanStack Query, SWR을 혼용하여 사용하기도 한다.

 

 


 

 

Q. Streaming Rendering 이란?

A. 서버에서 페이지 전체가 준비되기를 기다리지 않고 준비된 부분부터 먼저 클라이언트로 전송하는 방식이다. 
기존 방식(SSR)은 모든 데이터 준비 후에 HTML을 통째로 보냈다면, Streaming 방식은 준비된 컴포넌트부터 점진적으로 HTML을 보낸다.

 

이 방식을 사용하게되면 느린 데이터 때문에 페이지 전체가 늦어지는 것을 방지할 수 있으며 빠른 부분을 먼저 보여주게 되어 UX가 향상된다. 

 

 

예시

// app/page.tsx
import { Suspense } from 'react';
import UserInfo from './UserInfo';

export default function Page() {
  return (
    <div>
      <h1>메인 콘텐츠</h1>
      <Suspense fallback={<p>사용자 정보 로딩 중...</p>}>
        <UserInfo />
      </Suspense>
    </div>
  );
}

// app/UserInfo.tsx (Server Component)
export default async function UserInfo() {
  const res = await fetch('https://api.com/user');
  const data = await res.json();

  return <p>{data.name}</p>;
}

 

fetch 하는 동안 Suspense fallback으로 대체하다가 데이터가 준비 완료되면 해당 HTML 조각을 전송하는데,

Server Component는 준비될 때까지 렌더링을 지연하게되고 Next.js는 이걸 감지해서 HTML 조각을 순차적으로 클라이언트로 보낸다. 

→ 즉, Next.js는 Server Component에서 해당 컴포넌트(UserInfo)를 렌더링 하지않고 Suspense로 감싸진 나머지 컴포넌트들을 먼저 HTML로 만들어서 클라이언트로 전송한다.

그리고 이후 서버에서 fetch가 완료되면 해당 컴포넌트의 HTML 조각을 나중에 Streaming으로 이어서 전송한다.

 

 

예시

// app/page.tsx
import { Suspense } from 'react';
import Profile from './Profile'; // Server Component

export default function Page() {
  return (
    <div>
      <h1>대시보드</h1>

      <Suspense fallback={<p>로딩 중...</p>}>
        <Profile />
      </Suspense>
    </div>
  );
}

// app/Profile.tsx (Server Component)
export default async function Profile() {
  const res = await fetch('https://api.com/user', {
    next: { revalidate: 60 },
  });
  const user = await res.json();

  return <div>안녕하세요, {user.name}님!</div>;
}

 

흐름

  • 요청이 들어오면 Page() 컴포넌트 실행
  • 서버는 준비된 HTML만 먼저 클라이언트에 전송. 즉, <h1>대시보드</h1>은 바로 렌더링 가능
  • <Profile />은 Suspense로 감싸져있고 fetch에서 데이터가 안왔을 경우, React가 Profile 렌더링 할 수 없는 상황임을 판단
    • Suspense로 감싼 부분은 일단 빈 영역으로 처리
    • 클라이언트가 그 부분을 'placeholder + fallback UI'로 대체. <p>로딩 중...</p>을 렌더링
    • fallback UI는 클라이언트용이므로, 클라이언트 React가 렌더링한다.
  • 그리서 서버에서 진짜 데이터가 오면 HTML 조각을 script/stream으로 보내서 실시간 삽입한다. (hydration)

 

axios나 TanStack Query는 클라이언트에서 실행되기 때문에 Streaming에 영향이 없다. 

서버컴포넌트가 아니고 Streaming이 아니기에 CSR이후에만 데이터가 패치되기 때문에 브라우저에 전송되는 HTML은 처음부터 존재하지 않는다. 

 

 


 

 

Q. script/stream 란?

A. Next.js의 서버가 HTML을 나눠서 전송하고 그 HTML 조각이 클라이언트에서 <script> 태그를 통해 동적으로 삽입되는 방식을 말한다.

 

전통 SSR 

서버에서 모든 데이터를 준비한 뒤, 한번에 완성된 HTML을 통째로 전송한다.

<!-- 완성된 HTML 통째로 전송 -->
<html>
  <body>
    <h1>대시보드</h1>
    <p>안녕하세요, 김철수님!</p>
  </body>
</html>

 

Streaming 방식

서버가 준비된 부분부터 먼저 전송하고 나머지는 나중에 도착하는 HTML 조각으로 이어붙인다.

<!-- 먼저 전송되는 HTML -->
<html>
  <body>
    <h1>대시보드</h1>
    <!-- 여긴 아직 준비 안 됨 -->
<!-- 몇 초 후 서버가 추가로 보냄 -->
<script>
  // React가 이 script 안의 내용을 placeholder 자리에 삽입
  __REACT_STREAM.insert("profile", "<p>안녕하세요, 김철수님!</p>");
</script>

 

 

즉, 브라우저는 <script>태그로 전달된 HTML 조각을 보고 클라이언트 React가 그 조각을 Suspense 자리로 끼워넣는다.

그리고 이것을 사람들은 'React Streaming + Suspense fallback hydration'이라고 부른다.

 

 


 

 

axios

Promise 기반의 HTTP 클라이언트 라이브러리이다.

Node.js와 브라우저 양쪽에서 사용할 수 있고 fetch보다 편의성이 높다.

요청/응답 인터셉터, 자동 JSON 변환, 타임아웃 설정 등 다양한 기능을 제공한다.

장점으로는 인터셉터로 인증 처리, 공통 로직 삽입이 가능하며 에러 핸들링이 명확하다.

또한 요청 및 응답 구조가 일관되어 있고, 자동으로 JSON 파싱하여 res.data로 바로 사용가능하다.

단점는 장점에 반대되는 것인데, 기능이 많다보니 fetch보다 무거우며 네이티브 API가 아니다.

만약 요청/응답 전후에 토큰 자동 첨부나 에러메시지 등의 공통 로직이 필요한 경우나 axios에서 제공하는 기능등을 적극 활용하길 원할때 도입하는 것이 좋다.

 

예시

import axios from 'axios';

const response = await axios.get('/api/data');
console.log(response.data); // 응답 본문

 

 

axios와 fetch 간단 비교

항목 fetch axios
기본 지원 브라우저 내장 외부 라이브러리
JSON 파싱 수동 (res.json()) 자동 (res.data)
요청 인터셉터 ❌ 없음 ✅ 있음
응답 인터셉터 ❌ 없음 ✅ 있음
요청 취소 복잡 (AbortController) 간편 (cancelToken)
에러 처리 상태코드 400~500도 "성공"으로 처리됨 자동 reject (catch 가능)
SSR 최적화 ❌ Next.js SSR 비감지 ❌ Next.js SSR 비감지
사용 난이도 심플 강력하고 편리하지만 약간 무거움

 

 

예시

공통 설정과 인터셉터

모든 요청에 토큰을 자동으로 추가하고, 401 에러가 올 경우 자동 로그아웃 처리한다.

const api = axios.create({
  baseURL: '/api',
  headers: {
    Authorization: `Bearer ${getToken()}`,
  },
});

api.interceptors.response.use(
  res => res,
  err => {
    if (err.response?.status === 401) logout();
    return Promise.reject(err);
  }
);

 

 

에러 핸들링 

axios.isAxiosError()로 에러 타입 구분이 가능하고, 서버 메시지도 err.resposse.data에서 바로 접근 가능하다.

try {
  const res = await axios.get('/user');
} catch (err) {
  if (axios.isAxiosError(err)) {
    alert(err.response?.data?.message || '에러 발생!');
  }
}

 

 

axios는 개발 편의성과 실무에서의 확장성 면에서는 강력한 도구이다.

하지만 axios의 기능등을 활용하지 않을때에는 번들 크기가 조금 크므로 작은 프로젝트에서는 오버 스펙일 수 있다.

또한 Next.js App Router의 서버컴포넌트에서 사용 시 트래킹 문제가 있어 최적화가 어려운 단점이 있다.

 

 


 

 

Q. fetch의 요청 취소와 axios의 요청 취소에 대해 설명해줘

A. fetch와 axios 모두 API를 요청 한 이후에 중간에 요청을 취소할 수 있다.

 

예시

fetch 요청 취소 

AbortController 사용 (관련 글, AbortController란?)

const controller = new AbortController();

fetch('/api/data', { signal: controller.sig })
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('요청이 취소되었습니다!');
    }
  });

// 언제든지 요청 취소 가능
controller.abort();

 

표준 API로 네이티브로 내장된 기능이며, fetch()에 signal 옵션을 넘기고 필요 시 controller.abort()를 호출하면 된다.

AbortController는 한번쓰면 재사용이 불가하기 때문에 요청마다 새로 생성해야하며 controller.abort()가 호출되면 Promise가 reject된다. 에러처리는 AbortError라는 네이티브 에러 객체가 발생한다.

 

 

axios의 요청 취소 (v0.22이상)

마찬가지로 AbortController를 지원한다.

구버전은 CancelToken을 사용해야한다. (deprecated 이므로 설명x)

const controller = new AbortController();

axios.get('/api/data', { signal: controller.signal })
  .then(res => console.log(res.data))
  .catch(err => {
    if (err.code === 'ERR_CANCELED') {
      console.log('axios 요청이 취소되었습니다!');
    }
  });

controller.abort();

 

이것 역시 동일하게  axios에 signal 옵션을 넘기고 필요시 controller.abort()를 호출한다.

에러처리는 axios에서 만든 AxiosError 객체가 발생한다. (해당 객체는 code 필드가 존재, ERR_CANCELED)

 

 

 


 

 

Q. 에러처리 시 axios vs fetch. fetch는 상태코드 400~500이어도 '성공'으로 처리되는가?

A. axios는 상태코드가 400이상이면 자동으로 reject 처리해서 catch로 떨어지며, fetch는 상태코드가 400 이상이어도 성공처럼 보여진다.

 

fetch는 HTTP 상태 코드가 400~599여도 catch로 안 떨어지고 .then()으로 계속 이어지게 되어 실패(status: 404, 500) 인데도 성공처럼 보일 수 있다.

 

fetch는 네트워크 오류만 catch()로 처리하고 있기에 HTTP 오류 (400, 500)은 정상응답으로 간주된다.

그렇기 때문에 개발자가 직접 에러 핸들링을 해줘야한다.

  • 네트워크 오류는 브라우저가 아예 요청을 서버에 보내지 못하거나 서버 응답을 받지 못하는 경우 발생한다.
    • 인터넷 끊김, DNS 오류, 서버 연결 타임아웃, CORS 오류, 서버 다운되어 응답 없음, 브라우저 강제 요청 중단(AbortController.abort())
  • HTTP 오류는 서버는 요청을 받았고 응답도 했다. 그 응답의 HTTP 상태 코드가 4xx, 5xx 인 경우
    • 400: 잘못된 요청, 401: 인증 실패, 403: 권한없음, 404: 페이지 없음, 500: 서버 내부 오류, 503: 서버가 일시적으로 다운됨

 

예시

fetch('/api/not-found') // 404 응답이라고 가정
  .then((res) => {
    console.log('✅ fetch 성공!', res.ok); // false
    return res.json();
  })
  .catch((err) => {
    // 이 블록은 네트워크 오류(인터넷 끊김 등)에서만 실행됨
    console.error('❌ fetch 실패!', err);
  });

 

fetch는 HTTP 상태 코드가 400~500이어도 catch로 안 떨어지고 .then()으로 간다.

대신 res.ok가 false로 들어온다.

따라서 fetch는 '성공이냐 실패냐'를 직접 판단하는 로직을 추가해야한다.

if (!res.ok) {
  throw new Error(`HTTP 에러! 상태: ${res.status}`);
}

 

 


 

 

Q. axios가 내부적으로 fetch보다 한단계 더 감싼 구조여서 Next.js에서 접근할 수 없다는데, 한단계 더 감싼 구조 라는게 어떤 의미야?

A. axios는 내부적으로 fetch나 XMLHttpRequest를 사용하고 있다.

하지만 axios는 내부 구현이 추상화되어있기에(감싼 구조로 인해) Next.js는 axios의 내부에서 어떤 네트워크 요청이 일어나는지 '정적으로 추적 할 수 없다'

그래서 Next.js가 fetch의 SSR/ISR/Streaming 렌더링을 최적화 할때 axios 요청은 인식하지 못한다.

 

 


 

 

TanStack Query  (구 react-query)

React 애플리케이션에서 서버 상태를 캐싱하고 동기화하는 라이브러리이다.

이전 명칭이 react-query 였지만, 현재는 TanStack Query이며 React이외의 Vue, Solid, Svelte등에서도 사용가능하게 확장되었기 때문에 이름이 변경되었다.

서버 상태(Server State)를 관리하는 라이브러리이며 API 요청(fetch, axios 등)을 쉽게 다룰 수 있고 자동 캐싱, 리페치, 상태관리, 로딩 UI, 에러 핸들링을 도와준다. 

또한 isLoading, isError, data등 상태관리를 자동으로 제공하며 서버 상태를 앱에 쉽게 연결할 수 있다.

하지만 역시나 장점은 단점이 될 수 있다. 

개념이 많기 때문에 학습 곡선이 많으며 이것 역시 클라이언트 중심이기에 서버 컴포넌트 연동 시 고려가 필요하다.

import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

const fetchUser = () => axios.get('/api/user').then(res => res.data);

const { data, isLoading, error } = useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
});

 

복잡한 서버 상태 관리가 필요하거나, 데이터 재사용이나 자동 새로고침 혹은 낙관적 업데이트 등을 활용하고 싶을때 도입을 고려하면 좋다. (대규모 앱 또는 비동기 데이터 흐름이 많은 프로젝트)

반대로 단순한 프로젝트의 경우 오버스펙일 수 있다.

axios와 마찬가지로 서버컴포넌트 최적화가 어려우므로 클라이언트 중심의 앱일때 좋다.

 

핵심 기능

  • 자동 캐싱: 동일한 queryKey일 경우 네트워크 요청 없이 캐시에서 반환
  • 자동 리페치: 포커스 재 진입시 자동 새로고침
  • 배경 동기화: stale 상태면 백그라운드에서 데이터 갱신
  • Query Key 기반 구조: 고유 키로 각 요청을 독립적으로 관리
  • 로딩/에러/성공 상태 관리: isLoading, error, data, isFetching 등
  • Pagination과 Infinite Scroll 지원: 무한 스크롤/페이지네이션용 기능 내장
  • Mutations 지원: POST/PUT/DELETE 등도 캐시 무효화 등과 함께 처리 가능
  • 서버 상태와 클라이언트 상태 구분: Zustand/Recoil 등과 목적이 다르다

 

TanStack Query는 뭔데?

항목 fetch / axios TanStack Query
역할 단순 API 요청 서버 상태 전체 관리
캐싱 ❌ 없음 ✅ 자동 캐싱
상태관리 ❌ 수동 ✅ 자동 (로딩/에러 등)
중복 요청 방지 ❌ 직접 구현 ✅ 내장
클라이언트 전용 🔁 둘 다 사용 가능 ✅ 기본적으로 CSR 기반
SSR/RSC 최적화 ❌ 직접 처리 필요 ❌ 제약 있음 (hydrate 필요)

 

axios나 fetch로 요청을 보내고 TanStack Query로 상태관리나 캐싱, 리페치 등을 컨트롤 하는 조합으로 사용한다.

CSR 중심 SPA에서 TanStack Query 사용을 추천하고,

만약 Next.js App Router + Server Components 조합이라면 위에서 언급했든 fetch 중심으로 SSR 최적화 시키고 + 클라이언트 전환 시 TanStack Query 혼합 하여 사용할 수 있다.

 

 


 

 

SWR (Stale While Revalidate)

'오래된 데이터를 먼저 보여주고, 백그라운드에서 최신 데이터를 가져와 갱신한다' 전략을 가진 React 데이터 패칭 라이브러이이다.

Vercel에서 만든 공식 React 훅 기반 데이터 패칭 라이브러리이다보니 Next.js와 호환성이 좋다.

fetch 등을 래핑해서 캐시, 리페치, SSR 지원 등을 쉽게 처리한다. 

TanStack Query보다 간결하고 직관적인 API를 제공한다.

import useSWR from 'swr'

const fetcher = (url: string) => fetch(url).then(res => res.json())

function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher)

  if (isLoading) return <div>로딩중...</div>
  if (error) return <div>에러!</div>

  return <div>안녕하세요, {data.name}님!</div>
}

 

심플하고 직관적인 API이며 기본적인 캐싱과 리페치는 제공하는 장점이 있다.

단점으로는 TanStack Query보다는 기능이 적고 복잡한 서버 상태는 직접 구현해야한다.

단순한 데이터 요청에 최적이기 때문에 각 컴포너트별 독립적 요청이 많은 구조에서 좋으며, Next.js 프로젝트에서 페이지 단위의 정적/동적 데이터를 관리할 때 좋다.

 

 

핵심 기능

  • 캐싱: 브라우저 메모리에 캐시 저장
  • 빠른 응답: 캐시가 있다면 즉시 반환한다. (stale)
  • 백그라운드 갱신: 백그라운드에서 자동으로 최신 데이터를 가져온다
  • focus refetch: 탭을 다시 열거나 포커스하면 자동 갱신
  • 재시도: 실패 시 자동 재요청(ex: 네트워크 문제)
  • mutate: 데이터 수동 갱신도 가능 (mutate())

 

예시

fetch, axios, custom api 함수, query 함수 등을 SWR에서 사용가능하게 포맷 맞춰서 사용가능하다.

이때 SWR이 응답처리, 에러처리, 캐싱처리까지 맡아 해준다.

 

기본 fetch (fetch + .json())

const fetcher = (url: string) => fetch(url).then(res => res.json());

const { data } = useSWR('/api/user', fetcher);

 

다른 라이브러리 (axios, graphql, firebase, supabase 같은 모든 요청 함수) 

import axios from 'axios';

const fetcher = (url: string) => axios.get(url).then(res => res.data);

const { data } = useSWR('/api/user', fetcher);
const fetcher = (query: string) =>
  fetch('/graphql', {
    method: 'POST',
    body: JSON.stringify({ query }),
  }).then(res => res.json());

const { data } = useSWR('{ user { name } }', fetcher);
const fetcher = () => supabase.from('profiles').select('*');
const { data } = useSWR('getProfiles', fetcher);

 

 


 

 

Q. SWR은 브라우저 어떤 메모리에 캐시를 저장하지?

A. 브라우저의 전역 메모리(RAM)에 있는 자바스크립트 런타임 영역의 메모리에 저장한다.

브라우저의 전역 메모리(RAM)에 있는 in-memory object는 자바스크립트의 힙(Heap) 영역을 의미한다.

 

자바스크립트의 메모리 구조

영역  설명
스택(Stack) 현재 실행 중인 함수 호출, 지역 변수 저장
힙(Heap) 객체, 배열, 함수 등 참조형 데이터가 저장되는 공간
콜스택(Call Stack) 실행 컨텍스트(함수 호출 순서)
큐(Task/Microtask Queue) 비동기 콜백들 저장

 

 

SWR의 캐시는 힙에 저장된다.

SWR은 Map, Object 같은 참조형 데이터를 써서 캐시를 저장한다. 그래서 브라우저가 새로고침되거나 자바스크립트 런타임이 종료되면 그 힙 메모리도 전부 날아가게된다.

// 내부적으로 이런 구조로 저장됨
const cache = new Map(); // 힙에 올라감

cache.set('/api/user', { name: 'John' });

 

왜 힙에 저장할까?

  • React 앱이 실행되는 동안 살아있는 메모리 공간이다. (휘발성 캐시)
  • 접근 속도가 빠르고 자동으로 GC(Garbage Collection)도 처리된다.
  • 다른 컴포넌트에서도 공유가 가능하다. 
    • SWR은 context로 공유한다.

 

 

Q. '접근 속도가 빠르고 자동으로 GC(Garbage Collection)도 처리된다.' 에서 접근 속도는 어디와 비교해서 빠른거야?

A. SWR이 사용하는 힙(in-memory object)은 브라우저의 storage API들보다 훨씬 빠르다.

 

자바스크립트 힙에 있는 Map, Object는 동기적으로 CPU에서 바로 접근 가능하며 그냥 변수 접근하는 것이랑 거의 동일한 수준이다. 반면 storage API는 브라우저의 Storage 엔진을 거쳐야 하므로 느리고 비용이 든다.

 

예시

// in-memory
const cache = new Map();
cache.set('key', 'value');
cache.get('key'); // ⚡ 매우 빠름

// localStorage
localStorage.setItem('key', 'value');
localStorage.getItem('key'); // 🐢 느림, 직렬화/역직렬화 필요

 

그래서 SWR은 빠르게 응답 → 백그라운드에서 리패치 구조에서 캐시를 in-memory에 저장하는 게 속도 + UX 최적화에 유리한 것이다.

 

브라우저의 저장소 속도

저장소 동기/비동기 접근 속도 특징
in-memory (Map, Object) ✅ 동기 ⚡ 매우 빠름
(RAM에서 바로)
휘발성, JS 런타임 동안만 유지
메모리에 직접 저장되므로 직렬화/역직렬화 없이 동기 접근 가능
sessionStorage ✅ 동기 🐢 상대적으로 느림 문자열만 저장, 페이지 종료 시 초기화
localStorage ✅ 동기 🐢 느림 영속적이지만 UI thread blocking 가능
IndexedDB ❌ 비동기 🐢 매우 느림 구조화된 데이터 저장 가능, 가장 무거움

 

캐시 관리는 라이브러리마다 다르고, 내부 메모리/전략도 차이가 있다.

라이브러리 캐시 저장 위치 GCduqn 영속성
SWR JS 힙 메모리 (in-memory cache) ✅ GC 대상 ❌ 새로고침 시 초기화
TanStack Query JS 힙 메모리 (in-memory cache) ✅ GC 정책 명시적 있음 (staleTime, cacheTime) ❌ 새로고침 시 초기화
Axios ❌ 기본적으로 캐시 없음 - -
Axios + 어댑터(ex. axios-cache-adapter) 로컬스토리지, 세션스토리지, IndexedDB 등 (설정 따라 다름) ❌ (GC 대상 아님) ✅ 유지됨 (스토리지 기반)

 

axios는 기본적으로 캐시 기능이 없고, 매 요청마다 네트워크를 보내고 응답을 받을 뿐이다.

따라서 axios는 캐시 기능을 사용하려면 interceptor 또는 추가 어댑터가 필요하다. (ex: axios-cache-adaptor)

 

 

Q. in-memory의 특징 중 하나 인 '메모리에 직접 저장되므로 직렬화/역직렬화 없이 동기 접근 가능'하다는게 무슨 의미야?

A. 

  • 직렬화: 객체, 배열, 데이터 등을 문자열 또는 바이너리 형태로 변환하는 것
  • 역직렬화: 문자열로 저장된 데이터를 다시 객체나 원래의 데이터 구조로 복원하는 것

 

예시

const obj = { name: "Yunseo", age: 25 };

// 🔸 직렬화 (객체 → 문자열)
const jsonStr = JSON.stringify(obj);
console.log(jsonStr); // '{"name":"Yunseo","age":25}'

// 🔹 역직렬화 (문자열 → 객체)
const parsed = JSON.parse(jsonStr);
console.log(parsed.name); // 'Yunseo'

 

localStorage, sessionStorage, cookie는 오직 문자열(string)만 저장이 가능하다.

그래서 객체를 넣으려면 문자열로 변환해야하고 꺼내서 사용할때 다시 원래의 데이터 구조로 원복해 사용하는 직렬화/역직렬화 작업을 해줘야한다. 

또한 직렬화/역직렬화 작업에는 처리 비용이 들어서 일반적으로 성능에 영향이 크지는 않지만 특정 상황에서 영향이 커질 수 있다. 

  • 수백~수천번의 빈번한 저장/읽기 작업이나 데이터 크기가 매우 큰 경우.
  • 렌더링 사이클 중 사용하면 UI 렌더링 중 blocking이 발생하여 프레임 드랍 현상이 발생할 수 있다. (특히 리액트 컴포넌트에서 localStorage.getItem()을 직접 호출하는 경우, hydration이나 리렌더링 타임과 겹치면..)

 

반면, SWR이 사용하는 in-memory는 직렬화 필요없이 객체 그대로 저장하고 그대로 꺼내 사용할 수 있다.

문자열로 변환하거나 파싱하는 직렬화/역직렬화 과정이 필요없다.

메모리에 있는 주소를 직접 바라보는 느낌이라 속도가 더 빠르다. 

const cache = new Map(); 

cache.set('user', { name: 'John', age: 14 }); 

const user = cache.get('user');

 

비교

항목 in-memory (Map) localStorage / sessionStorage
직렬화 필요? ❌ 아님 (객체 그대로 저장됨) ✅ 필요 (JSON.stringify/parse)
접근 방식 ✅ 동기 (즉시 반환) ✅ 동기지만 문자열 파싱
속도 ⚡ 매우 빠름 🐢 느림 (문자열 처리 오버헤드)

 

 

Q. 왜 JSON.parse()가 느릴까?

A. 문자열 파싱 비용, 재귀 처리, 새로운 객체 생성, GC 부담 증가로 인해 느리다.

 

JSON은 텍스트 기반이기 때문에 내부적으로 문자를 하나하나 해석해서 객체 구조로 만들어야하는데 이때 문자열 파싱에 비용이 든다.

이때 중첩된 객체나 배열이 많을 경우 재귀로 처리하기에 파싱 시간이 기하급수적으로 증가하게된다.

또한 복원하는 객체는 참조가 아닌 새 객체가 된다.

 

팁!  structuredClone() - 네이티브 API 최신 복제 방식

const copy = structuredClone(obj);

 

깊은 복사를 지원하지만 함수(function) 및 돔 요소(HTMLElement) 등은 복제 불가능하다.

네이티브 API이므로 JSON.parse/stringify보다 빠르고 순환 참조가 일어나도 안전하게 복사된다.

 

순환 참조 가능 예시

const a = { foo: 1 };
a.self = a;
const b = structuredClone(a); // 순환 참조 OK

 

 


 

 

Q. 힙(heap)은 왜 in-memory object라고 하는거야?

A. 자바스크립트 객체는 힙 메모리에 저장되기 때문에 이를 가리켜 'in-memory object'라고 표현하는데, '객체가 브라우저 메모리안에 존재한다'는 의미다

 

자바스크립트 실행 시 메모리는 크게 2가지로 나뉘는데,

Stack과 Heap이다.

  • Stack은 원시값, 함수 실행 컨텍스트(매개변수, 지역변수 등)만 저장한다. 작고 빠르게 저장 가능한 값들을 위한 메모리 공간이다. 
  • Heap은 객체나 배열 등 복잡한 참조형 데이터를 저장한다. 

예시

const num = 1;                   // Stack
const user = { name: 'Yunseo' }; // Heap

 

변수 user는 Stack에 있고, 객체를 참조하는 주소를 가지고 있다.

실제 객체 user는 Heap에 있다.

const a = { x: 1 };
const b = a;

b.x = 2;
console.log(a.x); // 👉 2 (같은 힙 객체를 참조 중이기 때문!)
Stack                     Heap
───                 ────
| a |      ───→ | { x: 1 }    |
| b | ─┘           └────

 

 


 

 

Q. 브라우저 스토리지 엔진은 뭐야?

A. 브라우저 스토리지 엔진을 이해하려면 브라우저가 제공하는 다양한 저장 방식(Storage API)와 그 내부 구현 방식(엔진)을 구분해서 봐야한다.

 

브라우저가 제공하는 클라이언트 저장소 API들의 저장 메커니즘과 내부 구현 체계를 통틀어 부르는 개념이다.

즉, '브라우저가 데이터를 저장하는 방법(설계) + 그것을 관리하는 시스템' 을 말한다.

 

 

용어 의미
Storage API localStorage, sessionStorage, IndexedDB 등 JS에서 쓰는 API
Storage Engine 위 API를 실제로 브라우저 내부에 저장하는 구현체, 보통 LevelDB, SQLite, JSON 파일 등

 


 

최종

비교 설명

항목 fetch axios TanStack Query SWR
타입 API (내장) 라이브러리 상태 관리 라이브러리 상태 관리 라이브러리
캐싱
인터셉터 ❌ (axios와 같이 써야 함)
로딩/에러 관리 수동 수동 자동 자동
SSR 지원 직접 구현 필요 직접 구현 필요 Next.js 연동 필요 Next.js 친화적
복잡도 낮음 중간 높음 낮음

 

도입 기준 

상황 추천 도구
요청이 단순하고 lightweight한 접근 원함 fetch
인터셉터, 공통 설정, 응답 처리 일관성 필요 axios
클라이언트 상태와 서버 상태를 명확히 분리하고 자동 동기화 원함 react-query
간단하고 빠르게 캐싱/리페치 쓰고 싶고 Next.js를 사용 중 SWR

 

SWR과 TanStack Query 비교

항목 SWR TanStack Query
만든 곳 Vercel Tanner Linsley
주 목적 간단한 데이터 패칭 (fetch) 복잡한 서버 상태 관리 (Server State)
의존성 React 전용, 아주 가벼움 React 중심이지만 범용 라이브러리
캐싱 O O
자동 갱신 O (revalidation) O (refetch, stale time 등)
Pagination / Infinite 🔸 간단한 지원 ✅ 강력한 지원
Mutation 지원 🔸 제한적 ✅ 매우 강력
DevTools 🔸 없음 ✅ 있음
학습 난이도 ⭐ 쉬움 ⭐⭐ 중간~어려움
커스텀 캐시 전략 제한적 매우 유연함

 

- SWR은 간단하고 빠르게 구현하고 싶을때 사용할 수 있고, 오히려 대형 프로젝트나 캐시 컨트롤이 복잡할땐 TanStack Query가 유리할 수 있다.

 

 

 

GraphQL

SWR, TanStack Query 그리고 GraphQL은 서로 약간 다른 영역을 담당하는 도구들이다.

간단히 요약하면, GraphQL은 '데이터를 요청하는 방식'이고, SWR/TanStack Query는 '요청한 데이터를 관리하는 도구' 이다.

 

 

예시

REST API는 엔트포인트마다 정해진 응답을 주는데, Graph QL은 클라이언트가 원하는 형태로 요청할 수 있다.

# 예: GraphQL query
query {
  user(id: "123") {
    name
    email
    friends {
      name
    }
  }
}

 

SWR + GraphQL

import useSWR from 'swr'
import { request } from 'graphql-request'

const fetcher = (query) => request('/api/graphql', query)

const query = `
  {
    user(id: "1") {
      name
    }
  }
`

const { data, error } = useSWR(query, fetcher)

 

TanStack Query + GraphQL

import { useQuery } from '@tanstack/react-query'
import { request } from 'graphql-request'

const fetchUser = () => request('/api/graphql', `{
  user(id: "1") {
    name
  }
}`)

const { data, isLoading } = useQuery({
  queryKey: ['user', 1],
  queryFn: fetchUser,
})

 

Apollo Client는 GraphQL 전용 클라이언트이다. Graph QL + 캐싱 + 상태 관리까지 한번에 할 수 있다.

REST API는 기본적으로 사용하지 못한다.

  • 쿼리 작성은 GraphQL 문법으로만 가능하고, 요청 전송은 GraphQL endpoint로만 가능하다.
  • 내부 캐싱, 쿼리 키, normalization 등도 GraphQL schema 기반이다.

대신 SWR이나 TanStack Query 는 REST든 GraphQL이든 상관없이 쓸수있는 범용도구이다.

// 이런 GraphQL 쿼리만 다룰 수 있다
const QUERY = gql`
  query GetUser {
    user(id: "1") {
      name
      email
    }
  }
`

 

 

 

firebase 

백엔드 서비스 전체를 제공하는 플랫폼이다.

firebase의 장점은 실시간 데이터 스트림을 onSapshot()으로 받아 사용할 수 있다.

SWR이나 TanStack Query의 revalidation이나 refetching이랑은 좀 다르다.

아예 웹소켓 기반 푸시다.

이걸 쓰면 굳이 SWR이나 TanStack Query 없이도 자동 업데이트 구현 가능하다. 

onSnapshot(doc(db, "users", "123"), (doc) => {
  console.log("Real-time update!", doc.data())
})

 

기본적으로 GraphQL이 아니고, REST + 자체 SDK 방식이다.

import { getDoc, doc } from 'firebase/firestore'
const docSnap = await getDoc(doc(db, "users", "123"))

 

내부적으로 REST / 웹소켓 기반으로 작동하지만 SDK만 사용하면 된다.

SWR과 TanStack Query 둘다 사용이 가능하다.

 

 

예시

SWR + firebase

import useSWR from 'swr'
import { doc, getDoc } from 'firebase/firestore'

const fetchUser = async () => {
  const docRef = doc(db, 'users', '123')
  const docSnap = await getDoc(docRef)
  return docSnap.data()
}

const { data, error } = useSWR('user_123', fetchUser)

 

TanStack Query + firebase

import { useQuery } from '@tanstack/react-query'

const fetchUser = async () => {
  const docRef = doc(db, 'users', '123')
  const docSnap = await getDoc(docRef)
  return docSnap.data()
}

const { data, isLoading } = useQuery({
  queryKey: ['user', '123'],
  queryFn: fetchUser,
})

 

 

 

supabase

오픈소스로 firebase 대안으로 나왔다.

기본적으로 PostgreSQL 기반의 백엔드 서비스이다.

Rest도 되고, GraphQL도 되고, 실시간도 된다.(-> firebase의 onSnapshot()이랑 비슷)

 

 

 

반응형
최근에 올라온 글
최근에 달린 댓글
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Total
Today
Yesterday