next.js 입문2

Yunes·2023년 11월 12일
0

[포스코x코딩온]

목록 보기
42/47
post-thumbnail

배포

그냥 npm dev 를 해서 네트워크에 리소스를 보면 굉장히 큰 용량을 차지하고 있음을 알 수 있다. 배포용은 좀 더 최적화된 배포판이 따로 필요하다.

// package.json

 "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },

npm build : 프로덕션 서버 패포판을 만드는 명령어
npm start : 해당 배포한을 서비스하는 명령어

=> npm run build 시 .next 폴더에 빌드, 서비스되는 내역이 저장된다.

npm run build.next 에 만들어진 배포판을 npm start 를 통해 실행하니 리소스가 확 줄어든 모습을 볼 수 있다.

뼈대 만들기

// layout.tsx
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: "Web tutorials",
  description: "Generated by jihun",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html>
      <body>
        <h1>
          <a href="/">WEB</a>
        </h1>
        <ol>
          <li>
            <a href="/read/1">html</a>
          </li>
          <li>
            <a href="/read/2">css</a>
          </li>
        </ol>
        {children}
        <ul>
          <li>
            <a href="/create">Create</a>
          </li>
          <li>
            <a href="/update/1">Update</a>
          </li>
          <li>
            <input type="button" value="delete" />
          </li>
        </ul>
      </body>
    </html>
  );
}

Metadata 를 통해 메타데이터를 변경할 수 있다.

// page.tsx
import Image from "next/image";

export default function Home() {
  return (
    <>
      <h2>Welcome</h2>
      Hello, Web! - page.tsx
    </>
  );
}

라우팅

이미지 출처 : https://opentutorials.org/course/5098/32350

라우팅 : 경로에 따라 어떤 컨텐츠를 어떤 방식으로 보여줄 것인가를 결정하는 것

만약 localhost:3000/create 라는 경로로 라우팅을 하고 싶다면 src/app 밑에 create 라는 폴더를 만들고 해당 폴더 내에 page.tsx 파일을 만든다.

next.js 에서 localhost:3000/create 로 접속하면 src/app 밑에 create 라는 폴더가 있는지 찾는다.

해당 폴더에서 page.tsx 를 찾고 그 리턴을 만약 create 에 layout.tsx 가 있다면 해당 파일의 children 에 결합하고 없다면 부모 폴더인 app 의 layout.tsx 의 children 과 결합한다.

Dynamic routing

위와 같이 경로가 동적으로 결정되는 경우 모든 page 를 모든 경로에 다 만들어야 할까?

이런 상황에 대처하는 동적 라우팅 기능을 제공한다.

기본적으로 app 디렉토리 내의 폴더는 경로가 된다. 그럴때 [] 대괄호로 감싸면 동적으로 바뀌는 값이 되며 이 값을 서버 컴포넌트에서 가져오려면 props.params.[대괄호에 명시한 이름] 를 사용한다.

export default function Read(props: { params: { id: string } }) {
  return (
    <>
      <h2>Read!</h2>
      parameter : {props.params.id}
    </>
  );
}

Single Page Application

Next.js 를 사용시 그냥 좋아지는 것 : Server Side Rendering 을 기본적으로 제공

리액트는 자바스크립트 기술이기에 크롬 자바스크립트 비활성화를 실행하면 렌더링이 되지 않는다.

반면 Next.js 는 자바스크립트를 비활성화해도 새로고침하면 렌더링을 잘 한다. 그 이유는 SSR 에서는 서버에서 html 을 만들어 넘기기 때문이다.

Next.js 단점

SPA 에서는 바뀌는 컴포넌트만 재렌더링해주었지만 Next.js 는 SSR 이라 서버에서 html 을 만들어 보내줘야 해서 모든 것을 다시 만들어 html 을 보내느라 네트워크가 느릴때 사용자 입장에서 더 느리게 느껴진다.

또한 이미 방문한적 있는 페이지에 다시 방문해도 다시 처음부터 로딩한다. 이를 극복하기 위해 이전 코드중 a 태그를 Link 태그로 바꿔준다.

a 태그를 모두 Link 태그로 바꿔주면 그 다음엔 이전에 방문한 페이지의 경우 아예 서버랑 통신도 하지 않고 바로 페이지를 전환해준다. 사용자 입장에서는 빠르고 서비스 제공자 입장에서는 돈을 절약할 수 있다.

Single Page Application : 웹 페이지가 여러개임에도 마치 하나같이 동작하는 애플리케이션

정적 자원 이용하기

위와 같이 public 에 넣은 asset 들은 / 경로로 접속할 수 있게 된다.

css

src/app/layout.tsx 는 루트 레이아웃이기에 모든 곳에 전역적으로 적용되는 css 를 붙여넣을 수 있다.

결과

백엔드

Routing - Route Handlers 를 통해서 Next.js 로도 백엔드 API 를 구축할 수 있다.

참고 next.js docs Route Handlers

npx json-server --port 9999 --watch db.json

json-server 를 9999 포트에서 실행하되 db.json 의 내용을 바탕으로 하고 db.json 의 변경 내역을 즉시 반영하기 위해 --watch 를 사용한다.

실행후 db.json 파일이 생성된다.

위의 사진을 보면 http://localhost:9999/posts 로 접속하면 데이터를 조회할 수 있다는 것을 알 수 있다.

만약 topics 라는 주소로 접속시 글 목록을 보고 싶다면?

db.json 을 수정후 적용하면 적용된다.

개발자모드로 같은 동작을 확인할 수 있다.

글 목록 가져오기

정보를 단순히 보여주는 Sidebar 처럼 유저와의 상호작용이 별로 없는 경우 서버 컴포넌트를 사용하는 것이 유리하다.

버튼처럼 상호작용이 빈번한 경우는 클라이언트 컴포넌트를 사용하는 것이 유리하다.

client component

"use client";
import type { Metadata } from "next";
import "./globals.css";
import Link from "next/link";
import { useEffect, useState } from "react";

interface Topic {
  id: number;
  title: string;
}

// export const metadata: Metadata = {
//   title: "Web tutorials",
//   description: "Generated by jihun",
// };

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const [topics, setTopics] = useState<Topic[]>([]);
  useEffect(() => {
    fetch("http://localhost:9999/topics")
      .then((resp) => resp.json())
      .then((result: Topic[]) => {
        setTopics(result);
      });
  }, []);

  return (
    <html>
      <body>
        <h1>
          <Link href="/">WEB</Link>
        </h1>
        <ol>
          {topics.map((topic) => {
            return (
              <li key={topic.id}>
                <Link href={`/read/${topic.id}`}>{topic.title}</Link>
              </li>
            );
          })}
          <li>
            <Link href="/read/1">html</Link>
          </li>
          <li>
            <Link href="/read/2">css</Link>
          </li>
        </ol>
        {children}
        <ul>
          <li>
            <Link href="/create">Create</Link>
          </li>
          <li>
            <Link href="/update/1">Update</Link>
          </li>
          <li>
            <input type="button" value="delete" />
          </li>
        </ul>
      </body>
    </html>
  );
}

server component

import type { Metadata } from "next";
import "./globals.css";
import Link from "next/link";
import { useEffect, useState } from "react";

interface Topic {
  id: number;
  title: string;
}

export const metadata: Metadata = {
  title: "Web tutorials",
  description: "Generated by jihun",
};

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const resp = await fetch("http://localhost:9999/topics");
  const topics: Topic[] = await resp.json();
  return (
    <html>
      <body>
        <h1>
          <Link href="/">WEB</Link>
        </h1>
        <ol>
          {topics.map((topic) => {
            return (
              <li key={topic.id}>
                <Link href={`/read/${topic.id}`}>{topic.title}</Link>
              </li>
            );
          })}
          <li>
            <Link href="/read/1">html</Link>
          </li>
          <li>
            <Link href="/read/2">css</Link>
          </li>
        </ol>
        {children}
        <ul>
          <li>
            <Link href="/create">Create</Link>
          </li>
          <li>
            <Link href="/update/1">Update</Link>
          </li>
          <li>
            <input type="button" value="delete" />
          </li>
        </ul>
      </body>
    </html>
  );
}

글 읽기

// src/app/read/[id]/page.tsx

import { Topic } from "@/app/layout";

export default async function Read(props: { params: { id: string } }) {
  const resp = await fetch(`http://localhost:9999/topics/${props.params.id}`);
  const topic: Topic = await resp.json();
  return (
    <>
      <h2>{topic.title}</h2>
      {topic.body}
    </>
  );
}

글 생성

import type { Metadata } from "next";
import "./globals.css";
import Link from "next/link";
import { useEffect, useState } from "react";

export interface Topic {
  id: number;
  title: string;
  body: string;
}

export const metadata: Metadata = {
  title: "Web tutorials",
  description: "Generated by jihun",
};

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const resp = await fetch("http://localhost:9999/topics");
  const topics: Topic[] = await resp.json();
  return (
    <html>
      <body>
        <h1>
          <Link href="/">WEB</Link>
        </h1>
        <ol>
          {topics.map((topic) => {
            return (
              <li key={topic.id}>
                <Link href={`/read/${topic.id}`}>{topic.title}</Link>
              </li>
            );
          })}
          <li>
            <Link href="/read/1">html</Link>
          </li>
          <li>
            <Link href="/read/2">css</Link>
          </li>
        </ol>
        {children}
        <ul>
          <li>
            <Link href="/create">Create</Link>
          </li>
          <li>
            <Link href="/update/1">Update</Link>
          </li>
          <li>
            <input type="button" value="delete" />
          </li>
        </ul>
      </body>
    </html>
  );
}

update & delete 버튼 구현

// create/page.tsx

"use client";

import { FormEvent } from "react";
import { useRouter } from "next/navigation";

export default function Create() {
  const router = useRouter();
  return (
    <form
      onSubmit={(event: FormEvent<HTMLFormElement>) => {
        event.preventDefault();
        const titleInput = event.currentTarget.elements.namedItem(
          "title"
        ) as HTMLInputElement;
        const bodyInput = event.currentTarget.elements.namedItem(
          "body"
        ) as HTMLTextAreaElement;

        const title = titleInput.value;
        const body = bodyInput.value;

        const options = {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ title, body }),
        };
        fetch(`http://localhost:9999/topics`, options)
          .then((res) => res.json())
          .then((result) => {
            console.log(result);
            const lastId = result.id;
            router.push(`/read/${lastId}`);
          });
      }}
    >
      <p>
        <input type="text" name="title" placeholder="title" />
      </p>
      <p>
        <textarea name="body" placeholder="body"></textarea>
      </p>
      <p>
        <input type="submit" value="create" />
      </p>
    </form>
  );
}
// Control.tsx

"use client";
import Link from "next/link";
import { useParams } from "next/navigation";

export function Control() {
  const params = useParams();
  const id = params.id;
  console.log("id : ", id);
  return (
    <ul>
      <li>
        <Link href="/create">Create</Link>
      </li>
      {id ? (
        <>
          <li>
            <Link href={"/update/" + id}>Update</Link>
          </li>
          <li>
            <input type="button" value="delete" />
          </li>
        </>
      ) : null}
    </ul>
  );
}
// src/app/layout.tsx

import type { Metadata } from "next";
import "./globals.css";
import Link from "next/link";
import { useEffect, useState } from "react";
import { Control } from "./Control";

export interface Topic {
  id: number;
  title: string;
  body: string;
}

export const metadata: Metadata = {
  title: "Web tutorials",
  description: "Generated by jihun",
};

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const resp = await fetch("http://localhost:9999/topics");
  const topics: Topic[] = await resp.json();
  return (
    <html>
      <body>
        <h1>
          <Link href="/">WEB</Link>
        </h1>
        <ol>
          {topics.map((topic) => {
            return (
              <li key={topic.id}>
                <Link href={`/read/${topic.id}`}>{topic.title}</Link>
              </li>
            );
          })}
        </ol>
        {children}
        <Control />
      </body>
    </html>
  );
}

글 수정

"use client";

import { FormEvent, useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";

export default function Update() {
  const [title, setTitle] = useState("");
  const [body, setBody] = useState("");
  const router = useRouter();
  const params = useParams();
  const id = params.id;
  useEffect(() => {
    fetch("http://localhost:9999/topics/" + id)
      .then((resp) => resp.json())
      .then((result) => {
        setTitle(result.title);
        setBody(result.body);
      });
  }, []);
  return (
    <form
      onSubmit={(event: FormEvent<HTMLFormElement>) => {
        event.preventDefault();
        const titleInput = event.currentTarget.elements.namedItem(
          "title"
        ) as HTMLInputElement;
        const bodyInput = event.currentTarget.elements.namedItem(
          "body"
        ) as HTMLTextAreaElement;

        const title = titleInput.value;
        const body = bodyInput.value;

        const options = {
          method: "PATCH",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ title, body }),
        };
        fetch(`http://localhost:9999/topics/` + id, options)
          .then((res) => res.json())
          .then((result) => {
            console.log(result);
            const lastId = result.id;
            router.refresh();
            router.push(`/read/${lastId}`);
          });
      }}
    >
      <p>
        <input
          type="text"
          name="title"
          placeholder="title"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
        />
      </p>
      <p>
        <textarea
          name="body"
          placeholder="body"
          value={body}
          onChange={(e) => setBody(e.target.value)}
        ></textarea>
      </p>
      <p>
        <input type="submit" value="update" />
      </p>
    </form>
  );
}

글 삭제

"use client";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";

export function Control() {
  const params = useParams();
  const router = useRouter();
  const id = params.id;
  return (
    <ul>
      <li>
        <Link href="/create">Create</Link>
      </li>
      {id ? (
        <>
          <li>
            <Link href={"/update/" + id}>Update</Link>
          </li>
          <li>
            <input
              type="button"
              value="delete"
              onClick={() => {
                const options = { method: "DELETE" };
                fetch("http://localhost:9999/topics/" + id, options)
                  .then((resp) => resp.json())
                  .then((result) => {
                    router.refresh();
                    router.push("/");
                  });
              }}
            />
          </li>
        </>
      ) : null}
    </ul>
  );
}

환경변수

https://nextjs.org/docs/app/building-your-application/configuring/environment-variables

// read/[id]/page.tsx
import { Topic } from "@/app/layout";

export default async function Read(props: { params: { id: string } }) {
  const resp = await fetch(process.env.API_URL + `topics/${props.params.id}`, {
    cache: "no-store",
  });
  const topic: Topic = await resp.json();
  return (
    <>
      <h2>{topic.title}</h2>
      {topic.body}
    </>
  );
}

로컬 변수는 .env.local 에 만들어줬다.

환경변수는 보안상 클라이언트 측에서 쉽게 조회할 수 없게 서버 컴포넌트에서만 사용할 수 있다.

만약 브라우저를 위한 환경변수를 사용하고자 한다면 NEXTPUBLIC 접두사를 붙여서 사용해야 한다.

"use client";

import { FormEvent } from "react";
import { useRouter } from "next/navigation";

export default function Create() {
  const router = useRouter();
  return (
    <form
      onSubmit={(event: FormEvent<HTMLFormElement>) => {
        event.preventDefault();
        const titleInput = event.currentTarget.elements.namedItem(
          "title"
        ) as HTMLInputElement;
        const bodyInput = event.currentTarget.elements.namedItem(
          "body"
        ) as HTMLTextAreaElement;

        const title = titleInput.value;
        const body = bodyInput.value;

        const options = {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ title, body }),
        };
        fetch(process.env.NEXT_PUBLIC_API_URL + `topics`, options)
          .then((res) => res.json())
          .then((result) => {
            console.log(result);
            const lastId = result.id;
            router.refresh();
            router.push(`/read/${lastId}`);
          });
      }}
    >
      <p>
        <input type="text" name="title" placeholder="title" />
      </p>
      <p>
        <textarea name="body" placeholder="body"></textarea>
      </p>
      <p>
        <input type="submit" value="create" />
      </p>
    </form>
  );
}
profile
미래의 나를 만들어나가는 한 개발자의 블로그입니다.

0개의 댓글