그냥 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 과 결합한다.
위와 같이 경로가 동적으로 결정되는 경우 모든 page 를 모든 경로에 다 만들어야 할까?
이런 상황에 대처하는 동적 라우팅 기능을 제공한다.
기본적으로 app
디렉토리 내의 폴더는 경로가 된다. 그럴때 []
대괄호로 감싸면 동적으로 바뀌는 값이 되며 이 값을 서버 컴포넌트에서 가져오려면 props.params.[대괄호에 명시한 이름]
를 사용한다.
export default function Read(props: { params: { id: string } }) {
return (
<>
<h2>Read!</h2>
parameter : {props.params.id}
</>
);
}
Next.js 를 사용시 그냥 좋아지는 것
: Server Side Rendering 을 기본적으로 제공
리액트는 자바스크립트 기술이기에 크롬 자바스크립트 비활성화를 실행하면 렌더링이 되지 않는다.
반면 Next.js 는 자바스크립트를 비활성화해도 새로고침하면 렌더링을 잘 한다. 그 이유는 SSR 에서는 서버에서 html 을 만들어 넘기기 때문이다.
SPA 에서는 바뀌는 컴포넌트만 재렌더링해주었지만 Next.js 는 SSR 이라 서버에서 html 을 만들어 보내줘야 해서 모든 것을 다시 만들어 html 을 보내느라 네트워크가 느릴때 사용자 입장에서 더 느리게 느껴진다.
또한 이미 방문한적 있는 페이지에 다시 방문해도 다시 처음부터 로딩한다. 이를 극복하기 위해 이전 코드중 a 태그를 Link 태그로 바꿔준다.
a 태그를 모두 Link 태그로 바꿔주면 그 다음엔 이전에 방문한 페이지의 경우 아예 서버랑 통신도 하지 않고 바로 페이지를 전환해준다. 사용자 입장에서는 빠르고 서비스 제공자 입장에서는 돈을 절약할 수 있다.
Single Page Application
: 웹 페이지가 여러개임에도 마치 하나같이 동작하는 애플리케이션
위와 같이 public 에 넣은 asset 들은 /
경로로 접속할 수 있게 된다.
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 처럼 유저와의 상호작용이 별로 없는 경우 서버 컴포넌트를 사용하는 것이 유리하다.
버튼처럼 상호작용이 빈번한 경우는 클라이언트 컴포넌트를 사용하는 것이 유리하다.
"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>
);
}
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>
);
}
// 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>
);
}