주특기 플러스주차 개인과제인 Riot API를 활용한 League of Legends 정보 앱을 만들었다.
TypeScript와 Next.js에 익숙하지 않아 트러블이 꽤 많이 발생했다.
발생한 트러블들과 해결과정을 기록하고자 한다!
앱 내에서 사용되는 이미지는 모두 외부 서비스에서 제공받는 이미지였다.
ex) https://ddragon.leagueoflegends.com/cdn/14.24.1/img/champion/${champion.image.full}
성능 최적화 및 CDN 캐싱 기능 등의 이점이 있는<Image>
태그를 사용하고, src를 외부 서비스 주소로 설정했으나 계속 오류가 발생했다.
Invalid src prop (URL) on `next/image`, hostname "example.com" is not configured under images in your `next.config.js`.
Next.js는 보안 및 성능 최적화를 위해 외부 이미지를 허용된 도메인에서만 로드할 수 있도록 제한한다.
images.domains 설정에 해당 도메인을 명시하지 않으면, Next.js는 해당 이미지를 로드하지 않고 위와 같은 오류를 출력한다.
next.config.js 파일에서, 해당 이미지가 호스팅된 도메인을 images.domains
배열에 추가한다.
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ["ddragon.leagueoflegends.com", "wallpapers.com"], // 허용할 외부 이미지 도메인 추가
},
};
export default nextConfig;
로컬 프로젝트의 /public
폴더 안에 있는 이미지는 위의 설정 없이도 사용 가능하지만, CDN에서 제공한다면 도메인도 반드시 추가해야 한다.
generateMetadata
와 컴포넌트 내부에서 같은 데이터를 가져오기 위해 getChampionDetailData
를 두 번 호출하고 있다.
두 번 호출로 인한 API 호출 비용이 증가될 것 같지만, 아직 방법을 찾지 못해 두 번 호출하도록 했다.
Next.js의 동작 방식을 찾아보니
generateMetadata
는 Next.js의 동작상 페이지 렌더링 이전에 반드시 호출되므로, 이 시점에서 모든 데이터를 불러와야 한다. 즉 페이지 렌더링과는 별도로 실행되고 페이지 컴포넌트와 완전히 독립적으로 동작하기 때문에 데이터를 공유할 수 있는 메커니즘이 없다는 것 같다.
import { Champion } from "@/app/types/Champion";
import { getChampionDetailData } from "@/utils/serverApi";
import Image from "next/image";
type Props = {
params: {
id: string;
};
};
export async function generateMetadata({ params }: Props) {
const champion: Champion | null = await getChampionDetailData(params.id);
return {
title: champion
? `${champion.name} - My Riot App`
: `${params.id} - My Riot App`,
};
}
const ChampionDetailwithId = async ({ params }: Props) => {
const champion: Champion | null = await getChampionDetailData(params.id);
if (!champion) {
return <div>챔피언 정보를 찾을 수 없습니다.</div>;
}
return (
<div className="pb-5">
....
</div>
);
};
export default ChampionDetailwithId;
다크모드/라이트모드 기능 구현을 위해 next-theme을 사용했는데, 이런 오류가 발생했다.
Unhandled Runtime Error
Error: Text content does not match server-rendered HTML.
See more info here: https://nextjs.org/docs/messages/react-hydration-error
Text content did not match. Server: "Dark Mode" Client: "Light Mode"
...
<_>
<TopNav>
<header>
<nav>
<ThemeToggle>
<button>
"Dark Mode"
"Light Mode"
서버에서 렌더링된 HTML과 클라이언트 측에서 렌더링된 HTML이 일치하지 않을 때 발생되는 에러였다.
다크모드/라이트 모드 전환을 위한 컴포넌트인 ThemeToggle
컴포넌트가 클라이언트 측에서만 동작하는 useTheme
훅을 사용하고 있었다.
Next.js에서는 기본적으로 SSR을 사용하고 있는데, useTheme
을 사용하여 다크 모드와 라이트 모드를 클라이언트에서 동적으로 변경하고 있기 때문에 서버에서 렌더링한 텍스트가 클라이언트에서 업데이트될 때 일치하지 않게 되는 것이다.
즉 상황이,
1. 서버에서 페이지를 렌더링할 때는 다크 모드인지 라이트 모드인지를 알 수 없기 때문에 기본적으로 "다크 모드"나 "라이트 모드" 중 하나로 설정되지 않은 상태에서 HTML을 렌더링
2. 이후 클라이언트가 로드되면, 클라이언트에서 useTheme 훅이 실행되면서 실제 테마 상태(dark나 light)를 확인하고 이를 바탕으로 버튼 텍스트를 업데이트 -> 💥불일치 발생
useTheme 훅은 클라이언트에서만 사용할 수 있는 기능이기 때문에, mounted 상태를 사용하여 클라이언트에서만 렌더링되도록 처리했다.
"use client";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
const ThemeToggle = () => {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
// 컴포넌트가 클라이언트에서만 렌더링되도록 설정
setMounted(true);
}, []);
if (!mounted) {
// 클라이언트에서만 렌더링하도록 대체
return null;
}
return (
<button
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="p-2 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded"
>
{theme === "dark" ? "Light Mode" : "Dark Mode"}
</button>
);
};
export default ThemeToggle;
"use client"
: 이 구문은 Next.js에게 이 컴포넌트가 클라이언트에서만 렌더링되어야 함을 명시useEffect
: 클라이언트에서 렌더링되었을 때만 mounted
상태를 true
로 설정하고, 그 후에 ThemeToggle
을 렌더링한다. 서버에서는 이 컴포넌트를 렌더링하지 않으므로 서버와 클라이언트의 렌더링이 일치하게 된다!이렇게 하면 서버에서 렌더링된 HTML과 클라이언트에서 렌더링된 HTML이 일치하게 되어 Hydration Error를 해결할 수 있다.
vercel 배포 중에 이런 오류가 발생했다.
Next.js 13부터, API 라우트를 정의할 때는 NextRequest와 NextResponse를 사용해야 하며, NextApiRequest는 사용할 수 없다.
기존 코드는 NextApiRequest를 사용하여 req의 type을 지정해줬다.
NextApiRequest, NextApiResponse는 Next.js 12에서 사용되던 API 라우트에서만 사용되었던 객체 타입이였다.
import { NextApiRequest } from "next";
...
export async function GET(req: NextApiRequest) { ...}
수정 후 NextRequest로 변경하였다.
import { NextRequest } from "next/server";
export async function GET(req: NextRequest) {...}
분명히 Environment Variables에 Key와 Value를 입력한 후 Deploy 했는데 계속
Error: RIOT_API_KEY is not defined
에러가 발생했다..
프로젝트 Deploy가 중간에 완료되지 못해도, 해당 프로젝트의 Project Settings를 설정할 수 있었다.
settings -> environment-variables 로 들어가서 다시 key를 설정했다.
이후 reDeploy를 했더니 문제 해결 !!
typescript와 Next.js 가 아직 익숙하지 않아서 간단한 기능들인데도 시간도 오래 걸리고 오류도 많이 발생했다. 이 프로젝트를 통해서 ISR, SSG, SSR, CSR 과 같은 렌더링 방식도 좀 더 감이 잡히고, Next.js의 동작 방식 등을 더 찾아보고 사용해 볼 수 있었다. 에러를 겪으며 이론으로만 배웠던 개념들이 좀 더 이해가 되기도 하였다.