카카오_구름/리액트

리액트 심화 커리큘럼 2주차 : React 비동기 처리와 데이터 패칭 라이브러리

코딩의 세계 2025. 6. 20. 01:41


2주 차 : React 비동기 처리와 데이터 패칭 라이브러리

목표

  • 비동기 처리의 기본 개념인 Promise, async/await, fetch 사용법을 이해하고 설명할 수 있다.
  • React 컴포넌트에서 비동기 요청을 어떻게 다뤄야 하는지 (useEffect, 상태관리 등)을 구현해 볼 수 있다.
  • 데이터 패칭 라이브러리(SWR, TanStack Query 등)의 기본 사용법과 장점을 예제와 함께 설명할 수 있다.
  • 직접 fetch와 라이브러리 방식의 차이를 비교하여, 실무에서 적절한 선택 기준을 말할 수 있다.
  • loading, error, retry, cache, revalidate 등 실전 대응 키워드를 개념과 함께 체감할 수 있다.

참고 키워드

  • fetch(), async/await, useEffect()
  • SWR: useSWR(key, fetcher) > 필수
  • TanStack Query: useQuery(queryKey, fetchFn) > 필수
  • stale-while-revalidate / 캐싱 / refetch
  • API 에러 처리, 로딩 상태 처리

공유 방식

  • fetch + useEffect 예제와 라이브러리 사용 예제 각각 작성 및 토론
  • 최소 하나의 API(예: JSONPlaceholder, PokeAPI 등)를 기반으로 실습 결과 공유

참고 > 실제 라이엇 API를 이용한 내 코드는 글의 흐름 때문에 밑에 몰아서 넣어둠.

프론트엔드에서 데이터를 가져오는 것은 정말 매우 흔한 작업이다. 

특히 api를 이용해서 서로 소통을 하게 될 것이다.

이때에 리액트에서는 여럿 비동기 처리와 데이터 패칭을 하는 방법들이 존재한다.

📌 비동기 처리 기초 – Promise / async/await / fetch

자바스크립트는 기본적으로 동기적인 언어이다. 참고로 이때 동기적 / 비동기적인 것에 대해서 요약을 하자면 다음과 같다.

동기(Synchronous)

동기는 사전적으로 '동시에 일어난다'는 의미를 갖고 있다. 프로그래밍에서 동기는 작업이 순차적으로 진행되는 것을 의미한다.

즉, 한 작업이 시작되면 해당 작업이 완료될 때까지 다른 작업이 기다려야 한다. 동기 방식은 호출한 함수 또는 작업이 반환될 때까지 대기하는 동안 실행 흐름이 차단되는 특징이 있다.

동기 방식은 일반적으로 간단하고 직관적인 코드를 작성하기 쉽다. 하지만 여러 작업이 동시에 실행되어야 하는 경우, 각 작업의 완료를 기다리는 동안 시간이 소요되어 전체 프로세스의 성능이 저하될 수 있다. 또한 한 작업이 지연되면 다른 작업들도 모두 지연되는 문제가 발생할 수 있다.

비동기(Asynchronous)

비동기는 사전적으로 '동시에 일어나지 않는다'는 의미를 갖고 있다.

프로그래밍에서 비동기는 작업이 독립적으로 실행되며, 작업의 완료 여부를 기다리지 않고 다른 작업을 실행할 수 있는 방식을 의미한다. 즉, 비동기 방식은 작업이 시작되면 해당 작업이 완료될 때까지 기다리지 않고 다음 코드를 실행할 수 있다.

비동기 방식은 주로 I/O 작업이나 네트워크 요청과 같이 시간이 오래 걸리는 작업에 유용하다. 이러한 작업을 비동기적으로 처리하면, 프로그램은 작업이 완료되기를 기다리는 동안 다른 작업을 처리할 수 있으므로 전체적인 성능이 향상된다.

비동기 방식은 콜백(callback), 프라미스(Promise), async/await 등의 메커니즘을 통해 구현될 수 있다.


그리고 이때에 서버에서 데이터를 가져와야 하는 작업을 해야 할 때에는 "시간이 걸리기" 때문에 비동기 처리를 해야 하는 것이다.

기본적인 fetch와 async / await 관련 코드를 보자.

> 예시 코드

async function getUser() {
  try {
    const res = await fetch('https://api.example.com/user');
    const data = await res.json();
    console.log(data);
  } catch (err) {
    console.error('에러 발생:', err);
  }
}

아까 자바스크립트는 기본적으로 동기적인 함수로 이루어져 있다고 하였다.

함수를 비동기로 처리하고 싶다면 async를 함수 앞에 선언해주자.

위 함수는 fetch를 이용해서 api를 소통하게 되는 함수인데, 기본적으로 try / catch 구문을 써서 에러를 예외처리하게 된다.

fetch 함수 안에는 내가 받고자 하는 api를 기입해 준다. (보통 http 소통)

그것을 res라는 변수에 담고, 그 res를 json함수 형태로 data에 넣어주는 것이다. (여기에서는 콘솔로 찍지만 ui로 화면에 보여줄 수도 있다.)


Promise

Promise는 JavaScript의 비동기 처리에 사용되는 객체로써 비동기로 처리되는 결과를 동기처럼 반환한다. 실제로 동기처럼 처리되는 것이 아니라 미래의 데이터 처리가 완료된 시점에 결과를 제공하겠다는 ‘약속(프로미스)’를 반환하는 것이다.

- Promise 객체를 사용하는 이유

JavaScript에는 동기 작업과 비동기 작업이 있는데, 비동기로 처리되는 작업의 결과를 사용하기 위해 Promise 객체를 사용한다.

비동기 작업이 끝난 후의 결과를 이용해서 작업을 수행하려면 Promise 객체의 콜백함수로 작업을 지정해줘야 한다.

그래야 ‘순서를 보장’ 받을 수 있다. 이러한 콜백함수가 너무 많아지면, 수정이 복잡해지고 코드의 가독성도 떨어지게 된다.

이런 이펙트들을 방지하기 위해 then()을 이어 붙여 콜백함수 작업을 처리할 수 있도록 만들 수 있다.

코드 >

function getData() {
    return new Promise((resolve, reject) => {
	// 비동기작업
      fetch('데이터url', (response) => {
        resolve(response)
      })
    })
  }

 getData()
 .then(data => {
    console.log(data)
  })
.catch(err => {
    console.log(err)
  })

Promise 작업의 결과는 resolve와 reject로 받을 수 있는데, Promise 작업이 성공했을 때는 resolve로 어떤 이유로 실패했을 때는 reject로 받는 식으로 작동한다.


⚛️ React 컴포넌트에서 비동기 처리

자. 위에서는 자바스크립트에서 비동기 처리하는 것을 다루었다면 리액트에서는 어떻게 처리할까?

심플하다. React에서는 데이터를 가져오기 위해 보통 useEffect를 사용한다.

✅ 예시: useEffect + fetch

import { useState, useEffect } from 'react';

function UserInfo() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchData() {
      try {
        const res = await fetch('https://api.example.com/user');
        const data = await res.json();
        setUser(data);
      } catch (err) {
        console.error('에러 발생:', err);
      } finally {
        setLoading(false);
      }
    }

    fetchData();
  }, []);

  if (loading) return <p>로딩 중...</p>;
  return <div>{user.name}</div>;
}

예시 코드이다. 리액트에서는 훅을 사용하기 위해서 상단에 import문을 기입하면 된다.

이후 state (상태)를 이용해서 데이터 상태를 결정하게 된다.

처음 유저의 데이터는 null 처리를 한다. 이후 useEffect를 이용해서 컴포넌트가 처음 렌더링이 될 때에만 실행하도록 함수를 유도시킨다. (비동기 처리는 안에서 한다.)

또한, useEffect 안에서는 직접적으로 asycn / await를 쓸 수 없어서 따로 함수를 선언하고 그 함수에 써야만 한다.

이후 똑같이 fetch 함수를 자바스크립트 처리하듯 처리를 하자. 다만 finally 구문을 이용해서 항상 로딩 종료 상태 처리를 해줘야 한다. finally를 쓰면 데이터 패칭에 성공하든 실패하는 무조건 로딩 상태를 false로 전환해 준다.

(참고로 이렇게 데이터를 가져올 때에, 로딩이 생긴다. 로딩이 되고 있다는 것을 화면에 보여주는 것이 사용자 경험에 좋기 때문에 loading 구현을 해두는 것을 추천한다.)


데이터 패칭 라이브러리

그렇다면 데이터 패칭은 fetch만 있을까?

당연히 그럴 리가 없다. fetch에도 여럿 단점(?)이 있기에 이를 극복하기 위한 많은 라이브러리가 존재한다.

그중 SWR과 TanStack Query(구 리액트 쿼리)를 다뤄보자.


🛠️ SWR – 간단하고 직관적인 데이터 패칭

공식 사이트 > https://swr.vercel.app/ko

 

데이터 가져오기를 위한 React Hooks – SWR

SWR is a React Hooks library for data fetching. SWR first returns the data from cache (stale), then sends the fetch request (revalidate), and finally comes with the up-to-date data again.

swr.vercel.app

swr이 무엇일까?

swr는 다음과 같이 정의가 되어있다.

공식 사이트

HTTP 캐시 무효 전략인 stale-while-revalidate의 약자라고 한다.

SWR는 먼저 캐시로부터 데이터를 반환한 이후, fetch 요청을 하고, 최종적으로 최신화된 데이터를 가져오는 라이브러리다.

Stale-while-revalidate는 웹 페이지의 성능을 향상하기 위한 캐싱 전략이다. 캐시 된 데이터를 즉시 제공하면서 백그라운드에서 최신 데이터를 가져와 캐시를 업데이트한다. 

이렇게 하면 사용자는 항상 빠른 속도로 데이터를 볼 수 있으며, 동시에 최신 데이터로 업데이트된다. 

특징

  • 자동 캐싱
  • 자동 재검증 (revalidation)
  • 포커스 시 자동 (refetch)
  • 간단한 API
     
    동작 방식:
    1. 사용자가 페이지를 요청.
    2. 서버는 먼저 캐시된 데이터를 확인.
    3. 캐시된 데이터가 유효 기간(max-age) 내에 있으면 즉시 사용자에게 제공. 
       
    4. 만약 캐시된 데이터가 만료되었거나 (stale), 아예 없는 경우, 백그라운드에서 서버에 새로운 요청을 보내 데이터를 가져온다. 
       
    5. 새로 가져온 데이터는 캐시에 저장되고, 다음 요청부터는 최신 데이터가 제공. 
       
  • 장점:
    • 빠른 응답 속도: 캐시된 데이터를 먼저 제공하므로, 사용자는 페이지 로딩 시간을 기다릴 필요 없이 빠르게 데이터를 볼 수 있다. 
       
    • 최신 데이터 보장: 백그라운드에서 최신 데이터를 가져와 캐시를 업데이트하므로, 사용자는 항상 최신 데이터를 볼 수 있다. 
       
    • 서버 부담 감소: 캐시된 데이터를 먼저 제공하므로, 서버에 대한 요청 횟수를 줄여 서버 부담을 줄일 수 있다. 
       
  • 단점:
    • 최초 요청 시에는 캐시된 데이터가 없으므로, 서버에서 데이터를 가져와야 합니다. 이로 인해 초기 로딩 속도가 느려질 수 있다. 

     


코드

설치 >

npm i swr

터미널에 위 명령어로 설치하면 된다.

예시 코드를 보자. 실제 웹사이트에 있는 예시 코드다.

import useSWR from 'swr'
 
function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher)
 
  if (error) return <div>failed to load</div>
  if (isLoading) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}

먼저 최상단에 import를 해준다.

이 예시에서 useSWR 훅은 key 문자열과 fetcher 함수를 받는다. key는 데이터의 고유한 식별자이며 (보통은 API의 URL이 된다.) fetcher로 전달된다. fetcher는 데이터를 반환하는 어떠한 비동기 함수도 될 수 있다. 기본적인 fetch도 되고, Axios 같은 도구도 사용이 가능하다.

그리고 위 훅은 세 개의 값을 반환하게 된다.

요청에 기반하는 data가 있고, 로딩을 보여주는 isLoading. 그리고 에러를 처리하는 error가 있다.

쉽다. error가 나면 error 부분을 화면에 띄운다. 로딩이 있다면 isLoading 부분을 화면에 띄운다.

SWR는 내가 생각하기에 되게 직관적인 것 같다.


🧱 TanStack Query (구 react-query) – 실무에 강력한 기능

공식 사이트 > https://tanstack.com/query/latest

 

TanStack Query

Powerful asynchronous state management, server-state utilities and data fetching. Fetch, cache, update, and wrangle all forms of async data in your TS/JS, React, Vue, Solid, Svelte & Angular applications all without touching any "global state"

tanstack.com

(원래는 리액트 쿼리라는 이름이었으나 스벨트나 뷰같은 다른 프레임워크에도 활용할 수 있도록 업데이트가 되어서 이름이 바뀜)

TanStack Query란

TS/JS, React, Solid, Vue, Svelte 및 Angular를 위한 강력한 비동기 상태 관리

특징

  • 상세한 캐시 제어
  • retry, refetch, invalidate, pagination 등 실무 기능 풍부
  • SWR보다 구조화된 방식

🌱 장점

1. 동기화

  • 서버와 데이터를 동기화 => 오래된 데이터 새로고침 or 백그라운드에서 자동 갱신.
    • 컴포넌트가 마운트 되거나 사용자 초점이 앱으로 돌아올 때 오래된 데이터를 자동으로 새로고침하여 업데이트.
    • 데이터 업데이트를 최대한 신속하게 반영.

2. 성능 최적화

  • 캐싱과 백그라운드 새로고침, 불필요 요청 방지 등의 기능.
    • 페이지네이션 및 데이터 지연 로드와 같은 성능 최적화.

3. 데이터 자동 캐싱 기능

  • API에서 가져온 데이터를 캐싱 = 동일한 데이터의 불필요한 중복 요청 줄여줌.
    • 캐싱된 데이터를 사용하면서 백그라운드에서 새 데이터를 받아와 최신 상태를 유지.

4. Server State를 관리함.

5. 서버 데이터 복잡하게 연동 or 실시간 데이터 필요 or 빈번한 데이터 페칭이 필요한 경우에 특히 유용함.

6. 로딩 상태, 오류 상태, 성공 상태 등 복잡한 상태 관리를 자동으로 처리해 줌.

7. 서버 응답이 오기 전에 UI를 먼저 업데이트해 빠르게 반응하도록 할 수 있음.

8. 요청이 실패하면 특정 조건에 따라 자동으로 재시도를 수행할 수 있음.

 

코드

설치 >

npm i @tanstack/react-query

터미널에 위 명령어를 기입해 주면 설치가 가능하다. (참고로 번들 크기는 swr보다는 크다.)

이후 코드를 작성해 주면 된다.

사용법 >

상단에 Provicer로 감싸기

  • react 앱 최상단인 App.js에 Context Provicer로 하위 컴포넌트를 감싸고 queryClient를 넘겨준다.
    => 이 Context가 비동기 요청을 알아서 처리하는 background 계층이다.

당연히 이런 처리는 라이브러리가 알아서 해준다. (개꿀)

관련 코드 >

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

// 👉 React Query 기본 옵션 설정
const queryClient = new QueryClient(); // queryClient가 리렌더되면 Cache 다시 백지화되니까 App 바깥에 선언

const App = () => {
  return (
    <>
      <QueryClientProvider client={queryClient}>
        <Router /> //  <Component {...pageProps} />
        <ReactQueryDevtools initialIsOpen={false} />
      </QueryClientProvider>
    </>
  );
};

export default App;

쓸려면 상단에 import를 해준다. 이후 옵션을 변수에 할당해 준다. 할당은 리렌더시 캐시가 다시 포맷이 되기 때문에 App 바깥에 선언하는 것이 맞다.

이후 위 코드처럼 QueryClientProvider를 감싸주면 된다. (이 코드는 App.jsx에 쓰이는 코드)

✅ 예시: useQuery

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

const fetchUser = async () => {
  const res = await fetch('https://api.example.com/user');
  if (!res.ok) throw new Error('에러 발생!');
  return res.json();
};

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

  if (isLoading) return <p>로딩 중...</p>;
  if (error) return <p>에러 발생!</p>;

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

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

  • useQuery 훅을 가져오는 import.
  • 이 훅은 데이터 패칭, 캐싱, 리페치, 에러 처리까지 한 번에 관리해 준다.

이후 쭉 코드를 선언해 주면 된다. 함수는 async를 이용해서 비동기로 처리해 주면 된다.

함수는 fetch를 이용해서 api 요청을 해준다. 이후에 쓴 함수인 UserInfo 함수는 useQuery 훅을 호출하게 된다.

이 훅은 데이터 패칭을 실행하게 된다.

✅ useQuery 옵션

 
{
  queryKey: ['user'],     // 이 요청을 구분할 수 있는 고유 키
  queryFn: fetchUser,     // 위에서 만든 fetch 함수
}

 

  • queryKey: 같은 키를 쓰면 캐시가 재사용되고, 다르면 새로 요청합니다.
  • queryFn: 호출될 비동기 함수

이후 data는 우리가 가져올 데이터이고, error는 에러 객체를 반환한다. (데이터를 가져오는 것에 실패할 시) isLoading은 로딩 중인지에 대한 여부이다.


 

 

🔄 내부적으로 자동으로 해주는 것들

  • ✅ 로딩 상태 관리
  • ✅ 에러 상태 감지
  • ✅ 캐싱 (같은 키로 여러 번 요청 안 함)
  • ✅ 재시도 (기본 3회, 실패 시 자동 재시도)
  • ✅ 포커스 재요청 (탭 전환 후 다시 돌아오면 최신 데이터 요청)

즉, TanStack Query를 쓰면 fetch/useEffect로 일일이 상태 관리할 필요 없이, 실무에서 반복되는 로직을 간단하게 추상화해서 쓸 수 있기 때문에 실무에 좋다.


⚖️ fetch vs SWR vs TanStack Query

다음은 fetch와 swr, tanstack query을 비교한 표이다. (ai 만세)

코드량 많음 적음 보통
캐싱 ❌ 없음 ✅ 자동 ✅ 고급 캐싱 가능
로딩/에러 처리 수동 자동 자동 + 커스터마이징 용이
실무 사용 소규모 프로젝트용 간단한 프로젝트 추천 대규모 서비스, 협업에 적합
재요청 ❌ 수동 ✅ 포커스 시 자동 ✅ 설정으로 자동/수동 모두 가능

왼쪽부터 fetch와 swr, tanstack query이다.

✅ 결론: 실무에서

  • 작고 단순한 프로젝트: fetch + useEffect or SWR
  • 중대형, 팀 협업: TanStack Query를 추천
  • 캐싱이나 자동 재요청이 필요하다면 SWR or TanStack Query는 필수이다.

 

결구 요약하면 작은 프로젝트엔 fetch나 swr를 쓰고, 프로젝트 규모가 크다면 TanStack Query를 쓰는 것을 추천한다.


라이엇 api를 이용한
fetch vs SWR vs TanStack Query 🔥

이번에는 실제 api를 기반으로 실습한 것을 이야기해보겠다.

먼저 나는 라이엇 api를 이용해서 실제 유저의 이름 / 태그 / 티어와 점수를 가져오는 프로젝트를 생각해 보았다.

api 관련 사이트 링크는 다음과 같다.

https://developer.riotgames.com/

 

Riot Developer Portal

About the Riot Games API With this site we hope to provide the League of Legends developer community with access to game data in a secure and reliable way. This is just part of our ongoing effort to respond to players' and developers' requests for data and

developer.riotgames.com


먼저. 이 사이트에 들어가서 API키를 발급받았다. 이 키는 따로 내가 라이엇에 등록하지 않는다면 24시간 이후 다시 포맷을 해주어야 한다. api키는 .env.local에 넣어서 외부에 노출하지 않게 만들었다.

이후 이 api에 get 요청을 해서 데이터를 받아오게 된다.

riot.js

api를 요청하는 "공통"의 함수를 src/api/riot.js 에 작성해 주었다.

코드는 다음과 같다.

경로 > src/api/riot.js

const BASE_URL = 'https://asia.api.riotgames.com';

const getHeaders = () => ({
  'X-Riot-Token': import.meta.env.VITE_RIOT_API_KEY,
});

// gameName + tagLine으로 소환사 정보 조회
export async function fetchSummonerByRiotId(gameName, tagLine) {
  if (!gameName || !tagLine)
    throw new Error('gameName과 tagLine이 필요합니다.');

  const encodedGameName = encodeURIComponent(gameName);
  const encodedTagLine = encodeURIComponent(tagLine);

  const url = `${BASE_URL}/riot/account/v1/accounts/by-riot-id/${encodedGameName}/${encodedTagLine}`;

  const res = await fetch(url, {
    headers: getHeaders(),
  });

  if (!res.ok) {
    throw new Error(`API 요청 실패: 상태 코드 ${res.status}`);
  }

  return res.json();
}

// PUUID로 리그/티어 정보 조회 (KR shard)
export async function fetchLeagueInfoByPUUID(puuid) {
  if (!puuid) throw new Error('puuid가 필요합니다.');
  const url = `https://kr.api.riotgames.com/lol/league/v4/entries/by-puuid/${puuid}`;
  const res = await fetch(url, { headers: getHeaders() });
  if (!res.ok) throw new Error(`PUUID로 리그 정보 요청 실패 (${res.status})`);
  return res.json(); // 티어 정보 배열
}

함수는 크게 2개다. 하나는 유저의 puuid를 가져오는 코드. 나머지는 그 puuid를 가져와서 리그 정보를 가져오는 코드이다.

위는 유저의 이름과 태그를 기입해야 하며, 이를 통해 나오는 puuid가 유저의 티어 / 점수를 가져오게 된다.

(롤 api는 3을 하기 위해서는 2를 해야 하고, 2를 하기 위해서는 1을 해야 하는 흐름으로 만들게 된다카더라..)

함수는 모두 비동기로 처리가 되어있다.


위 파일은 먼저 베이스가 되는 url를 선언해 두는 형태이고, getHeaders 함수에 X-Riot-Token이라는 토큰에 내가 api를 세팅해 둔 키를 import 해준다. 이 함수는 추후 fetch에서 headers 요청에 쓰이게 된다.

(재사용성 및 확장성 고려)

(라이엇 api는 json의 형태로 우리에게 데이터를 준다.)


fetch 코드

이제 계속 쓰이는 함수를 미리 세팅했기에, 원할 때마다 가져오면 된다. (앞으로 모든 코드에서 가져올 예정)

먼저 fetch 코드를 예시로 두자면.

import { useEffect, useState } from 'react';
import { fetchSummonerByRiotId } from '../api/riot';

export function SummonerInfoFetch({ gameName, tagLine }) {
  const [data, setData] = useState(null);
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (!gameName || !tagLine) return;

    setLoading(true);
    setError('');
    setData(null);

    fetchSummonerByRiotId(gameName, tagLine)
      .then(setData)
      .catch((err) => setError(err.message))
      .finally(() => setLoading(false));
  }, [gameName, tagLine]);

  if (loading) return <p>로딩 중...</p>;
  if (error) return <p>❌ 에러: {error}</p>;
  if (!data) return null;

  // PUUID 콘솔에 출력
  console.log(`PUUID (${gameName}#${tagLine}):`, data.puuid);

  return (
    <div>
      <h3>소환사 정보 (fetch방식)</h3>
      <p>이름: {data.gameName}</p>
      <p>태그라인: {data.tagLine}</p>
    </div>
  );
}

위 코드는 gameName, tagLine이라는 두 값을 입력받아 Riot API를 호출하고, PUUID와 게임 이름, 태그라인을 화면에 표시하는 코드이다. useEffect + fetch 방식으로 비동기 데이터를 처리한다.

1. useState 3개로 상태 관리

const [data, setData] = useState(null);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
  • data: API 응답 데이터를 담는 상태 (처음엔 null)
  • error: 에러 메시지를 저장하는 상태
  • loading: 로딩 중 여부를 저장하는 상태 (처음엔 false)

2. useEffect 내부 – 의존성 기반 데이터 패칭

useEffect(() => {
  if (!gameName || !tagLine) return;

  setLoading(true);
  setError('');
  setData(null);

  fetchSummonerByRiotId(gameName, tagLine)
    .then(setData)
    .catch((err) => setError(err.message))
    .finally(() => setLoading(false));
}, [gameName, tagLine]);

 

  • gameName 또는 tagLine이 바뀔 때마다 다시 실행.
  • 둘 중 하나라도 없으면 return으로 실행 중단 (불완전한 요청 방지)
  • 상태 초기화:
    • loading을 true로 설정
    • 에러와 데이터 초기화
  • API 호출:
    • fetchSummonerByRiotId는 Riot API에 요청을 보내는 함수 (비동기)
    • 응답이 오면 setData
    • 에러가 발생하면 setError(err.message)
    • 무조건 마지막에 loading을 false로 바꿈

이후 모든 처리가 끝나면 ui가 최종적으로 렌더링이 될 것이다.


 

그다음은 swr 관련 코드이다.

코드 예시 >

import useSWR from 'swr';
import { fetchSummonerByRiotId } from '../api/riot';

export function SummonerInfoSWR({ gameName, tagLine }) {
  if (!gameName || !tagLine) return null;

  const key = ['summonerByRiotId', gameName, tagLine];

  const fetcher = () => fetchSummonerByRiotId(gameName, tagLine);

  const { data, error, isLoading } = useSWR(key, fetcher);

  if (isLoading) return <p>로딩 중...</p>;
  if (error) return <p>❌ 에러: {error.message}</p>;
  if (!data) return null;

  // PUUID 콘솔에 출력
  console.log(`PUUID (${gameName}#${tagLine}):`, data.puuid);

  return (
    <div>
      <h3>소환사 정보 (SWR방식)</h3>
      <p>이름: {data.gameName}</p>
      <p>태그라인: {data.tagLine}</p>
    </div>
  );
}

✅ 컴포넌트 전체 역할 요약

이 컴포넌트 SummonerInfoSWR는 gameName과 tagLine을 입력으로 받아, fetchSummonerByRiotId를 통해 소환사 정보를 SWR 방식으로 패칭(fetching)한다.

SWR은 useEffect + useState 없이도 비동기 처리, 캐싱, 로딩/에러 상태 관리를 자동으로 처리해 준다.

사실 기능적인 로직은 모든 코드가 동일하다고 봐도 무방하다.

일단 위 코드에서 swr를 쓰기 위해 상단에 import문을 기입한 것을 볼 수 있다.

이후 fetch처럼 비슷하게 처리하지만, key 값을 배열처리한다.

const key = ['summonerByRiotId', gameName, tagLine];

  • 이건 캐시 key 역할이 됨.
  • SWR은 key가 동일하면 요청을 캐싱한다.
  • ['summonerByRiotId', gameName, tagLine]처럼 배열로 만들면, 각각의 조합으로 고유한 요청 식별이 가능하다.

const fetcher = () => fetchSummonerByRiotId(gameName, tagLine);

  • SWR에서 사용하는 fetcher 함수.
  • 단순히 fetch가 아니라, Riot API에 맞게 만든 비동기 함수.

const { data, error, isLoading } = useSWR(key, fetcher);

  • useSWR 훅을 호출해 데이터를 요청.
  • 내부적으로:
    • 요청 자동 실행 (useEffect 없이도)
    • 캐싱, 재검증, 로딩/에러 상태 자동 관리.
  • 반환값:
    • data: 받아온 소환사 데이터
    • error: 요청 실패 시 에러
    • isLoading: 데이터 패칭 중 상태

 

결국 로직은 비슷하지만, useEffect 처리를 따로 안 한다는 것이 포인트.


마지막으로..

TanStack Query 예제를 보자.

예시 코드 >

import { useQuery } from '@tanstack/react-query';
import { fetchSummonerByRiotId } from '../api/riot';

export function SummonerInfoQuery({ gameName, tagLine }) {
  const { data, error, isLoading } = useQuery({
    queryKey: ['summonerByRiotId', gameName, tagLine],
    queryFn: () => fetchSummonerByRiotId(gameName, tagLine),
    enabled: !!gameName && !!tagLine,
  });

  if (isLoading) return <p>로딩 중...</p>;
  if (error) return <p>❌ 에러: {error.message}</p>;
  if (!data) return null;

  // PUUID 콘솔에 출력
  console.log(`PUUID (${gameName}#${tagLine}):`, data.puuid);

  return (
    <div>
      <h3>소환사 정보 (TanStack Query방식)</h3>
      <p>이름: {data.gameName}</p>
      <p>태그라인: {data.tagLine}</p>
    </div>
  );
}

텐스택 쿼리도 동일하게, 상단에 import를 해준다.

이후 동일하게 코드들을 처리하다.. useQuery 함수에 로직들을 처리해 주면 된다.

1. useQuery({...}) 사용

const { data, error, isLoading } = useQuery({
  queryKey: ['summonerByRiotId', gameName, tagLine],
  queryFn: () => fetchSummonerByRiotId(gameName, tagLine),
  enabled: !!gameName && !!tagLine,
});

✅ queryKey

queryKey는 요청을 식별하는 고유 키이다. 같은 키로 요청하면 캐시된 데이터를 자동으로 재사용하게 된다.
 

✅ queryFn

실제 데이터 요청함수이다. 여기에서는 fetchSummonerByRiotId가 될 것이다. 이 함수는 내가 맨 처음 riot.js에 분리해두었다.
 

✅ enabled

  • gameName, tagLine이 모두 있을 때만 쿼리를 실행한다.
  • 값이 없으면 요청 자체를 막음

이후에는 동일하게 처리함.


 src/App.jsx

이제 컴포넌트로 분리한 것을 App.jsx에 딱 모아두면 된다.

코드는 다음과 같다.

import { useState } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

import { SummonerInfoFetch } from './components/SummonerInfoFetch';
import { SummonerInfoSWR } from './components/SummonerInfoSWR';
import { SummonerInfoQuery } from './components/SummonerInfoQuery';
import { RankTierByPUUID } from './components/RankTierByPUUID';
import './App.css';

const queryClient = new QueryClient();

// QueryClientProvider는 애플리케이션의 최상위 컴포넌트에 위치해야 합니다.
// 소환사 정보를 게임 이름과 태그라인으로 조회하는 컴포넌트

export default function App() {
  const [gameName, setGameName] = useState('');
  const [tagLine, setTagLine] = useState('');

  return (
    <QueryClientProvider client={queryClient}>
      <div className="p-6 max-w-xl mx-auto space-y-6">
        <h1 className="text-2xl font-bold">🔍 소환사 정보 조회 비교</h1>

        <input
          type="text"
          placeholder="게임 이름 (예: Hide on bush)"
          value={gameName}
          onChange={(e) => setGameName(e.target.value)}
          className="border p-2 rounded w-full"
        />

        <input
          type="text"
          placeholder="태그라인 (예: KR1)"
          value={tagLine}
          onChange={(e) => setTagLine(e.target.value)}
          className="border p-2 rounded w-full"
        />

        <div className="p-4 border rounded shadow">
          <RankTierByPUUID gameName={gameName} tagLine={tagLine} />
        </div>

        <div className="grid grid-cols-1 gap-6 mt-6">
          <div className="p-4 border rounded shadow">
            <SummonerInfoFetch gameName={gameName} tagLine={tagLine} />
          </div>

          <div className="p-4 border rounded shadow">
            <SummonerInfoSWR gameName={gameName} tagLine={tagLine} />
          </div>

          <div className="p-4 border rounded shadow">
            <SummonerInfoQuery gameName={gameName} tagLine={tagLine} />
          </div>
        </div>
      </div>
    </QueryClientProvider>
  );
}

쭉 컴포넌트를 import 해주면 된다.

그리고 텐스택 쿼리는 위에서 말했듯이 QueryClientProvider로 감싸주어야 사용이 가능하다.