오마카세 급 Next.js 맛보기

soryeongk·2022년 1월 26일
2

1. 늘 그렇듯 설치 먼저 하실게요~

  • Node.js가 설치되어 있지 않다면 설치해주세요!
  • 이미 설치가 된 경우라면 터미널에 node -v를 입력하여 버전을 확인해주세요! 10.13 이상에서 사용가능합니다.

💡 If you are on Windows, we recommend downloading Git for Windows and use Git Bash that comes with it, which supports the UNIX-specific commands in this tutorial. Windows Subsystem for Linux (WSL) is another option.
윈도우 사용자는 UNIX 관련 명령어를 지원하는 Git Bash 사용을 권장합니다.

이제 원하는 프로젝트 공간으로 이동해서 yarn create next-app을 입력해주세요!

typescript를 사용하는 경우에는 yarn create next-app --typescript를 입력해주시면 됩니다.

설치 후 나오는 안내에서 볼 수 있듯이 CRA에서는 yarn start, CNA에서는 yarn dev를 입력합니다! 그리고 yarn dev를 입력한다고 해서 브라우저가 자동으로 실행되지는 않습니다. 직접 http://localhost:3000에 접속해야합니다.

  • dev - [next dev](https://nextjs.org/docs/api-reference/cli#development) : Next.js 개발 모드로 실행하는 명령어입니다.
  • build - [next build](https://nextjs.org/docs/api-reference/cli#build) : 프로덕션 사용을 위한 앱 빌드 실행 명령어입니다.
    • yarn build 화면 어떤 과정으로 build가 되는지 보기 좋게 설명해줍니다! page 구조와 함께 초기화해둔 페이지들이 SSG와 SSR 중 무엇을 사용했는지도 볼 수 있습니다.
  • start - [next start](https://nextjs.org/docs/api-reference/cli#production) : 프로덕션 서버 실행을 위한 명령어입니다. next build가 먼저 수행되어야 합니다.
    • yarn start 화면 서버를 실행하고 yarn dev를 하는 것과 같습니다. yarn build 후 yarn start를 하면 이미 서버의 초기값을 불러둔 상태라서 아주 빠르게 숑숑 움직이는 것을 볼 수 있습니다.
  • lint - [next lint](https://nextjs.org/docs/api-reference/cli#lint) : 내장된 ESLint를 실행하는 명령어입니다.

💡 다양한 옵션들

  • -ts, --typescript: project 내에서 typescript를 사용할 수 있게 합니다.
  • -e, --example [name]|[github-url]: githubURL로 예시 코드를 사용할 수 있게 합니다.
  • -example-path [path-to-example]: 드물게 GitHub URL에 슬래쉬 / 가 포함된 경우에 사용합니다.

엇 CRA보다 훨씬 빠른 것 같네요!
⇒ 넹넹구리. next js는 zero dependencies이기 때문이죠!

2. pages

우리를 괴롭히던 routing이 Next.js에서는 조금 사용이 편할지도 모르겠습니다 :)

아 물론 저와 제 친구가 정리한 react-router-dom v6(클릭)를 공부하신다면 괴롭힘 당하지 않으셨을지도 ㅋ

Next.js는 페이지 개념을 기반으로 구축되었습니다. 페이지는 pages디렉토리의 .js, .jsx, .ts, .tsx파일에서 내보낸 React 구성 요소 입니다.

페이지는 파일 이름에 따라 경로와 연결됩니다. 예를 들어 pages/soryeongk.js/soryeongk에 mapping이 됩니다. 파일 이름으로 동적 경로 매개변수를 추가할 수도 있습니다.

2-1. Pages with Dynamic Routes

Next.js는 다이나믹 라우트를 지원합니다.

예를 들어 상품 id별로 각기 다른 페이지를 만들어야 한다고 가정해보겠습니다. 기존 CRA에서의 라우팅 방식을 생각해보면 Router.js 파일을 다음과 같이 작성할 것입니다.

// Router.js

import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Web from "../Pages/Web";
import WebPost from "../Pages/WebPost";

const Router = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="web/*" element={<Web />}>
          <Route path=":id" element={<WebPost />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
};

export default Router;
// Web.js

import React from "react";
import { Link, Routes, Route, Outlet } from "react-router-dom";
import WebPost from "./WebPost";

const Web = () => {
  return (
    <div>
      <h1>This is Web</h1>
      <ul>
        <li>
          <Link to="1">Post #1</Link>
        </li>
        <li>
          <Link to="2">Post #2</Link>
        </li>
        <li>
          <Link to="3">Post #3</Link>
        </li>
        <li>
          <Link to="4">Post #4</Link>
        </li>
      </ul>

      <Outlet />
    </div>
  );
};

export default Web;

export default Web;

// --------------------------------------------------------------------------------
// WebPost.js

import React from "react";

const WebPost = () => {
  return <div>This is 포스트</div>;
};

export default WebPost;

Next.js는 조금 더 간단합니다.

pages 폴더 내에 products폴더를 만들고, 그 안에 [id].js 이름의 파일을 만듭니다. 그럼 products/1 products/2 등의 방식으로 해당 페이지에 접속할 수 있게 됩니다.

// index.js => 가장 첫 페이지에서 메뉴를 보여주고 싶어서 여기에 했음
import Head from "next/head";
import Link from "next/link";
import styles from "../styles/Home.module.css";

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <header>header</header>
      <main>
        <h1>This is Web</h1>
        <ul>
          <li>
            <Link href="products/1">Post #1</Link>
          </li>
          <li>
            <Link href="products/2">Post #2</Link>
          </li>
          <li>
            <Link href="products/3">Post #3</Link>
          </li>
          <li>
            <Link href="products/4">Post #4</Link>
          </li>
        </ul>
      </main>
      <footer>footer</footer>
    </div>
  );
}
// products/[id].js
const WebPost = () => {
  return <div>This is WebPost</div>;
};

export default WebPost;

Next.js 라우터에서도 SPA 페이지 간의 client-side 라우트 전환을 지원하고 있습니다. Next에서 조금 달라지는 것이 있다면 리액트 컴포넌트 중 Link라는 것을 제공합니다.

import Link from "next/link";를 입력하고 사용할 떄는 <Link href=""></Link> 로 사용하시면 됩니다.

Dynamic Routes의 원리를 생각해보자!

Next.js에서는 [param]의 형태로 동적 라우트가 가능하게 합니다.

import { useRouter } from 'next/router'

const Post = () => {
  const router = useRouter()
  const { pageId } = router.query

  return <p>Post: {pageId }</p>
}

export default Post

매치된 경로 파라미터는 query 파라미터를 통해 페이지로 전송되고 다른 query 파라미터와 병합됩니다. 예를 들어 봅시다.

  • /post/blah로 라우트된 페이지는 { "pageId" : "blah" }의 query를 갖습니다.
  • /post/blah?name=soryeongk라면, { "name" : "soryeongk", "pageId" : "blah"}의 query를 갖습니다.
  • /post/blah?pageId=314의 query는 그대로 { "pageId" : "bla" }입니다.
  • Multiple Dynamic Route도 같은 방식으로 가능합니다. /pages/post/[pageId]/[comment].js 방식으로 라우팅되는 /post/blah/H-i의 query는 { "pageId" : "blah", "comment" : "H-i" }입니다.

Client-side navigation 동적 라우트는 next/link로 동작합니다. 위의 예시들은 다음과 같이 활용될 수 있습니다.

import Link from 'next/link'

function Home() {
  return (
    <ul>
      <li>
        <Link href="/post/blah">
          <a>Go to pages/post/[pageId].js</a>
        </Link>
      </li>
      <li>
        <Link href="/post/blah?name=soryeongk">
          <a>Also goes to pages/post/[pageId].js</a>
        </Link>
      </li>
      <li>
        <Link href="/post/blah/H-i">
          <a>Go to pages/post/[pageId]/[comment].js</a>
        </Link>
      </li>
    </ul>
  )
}

export default Home

동적라우트는 스프레드 연산자같은 ...을 더해줌으로써 모든 경로를 catch하는 것도 가능합니다.

가령, pages/post/[...slug].js

  • /post/pageA
  • /post/pageA/componentA
  • /post/pageA/componentA/elementA

를 모두 가리킵니다.

각각의 query는

  • { "slug" : ['pageA'] }
  • { "slug" : ['pageA', 'componentA'] }
  • { "slug" : ['pageA', 'componentA', 'elementA'] }

입니다.

In CRA

import { useNavigation } from 'react-router-dom';

export function Component() {
	const navigate = useNavigation();

  return (
		<button onClick={() => navigate('post/pageId01')}>페이지 이동 얏호</button>
	);
}
import { useLocation } from 'react-router-dom';

export function Component() {
	const location = useLocation();
  const { pathname } = location;  // pathname에 pageId01

  return (
		<div>pageId = {pathname}</div>
	);
}

이런 식으로 받아오거나

import { useNavigation } from 'react-router-dom';

export function Component() {
	const navigate = useNavigation();
	const name = 'soryeongk';

  return (
		<button onClick={() => navigate('post/pageId01', {state : {name}})}>
			페이지 이동 얏호
		</button>
	);
}
import { useLocation} from 'react-router-dom';

export function Component() {
	const location = useLocation();
	const name = location.state.name;

  return (
		<div>pageId = {name}</div>
	);
}

2-2. Pre-rendering

기본적으로 Next.js는 모든 페이지를 미리 렌더링합니다. 즉, 클라이언트 측 JS에서 모든 작업을 수행하는 대신 각 페이지에 대해 미리 HTML을 생성합니다. 이는 성능 측면에서도 SEO(검색 엔진 최적화)에서도 더 좋은 결과를 낼 수 있습니다.

각 HTML은 해당 페이지에 필요한 최소한의 JS코드와 연결됩니다. 브라우저에서 페이지를 로드하면 해당 JS코드가 실행되어 페이지를 완전히 대화형으로 만듭니다.

Pre-rendering에도 두 가지 방식이 있습니다.

  • SSG: Static Site Generation(권장): HTML이 빌드 시 생성되며 각 요청에서 재사용됩니다.
  • SSR: Server-side Rendering: HTML이 매 요청마다 생성됩니다.

Next.js는 페이지마다 어떤 pre-rendering 방식을 사용할지를 정할 수 있습니다. 가능한 대부분의 페이지에 대해서는 Static Generation을 사용할 것을 권장합니다. build time에 사전 렌더링을 함으로써 유저의 요청이 있기 전에 렌더링이 가능하게 합니다.

만약 페이지의 콘텐츠/경로가 외부 데이터에 의존하는 경우에는 getStaticProps, getStaticPaths를 사용합니다. → 더 자세한 설명은 공식문서에서!

🖋️ Next.js가 일반 리액트보다 더 빠른 이유
일반 React를 사용하면 렌더링할 HTML이 클라이언트 측에서 생성되므로 검색 엔진은 HTML을 가져오기 위해 JavaScript 코드를 실행해야 합니다. Next.js에는 렌더링해야 하는 HTML이 서버 측에서 생성된 다음 클라이언트 측으로 전송되는 사전 렌더링 기능이 있습니다. 이 사전 렌더링 기능은 검색 엔진이 서버에서 직접 HTML을 가져오고 크롤링하는 동안 HTML을 생성할 필요가 없기 때문에 Next.js 애플리케이션에 향상된 SEO를 제공합니다.

Next.js에서 SSG를 권장하는 이유 - 완전 최신

  • 유저의 요청에 앞서 빌드 시 이미 페이지를 렌더를 위한 데이터가 있는 경우
  • 데이터가 headless CMS에서 오는 경우 → 잘 모르겠음
  • 사용자별이 아니라 publicly 캐시될 수 있는 경우
  • SEO를 위해 pre-render 혹은 매우 빠른 페이지가 필요한 경우

2-3. getStaticProps

TS를 사용한다면 다음과 같이 사용해야합니다.
타입스크립트: GetStaticProps

getStaticProps는 빌드 시기에 데이터를 불러와줍니다. 페이지에서 getStaticProps라는 이름의 async 함수를 export하면 Next.js는 getStaticProps 로부터 반환된 props를 사용하여 빌드타임에 페이지를 pre-render 해줍니다.

// 기본 구성

export async function getStaticProps(context) {
	return {
		props: {},
	}
}

context parameter의 keys

이상의 코드에서 context 파라미터는 다음의 키들을 갖습니다.

  • params : Dynamic Routes를 사용하는 페이지의 경로 매개변수입니다.
    • 뭔말이야요?
      [id].js라는 이름의 페이지(Next.js의 라우팅 방식)가 있다면, params{ id: ...} 의 형태입니다.
    • params를 사용하기 위해서는 getStaticPaths를 함께 사용해야 합니다.
  • preview : preview mode로 지정된 페이지에서는 true이고, 그렇지 않은 경우에는 undefined가 됩니다.
    • 왜 써?
      ⇒ 빌드시 생성된 draft 데이터가 아니라 사용자 요청에 맞추어 새로운 데이터를 보여주려고 하는 등의 특정한 상황에서만 SSG 방식에서 우회가 필요할 수 있습니다.
    • 이건 사용이 필요할 때 조금 더 깊게 찾아봐야할 듯!
  • previewData : setPreviewData로 set된 preview data를 포함합니다.
    • Preview Mode와 연관되어 있습니다.
  • local , locales, defaultLocale 등은 Internationlized Routing과 관련되어 있습니다.

getStaticProps가 반환하는 값

모두 optional이지만 한 개의 object는 반환해야합니다.

  • props : 해당 페이지에서 수신할 props 객체 입니다.
  • revalidate : 페이지 재생성이 발생할 수 있는 시간(초)으로 기본 값은 false입니다.
    • 본 값이 false라면 재생성을 하지 않겠다는 의미로 페이지는 다음 빌드까지 기존 빌드 상태로 캐시됩니다.
  • notFound : 404 에러 페이지를 반환할 수 있도록 합니다.
    • notFound: true를 사용하면 성공적으로 생성된 페이지가 있더라도 404를 반환합니다.

    • 작성자가 제거하는 사용자 생성 콘텐츠와 같은 사용 사례를 지원하기 위한 것입니다.

      export async function getStaticProps(context) {
        const res = await fetch(`https://.../data`)
        const data = await res.json()
      
        if (!data) {
          return {
            notFound: true,
          }
        }
      
        return {
          props: { data }, // props로 전달된 data들은 본 페이지의 component로 전달됩니다.
        }
      }
  • redirect : 내외부 리소스로 리디렉션할 수 있도록 하는 redirect value입니다.
    • { destination: string, parmanent: boolean } 의 형태로 작성합니다.
    • 간혹, 이전 HTTP 클라이언트가 올바르게 리디렉션되도록 사용자 지정 상태 코드를 할당해야 할 수도 있습니다. 이 경우에는 parmanent 속성 대신 statusCode 속성을 사용합니다. 또한, basePath: false 로 설정합니다.

2-4. Static Generation 맛보기

서버에서 향수 목록을 불러와 보여주고, 항목을 선택하면 해당 향수의 정보를 보여주는 페이지를 만들어보겠습니다.

// 향수 목록을 보여주는 페이지 perfumes.js
// 페이지 내 콘텐츠들이 외부 데이터에 의존하는 경우
import Link from "next/link";
import { client } from "./api/client";

const PerfumeList = ({ perfumes }) => {
  console.log(`perfumes`, perfumes);
  return (
    <div>
      {perfumes.map((perfume, idx) => (
        <li key={idx}>
          <Link href={`products/${perfume._id}`}>{perfume.perfume_name}</Link>
        </li>
      ))}
    </div>
  );
};

export async function getStaticProps() {
  const { data } = await client.get("/product");
  const perfumes = await data.data;

  return {
    props: {
      perfumes,
    },
  };
}

export default PerfumeList;
// 향수의 상세 정보를 보여주는 products/[id].js
// 페이지의 경로와 상세내용이 모두 외부 데이터에 의존하는 경우
import { client } from "../api/client";

const Product = ({ product }) => {
  console.log(`product`, product);
  return (
    <section>
      <h1>{product.perfume_name}</h1>
      <p>{product.description}</p>
    </section>
  );
};

export async function getStaticPaths() {
  // 모든 향수 정보를 불러옴
  const { data } = await client.get("/product");
  const products = await data.data;

  // 향수의 id를 주소값으로 사용하겠다고 세팅
  const paths = products.map((product) => ({
    params: {
      id: product._id,
    },
  }));

  return { paths, fallback: false };
}

export async function getStaticProps({ params }) {
  // params로 넘겨준 데이터를 기반으로 detail 정보를 불러와서 props로 넘겨줌
  const { data } = await client.get(`/product/detail/${params.id}`);
  const product = await data.data[0];

  return {
    props: {
      product,
    },
  };
}

export default Product;

getStaticProps와 getStaticPaths를 사용함으로써 build 시 html이 한 번에 렌더링되고, 매 요청마다 렌더링된 내용을 재사용할 수 있게 됩니다. (SSR은 매 요청마다 페이지를 렌더링합니다.)

참고로 fallbackpaths 이외의 경로들이 추후 요청이 들어오면 만들어 줄 것인가?에 대한 boolean 값입니다. fallback : false라면 404를 리턴합니다.

데이터가 있든 없든 가능하다면 Static Generation을 사용할 것을 권장합니다.

2-4. SSR은 언제 사용하나요?

지속적으로 업데이트되는 페이지의 경우, 유저의 요청이 있기 전에 사전 렌더링하는 것이 불가하기 때문에 이때는 Static Generation보다는 SSR을 사용하는 것이 좋습니다.

SSR은 요청마다 페이지를 렌더링하기 때문에 속도는 더 느릴 수 있지만, 항상 최신 상태를 유지한다는 장점이 있습니다. 이때는 getStaticProps가 아닌 getServerSideProps를 사용하면 됩니다.

import Link from "next/link";
import { client } from "./api/client";

const PerfumeList = ({ perfumes }) => {
  console.log(`perfumes`, perfumes);
  return (
    <div>
      {perfumes.map((perfume, idx) => (
        <li key={idx}>
          <Link href={`products/${perfume._id}`}>{perfume.perfume_name}</Link>
        </li>
      ))}
    </div>
  );
};

export async function getServerSideProps() {
  const { data } = await client.get("/product");
  const perfumes = await data.data;

  return {
    props: {
      perfumes,
    },
  };
}

export default PerfumeList;

3. Layout 사용하기

공통의 레이아웃 폼을 만들어 적용하는 것도 가능합니다.

먼저, 컴포넌트를 모아둘 폴더를 만듭니다. 그리고 공통으로 적용할 Layout을 만듭니다.

import Footer from "./Footer";
import Header from "./Header";

const MyLayout = ({ children }) => {
  return (
    <div>
      <Header />
	    {children}
      <Footer />
    </div>
  );
};

export default MyLayout;

그리고 레이아웃을 적용하고 싶은 곳에 다음과 같이 작성합니다.

// _app.js
import MyLayout from "../components/MyLayout";

function MyApp({ Component, pageProps }) {
  return (
    <MyLayout>
      <Component {...pageProps} />
    </MyLayout>
  );
}

export default MyApp;

한 가지 주의해야할 것이 있다면, Layout 파일은 말그대로 폼에 불과하고, Page가 아니기 때문에 getStaticProps, getServerSideProps 등의 pre-rendering이 불가합니다. SWR이나 useEffect를 사용하시길 바랍니다.

4. _app.js, _document.js, _error.js

Next.js에서 필수적으로 요구되는 파일들입니다. _app.js는 CRA에서의 app.js와 같다고 생각하면 되며, create next-app을 실행하면 자동으로 생성됩니다. 프로젝트 페이지를 탐색하고 공통의 레이아웃을 적용하는 등의 역할을 담당합니다.

  • index.js는 무엇인가요? default Home 페이지라고 생각하면 됩니다.
    // index.js
    import Main from "../components/Main";
    
    export default function Home() {
      return <Main />;
    }
    // _app.js
    import Head from "next/head";
    import MyLayout from "../components/MyLayout";
    
    function MyApp({ Component, pageProps }) {
      return (
        <>
          <Head>
            <title>Create Next App</title>
            <meta name="description" content="Generated by create next app" />
            <link rel="icon" href="/favicon.ico" />
          </Head>
          <MyLayout>
            <Component {...pageProps} />
          </MyLayout>
        </>
      );
    }
    
    export default MyApp;
    html의 head에 작성하던 내용을 여기서 작성하기도 하고, 전체 페이지를 불러와 라우팅할 수 있게 해줍니다.

_document.js와 _error.js는 자동으로 생성되지 않기 때문에 직접 만들어줘야 합니다.

_document.js는 없어도 실행은 되지만 기본 html 태그에 대한 보강을 해줍니다. head 안에 스타일을 넣거나 구글 애널리틱스, 외부 Font, meta 정보 등의 외부 스크립트, CDN 등을 주입할 때 사용합니다.

_error.js는 기본적으로 500error가 발생했을 때 나오는 페이지를 말하는데, error코드별로 페이지를 커스텀하여 사용할 수도 있습니다. 500에러에 대해서 500.js로, 404에러에 대해서는 404.js 등으로 생성해줄 수도 있습니다.

5. API Reference

import image from "next/image";

위에서 몇 가지 언급하고 사용했는데, next 자체에서 제공하는 API Reference가 몇 가지 있습니다. CRA와 크게 달라지는 점은 없는 것으로 기억하는데, 하나씩 다 짚어보기 보다는 필요할 때 찾아가면서 사용하는 것만으로도 충분했습니다! 만약 일반 img 태그를 사용하면, strict 모드에서는 아예 렌더링이 되지 않기도 하고, 최소한의 에러를 전달해주기도 하는데 에러 메시지에서 next/image를 사용하라고 안내를 해줍니다. 때문에 어렵지 않게 적용하실 수 있을거라 생각하여 생략합니다!

next/router next/link next/image next/script next/head next/amp next/server

next/router | Next.js

참고

Next.js 공식문서
Getting Started | Next.js

Next.js 작동 원리 보충 자료
[nextjs] nextjs는 어떻게 동작하는가?

pre-rendering 관련 보충 자료
[Next.js] 사전 렌더링(Pre-rendering): Static Generation과 SSR

공통 레이아웃 사용법 보충 자료
(Next.js) _app.js 를 이용하여 공통 레이아웃 사용하기

SSR vs SSG
[FE] SSR(Server-Side-Rendering) 그리고 SSG(Static-Site-Generation) (feat. NEXT를 중심으로)

profile
웹 프론트엔드 개발자 령이의 어쩌구 저쩌구

2개의 댓글

comment-user-thumbnail
2022년 2월 9일

좋은 글 감사합니다

답글 달기
comment-user-thumbnail
2022년 2월 9일

좋은 글 감사합니다!

답글 달기