공식 문서를 읽으면서 몰랐던 부분이나, 중요하다고 생각하는 부분만 정리한 게시글입니다.

시작

# js로 시작
npx create-next-app@latest

# ts로 시작
npx create-next-app@latest --typescript

라우팅

/src/pages 혹은 /pages를 기준으로 내부적으로 자동 라우팅처리를 해줍니다.
추가적으로 동적 라우팅을 하고 싶다면 [id].tsx같은 파일을 생성하면 됩니다.

  • export default로 내보내는 파일을 페이지 컴포넌트로 인식합니다.

사전 렌더링

Next.js로 생성한 페이지 컴포넌트는 기본적으로 빌드 시 정적인 html파일로 생성한 후 요청 시 html파일을 제공하는 방식으로 구현됩니다.

이런 방식을 사용하는 이유는 SEO에서의 이점도 있고 사용자가 페이지에 들어왔을 때 DOM을 만드는 CSR보다는 미리 만들어진 html을 제공하는 게 UX측면에서 매우 좋기 때문입니다.

1. getStaticProps

해당 메서드는 미리 HTML을 만들지만 해당 페이지를 랜더링하는데 필요한 외부 데이터가 있을 경우에 사용합니다.

아래의 예시 코드를 보면 Post페이지는 게시글에 대한 정보를 외부에서 받아서 사용하기 때문에 미리 정적인 HTML을 만들어 두는 것이 불가능하지만 getStaticProps()를 이용해서 빌드 시점에 외부 데이터를 패치해서 정적인 HTML을 만들어서 사용하게 됩니다.

빌드 시점에 정적인 HTML을 만드는 것이므로 사용자에 의해 추가되거나 삭제되지 않는 페이지, 즉 변하지 않는 페이지에 사용하는 것이 적합합니다.

  • getStaticProps() 예시 코드
// /posts/index.tsx
import type { GetStaticProps, NextPage } from "next";

type PostsResponse = {
  id: number;
  title: string;
  body: string;
  userId: number;
};

const Post: NextPage<{ posts: PostsResponse[] }> = ({ posts }) => {
  return (
    <article>
      <ul className="flex flex-col space-y-4">
        {posts.map((post) => (
          <li key={post.id} className="bg-slate-400 p-4 rounded-lg">
            <h4 className="font-bold text-xl">{post.title}</h4>
            <p>{post.body}</p>
          </li>
        ))}
      </ul>
    </article>
  );
};

// export getStaticProps는 고정입니다.
export const getStaticProps: GetStaticProps = async () => {
  const posts = await fetch("https://jsonplaceholder.typicode.com/posts").then((res) => res.json());
  
  // >> 테스트용, npm run build를 실행했을 경우 로그가 찍히는 것을 확인할 수 있습니다.
  // 하지만 npm start로 실행한 페이지에 접근할 때는 실행되지 않습니다.
  console.group("strat!!");
  console.log("getStaticProps 5초 대기 시작");
  await new Promise((resolve) => setTimeout(resolve, 5000));
  console.log("getStaticProps 5초 대기 끝");
  console.groupEnd();

  return {
    props: {
      posts,
    },
    /**
     * false or 초단위 숫자값
     * 초단위 시간(10초라고 가정하고 설명함): 최초 요청 후 10초 동안은 기존 페이지를 보여줌
     * 그리고 10초 이후에 접근하면 현재 getStaticProps()를 실행한 새로운 결과값으로 대체된 정적 HTML을 랜더링함
     * 또한 그 이후에 접근하면 위 과정을 반복함
     *
     * false일 경우에는 외부 데이터가 바뀌던 말건 상관없이 빌드 시점에 만들었던 정적 HTML을 계속 사용함
     * 단, res.unstable_revalidate("/posts");를 사용할 경우 그때 바로 현재 getStaticProps()를 실행함
     */
    revalidate: false,
  };
};

export default Post;

빌드 시점에 외부 데이터를 패치하는 것이므로 npm run dev 즉 개발 모드로 실행하게 되면 빌드를 하는 시점이 없으므로 정확하게 테스트가 불가능합니다.
npm run build npm start를 이용해서 확인해보면 .next/server/pages의 어딘가에 완성된 HTML이 존재하는 것을 알 수 있습니다.

2. getServerSideProps

서버에 요청이 오면 그 시점에 서버측에서 데이터를 채운 완성된 HTML을 브라우저에게 제공하는 방식입니다.
완성된 HTML을 제공하므로 로딩을 할 필요가 없는 장점이 있지만, HTML이 완성되어야 브라우저로 전달하므로 요청에 대한 응답 시간이 길어질 수 있습니다.

  • getServerSideProps() 예시 코드
// /posts/index.tsx
import type { GetServerSideProps, NextPage } from "next";

type PostsResponse = {
  id: number;
  title: string;
  body: string;
  userId: number;
};

const Post: NextPage<{ posts: PostsResponse[] }> = ({ posts }) => {
  return (
    <article>
      <ul className="flex flex-col space-y-4">
        {posts.map((post) => (
          <li key={post.id} className="bg-slate-400 p-4 rounded-lg">
            <h4 className="font-bold text-xl">{post.title}</h4>
            <p>{post.body}</p>
          </li>
        ))}
      </ul>
    </article>
  );
};

// export getServerSideProps는 고정입니다.
export const getServerSideProps: GetServerSideProps = async () => {
  const posts = await fetch("https://jsonplaceholder.typicode.com/posts").then(
    (res) => res.json()
  );

   // >> 테스트용, npm run build를 실행했을 경우 로그가 찍히지 않습니다.
  // 하지만 npm start로 실행한 페이지에 접근할 때마다 실행됩니다.
  console.group("strat!!");
  console.log("getServerSideProps 5초 대기 시작");
  await new Promise((resolve) => setTimeout(resolve, 5000));
  console.log("getServerSideProps 5초 대기 끝");
  console.groupEnd();

  return {
    props: {
      posts,
    },
  };
};

export default Post;

3. getStaticPaths

다이나믹 라우팅처리를 한 페이지에서 정적 페이지를 생성할 경우 반드시 사용해야합니다.
( 즉, [id].tsx에서 getStaticProps()를 사용하는 경우 )

반드시 사용해야 하는 이유는 순차적으로 생각해보면 이해하기 쉽습니다.
1. 다이나믹 라우팅으로 처리한 페이지의 경우에는 이론상 0 ~ infinite개의 페이지를 접근할 수 있습니다.
2. 하지만 getStaticProps같은 경우에는 빌드 시점에 정적인 HTML을 모두 만들어냅니다.
3. 그렇다면 빌드 시점에 Next.js입장에서는 몇 개의 페이지를 미리 만들어야 할지 알 수 있는 방법이 없습니다.

따라서 0 ~ infinite의 페이지를 만들 수 있는 페이지에서는 몇 개의 페이지를 정적으로 생성할 것인지에 대한 내용을 Next.js에게 알려주는 매개체로 getStaticPaths()를 사용하는 것입니다.

  • getStaticPaths() 예시 코드
// /posts/[id].tsx
import type { GetStaticPaths, GetStaticProps, NextPage, GetStaticPathsContext, GetStaticPropsContext } from "next";
import { useRouter } from "next/router";

type PostsResponse = {
  id: number;
  title: string;
  body: string;
  userId: number;
};

const PostDetail: NextPage<{ post: PostsResponse }> = ({ post }) => {
  const router = useRouter();

  // fallback: true라면 데이터를 패치하고 HTML을 만드는 동안 아래 코드가 실행됨 ( isFallback이 true가 됨 )
  if (router.isFallback)
    return <div>대충 로딩중이라는 내용... (, 최초 접근만 로딩중... )</div>;

  return (
    <>
      <h4 className="font-bold text-xl">{post.title}</h4>
      <p>{post.body}</p>
    </>
  );
};

export const getStaticPaths: GetStaticPaths = async (context: GetStaticPathsContext) => {
  // 여기서 100개의 post를 불러옴 ( id는 1~100까지 )
  // 하지만 fallback을 테스트하기 위해서 10개만 잘라서 사용함
  const posts: PostsResponse[] = (
    await fetch("https://jsonplaceholder.typicode.com/posts").then((res) =>
      res.json()
    )
  ).slice(0, 10);

  // 지정된 리턴값 형식이 정해져있어서 정해진 형식으로 변환해서 리턴해줌
  return {
    paths: posts.map((post) => ({
      params: {
        id: post.id + "",
      },
    })),
    // true or false or "blocking"
    // true: params에 해당하는 않는 요청일 경우 router.isFallback을 true로 만들고 HTML 생성
    // false: params에 해당하지 않는 요청이면 접근 자체를 막음
    // "blocking": params에 해당하지 않는 요청이면 HTML만들 때까지 응답 없음
    fallback: true,
  };
};

// getStaticPaths()에서 반환한 params들이  각 getStaticProps()의 매개변수로 들어옴
export const getStaticProps: GetStaticProps = async (context: GetStaticPropsContext) => {
  // 여기 빌드 시 하나하나 페이지마다 실행하기 때문에 거의 각 페이지마다 2초씩 대기해야함...
  console.group("strat!!");
  console.log("getStaticProps 2초 대기 시작");
  await new Promise((resolve) => setTimeout(resolve, 2000));
  console.log("getStaticProps 2초 대기 끝");
  console.groupEnd();

  const postId = context.params?.id;
  const post: PostsResponse = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${postId}`
  ).then((res) => res.json());

  return {
    props: {
      post,
    },
  };
};

export default PostDetail;

직접 fallback을 바꿔보면서 rm -rf .next && npm run build && npm start를 실행해보면 위에 적은 설명이 무슨 의미인지 더 제대로 이해할 수 있습니다.

레이아웃

특정 페이지에 다른 레이아웃을 지정할 때 사용하는 방법이 있어서 추후에 사용해 본 뒤 정리함

이미지

Next.js에서는 <img />를 사용하기 보다는 Next.js에서 제공해주는 <Image />를 사용하면 여러가지 이점이 있습니다.

  • 자세한건 공식 문서 참고
  • 외부 이미지 사용 시 next.config.js 수정법
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  images: {
    // 아래 부분 수정 ( aws s3를 이용해서 이미지 업로드 후 사용했기 때문에 그에 맞는 url을 등록함 )
    // "https://"를 제외한 나머지 root url을 입력하면 됩니다.
    domains: ["blemarket.s3.ap-northeast-2.amazonaws.com"],
  },
};

module.exports = nextConfig;
  • 실제 사용 예시
import Image from "next/image";

const Photo = () => {
  return (
    <>
      <h1 className="">Photo</h1>
      <figure className="relative w-[500px] h-[400px] bg-black">
        <Image
          src="https://blemarket.s3.ap-northeast-2.amazonaws.com/images/KakaoTalk_20220121_194743221_02_1649217381483"
          layout="fill"
          className="object-contain"
          // 이미지의 질
          quality={50}
          // 스크롤 이전에 보여지는 이미지에게 true값을 부여하면 더 높은 순위로 로드됨
          priority={true}
        />
      </figure>
    </>
  );
};

export default Photo;

기본적으로 <Image />를 사용할 땐 src, layout, alt는 필수로 조건부로 widthheight를 사용해야합니다.
이때 이미지를 감싸는 상위태그에 position: relative를 부여하고 <Image />object-fit: 속성을 주는 것이 좋다고 들었습니다. ( 이유는 모름 )

환경변수

어떤 .env를 사용할 지 우선순위가 정해져있습니다.
.env.local > .env.development = .env.production > .env

또한 기본적으로 .envnode.js가 실행되는 곳 즉, 서버측에서만 사용이 가능하지만 프론트에서도 같이 변수로 공유해서 사용하고 싶다면 접두사로 NEXT_PUBLIC을 붙이면 됩니다.
하지만 프론트에서 사용가능하다는 의미는 사용자가 변수값에 접근할 수 있다는 의미니 중요한 키값이나 토큰같은 내용을 프론트와 공유해서 사용하면 안됩니다.

스크립트

import Script from 'next/script'

// 대충 어느 컴포넌트 내부
<Script src="대충 cdn" strategy="?" onLoad={() => console.log("스크립트 실행 이후에 실행")} onError={console.error} />
/**
* strategy에 들어갈 수 있는 값들
* 1. afterInteractive(기본값): javascript가 실행된 후 실행 ( 페이지완성 이후에 실행 )
* 2. beforeInteractive: javascript가 실행되기 이전에 실행 ( 페이지 완성 이전에 실행 )
* 3. lazyOnload: 페이지 완성 후 모든 리소스 가져온 후 실행
* 4. worker: 정식 기능 아님
*/
// "onLoad"는 "afterInteractive"와 "lazyOnload"일 경우에만 사용이 가능합니다.
// "onError"은 스크립트가 로드되지 않은 경우에 실행

나중에

On-Demand Revalidation?

https://nextjs.org/docs/routing/introduction

0개의 댓글