내일배움캠프 React_7기 TIL - 39. 주특기 플러스주차 과제 트러블슈팅

·2024년 12월 18일
0

주특기 플러스주차 개인과제인 Riot API를 활용한 League of Legends 정보 앱을 만들었다.
TypeScript와 Next.js에 익숙하지 않아 트러블이 꽤 많이 발생했다.
발생한 트러블들과 해결과정을 기록하고자 한다!

Image Domain 설정

앱 내에서 사용되는 이미지는 모두 외부 서비스에서 제공받는 이미지였다.
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에서 제공한다면 도메인도 반드시 추가해야 한다.

getChampionDetailData() 2번 호출 (미해결)

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;

React Hydration Error (next-theme)

다크모드/라이트모드 기능 구현을 위해 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;
  1. "use client": 이 구문은 Next.js에게 이 컴포넌트가 클라이언트에서만 렌더링되어야 함을 명시
  2. useEffect: 클라이언트에서 렌더링되었을 때만 mounted 상태를 true로 설정하고, 그 후에 ThemeToggle을 렌더링한다. 서버에서는 이 컴포넌트를 렌더링하지 않으므로 서버와 클라이언트의 렌더링이 일치하게 된다!

이렇게 하면 서버에서 렌더링된 HTML과 클라이언트에서 렌더링된 HTML이 일치하게 되어 Hydration Error를 해결할 수 있다.

API 라우트에 잘못된 GET 핸들러 사용?


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) {...}

vercel API_KEY is not defined

분명히 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의 동작 방식 등을 더 찾아보고 사용해 볼 수 있었다. 에러를 겪으며 이론으로만 배웠던 개념들이 좀 더 이해가 되기도 하였다.

profile
내배캠 React_7기 이수중

0개의 댓글

관련 채용 정보