1. Vercel ๋ฐฐํฌ
Next.js๋ Vercel์์ ๋ง๋ ํ๋ ์์ํฌ๋ผ์, ๋ฐฐํฌ ๊ณผ์ ์ด ์์ฃผ ๋จ์ํฉ๋๋ค.
(1) GitHub์ ์ฝ๋ ์ฌ๋ฆฌ๊ธฐ
git init
git add .
git commit -m "first next.js app"
git branch -M main
git remote add origin https://github.com/๋ด์์ด๋/next-tutorial.git
git push -u origin main
(2) Vercel ์ฐ๊ฒฐ
Vercel ์ ์ โ GitHub ์ฐ๋
"New Project" โ ๋ฐฉ๊ธ ์ฌ๋ฆฐ ์ ์ฅ์ ์ ํ
Framework: Next.js ์๋ ๊ฐ์ง
Deploy ๋ฒํผ ํด๋ฆญ โ ์๋ ๋ฐฐํฌ ์๋ฃ ๐
- ๊ธฐ๋ณธ URL:
https://next-tutorial.vercel.app
- ํ์ง๋ง ์ URL์ด ์ด๋ฏธ ์์ผ๋ฏ๋ก ๋ฐฐํฌ์ ์ฑ๊ณตํ๋ฉด ๋ค๋ฅธ URL์ด ๋๋คํ๊ฒ ์์ฑ๋ฉ๋๋ค.
- ํ๊ธฐ ๋งํฌ๋ ์ ๊ฐ ๋ฐฐํฌํ๋ฉด์ ์ฌ๋ฆฐ ๋งํฌ์ ๋๋ค.
- https://next-tutorial-ten-teal.vercel.app
ํน ๋ค์๊ณผ ๊ฐ์ด ์๋ฌ๊ฐ ๋ ์ ์์ต๋๋ค.
์๋ฌ๊ฐ ๋๋ ์์ธ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
1) ESLint ๊ท์น ์๋ฐ(
no-explicit-any
)๋ก ๋น๋ ์คํจsrc/app/posts/page.tsx: Unexpected any src/app/posts-client/page.tsx: Unexpected any
2) Route Handler ๋ ๋ฒ์งธ ์ธ์ ํ์ ๋ถ์ผ์น๋ก ๋น๋ ์คํจ
Route "src/app/api/todos/[id]/route.ts" has an invalid "DELETE" export: Type "Params" is not a valid type for the function's second argument. ... Type "{ params: { id: string } }" is not a valid type for the function's second argument.
โ ํด๊ฒฐ:
Post
ํ์ ์ ์ธํด์any
์ ๊ฑฐsrc/app/posts/page.tsx
type Post = { userId: number; id: number; title: string; body: string; }; export default async function PostsPage() { const res = await fetch("https://jsonplaceholder.typicode.com/posts", { // ๋ฐ๋ชจ์์ ํญ์ ์ต์ ๋ณด๊ธฐ cache: "no-store", }); const posts: Post[] = await res.json(); return ( <main> <h1>Posts ๋ชฉ๋ก</h1> <ul> {posts.slice(0, 5).map((post) => ( <li key={post.id}> <strong>{post.title}</strong> <p>{post.body}</p> </li> ))} </ul> </main> ); }
src/app/posts-client/page.tsx
"use client"; import { useState } from "react"; type Post = { userId: number; id: number; title: string; body: string; }; export default function PostsClientPage() { const [posts, setPosts] = useState<Post[]>([]); const loadPosts = async () => { const res = await fetch("https://jsonplaceholder.typicode.com/posts"); const data: Post[] = await res.json(); setPosts(data.slice(0, 5)); }; return ( <main> <h1>Client Fetch Example</h1> <button onClick={loadPosts}>๊ธ ๋ถ๋ฌ์ค๊ธฐ</button> <ul> {posts.map((post) => ( <li key={post.id}> <strong>{post.title}</strong> <p>{post.body}</p> </li> ))} </ul> </main> ); }
src/app/api/todos/[id]/route.ts
import { NextResponse } from "next/server"; import { store } from "../store"; export async function DELETE(req: Request) { const { pathname } = new URL(req.url); const id = pathname.split("/").pop(); if (!id) { return NextResponse.json({ error: "Bad Request" }, { status: 400 }); } const ok = store.remove(id); if (!ok) { return NextResponse.json({ error: "Not Found" }, { status: 404 }); } return NextResponse.json({ ok: true }); }
์ด๋ ๊ฒ ์์ ํ์ ๋ค์ ๋ฐฐํฌํ์๋ฉด ๋น๋ ํต๊ณผํ๊ฒ ๋ฉ๋๋ค.
1) ESLint ๊ท์น ์๋ฐ(
no-explicit-any
) ์กฐ์น- Next.js๋
next build
๋จ๊ณ์์ ESLint + ํ์ ์ฒดํฌ๋ฅผ ํจ๊ป ๋๋ฆฝ๋๋ค. - 3๊ฐ ์์ ์์ ๋น ๋ฅด๊ฒ ๋ณด์ฌ๋๋ฆฌ๋ ค๊ณ
any
๋ฅผ ์ด ๋ถ๋ถ์ด, ํ๋ก์ ํธ์ ESLint ๊ท์น@typescript-eslint/no-explicit-any
์ ๊ฑธ๋ ค ๋น๋๊ฐ ์คํจํ์ต๋๋ค. - ์กฐ์น:
Post
ํ์ ์ ๋ช ํํ ์ ์ธํ๊ณ , ์ํ(useState<Post[]>
)์fetch
์๋ต์ ํ์ ์ ๋ถ์ฌํ์ฌ ํด๊ฒฐํ์ต๋๋ค.
2) Route Handler ๋ ๋ฒ์งธ ์ธ์ ํ์ ๋ถ์ผ์น ์กฐ์น
Route "src/app/api/todos/[id]/route.ts" has an invalid "DELETE" export: Type "Params" is not a valid type for the function's second argument. ... Type "{ params: { id: string } }" is not a valid type for the function's second argument.
Next.js(App Router)์์๋ Route Handler ์๊ทธ๋์ฒ๋ฅผ ์๊ฒฉํ๊ฒ ๊ฒ์ฌํฉ๋๋ค.
์กฐ์น: ๋ ๋ฒ์งธ ์ธ์ ์์ฒด๋ฅผ ์์ ๊ณ ,
req.url
์์id
๋ฅผ ํ์ฑํ๋ ๋ฐฉ์์ผ๋ก ๋ณ๊ฒฝํ์ต๋๋ค.์ด๋ฌ๋ฉด Next์ โ๋ ๋ฒ์งธ ์ธ์ ํ์ ๊ฒ์ฌโ๋ฅผ ์์ ํํผํ ์ ์์ด ์์ ์ ์ผ๋ก ๋น๋๊ฐ ํต๊ณผํฉ๋๋ค.
- ๊ธฐ๋ณธ URL:
2. ํ๊ฒฝ๋ณ์ ๊ด๋ฆฌ
์ค์ ํ๋ก์ ํธ์์๋ API ํค, DB ์ ์ ์ ๋ณด ๋ฑ ๋ฏผ๊ฐํ ๊ฐ์ ์ง์ ์ฝ๋์ ๋ฃ์ผ๋ฉด ์ ๋ฉ๋๋ค.
Next.js์์๋ .env.local
ํ์ผ์ ์ฌ์ฉํฉ๋๋ค.
(1) .env.local
์์ฑ
NEXT_PUBLIC_API_URL=https://myapi.com
SECRET_KEY=abcdef123456
- NEXT_PUBLIC_ prefix๊ฐ ์์ผ๋ฉด ํด๋ผ์ด์ธํธ์์ ์ ๊ทผ ๊ฐ๋ฅ
- prefix๊ฐ ์์ผ๋ฉด ์๋ฒ์์๋ง ์ฌ์ฉ ๊ฐ๋ฅ
(2) ์ฌ์ฉํ๊ธฐ
// ์๋ฒ ์ ์ฉ (app/api/hello/route.ts)
export async function GET() {
console.log(process.env.SECRET_KEY); // ์๋ฒ ์ ์ฉ
return Response.json({ ok: true });
}
// ํด๋ผ์ด์ธํธ (app/page.tsx)
export default function Home() {
return <p>API URL: {process.env.NEXT_PUBLIC_API_URL}</p>;
}
โ ๏ธ .env.local
์ git์ ์ฌ๋ฆฌ์ง ์๋๋ก .gitignore
์ ํฌํจํด์ผ ํฉ๋๋ค.
3. ์ด๋ฏธ์ง ์ต์ ํ
Next.js์ <Image>
์ปดํฌ๋ํธ๋ ์๋์ผ๋ก:
- ํฌ๊ธฐ ์ต์ ํ
- WebP ๋ณํ
- ์ง์ฐ ๋ก๋ฉ(lazy loading)
- ๋ฐ์ํ ์ฌ์ด์ฆ ์กฐ์
์ ์ง์ํฉ๋๋ค.
(1) ์ฌ์ฉ ์์
import Image from "next/image";
import profilePic from "../public/profile.png";
export default function Profile() {
return (
<div>
<h2>ํ๋กํ</h2>
<Image src={profilePic}
alt="Profile"
width={200}
height={200}
priority
/>
</div>
);
}
๐ public/
ํด๋์ ์ด๋ฏธ์ง ์ธ์๋ ์ธ๋ถ ๋๋ฉ์ธ ์ด๋ฏธ์ง๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด
next.config.mjs
์์ ํ์ฉํด์ผ ํฉ๋๋ค:
export default {
images: {
domains: ["example.com"],
},
};
4. SEO ๊ธฐ๋ณธ ์ต์ ํ
(1) ๋ฉํ๋ฐ์ดํฐ ์ค์
app/layout.tsx
๋๋ ๊ฐ page.tsx
์์ export const metadata
๋ก ์ค์ ํฉ๋๋ค.
export const metadata = {
title: "Next.js ๊ธฐ์ด ๊ฐ์ข",
description: "Next.js ๊ฐ๋
๊ณผ ์ฌ์ฉ๋ฒ์ ๋ฐฐ์ฐ๋ ๊ธฐ์ด ๊ฐ์ข",
openGraph: {
title: "Next.js ๊ธฐ์ด ๊ฐ์ข",
description: "Next.js ๊ฐ๋
๊ณผ ์ฌ์ฉ๋ฒ์ ๋ฐฐ์ฐ๋ ๊ธฐ์ด ๊ฐ์ข",
url: "https://next-tutorial.vercel.app",
siteName: "Next.js ๊ฐ์ข",
images: [
{
url: "https://next-tutorial.vercel.app/og-image.png",
width: 1200,
height: 630,
},
],
locale: "ko_KR",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Next.js ๊ธฐ์ด ๊ฐ์ข",
description: "Next.js ๊ฐ๋
๊ณผ ์ฌ์ฉ๋ฒ์ ๋ฐฐ์ฐ๋ ๊ธฐ์ด ๊ฐ์ข",
images: ["https://next-tutorial.vercel.app/og-image.png"],
},
};
๐ ์ด๋ ๊ฒ ํ๋ฉด ๊ตฌ๊ธ ๊ฒ์ + SNS ๊ณต์ ์นด๋๊น์ง ์๋ ๋์๋ฉ๋๋ค.
(2) Canonical ๋งํฌ ์ถ๊ฐ (์ค๋ณต URL ๋ฐฉ์ง)
app/layout.tsx
export const metadata = {
metadataBase: new URL("https://next-tutorial.vercel.app"),
alternates: {
canonical: "/",
},
};
5. ์ค๋์ ์ ๋ฆฌ
- GitHub + Vercel๋ก ์๋ ๋ฐฐํฌ ํ์ดํ๋ผ์ธ ๊ตฌ์ถ
.env.local
ํ๊ฒฝ๋ณ์๋ก ๋ณด์์ฑ ํ๋ณด<Image>
์ปดํฌ๋ํธ๋ก ์ด๋ฏธ์ง ์ต์ ํmetadata
API๋ก SEO/SNS ๋์
๐ฌ ๋๊ธ
โป ๋ก๊ทธ์ธ ํ ๋๊ธ์ ์์ฑํ ์ ์์ต๋๋ค.