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

lol-main

:rocket: 베포 링크

:one: 프로젝트 설명

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

:two: STACK

Next JS TypeScript React Query TailwindCSS JavaScript GitHub

:three: 프로젝트 구조

  • 1차에선 프로젝트 초기 세팅 및 API 세팅을 했습니다.
전체 프로젝트 구조 📦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) app > _components

공통 컴포넌트 디렉토리, 초기 세팅 및 1차 작업에서는 챔피언 위주로 작업해서 ItemList 추가할 예정이다.

  • ChampionList.tsx: 챔피언 리스트
  • ChampionCard.tsx: 챔피언 리스트에 출력될 챔피언 카드
  • ItemList.tsx: 아이템 리스트
  • ItemCard.tsx: 아이템 카드

:pushpin: 4-2) app > api

api key값과 챔피언 로테이션 라우터 핸들러를 세팅해줬다.

:pushpin: 4-3) app > utils

초기 api 세팅 디렉토리, api 관련 세팅을 해줬다.

  • serverApi.ts: API 최신 버전, 아이템, 챔피언 최신 정보를 가져오고 해당 버전에 맞는 데이터를 요청하여 반환함

:pushpin: 4-4) app > champions

챔피언 리스트, 챔피언 상세페이지, 해당 페이지에서 최신 챔피언 리스트를 조회할 수 있으며, 클릭해서 상세 정보를 확인할 수 있다.

:pushpin: 4-5) app > items

아이템 리스트, 아이템 상세페이지, 해당 페이지에서 리그오브레전드의 아이템 리스트를 확인할 수 있다.

:pushpin: 4-6) app > rotation

매주 무료로 플레이할 수 있는 챔피언들의 목록을 확인할 수 있다.

:pushpin: 4-7) app > layout.tsx

공통 헤더 및 푸터 를 생성 후 헤더에 챔피언,아이템,로테이션 메뉴를 생성

:pushpin: 4-8) app > types

이번 프로젝트에서 사용할 정보를 정의한 타입 인터페이스다.

:five: 작업 목록

:pushpin: 5-1) 환경변수 파일 생성 및 세팅

next.js로 프로젝트 생성 후 처음으로 한 작업이다.

  1. .env 환경 변수 파일 생성
    • 해당 파일에서 자주 사용될 api key값과 base url을 정의해주었다.
  2. app > api > apiKey.ts 파일을 생성
    • 해당 파일에서 RIOT_API_KEY, RIOT_BASE_URL 변수를 생성 후 export 해줌
export const RIOT_API_KEY = process.env.NEXT_PUBLIC_RIOT_API_KEY;
export const RIOT_BASE_URL = process.env.NEXT_PUBLIC_RIOT_URL;

:pushpin: 5-2) 레이아웃 설정

두 번째로 레이아웃을 설정해주었다.

  1. header, footer 생성
  2. 이번 프로젝트에서 사용하는 페이지 Link 설정
    • 작업하면서 편하게 페이지를 확인할 수 있게 미리 설정해주었다.
    • 초기 세팅 이후 스타일 및 추가 컨텐즈 적용 예정
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import Link from "next/link";

const geistSans = localFont({
  src: "./fonts/GeistVF.woff",
  variable: "--font-geist-sans",
  weight: "100 900",
});
const geistMono = localFont({
  src: "./fonts/GeistMonoVF.woff",
  variable: "--font-geist-mono",
  weight: "100 900",
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">      
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <header id="header">
          <div className="inner">
            <div id="logo">
              <Link href="/"></Link>
            </div>            
            <nav>
              <ul>
                <li><Link href="/rotation">Rotation</Link></li>
                <li><Link href="/champions">Champions</Link></li>
                <li><Link href="/items">Items</Link></li>
              </ul>
            </nav>
          </div>
        </header>
        {children}
        <footer id="footer">
          <div className="inner">
            푸터
          </div>
        </footer>
      </body>
    </html>
  );
}

:pushpin: 5-3) 데이터 확인 및 Type 정의

처음 데이터를 어떻게 확인해야 할지 고민하던 중 같은 조원의 도움으로 API 정리가 잘 되어있는 블로그 - 롤 API 활용을 위한 정보를 알게되었다. 해당 블로그에서 최신 버전의 값을 매칭해서 데이터를 확인 후 타입을 정의해주었다.

  1. 블로그에서 챔피언, 아이템 데이터를 확인
  2. app > types 폴더 생성 및 각 타입의 ts 파일 생성
    • Champion.ts
    • ChampionRotation.ts
    • Item.ts
  3. 사용할 데이터의 타입을 적용해주었다.
// app > types > Item.ts

export interface ItemType {
  name: string;
  description: string;
  plaintext: string;
  image: {
    full: string;
    sprite: string;
  };
  gold: {
    total: number;
  };

}

:pushpin: 5-4) 최신 버전 함수 생성

  • 이번 프로젝트에서 가장 많이 재사용될 최신 버전을 반환하는 함수를 만들어 주었다.
  • 해당 json을 들어가면, 배열로 모든 버전을 확인할 수 있으며 가장 최신 버전을 가져오기 위해 배열의 첫 번째 인덱스를 반환해주었다.
// app > utils > serverApi.ts
export const getLatestVersionUrl = async () => {
  const latestVersions = `${RIOT_BASE_URL}/api/versions.json`;
  const res = await fetch(latestVersions);
  const version: string[] = await res.json();
  const latestVersion = version[0];
  return latestVersion;
}

:pushpin: 5-5) 챔피언 리스트 페이지 설정

챔피언 리스트 페이지에서는 ChampionList 비동기 컴포넌트를 정의하고 렌더링한다.

import ChampionList from "../_components/champions/ChampionList"

const ChampionPage = async () => <ChampionList/>

export default ChampionPage

:pushpin: 5-6) 챔피언 리스트 컴포넌트 설정

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

  1. 렌더링 방식: ISR
    • 재검증 시간(revalidate)은 하루(86400초)로 정의한다.
  2. 챔피언 데이터를 가져오는 함수 생성
    • fetch를 사용해서, 최신 버전의 챔피언 리스트를 반환하는 함수를 생성했다.
  3. props로 데이터 전달
    • 해당 데이터를 ChampionCard 컴포넌트로 전달해줌
// app > utils > serverApi.ts
export const getChampions = async () => {
  try {
    const latestVersion = await getLatestVersionUrl();
    const res = await fetch(`${RIOT_BASE_URL}/cdn/${latestVersion}/data/ko_KR/champion.json`, {
      next: {
        revalidate: 86400,
      }
    });
    const data = await res.json();
    const championList: ChampionInfo[] = Object.values(data.data); 
    return championList;
  } catch (error) {
    console.error('챔피언 데이터 가져오기 오류:', error);
    throw error; // 오류를 다시 던져 호출자에게 알림
  }
};

// app > _components > ChampionList
import { getChampions } from "@/app/utils/serverApi";
import ChampionCard from "./ChampionCard";

const ChampionList = async () => {
  const champions = await getChampions();

  return (
    <div id="championList" className="flex flex-wrap">
      {
        champions.map(champion => {
          return (
            <ChampionCard 
              key={champion.id} 
              champion={champion} // champion prop 전달
            />
          );
        })
      }
    </div>
  );
}

export default ChampionList;

:pushpin: 5-7) 챔피언 카드

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

  1. prop 타입 정의
    • ChampionCardProps: 해당 컴포넌트가 받을 champion prop 타입을 정의함
  2. 컴포넌트 타입 정의
    • React.FC: Function Component 타입의 줄임말로, React + Typescript 조합으로 개발할 때 사용하는 타입이다.
    • 함수형 컴포넌트 사용 시 타입 선언에 쓸 수 있도록 React에서 제공하는 타입.
// app > _components > ChampionList
import { RIOT_BASE_URL } from "@/app/api/apiKey";
import { ChampionInfo } from "@/app/types/Champion";
import Image from "next/image";
import Link from "next/link";

type ChampionCardProps = {
  champion: ChampionInfo; // champion prop을 정의
};

const ChampionCard: React.FC<ChampionCardProps> = ({ champion }) => {
  
  return (
    <div>
      <h2>{champion.name}</h2>
      <p>{champion.title}</p>
      {champion.image && (
        <Image 
          src={`${RIOT_BASE_URL}/cdn/img/champion/splash/${champion.id}_0.jpg`} 
          width={256}
          height={151}
          alt={champion ? champion.name : "챔피언 이미지"} 
        />
      )}
      <Link href={`/champions/${champion.id}`}>상세페이지</Link>
    </div>
  );
};

export default ChampionCard;

:fire: 회고

처음 진행하는 타입스크립트 + Next.js 프로젝트.. 아직 너무 손에 안익어서 세팅만 몇 시간이 걸린지 모르겠다. 이번 개인과제 기간은 널널하니 느리더라도 완료할 수 있도록 노력해보려 한다.

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

API를 세팅하는 과정에서 어려움이 있었지만 화면에 최신 챔피언 리스트를 잘 뿌려준 것

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

타입스크립트가 생각보다 많이 어렵게 느껴진다. 처음 데이터를 확인하고 세팅했지만, 함수를 통해 반환하는 과정에서 데이터가 어떻게 반환하는지 잘 확인해야 했으며, 익숙하지 않아서 실수가 많았다.

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

Thunder Client를 사용해서, 데이터를 잘 확인하고 타입을 작성해주는 것이 현재로선 최선의 해결책..일까..? TypeScript와 Next.js를 공부하고 처음 진행하는 프로젝트라 어려움이 많지만 어쩔 수 없다. 많이 사용해보며 익숙해지도록 노력해야겠다.