[2차] Next.js 리그오브레전드 정보 프로젝트 (모든 리스트 페이지 구현 완료)
[2차] Next.js 리그오브레전드 정보 프로젝트
베포 링크
- vercel
- github
- https://github.com/rarrit/lol-info-app
- branch
- feat/champion
- feat/item
- feat/rotation
프로젝트 설명
NextJS와 TypeScript을 사용하고 Riot API를 활용하여 리그오브레전드의 다양한 데이터를 조회하고 이를 기반으로 생성한 프로젝트 입니다.
STACK
프로젝트 구조
- 2차에선 아이템, 로테이션 페이지를 완성했습니다.
전체 프로젝트 구조
📦app┣ 📂_components
┃ ┣ 📂champions
┃ ┃ ┣ 📜ChampionCard.tsx
┃ ┃ ┗ 📜ChampionList.tsx
┃ ┗ 📂items
┃ ┃ ┣ 📜itemCard.tsx
┃ ┃ ┣ 📜itemDesc.tsx
┃ ┃ ┗ 📜itemList.tsx
┣ 📂api
┃ ┣ 📂rotation
┃ ┃ ┗ 📜route.ts
┃ ┗ 📜apiKey.ts
┣ 📂champions
┃ ┣ 📂[id]
┃ ┃ ┗ 📜page.tsx
┃ ┣ 📜layout.tsx
┃ ┣ 📜loading.tsx
┃ ┗ 📜page.tsx
┣ 📂fonts
┃ ┣ 📜GeistMonoVF.woff
┃ ┗ 📜GeistVF.woff
┣ 📂items
┃ ┣ 📜loading.tsx
┃ ┗ 📜page.tsx
┣ 📂rotation
┃ ┣ 📜loading.tsx
┃ ┗ 📜page.tsx
┣ 📂types
┃ ┣ 📜Champion.ts
┃ ┣ 📜ChampionRotation.ts
┃ ┗ 📜Item.ts
┣ 📂utils
┃ ┣ 📜riotApi.ts
┃ ┗ 📜serverApi.ts
┣ 📜favicon.ico
┣ 📜global-error.tsx
┣ 📜globals.css
┣ 📜layout.tsx
┣ 📜loading.tsx
┗ 📜page.tsx
작업 목록
4-1) 아이템 리스트 페이지 설정
아이템 리스트 페이지에서는 ItemList
컴포넌트를 렌더링한다.
import ItemList from "@/app/_components/items/itemList"
const ItemListPage = () => <ItemList/>
export default ItemListPage
4-2) 아이템 리스트 컴포넌트 설정
Riot API에서 가져온 아이템 데이터를 기반으로 아이템 목록을 화면에 렌더링한다.
- 렌더링 방식: SSG
- 빌드 시점에 페이지를 정적으로 생성 (Next.js 기본 컴포넌트 렌더링 방식)
- 데이터를 가져오는 함수 생성
- fetch를 사용해서, 최신 버전의 아이템 리스트를 반환하는 함수를 생성했다.
- props로 데이터 전달
- 해당 데이터를 기반으로
ItemCard
컴포넌트로 전달해줌
- 해당 데이터를 기반으로
// app > utils > serverApi.ts
export const getItems = async () => {
try{
const latestVersion = await getLatestVersionUrl();
const res = await fetch(`${RIOT_BASE_URL}/cdn/${latestVersion}/data/ko_KR/item.json`,
{
cache: "force-cache"
}
);
const data = await res.json();
const itemList: ItemType[] = Object.values(data.data);
return itemList;
}catch(error){
console.error("아이템 API 에러:", error);
throw error;
}
}
// app > _components > itemList.tsx
import { getItems } from "@/app/utils/serverApi";
import ItemCard from "./itemCard";
const ItemList = async () => {
const items = await getItems();
if(!items) return <div>아이템이 없습니다.</div>
return (
<div id="itemList" className="w-full bg-lol04 bg-fixed bg-center bg-no-repeat py-[60px] max-t:px-[15px]">
<div className="inner w-full max-w-[1440px] m-auto">
<h1
className="flex justify-center text-[60px] mt-[30px] mb-[40px] max-m:text-[30px]"
style=
>ALL ITEM</h1>
<div className="list flex flex-wrap gap-[10px] w-full">
{
items.map(item => {
return (
<ItemCard
key={item.name}
item={item}
/>
)
})
}
</div>
</div>
</div>
)
}
export default ItemList
4-3) 아이템 카드 컴포넌트 설정
챔피언의 이름,타이틀,이미지 등 화면에 표시하는 역할을 하는 컴포넌트이다.
- prop 타입 정의
-
ItemCardProps
: 해당 컴포넌트가 받을item
prop 타입을 정의함
-
- 컴포넌트 타입 정의
-
React.FC
: Function Component 타입의 줄임말로, React + Typescript 조합으로 개발할 때 사용하는 타입이다.
- 함수형 컴포넌트 사용 시 타입 선언에 쓸 수 있도록 React에서 제공하는 타입.
-
- 전달받은
item
데이터를 기반으로 바인딩을 해주었다. - Image 컴포넌트를 사용하여, 이미지 최적화 적용해줌
// app > _components > itemCard.tsx
import { RIOT_BASE_URL } from "@/app/api/apiKey";
import { ItemType } from "@/app/types/Item"
import { getLatestVersionUrl } from "@/app/utils/serverApi";
import Image from "next/image";
import ItemDesc from "./itemDesc";
type ItemCardProps = {
item: ItemType;
}
const ItemCard: React.FC<ItemCardProps> = async ({ item }) => {
const latestVersion = await getLatestVersionUrl();
return (
<div className="relative w-[calc(20%-10px)] bg-[rgba(0,0,0,.4)] rounded-[10px] max-n:w-[calc(25%-10px)] max-t:w-[calc(33.333%-10px)] max-m:w-[calc(50%-10px)] max-sm:w-full">
<div className="info flex flex-col items-center justify-center p-[20px]">
{item.image && (
<Image
src={`${RIOT_BASE_URL}/cdn/${latestVersion}/img/item/${item.image.full ? item.image.full : item.image.sprite}`}
className="rounded-[10px]"
width={64}
height={64}
alt={item ? item.name : "챔피언 이미지"}
/>
)}
<strong className="mt-[10px] text-[rgba(245,79,79,1)] text-center max-m:text-[14px]">{item.name}</strong>
<p><span className="text-[18px] font-bold max-m:text-[16px]">{item.gold.total}</span>원</p>
<p className="item-desc">{item.description}</p>
</div>
</div>
)
}
export default ItemCard
4-4) Rotation Route handler 생성 및 서버액션 설정
-
api > rotation > route.ts
파일 생성- 로테이션 챔피언 정보를 가져오기 위한
route handler
를 생성해주었다.
- 로테이션 챔피언 정보를 가져오기 위한
- 아래의 코드 작업을 하면서
X-Riot-Token
값을 적용하지 않아서 데이터가 정상적으로 노출되지 않는 이슈가 생겼음- 처음 엔드포인트를 계속 확인해도 데이터가 노출되지 않아서 발제 문서를 다시 확인했고, 토큰값을 적용 후 확인해보니 정상적으로 확인이 되었음
-
localhost:3000/api/rotation
에서 확인함 (배열로 id 값 확인)
-
utils > riotApi.ts
파일 생성- 해당 파일에서 로테이션 챔피언을 가져오는 서버액션 함수를 생성했다.
-
champions
변수에 챔피언 리스트를 할당해줌 -
freeChampionIds
변수에 데이터의 로테이션 아이디값을 받아옴 -
rotation
변수에 챔피언리스트의 키값 = 로테이션 아이디값 확인해서 있는 데이터만 할당해주고 반환함
// api > rotation > route.ts
import { RotationType } from "@/app/types/ChampionRotation";
import { RIOT_API_KEY } from "../apiKey";
export async function GET(){ // eslint-disable-line no-unused-vars
const res = await fetch(`https://kr.api.riotgames.com/lol/platform/v3/champion-rotations`, {
headers: {
'Content-Type': 'application/json',
'X-Riot-Token': RIOT_API_KEY as string
}
});
const data: RotationType = await res.json();
return Response.json({ data });
}
// utils > riotApi.ts
import { getChampions } from './serverApi';
import { ChampionInfo } from '../types/Champion';
export const getChampionRotation = async () => {
const champions = await getChampions();
const response = await fetch('/api/rotation');
const result = await response.json();
const freeChampionIds = result.data.freeChampionIds;
const rotation: ChampionInfo[] = Object.values(champions).filter((champion) =>
freeChampionIds.includes(Number(champion.key))
);
return rotation;
};
4-5) Rotation 페이지
해당 페이지는 use client
를 사용해서 CSR 렌더링 방식으로 구현했다.
-
useEffect
와fetch
를 사용해서 로테이션 챔피언 데이터를 가져옴 -
useState
의 setter 사용해서 로테이션 챔피언 데이터로 상태를 업데이트함 - 업데이트된 상태를 통해 데이터를 바인딩해주었다.
"use client";
import { useEffect, useState } from "react";
import { ChampionInfo } from "../types/Champion";
import { getChampionRotation } from "../utils/riotApi";
import Image from "next/image";
import { RIOT_BASE_URL } from "../api/apiKey";
import Link from "next/link";
import Loading from "./loading";
const RotationPage = () => {
const [rotationChampion, setRotationChampion] = useState<ChampionInfo[]>([]);
useEffect(() => {
const fetchData = async () => {
const rotationsChampion = await getChampionRotation();
console.log(rotationsChampion);
// 필터링된 로테이션 챔피언 목록을 상태에 저장
setRotationChampion(rotationsChampion);
};
fetchData();
}, []);
return (
<div id="championList" className="w-full bg-lol03 bg-fixed bg-center bg-no-repeat py-[60px] px-[15px]">
<div className="inner w-full max-w-[1440px] m-auto">
<h1
className="flex justify-center text-[60px] mt-[30px] mb-[40px] max-m:text-[30px]"
style=
>Rotation Champion</h1>
<ul className="list flex flex-wrap gap-[10px] w-full">
{rotationChampion.map((champ) => (
// <li key={champ.key}>
// <h2>{champ.name}</h2>
// <p>{champ.title}</p>
// </li>
<li key={champ.key} className="group relative w-[calc(25%-10px)] overflow-hidden max-n:w-[calc(33.333%-10px)] max-t:w-[calc(50%-10px)] max-m:w-[100%] rounded-[10px] shadow-custom">
<div className="champion-list-image w-full py-[30%]">
{champ.image && (
<Image
src={`${RIOT_BASE_URL}/cdn/img/champion/splash/${champ.id}_0.jpg`}
className="w-full h-full transition-all duration-500 ease-in-out transform group-hover:scale-110"
layout="fill"
alt={champ ? champ.name : "챔피언 이미지"}
/>
)}
</div>
<h2 className="absolute top-[5px] right-[5px] z-20 min-w-[120px] flex items-center justify-center p-[8px] font-medium bg-black bg-opacity-70 rounded-[10px] max-t:text-[14px]">{champ.name}</h2>
<div className="info absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-50 transition duration-500 ease-in-out group-hover:bg-opacity-0"></div>
<Link href={`/champions/${champ.id}`} className="absolute top-0 left-0 flex w-full h-full opacity-0 z-30">{champ.name} 상세페이지로 이동</Link>
</li>
))}
</ul>
</div>
</div>
);
};
export default RotationPage;
회고
어렵다… API 문서를 정리하지 않고 시작해서 그런것인지 모르겠는데, 데이터를 확인하고 렌더링하는 과정까지 생각보다 시간이 많이 걸렸다. 챔피언 데이터를 반환하는 함수를 생성하고 아이템까지는 쉽게 작업했으나, 로테이션 데이터를 받아왔을 때 내가 생각한 데이터가 아닌 숫자로된 배열로 데이터가 들어와져서 기존 챔피언 데이터에서 필터링 하는 과정과 타입을 지정하는 부분에서 시간이 정말 많이 소요되었다.
Keep - 현재 만족하고 있는 부분
어쨌든 성공함.. (시간이 많이 걸렸지만..)
Problem - 불편하게 느끼는 부분
너무 정신이 없다. API 요청에 대한 결과를 너무 효율적이지 못하게 확인하고 있는 것 같은 느낌..? 중간에 데이터를 다시 확인하고 싶을 때나 요청이 제대로 이루어지지 않았을 때 어디부터 확인하고 진행해야되는 지 스스로 체계가 안잡힌 것 같다.
Try - problem에 대한 해결책, 당장 실행 가능한 것
나만 그런건가 싶어서 다른 사람들은 어떻게 하는지 물어보았고, Thunder Client
와 Postman
을 사용해서 API를 관리하는걸 알게되었다. 사실 이전까지 Thunder Client 활용을 잘 못했는데, 같은 기수의 사람들의 경험을 듣고난 후 느낀것은 내가 정말 비효율적으로 작업하고 있었다는 것이었다. (확인이 필요할 때 마다 하나하나 확인해서 다시 url에 입력함…) Thunder Client
를 적극 활용해보면서 작업해보려 한다.