NextJS 앱 최적화하기

·2024년 3월 29일
0

NextJS

목록 보기
11/26
post-thumbnail

🔗 레파지토리에서 보기

📌 NextJS 앱 최적화하기 - head

📖 <head> 메타데이터의 필요성 분석하기

기존 프로젝트를 봤을 때, <head>의 메타데이터가 부족하다는 것을 느낄 수 있다. 메타데이터가 있어야 탭에 표시될 제목과 같이 세부적인 요소를 반영함으로써 사용자의 경험의 질을 원하는 정도까지 끌어올릴 수 있다. 또한 검색 엔진에도 메타데이터는 필수적인 부분이다. Google 크롤러 같은 검색 엔진 크롤러가 확인하는 게 메타데이터에 설정된 제목과 설명이기 때문이다.


📖 <head> 콘텐츠 구성하기

  • 특수 Head 태그 사이에 <head> 섹션이라는 HTML 요소를 추가하고 나면 Next.js가 알아서 해당 요소를 <Head> 섹션에 추가할 것이다.
  • Head 태그 사이에 보통 head 태그에 들어갈만한 어떤 HTML 요소든 추가가 가능하다.
  • <meta name="description"> : 검색엔진에 필요한 속성. 검색 결과를 출력할 때 같이 출력되는 설명문이 된다.
// pages/index.js
import Head from "next/head";

import { getFeaturedEvents } from "../helper/api-util.js";
import EventList from "../components/events/event-list";

function HomePage({ featuredEvents }) {
  return (
    <div>
      <Head>
        <title>NextJS Events</title>
        <meta
          name="description"
          content="Find a lot of great events that allow you to evolve..."
        />
      </Head>
      <EventList items={featuredEvents} />
    </div>
  );
}

export async function getStaticProps() {
  const featuredEvents = await getFeaturedEvents();

  if (!featuredEvents) {
    return { notFound: true };
  }

  return {
    props: {
      featuredEvents: featuredEvents,
    },
    revalidate: 1800, // 30분에 한번씩 페이지 재생성.
  };
}

export default HomePage;

📖 동적 <head> 콘텐츠 추가하기

// pages/events/[eventId].js
import Head from "next/head";
import { Fragment } from "react";

import { getEventById, getFeaturedEvents } from "../../helper/api-util";
import EventSummary from "../../components/event-detail/event-summary";
import EventLogistics from "../../components/event-detail/event-logistics";
import EventContent from "../../components/event-detail/event-content";
import ErrorAlert from "../../components/ui/error-alert";

function EventDetailPage({ event }) {
  if (!event) {
    return (
      <div className="center">
        <p>Loading...</p>
      </div>
    );
  }

  return (
    <Fragment>
    {/* 동적 head */}
      <Head>
        <title>{event.title}</title>
        <meta name="description" content={event.description} />
      </Head>
      <EventSummary title={event.title} />
      <EventLogistics
        date={event.date}
        address={event.location}
        image={event.image}
        imageAlt={event.title}
      />
      <EventContent>
        <p>{event.description}</p>
      </EventContent>
    </Fragment>
  );
}

export default EventDetailPage


// pages/events/[...slug].js
import Head from "next/head";
import { Fragment, useEffect, useState } from "react";

import useSWR from "swr";
import { useRouter } from "next/router";
import EventList from "../../components/events/event-list";
import ResultsTitle from "../../components/events/results-title";
import Button from "../../components/ui/button";
import ErrorAlert from "../../components/ui/error-alert";

function FilteredEventsPage() {
  const router = useRouter();
  const [loadedEvents, setLoadedEvents] = useState();

  const filterData = router.query.slug;

  const { data, error } = useSWR(
    "https://nextjs-course-demo-846e7-default-rtdb.firebaseio.com/events.json",
    (url) => fetch(url).then((res) => res.json())
  );

  useEffect(() => {
    if (data) {
      const events = [];

      for (const key in data) {
        events.push({
          id: key,
          ...data[key],
        });
      }

      setLoadedEvents(events);
    }
  }, [data]);

  if (!loadedEvents) {
    return <p className="center">Loading...</p>;
  }

  const filteredYear = +filterData[0];
  const filteredMonth = +filterData[1];

  if (
    isNaN(filteredYear) ||
    isNaN(filteredMonth) ||
    filteredYear > 2030 ||
    filteredYear < 2021 ||
    filteredMonth < 1 ||
    filteredMonth > 12 ||
    error
  ) {
    return (
      <Fragment>
        <ErrorAlert>
          <p>Invalid filter. Please adjust your values!</p>
        </ErrorAlert>
        <div className="center">
          <Button link="/events">Show All Events</Button>
        </div>
      </Fragment>
    );
  }

  let filteredEvents = loadedEvents.filter((event) => {
    const eventDate = new Date(event.date);
    return (
      eventDate.getFullYear() === filteredYear &&
      eventDate.getMonth() === filteredMonth - 1
    );
  });

  if (!filteredEvents || filteredEvents.length === 0) {
    return (
      <Fragment>
        <ErrorAlert>
          <p>No events found for the chosen filter!</p>
        </ErrorAlert>
        <div className="center">
          <Button link="/events">Show All Events</Button>
        </div>
      </Fragment>
    );
  }

  const date = new Date(filteredYear, filteredMonth - 1);

  return (
    <Fragment>
    {/* 동적 head */}
      <Head>
        <title>Filtered Events</title>
        <meta
          name="description"
          content={`All events for ${filteredMonth}/${filteredYear}`}
        />
      </Head>
      <ResultsTitle date={date} />
      <EventList items={filteredEvents} />
    </Fragment>
  );
}


export default FilteredEventsPage;

📖 컴포넌트 내에서 논리 재사용하기

  • [...slug].js에 if 문에 따라 head를 재사용하도록 할 수 있다.
  • page header를 따로 상수/변수를 통해 설정 후 JSX로 리턴되는 부분에 동적으로 입력 가능하다.
// pages/events/[...slug].js
import Head from "next/head";
import { Fragment, useEffect, useState } from "react";

import useSWR from "swr";
import { useRouter } from "next/router";
import EventList from "../../components/events/event-list";
import ResultsTitle from "../../components/events/results-title";
import Button from "../../components/ui/button";
import ErrorAlert from "../../components/ui/error-alert";

function FilteredEventsPage() {
  const router = useRouter();
  const [loadedEvents, setLoadedEvents] = useState();

  const filterData = router.query.slug;

  const { data, error } = useSWR(
    "https://nextjs-course-demo-846e7-default-rtdb.firebaseio.com/events.json",
    (url) => fetch(url).then((res) => res.json())
  );

  useEffect(() => {
    if (data) {
      const events = [];

      for (const key in data) {
        events.push({
          id: key,
          ...data[key],
        });
      }

      setLoadedEvents(events);
    }
  }, [data]);

  let pageHeadData = (
    <Head>
      <title>Filtered Events</title>
      <meta name="description" content={`A List of filtered events.`} />
    </Head>
  );

  if (!loadedEvents) {
    return (
      <>
        {pageHeadData}
        <p className="center">Loading...</p>
      </>
    );
  }

  const filteredYear = +filterData[0];
  const filteredMonth = +filterData[1];

  pageHeadData = (
    <Head>
      <title>Filtered Events</title>
      <meta
        name="description"
        content={`All events for ${filteredMonth}/${filteredYear}`}
      />
    </Head>
  );

  if (
    isNaN(filteredYear) ||
    isNaN(filteredMonth) ||
    filteredYear > 2030 ||
    filteredYear < 2021 ||
    filteredMonth < 1 ||
    filteredMonth > 12 ||
    error
  ) {
    return (
      <Fragment>
        {pageHeadData}
        <ErrorAlert>
          <p>Invalid filter. Please adjust your values!</p>
        </ErrorAlert>
        <div className="center">
          <Button link="/events">Show All Events</Button>
        </div>
      </Fragment>
    );
  }

  let filteredEvents = loadedEvents.filter((event) => {
    const eventDate = new Date(event.date);
    return (
      eventDate.getFullYear() === filteredYear &&
      eventDate.getMonth() === filteredMonth - 1
    );
  });

  if (!filteredEvents || filteredEvents.length === 0) {
    return (
      <Fragment>
        {pageHeadData}
        <ErrorAlert>
          <p>No events found for the chosen filter!</p>
        </ErrorAlert>
        <div className="center">
          <Button link="/events">Show All Events</Button>
        </div>
      </Fragment>
    );
  }

  const date = new Date(filteredYear, filteredMonth - 1);

  return (
    <Fragment>
      {pageHeadData}
      <ResultsTitle date={date} />
      <EventList items={filteredEvents} />
    </Fragment>
  );
}

export default FilteredEventsPage;

📖 _app.js 파일 작업하기

  • _app.js 파일은 표시되는 모든 페이지에서 렌더링되는 root 어플리케이션 컴포넌트이다.
  • <meta name="viewport" content="initial-scale=1.0, width=device-width" /> : 반응형 페이지의 스케일을 적정값으로 만드는 데 자주 쓰이는 태그이다. 따라서 해당 태그는 일부 페이지가 아닌 모든 페이지에 적용이 되야한다.
import Head from "next/head";

import Layout from "../components/layout/layout";
import "../styles/globals.css";

function MyApp({ Component, pageProps }) {
  return (
    <Layout>
      <Head>
        <meta name="viewport" content="initial-scale=1.0, width=device-width" />
      </Head>
      <Component {...pageProps} />
    </Layout>
  );
}

export default MyApp;

📖 <head> 콘텐츠 병합하기

  • Next.js는 여러 <Head> 요소를 알아서 병합해준다.
  • 각각의 컴포넌트 내부에 여러 <Head> 섹션이 있더라도 Next.js가 전부 병합해준다.
  • 또한 Next.js는 <Head> 작성 중 발생하는 충돌도 자동적으로 해결해준다. 만약 같은 요소가 여러 개 있다면 가장 최근의 요소만 반영하는 식으로 해결.

페이지 컴포넌트는 어플리케이션 컴포넌트보다 나중에 렌더링되므로 뒤에 렌더링되는 페이지 컴포넌트의 <Head> 섹션이 우선 표시된다.


📖 _document.js 파일

  • _app.js만이 전반적인 어플리케이션 설정을 할 수 있는 것은 아니다.
  • _document.js도 _app.js처럼 pages 아래에 생성해야만 한다.

_app.js는 어플리케이션 셸(Shell)이다. 따라서 _app.js는 HTML 문서의 body 섹션 속 루트 컴포넌트라고 생각하면 된다.
_document.js는 전체 HTML 문서를 커스터마이징할 수 있게 해준다. (HTML 문서를 구성하는 모든 요소)

// pages/_document.js
import Document, { Html, Head, Main, NextScript } from "next/document";
class MyDocument extends Document {
  render() {
    return (
      <Html>
        <Head></Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;
  • document는 클래스 기반이며 next/document를 확장하여 작성되어야한다.
  • Head 컴포넌트는 next/head에서 임포트하는 Head 컴포넌트와는 다르다.
    • next/head의 Head는 렌더링된 페이지의 Head 콘텐츠를 조정하기 위해서 JSX 코드 어디서나 사용된다.
    • next/document의 Head는 _document에 구축할 특수한 문서 컴포넌트에만 사용된다.

위의 클래스가 오버라이드하지 않는 이상, 문서가 가지는 기본 구조가 된다. 만약 오버라이드하려면 해당 구조를 다시 만들어야한다.


// pages/_document.js
import Document, { Html, Head, Main, NextScript } from "next/document";
class MyDocument extends Document {
  render() {
    return (
      <Html lang="en">
        <Head></Head>
        <body>
          <div id="overlays" />
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;
  • <div id="overlays" /> : HTML 콘텐츠를 어플리케이션 컴포넌트 트리 외부에 추가할 수 있게 해준다. (ex. React의 portal)

📌 이미지 최적화하기

📖 이미지 자세히 살펴보기

  • 이미지가 최적화되지 않으면 원본 크기 그대로 페칭된다. 또한 이미지 유형도 원본과 같은 유형으로 불러오게 되는데 이는 브라우저가 지원하든, 지원하지 않든 상관 없이 불러온다.

📖 Next Image 컴포넌트 & 기능을 통해 이미지 최적화하기

  • <Image> 컴포넌트를 사용하면 Next.js에서 여러버전의 이미지를 요청이 들어올 때마다 바로 생성해주는데 각 운영 체제와 장치 크기에 최적화되도록 한다.
  • 그리고 그러한 이미지들은 캐시에 저장되어 유사한 장치에 요청이 들어왔을 때 활용할 수 있다.
// components/events/event-item.js
<Image src={"/" + image} alt={title} width={240} height={160} />
  • width height : 원본 크기의 이미지 크기가 아니라 페이지에 표시하고자 하는 크기를 말한다.

<Image> 컴포넌트를 사용하면 품질은 낮추되 이미지에는 영향을 미치지 않고 이미지의 용량도 줄어들게 되며 이미지의 유형도 Chrome에 최적화된 WebP로 변한다.

이렇게 생성된 이미지는 .next 폴더에 저장된다. 이러한 이미지들은 필요할 때마다 최적화되어 생성되는 것으로 미리 생성해 두는 것이 아니라 요청이 있을 때 생성하고 저장해 두었다가 나중에 유사한 기기에서 요청이 들어왔을 때 바로 이미지를 꺼내서 렌더링하는 방식이다.

  • 또한 기본 이미지가 지연 로딩(lazy load)되어 보이지 않는 상태에서는 Next.js가 다운로드하지 않는다. 필요할 때에만 로딩하기 때문에 발생하는 요청 수를 더욱 줄일 수 있고 그 페이지에 할당되는 대역폭도 줄일 수 있다. → 필요 없는 것을 로딩하지 않으니 도움이 되는 기능이라고 할 수 있다.

📖 next/image 문서 살펴보기

🔗 Image

  • 이미지에 대한 더 세부적인 프로퍼티가 있다.

0개의 댓글

관련 채용 정보