3. Next JS App Router로 TODO

jonyChoiGenius·2023년 12월 13일
0

NEXT JS 13 투두앱

목록 보기
3/4

설치하기

https://nextjs.org/docs/pages/api-reference/create-next-app
npx create-next-app@latest 혹은 yarn create next-app을 통해 설치를 진행할 수 있다.

prompts가 나오는데 아래와 같이 설정하였다.

What is your project named?  next-js-todo
Would you like to use TypeScript?  Yes
Would you like to use ESLint?  Yes
Would you like to use Tailwind CSS?  Yes
Would you like to use `src/` directory?  No 
Would you like to use App Router? (recommended)  Yes
Would you like to customize the default import alias (@/*)?  Yes

앱 라우팅

https://nextjs.org/docs/app/building-your-application/routing
앱 라우팅은 app 폴더를 루트로하는 디렉토리 라우팅 시스템이다.
각각의 폴더에는 파일 규칙을 통한 파일 세트를 제공한다.

layout.tsx : 레이아웃을 지정한다. nested되어 하위 폴더에도 동일한 레이아웃이 적용된다.
page.tsx : 경로의 고유한 ui이다.

그 밖에 loading, not-found, error 등의 파일을 사용할 수 있다.

app/layout.tsx 파일을 수정하여 NavBar가 모든 화면에서 노출되도록 하였다.

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import NavBar from "@/components/layout/NavBar";

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

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

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <NavBar />
        {children}
      </body>
    </html>
  );
}

Client Component

https://nextjs.org/docs/app/building-your-application/rendering/client-components

App Router 하에서 모든 컴포넌트는 리액트 서버 컴포넌트를 기반으로 작동된다.

서버 컴포넌트의 특징은

  • 데이터를 가져온다.
  • 백엔드의 자원에 직접 접근한다. (가령, SQL문을 사용하여 데이터를 가져오는 등이다.)
  • 서버 측에서 데이터를 관리하고, 자원을 사용한다. 클라이언트 측에서는 민감한 정보에 접근할 수 없고, 자바 스크립트 사용이 줄어들게 된다.

반면, 클라이언트 컴포넌트와 달리 useEffect, useState 등의 훅을 사용할 수 없고, onClick, onChange도 사용할 수 없고, 브라우저에 접근하거나, 브라우저의 API를 사용할 수 없다.

즉 App Router에서 위와 같은 기능들을 사용하려면 Client Component를 별도로 생성해주어야 한다.

앞선 코드에서 NavBar를 Client Component로 생성해보자.

"use client"라는 React 디렉티브를 통해 클라이언트 컴포넌트라는 바운더리를 설정한다.

이후에는 RoutesMap를 통해 경로를 관리하고, 링크를 연결한다.

components\layout\NavBar.tsx

"use client";

import Link from "next/link";
import {
  usePathname,
} from "next/navigation";

const RoutesMap = {
  READ: "/todo/read",
  CREATE: "/todo/create",
} as const;

const NavBar = () => {
  const pathname = usePathname();

  const routesEntries = Object.entries(RoutesMap);

  return (
    <div style={{ display: "flex", gap: "10px" }}>
      {routesEntries.map((e) => {
        const [tabName, path] = e;
        const isCurrentPath = pathname === path;
        const style = {
          border: "1px solid black",
          backgroundColor: isCurrentPath ? "LightBlue" : "",
        };

        return (
          <Link key={tabName} href={path}>
            <button
              key={tabName}
              type="button"
              style={style}
            >
              {tabName}
            </button>
          </Link>
        );
      })}
    </div>
  );
};

export default NavBar;

라우팅

https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating

Next js에서 링크를 만들 때에는 import Link from 'next/link'로 불러온 컴포넌트를 활용한다.

한편 onClick 이벤트 등으로 경로를 변경하고 싶을 때에는 import { useRouter } from "next/navigation";로 불러온 useRouter 훅을 사용한다. 이때 Pages Router에서는 "next/router"를 사용했지만, App Router에서는 "next/navigation"을 사용함에 유의하자.

위의 코드에서 일부를 onClick과 router를 이용하는 예시로 수정해본 것이다.

import { useRouter } from "next/navigation";
const router = useRouter();
...
        return (
          <Link key={tabName} href={path}>
            <button
              onClick={() => {
                router.push(path);
              }}
              key={tabName}
              type="button"
              style={style}
            >
              {tabName}
            </button>
        );

한편 next js 13의 next/navigation에는 usePathname(공식문서), useSearchParams(공식문서)을 통해 현재 경로나 쿼리 스트링을 받아올 수 있다.

데이터 페칭과 캐싱

https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating

App Router의 서버 컴포넌트는 데이터를 페칭할 수 있으며, 페칭한 데이터는 캐싱된다. 즉 Tanstack Query나 SWR과 같은 서버 측 상태 관리를 자동으로 한다.

아래의 코드를 보자. 백엔드 측은 지난번 Express로 TODO에서 만든 서버를 활용했다.
app\todo\read\page.tsx

const UserRead = async () => {
  const res = await fetch("http://localhost:8080/user");
  const data = await res.json();
  return <div>{JSON.stringify(data)}</div>;
};

export default UserRead;

페이지를 이동한 후, 다시 '/todo/read'로 돌아와도 새로운 요청을 보내지 않고 캐싱한 데이터를 사용한다.

리발리데이트

데이터를 캐싱한다는 점에서, App Router는 Pages Router 기반의 SSG공식문서와 동일하게 작동한다.

Timebase Revalidation (ISG)

여기에 revalidation 옵션을 주면 Pages Router의 ISG공식문서와 같은 방식으로 작동시킬 수 있다.

const res = await fetch("http://localhost:8080/user", { next: { revalidate: 5 } }); // 5초 뒤에 revalidation

'cache' Options

Pages Router의 SSR공식문서을 구현하려면 cache 옵션을 주면 된다.
cache 옵션의 기본값은 { cache: 'force-cache' } 이다. 이는 모든 값을 캐싱하여 SSG처럼 사용하겠다는 것을 의미한다. 해당 옵션을 'no-cache'로 바꾸면, 값을 캐싱하지 않는다는 의미이며, 클라이언트 측의 요청이 들어올 때마다 데이터를 새로 fetch하므로 SSR처럼 동작한다. 공식문서app-router-migration

const res = await fetch("http://localhost:8080/user", { cache: 'force-cache' });

On-demand revalidation

서버측 상태관리 라이브러리처럼 태그를 통해서 revalidation을 진행할 수도 있다.

  const res = await fetch("http://localhost:8080/user", {
    next: { tags: ["user-list"] },
  });

해당 태그는 import { revalidateTag } from "next/cache";를 통해 revalidate할 수 있다.

import { revalidateTag } from "next/cache";

revalidateTag("user-list");

user에서 불러온 데이터를 하나의 상태로 만들기 위해 코드를 분리한다.
lib\getUsers.ts

const getUsers = async () => {
  const res = await fetch("http://localhost:8080/user", {
    next: { tags: ["user-list"] },
  });
  const datas = await res.json();
  console.log(datas);
  return datas;
};

export default getUsers;

수정된 read 페이지는 아래와 같다.

app\todo\read\page.tsx

import getUsers from "@/lib/getUsers";

const UserRead = async () => {
  const data = await getUsers();
  return <div>{JSON.stringify(data)}</div>;
};

export default UserRead;

같은 코드를 사용하여 app\todo\create\page.tsx도 만들어보자

import getUsers from "@/lib/getUsers";

const UserCreate = async () => {
  const users = await getUsers();

  return (
    <div>
      {JSON.stringify(users)}
    </div>
  );
};

export default UserCreate;

"/todo/read"와 "/todo/create"를 오가도 모두 같은 데이터를 보여주며, 데이터가 캐싱되어 있기 때문에 요청을 추가로 주고 받지 않는다.

서버에 Post 요청을 보내기 위해 create/page.tsx에 Form을 하나 추가한다.
이때 앞서 언급했듯, onChange, useState 등을 사용하기 위해서는 Client Component를 사용해야 한다. 따라서 컴포넌트를 분리해야 한다.

import CreateForm from "@/components/create/CreateForm";
import getUsers from "@/lib/getUsers";

const UserCreate = async () => {
  const users = await getUsers();

  return (
    <div>
      <CreateForm />
      {JSON.stringify(users)}
    </div>
  );
};

export default UserCreate;

/components/create/CreateForm.tsx 는 아래와 같다.

"use client";
import createUsers from "@/lib/createUsers";
import { FormEvent, useState } from "react";

const CreateForm = () => {
  const [username, setUsername] = useState("");
  const [email, setEmail] = useState("");
  const onSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    createUsers({ username, email }).then((res) => {
      setUsername("");
      setEmail("");
    });
  };

  return (
    <form onSubmit={onSubmit}>
      <input
        placeholder="이름"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
      />
      <input
        placeholder="이메일"
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <button type="submit">사용자 생성하기</button>
    </form>
  );
};

export default CreateForm;

응답이 성공하면 revalidate를 진행한다.

"use server";
import { revalidateTag } from "next/cache";

const createUsers = async (params: { username: string; email: string }) => {
  const res = await fetch("http://localhost:8080/user", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      username: params.username,
      email: params.email,
    }),
  });
  revalidateTag("user-list");

  return res.json();
};

export default createUsers;

Server Action과 변이

https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#revalidating-data

만일 next js 14 버전 이상을 사용한다면, 아래와 같이 Form을 server 컴포넌트로 작성할 수 있다.

import createUsers from "@/lib/createUsers";

const CreateForm = () => {
  return (
    <form action={createUsers}>
      <input placeholder="이름" name="username" />
      <input placeholder="이메일" type="email" name="email" />
      <button type="submit">사용자 생성하기</button>
    </form>
  );
};

export default CreateForm;

이때 form의 action은 formData를 파라미터로 받아 작동하는 함수이다.

create 유저가 formData를 받아서 작동하도록 수정하자.

"use server";
import { revalidateTag } from "next/cache";

const createUsers = async (formData: FormData) => {
  const res = await fetch("http://localhost:8080/user", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      username: formData.get("username"),
      email: formData.get("email"),
    }),
  });
  revalidateTag("user-list");

  return res.json();
};

export default createUsers;

이와 같이 NavBar와 서버 액션인 createUsers를 제외한 나머지를 서버 컴포넌트로 작성 완료하였다.

profile
천재가 되어버린 박제를 아시오?

0개의 댓글

관련 채용 정보