9 분 소요


1. Next.js 설치

공식홈페이지의 내용을 참조하여 설치하자 클릭

npx create-next-app@latest


그 후, 아래와 같이 세팅하기!


파일 구조!



2. Next.js 특징

2.1 layout, page 컴포넌트

페이지를 구성하는 layout, page 컴포넌트의 역할을 살펴보자.

  • layout.tsx
    • 리액트 컴포넌트이다.
    • children을 가지고 있다.
  • page.tsx
    • 리액트 컴포넌트이다.


src > app > page.tsx 파일을 다음과 같이 수정해보자

export default function Home() {
  return <div>안녕 Next.js! 반가워!</div>;
}


그 후 아래 명령어로 실행하기!

yarn dev

page.tsx 파일이 UI를 핸들링 하는 주 파일이라는 것을 알 수 있다.


2.2 package.json 파일

package.json 파일의 scripts > dev / build / start를 살펴보자

  • dev(npm run dev) : 개발자가 개발하는 중 사용하게 될 방법이다.
  • build(npm run build) : production 레벨로 배포하기 전 필요한 빌드 작업 과정을 실행하기 위한 방법이다.
  • start(npm run start) : 만들어진 build 파일을 이용하여 실행시키는 방법이다.

따라서, build하지 않고 start를 하는 경우 실행될 수 없다.



3. 라우팅 이해하기

3.1 주요 용어

  • Tree
    • 계층 구조를 시각적으로 잘 보기 위한 규칙
  • Subtree
    • tree의 한 부분
    • root부터 시작해서 leaf들에 이르기까지의 범위
  • Root
    • Tree 또는 Subtree의 첫 번째 노드
    • root layout 같은 것이 된다.

  • Leaf
    • children이 더이상 없는 node를 말한다.
  • URL Segment
    • 슬래시(/)로 분류된 URL path의 한 부분을 말한다.

  • URL Path
    • 도메인(www.sample-web.com) 이후 따라오는 전체 URL 부분을 말한다.


3.2 파일(폴더) 기반 라우팅

3.2.1 page.tsx

  • page.tsx는 main ui가 표시될 곳이다.
  • 따라서 앞으로 page.tsx 파일을 생성하면서 코딩을 하면 된다.


3.2.2 static routing

React에서 Routing을 구현하기 위해 우리는 react-router-dom 패키지를 설치하고 세팅을 했지만, next.js에서는 static routing을 제공하기 때문에, 세팅을 할 필요 없이 폴더를 기반으로 실행이 가능하다.

src > app 폴더 밑에 test 폴더를 새로 만들고 그 안에 page.tsx 파일을 만들어보자

// src > app > test > page.tsx
import React from "react";

// 페이지 컴포넌트 생성시 컨벤션 : 경로에 따른 이름 + 페이지
const TestPage = () => {
  return <div>하이하이 테스트페이지!</div>;
};

export default TestPage;

브라우저에서 접근하면 폴더 기반의 라우팅이 되는 것을 확인할 수 있다!


http://localhost:3000/a/b/c 경로로 접근했을 때 page가 나오게 하려면 어떻게해야할까?

a폴더 아래 b폴더 아래 c폴더 아래 page.tsx가 있다는 의미이므로 아래와 같은 방법으로 접근하면 된다.


3.2.3 dynamic routing

react-router-dom에서 특정 경로가 dynamic한 parameter에 의해 계속 변할 때 아래와 같이 설정해주었다.

const Router = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
        <Route path="/works" element={<Works />} />
        <Route path="/products/:id" element={<Product />} />
      </Routes>
    </BrowserRouter>
  );
};


하지만, Next에서는 폴더 이름을 대괄호로 감싸는 방식으로 라우팅 처리를 한다.

이렇게 하게 되면, 패턴에 일치하는 모든 경로를 페이지로 연결하게 된다.

app/posts/[id]/page.tsx 파일의 경우 /posts/1 /posts/2 등 경로에 대해 동적으로 페이지를 생성하게 되는 것이다.


직접 사용해보자

// app>test>[id]>page.tsx
import React from "react";

const TestDetailPage = ({
  params,
}: {
  params: {
    id: string;
  };
}) => {
  return <div>Detail page:{params.id}</div>;
};

export default TestDetailPage;


3.2.4 route groups

폴더 생성시 routing에 포함이 되지 않게 하기 위해서 대괄호 ()로 감싸주면 된다. (라우팅이 아닌 단순한 분류를 하고 싶을 때 사용한다!)

import React from "react";

const AdminAboutPage = () => {
  return <div>AdminAboutPage</div>;
};

export default AdminAboutPage;
  • http://localhost:3000/admin/about -> 404 error
  • http://localhost:3000/about -> 접근 가능

⚠️ 대괄호 ()로 감싼 폴더는 route groups이 아니기 때문에 다른 경로에 about page가 존재해서는 안된다.


3.3 speciall files

Next.js에서는 Routing을 폴더 기반, 즉 Nested Folders(중첩된 폴더)로 구현을 하고 있다. 이 과정에서 어떤 라우트에 대한 특정 처리를 위해 생성되는 여러 speciall files이 존재한다.

  • 특정 처리의 종류
    • 특정 경로 하위에 있는 routing은 모두 공통 layout을 적용하고 싶을 때
    • 특정 컴포넌트가 로딩중일 때, 오류가 발생했을 때 보여주고 싶은 UI가 있을 때 등


3.3.1 layout

layout파일은 어떤 segment와 그의 자식 node에 있는 요소들이 공통적으로 적용받게 할 UI를 정의한다.

자식 node에 있는 요소들이 공통 적용을 받아야 하기 때문에 반드시 children prop이 존재해야 한다. -동일 layout 안에서 다른 경로를 계속해서 왔다갔다 할 때 re-rendering이 일어나지 않는다.


React.js를 사용했을 때는 react-router-dom으로 이렇게 구현하였다.

createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<Root />}>
      <Route path="contact" element={<Contact />} />
      <Route
        path="dashboard"
        element={<Dashboard />}
        loader={({ request }) =>
          fetch("/api/dashboard.json", {
            signal: request.signal,
          })
        }
      />
      <Route element={<AuthLayout />}>
        <Route path="login" element={<Login />} loader={redirectIfUser} />
        <Route path="logout" action={logoutUser} />
      </Route>
    </Route>
  )
);


Next.js를 사용하여 layout을 구현해보자
목표 : (admin) 하위 경로의 모든 페이지들은 admin의 style 영향을 받게 하기

먼저 (admin) 하위 경로에 새롭게 sample 폴더를 만들자.

// src > app > (admin) > sample > page.tsx
import React from "react";

const SamplePage = () => {
  return <div>코리🐶</div>;
};

export default SamplePage;


그 후, 해당 폴더에 layout.tsx 파일을 만들자


Layout이 children을 포함하고 있어야만 하위에 있는 컴포넌트들을 렌더링 할 수 있다. 따라서 아래와 같이 children을 포함시켜서 공통 UI를 만들자.

// src > app > (admin) > layout.tsx
import React from "react";

const AdminLayout = ({ children }: { children: React.ReactNode }) => {
  return (
    <div>
      <p>🩷</p>
      {children}
    </div>
  );
};

export default AdminLayout;


Next.js 프로젝트를 생성하고 나면 이미 root 경로에 layout.tsx 파일이 존재하는데, 아래 코드를 입력해서 공통 레이아웃이 변경되는지 확인해보자.

// src > app > layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <nav>
          <a href="/">Home</a>
          <a href="/about">About</a>
          <a href="/contact">Contact</a>
          <a href="/blog">Blog</a>
        </nav>
        {children}
      </body>
    </html>
  );
}


3.3.2 template

template 파일은 layout과 상당히 유사한 컴포넌트이다.

  • 하지만, “상태를 유지”와 “리-렌더링” 에 있어서 차이점이 존재한다.
  • 경로 전반에 걸쳐서 상태가 유지되는 레이아웃과 달리, 템플릿은 라우팅을 탐색할 때 각 하위 항목에 대해 새 인스턴스를 만든다.
  • 즉, User 입장에서 동일한 Template을 공유하는 경로 사이를 왔다갔다 할 때 DOM 요소가 다시 생성된다는 것이다.


그래서 이런 use-case가 존재한다.

  1. 템플릿을 통한 페이지 open animation
    • 페이지 간 전환 시 애니메이션을 계속해서 주고 싶을 때 layout으로 만들어놓으면, 최초 렌더링시에만 animation이 적용되고 끝나버린다. 이 때, template을 사용한다.
  2. useEffect, useState에 의존하는 기능


layout vs template

테스트페이지 → 테스트 페이지 1 → 테스트 페이지 2 를 계속 왔다갔다 해보자

// src > app > test > layout.tsx
// src > app > test > template.tsx
"use client";

import Link from "next/link";
import React from "react";

const TestTemplate = ({ children }: { children: React.ReactNode }) => {
  useEffect(() => {
    console.log("최초 렌더링 한 번만 호출합니다.");
  }, []);

  return (
    <div className="m-8 p-8 bg-white">
      <h1>테스트 페이지</h1>
      <p>테스트 경로 하위에서의 이동을 확인해봅니다.</p>
      <nav>
        <ul>
          <li>
            <Link href="/test">테스트 페이지</Link>
          </li>
          <li>
            <Link href="/test/1">테스트 페이지 1</Link>
          </li>
          <li>
            <Link href="/test/2">테스트 페이지 2</Link>
          </li>
        </ul>
      </nav>
      {children}
    </div>
  );
};

export default TestTemplate;
  • layout을 사용하는 경우 : console.log가 최초 한 번만 호출된다.



  • template을 사용하는 경우 : 페이지를 이동할 때마다 호출된다.

➡️ 결론: 특정한 이유가 있지 않는 한, layout.tsx를 사용하기!


3.3.3 not-found

404 not found 페이지를 react-router-dom에서는 다음과 같이 세팅했었다.

<Router>
  <Routes>
    <Route path={"/home"} element={<Home />} />
    <Route path={"/*"} element={<h1>404: Not Found</h1>} />
  </Routes>
</Router>


next.js에서는 우리가 별도 설정을 하지 않아도 기본 스타일이 된 not found 페이지를 제공한다.


아래와 같이 직접 스타일을 변경할 수 있다.

// src > app > not-found.tsx
import React from "react";

const NotFound = () => {
  return <div>존재하지 않는 페이지입니다.</div>;
};

export default NotFound;


3.3.4 metadata와 SEO

Next.js는 향상된 SEO를 제공하기 위해서 html head에 삽입했었던 많은 정보를 metadata는 객체 형태로 지원하고 있다.


리액트에서는 vite를 쓰던 일반 CRA를 쓰던 index.html 파일이 존재했고, 이 파일의 head 태그에 SEO(Search Engine Optimization, 검색 엔진 최적화) 향상을 위해 여러 meta, link 태그 등 메타데이터를 작성했다.


하지만 Next.js에서는 Config-based Metadata를 활용할 수 있다. 두 가지 방법이 존재한다.

① page.tsx 또는 layout.tsx 어디든지 아래의 코드를 삽입하면 적용이 된다.

export const metadata: Metadata = {
  title: "타이틀 지정!!",
  description: "This is awesome Website",
};
  • metadata in page.tsx : 해당 page.tsx 컴포넌트에만 적용
  • metadata in layout.tsx : 해당 layout의 하위 요소에 모두 적용


page.tsx 케이스는 해당 페이지만 metadata의 적용을 받는다.

// src > app > page.tsx
import { Metadata } from "next";

export const metadata: Metadata = {
  title: "타이틀 지정!!",
  description: "This is awesome Website",
};

export default function Home() {
  return <div>안녕 Next.js! 반가워!</div>;
}


layout.tsx 케이스는 해당 layout의 하위 요소에 metadata를 공통으로 적용한다.

// src > app > layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "타이틀!",
  description: "Generated by create next app",
};

export default function RootLayout({

...기존코드


② dynamic

dynamic route를 갖고있는 route에서 동적으로 변경되는 params를 기반으로 metadata를 변경하고 싶을 때 generateMetadata function을 사용한다.

import React from "react";

type Props = {
  params: {
    id: string;
  };
};

export function generateMetadata({ params }: Props) {
  return {
    title: `Detail 페이지 : ${params.id}`,
    description: `Detail 페이지 : ${params.id}`,
  };
}

const TestDetailPage = ({ params }: Props) => {
  return <div>Detail 페이지 : {params.id}</div>;
};

export default TestDetailPage;

즉, 동적으로(dynamic) 들어온 params에 대해서도 metadata를 이용할 수 있는 것이다.

자세한 내용은 여기를 확인하자



4. 페이지 이동과 관련된 기능 목록

Next.js는 <Link>라는 리액트 컴포넌트를 제공한다. 크게 두 가지 주요 역할을 한다.

① Link 태그는 prefetching을 지원한다.

  • Next.js의 <Link> 컴포넌트는 뷰포트에 링크가 나타나는 순간 해당 페이지의 코드와 데이터를 미리 가져오는 프리페칭⭐ 기능을 지원한다.
    • 뷰포트가 링크에 나타나는 순간이란 사용자가 웹 페이지를 스크롤하거나 페이지를 이동하면서 해당 링크가 실제로 사용자의 화면(즉, 뷰포트 내)에 보이기 시작하는 순간을 말한다.
  • 사용자가 링크를 클릭하기 전에 데이터를 미리 로드함으로써 사용자가 링크를 클릭했을 때 거의 즉시 페이지를 볼 수 있게 한다.


② Link 태그는 route 사이에 client-side navigation을 지원한다.

  • <Link> 컴포넌트는 브라우저가 새 페이지를 로드하기 위해 서버에 요청을 보내는 대신, 클라이언트 측에서 페이지를 바꾸어 주기 때문에 페이지 전환 시 매우 빠른 사용자 경험(UX)을 제공한다.
  • 페이지의 HTML을 서버에서 다시 가져올 필요 없이, 필요한 JSON 데이터만 서버로부터 가져와서 클라이언트에서 페이지를 재구성하여 렌더링한다.
  • 결국 Link 태그는 a 태그를 만들어내기 때문에 SEO가 유리하다.

  • 변경 전 (a태그 쓰지 말것!!)
// src > app > layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Sparta Next App",
  description: "This is awesome Website",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <nav>
          <a href="/">Home</a>
          <a href="/about">About</a>
          <a href="/contact">Contact</a>
          <a href="/blog">Blog</a>
        </nav>
        {children}
      </body>
    </html>
  );
}


  • 변경 후
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Link from "next/link";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Sparta Next App",
  description: "This is awesome Website",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <nav>
          <Link href="/">Home</Link>
          <Link href="/about">About</Link>
          <Link href="/contact">Contact</Link>
          <Link href="/blog">Blog</Link>
        </nav>
        {children}
      </body>
    </html>
  );
}


4.2 useRouter

useRouter를 사용할 때는 항상 코드 최상단에 “use client”를 삽입해야 한다!⭐

  • a 태그를 알아차릴 수 없기 때문에 크롤러 입장에서는 해당 요소가 “이동을 원한다”라는 것을 알 수 없다. ⇒ SEO 불리
  • 대부분 onClick 같은 이벤트 핸들러에서 사용한다.
  • 클릭 후 로직의 순서에 따라 실행하므로, 즉시 이동이 아니다.
"use client";

import { useRouter } from "next/navigation";

export default function Test () {
	const router = useRouter();

	const handleButtonClick = () => {
		로직1();
		로직2();

		...

		router.push("/new_location");
	}

	return <button onClick={handleButtonClick}>클릭!</button>
}


"use client";

import { useRouter } from "next/navigation";
import React, { useEffect } from "react";

const TestTemplate = ({ children }: { children: React.ReactNode }) => {
  const router = useRouter();

  useEffect(() => {
    console.log("최초 렌더링 한 번만 호출합니다.");
  }, []);

  return (
    <div className="m-8 p-8 bg-black">
      <nav>
        <ul>
          <li
            onClick={() => {
              router.push("/test");
            }}
          >
            테스트 페이지
          </li>
          <li
            onClick={() => {
              router.push("/test/1");
            }}
          >
            테스트 페이지 1
          </li>
          <li
            onClick={() => {
              router.push("/test/2");
            }}
          >
            테스트 페이지 2
          </li>
        </ul>
      </nav>
      {children}
    </div>
  );
};

export default TestTemplate;


4.2.1 history stack

  • router.push
    • 새로운 URL을 히스토리 스택에 추가한다.
    • 사용자가 router.push로 페이지를 이동하면, 이동한 페이지의 URL이 히스토리 스택의 맨 위에 쌓인다.
    • 이후 사용자가 브라우저의 ‘뒤로 가기’ 버튼을 클릭하면, 스택에서 가장 최근에 추가된 URL로부터 이전 페이지(URL)로 돌아간다.
  • router.replace
    • 현재 URL을 히스토리 스택에서 새로운 URL로 대체한다.
    • 현재 페이지의 URL이 새로운 URL로 교체되며, ‘뒤로 가기’를 클릭했을 때 이전 페이지로 이동하지만, 교체된 페이지로는 돌아갈 수 없다.
    • 현재 페이지를 히스토리에서 완전히 대체한다.
  • router.back
    • 사용자를 히스토리 스택에서 한 단계 뒤로 이동시킨다.
    • 마치 브라우저의 ‘뒤로 가기’ 버튼을 클릭한 것과 같은 효과를 내며, 사용자를 이전에 방문했던 페이지로 돌아가게 한다.
  • router.reload
    • 현재 페이지를 새로고침한다.
    • 히스토리 스택에 영향을 미치지 않는다.
    • 페이지의 데이터를 최신 상태로 업데이트하고 싶을 때 사용할 수 있다.

⭐ 로그아웃시 이전 페이지로 이동할 필요가 없으니 replace가 효율적이다!


댓글남기기