:ledger: [2차] Next.js 리그오브레전드 정보 프로젝트

lol-main

:rocket: 베포 링크

:one: 프로젝트 설명

NextJS와 TypeScript을 사용하고 Riot API를 활용하여 리그오브레전드의 다양한 데이터를 조회하고 이를 기반으로 생성한 프로젝트 입니다.

:two: STACK

Next JS TypeScript React Query TailwindCSS JavaScript GitHub

:three: 프로젝트 구조

  • 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

:four: 작업 목록

:pushpin: 4-1) 아이템 리스트 페이지 설정

아이템 리스트 페이지에서는 ItemList 컴포넌트를 렌더링한다.

import ItemList from "@/app/_components/items/itemList"

const ItemListPage = () => <ItemList/>

export default ItemListPage

:pushpin: 4-2) 아이템 리스트 컴포넌트 설정

Riot API에서 가져온 아이템 데이터를 기반으로 아이템 목록을 화면에 렌더링한다.

  1. 렌더링 방식: SSG
    • 빌드 시점에 페이지를 정적으로 생성 (Next.js 기본 컴포넌트 렌더링 방식)
  2. 데이터를 가져오는 함수 생성
    • fetch를 사용해서, 최신 버전의 아이템 리스트를 반환하는 함수를 생성했다.
  3. 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

:pushpin: 4-3) 아이템 카드 컴포넌트 설정

챔피언의 이름,타이틀,이미지 등 화면에 표시하는 역할을 하는 컴포넌트이다.

  1. prop 타입 정의
    • ItemCardProps: 해당 컴포넌트가 받을 item prop 타입을 정의함
  2. 컴포넌트 타입 정의
    • React.FC: Function Component 타입의 줄임말로, React + Typescript 조합으로 개발할 때 사용하는 타입이다.
    • 함수형 컴포넌트 사용 시 타입 선언에 쓸 수 있도록 React에서 제공하는 타입.
  3. 전달받은 item 데이터를 기반으로 바인딩을 해주었다.
  4. 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

:pushpin: 4-4) Rotation Route handler 생성 및 서버액션 설정

  1. api > rotation > route.ts 파일 생성
    • 로테이션 챔피언 정보를 가져오기 위한 route handler를 생성해주었다.
  2. 아래의 코드 작업을 하면서 X-Riot-Token 값을 적용하지 않아서 데이터가 정상적으로 노출되지 않는 이슈가 생겼음
    • 처음 엔드포인트를 계속 확인해도 데이터가 노출되지 않아서 발제 문서를 다시 확인했고, 토큰값을 적용 후 확인해보니 정상적으로 확인이 되었음
    • localhost:3000/api/rotation 에서 확인함 (배열로 id 값 확인)
  3. 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;
};

:pushpin: 4-5) Rotation 페이지

해당 페이지는 use client를 사용해서 CSR 렌더링 방식으로 구현했다.

  1. useEffectfetch를 사용해서 로테이션 챔피언 데이터를 가져옴
  2. useState의 setter 사용해서 로테이션 챔피언 데이터로 상태를 업데이트함
  3. 업데이트된 상태를 통해 데이터를 바인딩해주었다.
"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;

:fire: 회고

어렵다… API 문서를 정리하지 않고 시작해서 그런것인지 모르겠는데, 데이터를 확인하고 렌더링하는 과정까지 생각보다 시간이 많이 걸렸다. 챔피언 데이터를 반환하는 함수를 생성하고 아이템까지는 쉽게 작업했으나, 로테이션 데이터를 받아왔을 때 내가 생각한 데이터가 아닌 숫자로된 배열로 데이터가 들어와져서 기존 챔피언 데이터에서 필터링 하는 과정과 타입을 지정하는 부분에서 시간이 정말 많이 소요되었다.

:pushpin: Keep - 현재 만족하고 있는 부분

어쨌든 성공함.. (시간이 많이 걸렸지만..)

:pushpin: Problem - 불편하게 느끼는 부분

너무 정신이 없다. API 요청에 대한 결과를 너무 효율적이지 못하게 확인하고 있는 것 같은 느낌..? 중간에 데이터를 다시 확인하고 싶을 때나 요청이 제대로 이루어지지 않았을 때 어디부터 확인하고 진행해야되는 지 스스로 체계가 안잡힌 것 같다.

:pushpin: Try - problem에 대한 해결책, 당장 실행 가능한 것

나만 그런건가 싶어서 다른 사람들은 어떻게 하는지 물어보았고, Thunder ClientPostman을 사용해서 API를 관리하는걸 알게되었다. 사실 이전까지 Thunder Client 활용을 잘 못했는데, 같은 기수의 사람들의 경험을 듣고난 후 느낀것은 내가 정말 비효율적으로 작업하고 있었다는 것이었다. (확인이 필요할 때 마다 하나하나 확인해서 다시 url에 입력함…) Thunder Client를 적극 활용해보면서 작업해보려 한다.