본문 바로가기
REACT/롤 전적 사이트

[3] 매치 정보 가져오기

by 쪼꼬에몽 2025. 8. 13.

1. match ID 가져오기

여기서 matchId를 가져와 각 match에 대한 정보를 다른 api에서 불러온다.

 

딱히 힘든 부분은 없었다. 시작과 count를 0과 20으로 상수 설정을 하였다. 

 

import useFetch from '@/hooks/fetch/useFetch.ts';
import { MATCH_ID } from '@/api/url.ts';

export default function useMatchId({ puuid, enabled }) {
  const { data, isLoading, isError } = useFetch({
    key: 'matchId',
    value: puuid,
    url: MATCH_ID(puuid),
    options: {
      enabled,
    },
  });

  return { data, isLoading, isError };
}
import useFetch from '@/hooks/fetch/useFetch.ts';
import { MATCH_ID } from '@/api/url.ts';
import type { userInfoProps } from '@/types/user.ts';

export default function useMatchId({ puuid, enabled }: userInfoProps) {
  const { data, isLoading, isError } = useFetch<string[]>({
    key: 'matchId',
    value: puuid,
    url: MATCH_ID(puuid),
    options: {
      enabled,
    },
  });

  return { data, isLoading, isError };
}

use 훅에서 useFetch로 데이터를 보내 얻는다.

 

2. 각 매치 정보 가져오기

위에서 가져온 매치Id로 각 매치의 정보를 가져온다.

 

더보기

롤(League of Legends) Match V5 API 상세 설명

제공해주신 GET /lol/match/v5/matches/{matchId} API는 특정 게임(match)의 상세 데이터를 가져오는 핵심적인 API입니다. 이 API를 통해 얻어온 데이터는 롤 전적 검색 사이트에서 가장 중요하게 사용되는 부분으로, 한 게임의 모든 것을 담고 있다고 볼 수 있습니다.

API 응답은 크게 metadatainfo 두 개의 JSON 객체로 나뉩니다. 각 객체와 그 하위 항목들이 어떤 기능을 하는지 자세히 설명해 드리겠습니다.

1. metadata 객체: 경기에 대한 기본 정보

이 객체는 해당 경기의 메타데이터, 즉 데이터 자체에 대한 정보를 담고 있습니다.

항목 데이터 타입 설명
dataVersion string 이 데이터의 버전 정보입니다. 라이엇 게임즈가 데이터 구조를 업데이트할 때마다 변경될 수 있습니다.
matchId string 경기의 고유 ID입니다. (예: "KR_7755234865") 이 ID를 통해 특정 경기를 조회할 수 있습니다.
participants List[string] 해당 경기에 참여한 모든 소환사의 PUUID 리스트입니다. PUUID는 계정마다 부여되는 영구적이고 고유한 ID입니다.

2. info 객체: 경기의 모든 상세 내용

경기의 실질적인 모든 데이터가 이 객체 안에 들어있습니다. 게임 시간, 모드, 참가자 개개인의 성적, 팀 정보 등이 포함됩니다.

2-1. 게임 전반 정보

항목 데이터 타입 설명
gameCreation long 게임이 생성된 시점의 Unix 타임스탬프입니다. (로딩 화면 시작)
gameDuration long 게임 진행 시간입니다. 11.20 패치 이후로는 초(second) 단위로 계산됩니다.
gameEndTimestamp long 게임이 종료된 시점의 Unix 타임스탬프입니다.
gameId long 게임의 고유 번호입니다.
gameMode string 게임 모드(예: CLASSIC, ARAM, URF)를 나타냅니다.
gameType string 게임 타입(예: MATCHED_GAME - 매칭 게임, CUSTOM_GAME - 사용자 설정 게임)을 나타냅니다.
gameVersion string 해당 경기가 진행된 게임 클라이언트 버전입니다. (예: 14.15.586.331) 패치 버전을 확인할 수 있습니다.
mapId int 게임이 진행된 맵의 ID입니다. (예: 11 - 소환사의 협곡, 12 - 칼바람 나락)
platformId string 경기가 진행된 서버 플랫폼입니다. (예: "KR")
queueId int 게임 큐(Queue)의 ID입니다. (예: 420 - 솔로 랭크, 430 - 일반 게임, 450 - 무작위 총력전)
participants List[ParticipantDto] 가장 중요한 데이터 중 하나로, 10명의 참가자 각각의 상세 정보를 담은 리스트입니다. (아래 ParticipantDto에서 상세 설명)
teams List[TeamDto] 블루 팀(100)과 레드 팀(200)의 정보를 담은 리스트입니다. (아래 TeamDto에서 상세 설명)
tournamentCode string 토너먼트 코드로 생성된 게임일 경우 해당 코드가 표시됩니다.

2-2. ParticipantDto: 참가자 개별 데이터

info.participants 리스트에 포함된 각 참가자의 모든 정보를 담고 있습니다. 전적 검색의 핵심 데이터입니다.

주요 항목:

항목 데이터 타입 설명
summonerName string 소환사 이름 (인게임 닉네임)
riotIdGameName / riotIdTagline string 라이엇 ID와 태그라인 (2024년 도입)
puuid string 플레이어의 고유 PUUID
championName string 플레이한 챔피언의 영문 이름 (예: "Aatrox", "Ahri")
championId int 플레이한 챔피언의 고유 ID
win boolean 승리 여부 (true/false)
kills / deaths / assists int 킬, 데스, 어시스트
item0, item1, ... item6 int 보유 아이템 7개(장신구 포함)의 각 아이템 ID
summoner1Id / summoner2Id int 사용한 소환사 주문(스펠) 2개의 ID
goldEarned int 획득한 총 골드
totalDamageDealtToChampions int 챔피언에게 가한 총 피해량
totalMinionsKilled int CS (미니언 처치 수). 정글 몬스터는 neutralMinionsKilled에 포함됩니다.
wardsPlaced / wardsKilled int 설치한 와드 수 / 파괴한 와드 수
visionScore int 시야 점수
teamPosition string 팀 내에서 플레이한 포지션 (예: TOP, JUNGLE, MIDDLE, BOTTOM, UTILITY)
lane string 실제 라인 (teamPosition보다 덜 정확할 수 있음)
champLevel int 최종 챔피언 레벨
pentaKills, quadraKills, ... int 펜타킬, 쿼드라킬 등 멀티킬 횟수
perks PerksDto 선택한 룬 정보 (핵심 룬, 보조 룬, 능력치 파편)
challenges ChallengesDto 도전과제 관련 데이터. kda, killParticipation (킬 관여율), damagePerMinute (분당 피해량) 등 계산된 수치가 포함되어 매우 유용합니다.

2-3. TeamDto: 팀 데이터

info.teams 리스트에 포함된 각 팀(블루/레드)의 정보를 담고 있습니다.

항목 데이터 타입 설명
teamId int 팀 ID (100: 블루 팀, 200: 레드 팀)
win boolean 팀의 승리 여부 (true/false)
bans List[BanDto] 해당 팀이 밴(Ban)한 챔피언 목록과 밴 순서
objectives ObjectivesDto 팀이 달성한 오브젝트 정보

2-4. ObjectivesDto: 오브젝트 상세 데이터

TeamDto.objectives에 포함되어 있으며, 팀별로 획득한 오브젝트 정보를 나타냅니다.

항목 데이터 타입 설명
baron ObjectiveDto 바론 킬 횟수 및 첫 바론 킬 여부
dragon ObjectiveDto 드래곤 킬 횟수 및 첫 드래곤 킬 여부
inhibitor ObjectiveDto 파괴한 억제기 수 및 첫 억제기 파괴 여부
riftHerald ObjectiveDto 전령 킬 횟수 및 첫 전령 킬 여부
tower ObjectiveDto 파괴한 타워 수 및 첫 타워 파괴 여부

요약 및 활용 방안

  • 기본적인 전적 표시: info.participants를 순회하며 각 플레이어의 summonerName, championName, kills, deaths, assists, item0-6, totalMinionsKilled 등을 가져와 화면에 표시합니다.
  • KDA 및 킬 관여율 계산:
    • KDA는 challenges.kda를 직접 사용하거나, (kills + assists) / deaths 로 계산할 수 있습니다.
    • 킬 관여율은 challenges.killParticipation 값을 퍼센트로 변환하여 사용합니다.
  • 승/패 확인: info.participants 내의 win boolean 값을 통해 승패를 구분하고 배경색 등을 다르게 표시합니다.
  • 팀 전체 정보: info.teams에서 bans 정보를 가져와 밴픽 현황을 보여주고, objectives를 통해 각 팀의 드래곤, 바론 획득 현황을 시각적으로 표시할 수 있습니다.
  • 빌드 정보: perks에서 룬 정보를, summoner1Id/summoner2Id에서 스펠 정보를 가져와 아이템 빌드와 함께 보여줍니다.
  • 상세 분석: challenges 객체 안에는 damagePerMinute(분당 데미지), goldPerMinute(분당 골드) 등 심화된 분석 지표가 많아 이를 활용해 더 상세한 정보를 제공할 수 있습니다.

이처럼 match-v5 API 하나만으로도 한 경기에 대한 거의 모든 정보를 얻을 수 있으며, 롤 전적 검색 서비스의 가장 기본이자 핵심적인 데이터를 구성하게 됩니다. 이 구조를 잘 이해하고 필요한 데이터를 추출하여 프론트엔드에서 가공하면 풍부한 전적 정보를 사용자에게 제공할 수 있습니다.

현재 matchId에 20개의 id가 배열로 구성되어 있다.

각 id마다 해당 데이터를 얻으려고 map을 써보았는데 오류가 발생했다.

const {
  data: matchId,
  isLoading: matchIdIsLoading,
  isError: matchIdIsError,
} = useMatchId({ puuid: puuidData?.puuid ?? '', enabled: !!puuidData?.puuid });

const {
  data: matchInfo,
  isLoading: matchInfoIsLoading,
  isError: matchInfoIsError,
} = matchId.map((item) => {
  useMatchInfo({ matchId: item ?? '', enabled: !!item });
});

matchId를 map으로 돌면서 useMatchInfo에 matchId 값인 item을 넣으려고 했는데 오류가 발생했다.

ESLint: React Hook "useMatchInfo" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.(react-hooks/rules-of-hooks)

훅을 callback 함수에 넣을 수 없다는 오류이다. 즉, React Hooks의 규칙을 위반한다.

map 안에서 Hook을 호출하면 안되는 이유?

Hooks는 반복문, 조건문, 중첨 함수 안에서 호출될 수 없음.

Hooks는 항상 React 컴포넌트 함수의 최사위에서만 호출되어야 함.

React는 매 렌더링마다 Hook이 항상 동일한 순서로 호출될 것이라고 가정하고 내부 상태 관리함.

matchId.map(item =? { useMatchInfo(...) })는 matchId 배열을 순회하는 반복문 안에서 Hook 호출함.

만약 matchId 배열 길이가 렌더링 사이에 바뀐다면, Hook의 호출 순서와 개수가 달라지게 되어 React가 상태를 제대로 추적할 수 없게 됨.

해결법

useQueries 사용하기 (React Query / TanStack Query)

- useQueries를 사용하면 여러 개의 쿼리를 병렬로 보냄. 모든 매치 정보 요청을 동시에 병렬로 보냄.

- 한 번의 렌더링으로 모든 데이터를 가져오고 상태를 업데이트함.

현재에서는 자식 컴포넌트를 만들어야 하므로 자식 컴포넌트로 분리하는 방법을 사용했다.

자식 컴포넌트로 분리하기

- 데이터 패칭 Hook을 호출하는 부분을 별도의 자식 컴포넌트로 분리함.

- matchId 하나를 props로 받아 useMatchInfo Hook을 호출함.

 

Home에서 아래와 같이 분리한다.

{matchId && matchId.map((matchId) => <MatchInfo key={matchId} matchId={matchId} />)}

 

자식에서 아래와 같이 호출한다. 

import useMatchInfo from '@/hooks/fetch/useMatchInfo.ts';

export default function MatchInfo({ matchId }) {
  const {
    data: matchInfo,
    isLoading: matchInfoIsLoading,
    isError: matchInfoIsError,
  } = useMatchInfo({ matchId: matchId ?? '', enabled: !!matchId });

  if (matchInfoIsLoading) return <p>매치 정보 로딩중...</p>;
  if (matchInfoIsError) return <p>매치 정보 로드 중 에러 발생</p>;

  return (
    <>
      {matchInfo && (
        <>
          <p>{matchInfo.metadata.matchId}</p>
          <p>{matchInfo.info.gameCreation}</p>
        </>
      )}
    </>
  );
}

 

하지만 오류가 발생했다. 

api를 초당 불러오는데 제한을 둔다. 

현재 코드는 병렬 처리를 하는데 1초도 안되는 시간동안 20개의 데이터를 요청한다.

이를 해결하기 위해서는 페이지내이션을 두어 정보를 부분화하여 보여주던지,

async/await를 통한 시간 지연을 두어야 한다.

 

여기서는 페이지내이션을 통해 5개씩 정보가 보이게 할 것이다.

왜냐하면 정보가 20개 이상으로 설정할 수 있기 때문에 페이지를 두는 것이 더 좋을 것으로 판단된다.

const {
  data: matchId,
  isLoading: matchIdIsLoading,
  isError: matchIdIsError,
} = useMatchId({ puuid: puuidData?.puuid ?? '', enabled: !!puuidData?.puuid });

const [visibleMatchId, setVisibleMatchId] = useState(5);

const handleLoadMore = () => {
  setVisibleMatchId((prevCount) => prevCount + 5);
};

const visibleMatchIds = (matchId ?? []).slice(0, visibleMatchId);

useState에 5를 두어 버튼을 누를 때마다 5씩 데이터가 더 보이게 하였다.

matchId 배열에서 slice 메서드를 통해 원하는 length만큼 데이터가 보이게 하였다.

에러가 보이지 않고 잘 표시되었다.

3. 가장 많이 사용하는 챔피언 가져오기

저기서 챔피언 아이디를 가져오면 된다.

이전까지 api를 가져오는 방법은 같다.

const bestChampionId = championId?.map((champion) => champion.championId);

 

champhionId만 가져오기 위해 map을 사용했다.

챔피언id에 해당하는 챔피언을 가져와야 한다.

ddragon.leagueoflegends.com/cdn/15.15.1/data/ko_KR/champion.json

여기서 챔피언 데이터를 가져온다.

championId는 key와 같은 것을 가져오면 된다.

15.15.1은 패치 버전이다. 최신 것을 가져오면 된다.

import useChampionInfo from '@/hooks/fetch/useChampionInfo.ts';

export default function BestChampion({ championId, championIsLoading, championIsError }) {
  const bestChampionId = championId?.map((champion) => champion.championId);

  const {
    data: championInfo,
    isLoading: championInfoIsLoading,
    isError: championInfoIsError,
  } = useChampionInfo();

  const bestChampionInfo = championInfo.map((item) => item.key === bestChampionId);

  console.log(bestChampionInfo);

  if (championIsLoading || championInfoIsLoading) return <p>챔피언 정보 로딩중...</p>;
  if (championIsError || championInfoIsError) return <p>챔피언 정보 로드 중 에러 발생</p>;

  return (
    <>

    </>
  );
}

id별로 key에 해당하는 값을 가져오려고 했는데 오류가 발생했다.

챔피언 정보가 객체여서 배열로 바꾸어야 한다.

컴포넌트에서 바꾸려면 복잡해 보이니 hook에서 바꿔보자.

import useFetch from '@/hooks/fetch/useFetch.ts';
import { CHAMPION_INFO_URL } from '@/api/url.ts';

export default function useChampionInfo({ enabled }) {
  const { data, isLoading, isError } = useFetch({
    key: 'championInfo',
    value: '',
    url: CHAMPION_INFO_URL,
    options: {
      enabled,
    },
  });

  const championData = data ? Object.values(data.data) : [];

  return { data: championData, isLoading, isError };
}

data를 배열로 바꾸면 된다.

const championData = data ? Object.values(data.data) : [];

return { data: championData, isLoading, isError };

Object.values()를 쓰면 객체 value를 배열로 바꾼다.

const bestChampionId = championId?.map((champion) => champion.championId) ?? [];

const {
  data: championInfo,
  isLoading: championInfoIsLoading,
  isError: championInfoIsError,
} = useChampionInfo({ enabled: Boolean(bestChampionId) });

const bestChampionInfo = championInfo.filter((champ) =>
  bestChampionId.includes(Number(champ.key))
);

 

필요한 핵심 api를 다 가져왔다.

이제 이 api를 이용해 ui를 꾸며보도록 하겠다.

match 부분도 더 추가해 보겠다.