๋ณธ ๊ธ์ ํจ์คํธ์บ ํผ์ค โ Next.js ์ค๋ฌด ๊ฐ์ ์ค Part 8. Next.js 13์ผ๋ก ์๋ฐ ์์ฝ ํ๋ซํผ ๋ง๋ค๊ธฐ๋ฅผ ์๊ฐํ๋ฉฐ ํ์ตํ ๋ด์ฉ์ ์ ๋ฆฌํ ๊ฒ์ ๋๋ค. ๐๐ป
- Tailwind playground: Tailwind ์ค์ต ์ฌ์ดํธ
์์ฃผ ์ฌ์ฉํ๋ Tailwind ์ ํธ๋ฆฌํฐ ํด๋์ค
bg-{color}
: ๋ฐฐ๊ฒฝ์ ์ง์
text-{color}
: ํ ์คํธ ์์ ์ง์
font-{weight}
: ํฐํธ ๊ตต๊ธฐ ์ง์
p-{size}
,m-{size}
: ํจ๋ฉ/๋ง์ง ํฌ๊ธฐ ์ค์
w-{size}
,h-{size}
: ๋๋น/๋์ด ์ค์
grid
,flex
: ๋ ์ด์์ ๊ตฌ์ฑ์ฉ ํด๋์ค
rounded-{radius}
: ํ ๋๋ฆฌ ๋ฅ๊ธ๊ธฐ ์ค์
border-{width}
: ํ ๋๋ฆฌ ๊ตต๊ธฐ ์ค์
layout.tsx
๋ก ๊ณตํต ๋ ์ด์์ ์ค๊ณapp/layout.tsx
๋ Next.js 13 App Router ํ๊ฒฝ์์ ์ต์์ ๋ ์ด์์์ ์ ์ํ๋ ํ์ผ๋ก ์ ์ญ์ ์ธ UI ๊ตฌ์กฐ ๋ฐ ์ค์ ์ ๋ด๋นํ๋ค.
Next.js ๊ณต์๋ฌธ์: layouts-and-pages
- ๋ชจ๋ ํ์ด์ง์์ ๊ณตํต์ผ๋ก ์ฌ์ฉํ๋ UI ์์(ex. Header, Footer, Modal ๋ฑ)๋ฅผ ๊ตฌ์ฑํ๋ ๊ณต๊ฐ์ผ๋ก
<html>
๋ฐ<body>
ํ๊ทธ๊น์ง ํฌํจํด ์ ์ฒด HTML ๋ฌธ์์ ๋ผ๋๋ฅผ ๊ตฌ์ฑํ๋ค.layout.tsx
๋ ๊ฒฝ๋ก๋ง๋ค ๋ณ๋๋ก ์ ์ํ ์ ์์ด (app/about/layout.tsx
,app/dashboard/layout.tsx
๋ฑ) ์ค์ฒฉ ๋ ์ด์์(Nested Layout) ๊ตฌ์ฑํ๊ธฐ์ ์ ์ฉํ๋ค.- ๊ณตํต ๋ ์ด์์์ ์์
layout.tsx
์์ ๊ฐ ์น์ ์ ๊ฐ๋ณ ๋ ์ด์์์ ํ์layout.tsx
์์ ๊ด๋ฆฌํจ์ผ๋ก์จ ์ ์ง๋ณด์์ฑ๊ณผ ์ฌ์ฌ์ฉ์ฑ์ ๋์ผ ์ ์๋ค.
์ํ ๊ด๋ฆฌ(Recoil, Redux ๋ฑ), ํ
๋ง, ๋คํฌ๋ชจ๋, ์ธ์
๋ฑ๊ณผ ๊ฐ์ด ์ ์ญ์ ์ผ๋ก ์ ์ฉ๋๋ ์ค์ ์ด๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ด๊ธฐํ ์ฝ๋๋ layout.tsx
์ ์ง์ ์์ฑํ๊ธฐ๋ณด๋ค app/provider.tsx
๋ก ๋ถ๋ฆฌํด์ ๊ด๋ฆฌํ๋ ๊ฒ์ด ์ผ๋ฐ์ ์ด๋ค.
layout.tsx
์ ์ญํ ์ด ๋ช
ํํด์ง๊ณ ์ฝ๋์ ๊ฐ๋
์ฑ๊ณผ ์ ์ง๋ณด์์ฑ์ด ํฅ์๋๋ค. react-icons ํจํค์ง๋ฅผ ํตํด ๋ค์ํ ์์ด์ฝ์ ์ฝ๊ฒ ๋ถ๋ฌ์ฌ ์ ์๋ค. ์ํ๋ ๋์์ธ ์คํ์ผ์ ๊ฒ์ํ์ฌ ํ์ํ ์์ด์ฝ์ ์ ํํ๋ฉด ๋๋ค.
import { AiOutlineSearch } from 'react-icons/ai'
import { AiOutlineMenu } from 'react-icons/ai'
import { AiOutlineUser } from 'react-icons/ai'
not-found
๋ฅผ ํตํ ์ปค์คํ
404 ํ์ด์ง ๊ตฌํNext.js 13 App Router ํ๊ฒฝ์์๋ ํ๋ก์ ํธ ๋ฃจํธ์ app/
๋๋ ํ ๋ฆฌ ๋ด๋ถ์ not-found.tsx
ํ์ผ์ ์์ฑํ์ฌ 404 ํ์ด์ง๋ฅผ ์ปค์คํฐ๋ง์ด์งํ ์ ์๋ค. ํด๋น ํ์ด์ง๋ ์กด์ฌํ์ง ์๋ ๊ฒฝ๋ก๋ก ์ ๊ทผํ์ ๋ ์๋์ผ๋ก ๋ ๋๋ง๋๋ค.
Next.js ๊ณต์๋ฌธ์: not-found.js
not-found.tsx
๋ App Router ์ ์ฉ ํ์ผ๋ก ๊ธฐ์กด Pages Router์ 404.js์๋ ๋ค๋ฅด๊ฒ ์๋์ผ๋ก ๋ผ์ฐํ ์ฒ๋ฆฌ๋๋ค.- ์ปดํฌ๋ํธ ๋ด๋ถ์์
notFound()
์ ํธ๋ฆฌํฐ ํจ์๋ฅผ ํธ์ถํ์ฌ ํ๋ก๊ทธ๋๋ฐ์ ์ผ๋ก 404 ํ์ด์ง๋ฅผ ํธ๋ฆฌ๊ฑฐํ ์๋ ์๋ค.import { notFound } from 'next/navigation'; export async function generateStaticParams() { // ... if (!isValid) { notFound(); // ์กฐ๊ฑด์ ๋ฐ๋ผ 404 ํ์ด์ง๋ก ๋ฆฌ๋๋ ์ } }
Prisma Client๋ ํ์ ์์ ์ฑ๊ณผ ์๋ ์์ฑ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ORM(Object-Relational Mapping) ๋๊ตฌ๋ก SQL ์ฟผ๋ฆฌ๋ฅผ ์ง์ ์์ฑํ์ง ์๊ณ ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์์ ํ๊ฒ ์ํธ์์ฉํ ์ ์๋ค.
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
// โ
์ฌ์ฉ์ ์์ฑ (Create)
const user = await prisma.user.create({
data: {
username: 'john.doe',
email: 'john@example.com',
},
})
// โ
์ ์ฒด ์ฌ์ฉ์ ์กฐํ (Read)
const allUsers = await prisma.user.findMany()
// โ
ํน์ ์ฌ์ฉ์ + ๊ฒ์๊ธ ํฌํจ ์กฐํ (Read + ๊ด๊ณ ํฌํจ)
const userWithPosts = await prisma.user.findUnique({
where: { id: 1 },
include: { posts: true },
})
.create()
๋ฉ์๋๋ ํ ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ์๋ก ์ถ๊ฐํ ๋ ์ฌ์ฉ.findMany()
ํ
์ด๋ธ์ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ๋ ์ฌ์ฉ.findUnique()
ํน์ ์กฐ๊ฑด(์: id)์ ํด๋นํ๋ ๋จ์ผ ๋ ์ฝ๋๋ฅผ ์ฐพ์ ๋ ์ฌ์ฉinclude
์ต์
์ผ๋ก ๊ด๊ณ๊ฐ ์๋ ๋ค๋ฅธ ํ
์ด๋ธ(post ๋ฑ)์ ๋ฐ์ดํฐ๋ ํจ๊ป ์กฐํ ๊ฐ๋ฅ// โ
์ฌ์ฉ์ ์ ๋ณด ์์
const updatedUser = await prisma.user.update({
where: { id: 1 },
data: { username: 'new_username' },
})
const updatedMany = await prisma.user.updateMany({
where: { role: 'user' },
data: { role: 'member' },
})
// โ
์ฌ์ฉ์ 1๋ช
์ญ์
const deletedUser = await prisma.user.delete({
where: { id: 1 },
})
// โ
ํน์ ์กฐ๊ฑด์ ๋ง์กฑํ๋ ์ฌ์ฉ์๋ค ์ญ์
const deletedUsers = await prisma.user.deleteMany({
where: {
name: { contains: 'Kim' },
},
})
.update()
์กฐ๊ฑด์ ๋ง๋ ํ๋์ ๋ฐ์ดํฐ๋ฅผ ์์ ํ ๋ ์ฌ์ฉ.updateMany()
์กฐ๊ฑด์ ํด๋นํ๋ ์ฌ๋ฌ ๊ฐ์ ๋ ์ฝ๋๋ฅผ ํ ๋ฒ์ ์์ ๊ฐ๋ฅ .delete()
ํน์ ์กฐ๊ฑด์ ํด๋นํ๋ ํ ๊ฑด์ ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ ๋ ์ฌ์ฉํด.deleteMany()
์กฐ๊ฑด์ ํด๋นํ๋ ์ฌ๋ฌ ๋ฐ์ดํฐ๋ฅผ ํ ๋ฒ์ ์ญ์ ํ ๋ ์ฌ์ฉconst paginatedUsers = await prisma.user.findMany({
skip: 0,
take: 10,
orderBy: {
username: 'asc',
},
})
skip
: ๊ฑด๋๋ธ ํญ๋ชฉ ์ (์: 0์ด๋ฉด ์ฒ์๋ถํฐ)take
: ๊ฐ์ ธ์ฌ ํญ๋ชฉ ์ (ํ์ด์ง๋น 10๊ฐ ๋ฑ)orderBy
: ์ ๋ ฌ ๊ธฐ์ค ์ค์ (asc
: ์ค๋ฆ์ฐจ์, desc
)๊ฐ๋ฐ ์ด๊ธฐ ํ
์คํธ๋ฅผ ์ํ ๋๋ฏธ ๋ฐ์ดํฐ๋ฅผ ์๋์ผ๋ก ์ฝ์
ํ๊ณ ์ถ์ ๋๋ seed.ts
ํ์ผ์ ๋ง๋ค์ด ์ฌ์ฉํ ์ ์๋ค.
// prisma/seed.ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function seed() {
await prisma.room.createMany({
data: [
{ title: '์์ธ ๊ฐ์ฑ ์์', location: '์์ธ', price: 85000 },
{ title: '๋ถ์ฐ ์ค์
๋ทฐ ํ์
', location: '๋ถ์ฐ', price: 120000 },
{ title: '๊ฐ๋ฆ ์กฐ์ฉํ ์ฐ์ฅ', location: '๊ฐ๋ฆ', price: 95000 },
],
})
}
seed()
.catch((e) => {
throw e
})
.finally(async () => {
await prisma.$disconnect()
})
npx prisma db seed
createMany()
๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ฉด ์ฌ๋ฌ ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ํ ๋ฒ์ ์ฝ์
ํ ์ ์์ผ๋ฉฐ npx prisma db seed
๋ช
๋ น์ด๋ก ํด๋น ์๋ ๋ฐ์ดํฐ๋ฅผ ์ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฉํ ์ ์๋ค.
์ค์ ๋ฐ์ดํฐ๊ฐ ์๊ธฐ ๋๋ฌธ์ UI๋ฅผ ํ ์คํธํ๊ณ ๋ ์ด์์์ ๊ตฌ์ฑํ๊ธฐ ์ํด faker ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ฉํด ๋๋ฏธ ๋ฐ์ดํฐ๋ฅผ ์์ฑํ๋ค. (faker)
๐ก faker๋ ์ด๋ฆ, ์ฃผ์, ์ด๋ฏธ์ง, ์ค๋ช ๋ฑ ๋ค์ํ ํํ์ ๋๋ค ๋ฐ์ดํฐ๋ฅผ ์์ฝ๊ฒ ์์ฑํ ์ ์๋ ์ ํธ๋ฆฌํฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก ๋๋ฏธ ๋ฐ์ดํฐ๊ฐ ํ์ํ ํ ์คํธ ํ๊ฒฝ์์ ์ ์ฉํ๋ค. ๐คฉ
seed ํ์ผ์์ Mock ๋ฐ์ดํฐ ์์ฑ ํจ์๋ฅผ ์์ฑํ๊ณ , Prisma์ createMany()๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ฝ์ ํ๋ฉด ์ค์ ๋ก Supabase DB์ ํ ์คํธ ๋ฐ์ดํฐ๊ฐ ์ฑ์์ง๋ ๊ฒ์ ํ์ธํ ์ ์๋ค. ๐คช
์ด๋ ๊ฒ ์์ฑํ Mock ๋ฐ์ดํฐ๋ ์๋์ ๊ฐ์ด findMany()
๋ฉ์๋๋ก ์กฐํํ ์ ์๋ค.
async function getRooms() {
const prisma = new PrismaClient()
const data = await prisma.room.findMany()
return { data }
}
PrismaClient
๋ฅผ ํตํด prisma
์ธ์คํด์ค๋ฅผ ์์ฑํ๊ณ prisma.room.findMany()
๋ฅผ ํธ์ถํด room ํ
์ด๋ธ์ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์จ๋ค.์ฌํ์ง, ์ฒดํฌ์ธ/์ฒดํฌ์์ ๋ ์ง, ๊ฒ์คํธ ์๋ฅผ ์
๋ ฅ๋ฐ๋ ํํฐ UI๋ก ๊ฐ ํํฐ ํญ๋ชฉ์ useState
๋ฅผ ํตํด ๊ฐ๋ณ ์ํ๋ก ๊ด๋ฆฌํ๋ฉฐ, ํ์ฌ ์ด๋ค ํํฐ๊ฐ ํ์ฑํ๋์ด ์๋์ง๋ฅผ ๋ํ๋ด๋ detailFilter
, ์
๋ ฅ๊ฐ์ ์ ์ฅํ๋ filterValue
์ํ๋ฅผ ํจ๊ป ๊ตฌ์ฑํ๋ค. (์ ์ญ ์ํ ๊ด๋ฆฌ๋ฅผ ์ํด Recoil๋ก ๋ฆฌํฉํ ๋ง ์์ )
์ฒดํฌ์ธ/์ฒดํฌ์์ ๋ ์ง ์ ํ์ ์ํด react-calendar์ dayjs๋ฅผ ํ์ฉํด ์บ๋ฆฐ๋ UI๋ฅผ ๊ตฌ์ฑํ๋ค. ๋ ์ง ํฌ๋งท์ dayjs๋ก ์ฒ๋ฆฌํ๊ณ , react-calendar์ ๊ธฐ๋ณธ ์คํ์ผ์ global.css
์ ๊ฐ์ ธ์ ์ปค์คํฐ๋ง์ด์งํ์ฌ ์ํ๋ ํํ๋ก ์์ ํ๋ค.
๐ก react-calendar๋ ๋ฆฌ์กํธ ํ๊ฒฝ์์ ๊ฐ๋จํ ์ค์ ๋ง์ผ๋ก ๋ ์ง ์ ํ ๊ธฐ๋ฅ์ ๊ตฌํํ ์ ์๋ ์บ๋ฆฐ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก, ๊ธฐ๋ณธ์ ์ธ API๋ง์ผ๋ก๋ ๋น ๋ฅด๊ฒ ์ ์ฉ ๊ฐ๋ฅํ๋ค.
defaultValue
: ์ด๊ธฐ ์ ํ ๋ ์งformatDay
: ๋ ์ง ์ถ๋ ฅ ํฌ๋งทminDate
: ์ ํ ๊ฐ๋ฅํ ์ต์ ๋ ์ง ์ ํ๊ฒ์ ํํฐ์ ์ํ ๊ด๋ฆฌ๋ฅผ ๊ธฐ์กด useState
๊ธฐ๋ฐ์์ ์ ์ญ ์ํ ๊ด๋ฆฌ ๋๊ตฌ์ธ Recoil๋ก ์ ํํ๋ค. Recoil์ Facebook(๋ฉํ)์์ ๊ฐ๋ฐํ React ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก, atom
๊ณผ selector
๊ฐ๋
์ ๊ธฐ๋ฐ์ผ๋ก ๊ตฌ์ฑ๋๋ฉฐ ๋ณต์กํ ์ปดํฌ๋ํธ ํธ๋ฆฌ์์๋ ์ํ ๊ณต์ ๊ฐ ๊ฐํธํด์ง๋ค.
Recoil ์ฃผ์ ๊ฐ๋
Atom
: Recoil์์ ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ์ํ ๋จ์๋ก, ์ฌ๋ฌ ์ปดํฌ๋ํธ์์ ์ฝ๊ณ ์ธ ์ ์๋ ์ ์ญ ์ํSelector
: ํ๋ ์ด์์ atom ๋๋ selector ๊ฐ์ ๊ธฐ๋ฐ์ผ๋ก ๊ณ์ฐ๋ ํ์ ์ํ๋ฅผ ์์ฑํ๋ ํจ์ (๋๊ธฐ/๋น๋๊ธฐ ๋ชจ๋ ๊ฐ๋ฅ)
์์ฃผ ์ฌ์ฉํ๋ Recoil ํ
useRecoilState
atom์ ๊ฐ์ ์ฝ๊ณ ์ฐ๋ ์์ ๋ฐํuseRecoilValue
atom ๋๋ selector์ ๊ฐ๋ง ๋ฐํ (์ฝ๊ธฐ ์ ์ฉ)useSetRecoilState
๊ฐ์ ์ค์ ํ๋ ํจ์๋ง ๋ฐํuseResetRecoilState
atom ๊ฐ์ ์ด๊ธฐ ์ํ๋ก ๋๋๋ฆผ
useState
๋ก ๊ฐ๋ณ ๊ด๋ฆฌํ๋ filterValue
, detailFilter
๋ฑ์ ์ํ๋ ๊ฐ๊ฐ atom
์ผ๋ก ์ ์ํ๊ณ ์ปดํฌ๋ํธ์์๋ useRecoilState
๋ฅผ ํตํด ์ ์ญ์ ์ผ๋ก ์ ๊ทผํ๋๋ก ๋ฆฌํฉํ ๋งํ๋ค.
const [filterValue, setFilterValue] = useRecoilState(filterState)
const [detailFilter, setDetailFilter] = useRecoilState(detailFilterState)
Next.js 12๊น์ง๋ getStaticProps, getServerSideProps, getStaticPaths ๋ฑ ํน์ํ ํจ์ ๊ธฐ๋ฐ์ผ๋ก SSG/SSR/ISR์ ๊ตฌํํ์ง๋ง,
Next.js 13(App Router)๋ถํฐ๋ fetch()
ํจ์์ ์บ์ ์ค์ (cache)๊ณผ ๋ฆฌ๋ฐธ๋ฆฌ๋ฐ์ด์
(next.revalidate) ์ต์
์ ์กฐํฉํด ๋ ๋๋ง ์ ๋ต์ ์ ํํ ์ ์๋ค.
const res = await fetch('https://api.example.com/rooms', {
cache: 'no-store',
});
const res = await fetch('https://api.example.com/rooms', {
cache: 'force-cache', // ์๋ต ๊ฐ๋ฅ
});
const res = await fetch('https://api.example.com/rooms', {
next: { revalidate: 10 }, // 10์ด๋ง๋ค ๋ฐฑ๊ทธ๋ผ์ด๋ ๊ฐฑ์
});
๐ Next.js 13์ fetch()์ ๊ฐ ์บ์ฑ ์ ๋ต(SSG, SSR, ISR)์ ๋ฐ๋ฅธ ๋์ ์ฐจ์ด๋ฅผ ํ์ธํ๊ธฐ ์ํด ๋๋ค ์ซ์ API(https://www.random.org)๋ฅผ ํ์ฉํด ์ค์ต์ ์งํํ๋ค.
- SSR: ์์ฒญํ ๋๋ง๋ค ์ ์ซ์๊ฐ ๋ฐํ๋์ด ๋งค๋ฒ ๋ค๋ฅธ ๊ฐ ํ์ธ ๊ฐ๋ฅ
- SSG: ๋น๋ ์ดํ ์ซ์๊ฐ ๊ณ ์ ๋์ด ๋์ผํ ๊ฐ์ด ๊ณ์ ํ์๋จ
- ISR: ์ค์ ํ revalidate ์๊ฐ์ ๋ฐ๋ผ ์ผ์ ์ฃผ๊ธฐ๋ก ์ซ์ ๊ฐฑ์ . ๊ทธ ์ธ์ ๊ธฐ์กด ๋ฐ์ดํฐ ์ ์ง
๐ก yarn build, yarn start๋ก ์คํํ ๋ค DevTools๋ ์๋ฒ ๋ก๊ทธ, ํน์ E-Tag ์๋ต ํค๋ ๋ฑ์ ํตํด ์บ์ ์ ๋ฌด๋ฅผ ์ ํํ ๊ฒ์ฆํ ์ ์๋ค.
์์ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๊ธฐ ์ํด Next.js 13์ Route Handlers๋ฅผ ํ์ฉํด API ์๋ํฌ์ธํธ๋ฅผ ๊ตฌ์ฑํ๊ณ ํด๋ผ์ด์ธํธ ์ธก์์๋ ์ด๊ธฐ์๋ fetch
๋ก ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ๋ค๊ฐ ์ดํ์๋ React Query๋ฅผ ๋์
ํด ์์ ์ ์ด๊ณ ํจ์จ์ ์ธ ๋ฐฉ์์ผ๋ก ๋ฆฌํฉํ ๋งํ์๋ค.
Route Handlers๋?
Route Handlers๋app/
๋๋ ํ ๋ฆฌ ๋ด์์route.ts
๋๋route.js
ํ์ผ๋ก ๊ตฌ์ฑํด API ์์ฒญ์ ์ฒ๋ฆฌํ๋ ๊ธฐ๋ฅ์ด๋ค. ๊ธฐ์กดpages/api
๊ธฐ๋ฐ API Routes์ ๋น์ทํ์ง๋ง App Router ํ๊ฒฝ์ ๋ง์ถฐ ๋ ์ง๊ด์ ์ด๊ณ ์ ์ฐํ API ๊ตฌ์ฑ์ด ๊ฐ๋ฅํ๋ค.
app/api/rooms/route.ts
์ฒ๋ผ ํ์ผ์ ๊ตฌ์ฑํ๋ฉด /api/rooms
๊ฒฝ๋ก์์ HTTP ์์ฒญ์ ์ฒ๋ฆฌํ ์ ์๋ค.GET
, POST
, PUT
, PATCH
, DELETE
๋ฑ ๋ค์ํ ๋ฉ์๋๋ฅผ ์ง์ํ๋ฉฐ GET
์ ๊ฒฝ์ฐ Response ๊ฐ์ฒด๋ฅผ ๋ช
์์ ์ผ๋ก ๋ฐํํด์ผ ํ๋ค.NextResponse.json(data, { status: 200 })
ํํ๋ก ๋ฐํํ์ฌ ์ํ ์ฝ๋์ JSON ๋ฐ์ดํฐ๋ฅผ ํจ๊ป ์ ๋ฌํ๋ค.Next.js 13์ Route Handlers๋ฅผ ํ์ฉํด
app/api/rooms/route.ts
๊ฒฝ๋ก์ GET ์์ฒญ์ ์ฒ๋ฆฌํ๋ API๋ฅผ ๊ตฌ์ฑํ๋ค. ํด๋น ํธ๋ค๋ฌ์์๋ Prisma Client๋ฅผ ์ฌ์ฉํด room ํ ์ด๋ธ์ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๊ณ ,NextResponse.json()
์ ํตํด ์ํ ์ฝ๋ 200๊ณผ ํจ๊ป JSON ํํ๋ก ์๋ต์ ๋ฐํํ๋ค.
React Query๋ก CSR ๋ฐ์ดํฐ ์์ฒญํ๊ธฐ
fetch('/api/rooms')
๋ฅผ ์ฌ์ฉํด ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ์ง๋ง, ์ํ ๊ด๋ฆฌ์ ๋ก๋ฉ ์ฒ๋ฆฌ, ์๋ฌ ํธ๋ค๋ง์ ํจ์จ์ ์ผ๋ก ํ๊ธฐ ์ํด React Query์ useQuery ํ ์ผ๋ก ๋ณ๊ฒฝํ๋ค.
- ๋น๋๊ธฐ ์์ฒญ๊ณผ ๋์์ ๋ก๋ฉ, ์๋ฌ, ์ฑ๊ณต ์ํ๋ฅผ ๋ช ํํ๊ฒ ๋ถ๋ฆฌํด ๊ด๋ฆฌํ ์ ์์ด ์ ์ง๋ณด์์ ์ฉ์ดํ๋ค.
- React Query๋ ๋ฐ์ดํฐ ์บ์ฑ, ๋ฆฌํ์น, ์ค๋ฅ ์ฌ์๋ ๋ฑ ๋ค์ํ ๊ธฐ๋ฅ์ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํด ๋ณด๋ค ์์ ์ ์ธ ์ํ ๊ด๋ฆฌ๊ฐ ๊ฐ๋ฅํ๋ค.
์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์ํค๊ธฐ ์ํด, ํ์ด์ง ํ๋จ์ ๋๋ฌํ๋ฉด ์๋์ผ๋ก ๋ค์ ์์ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋ ๋ฌดํ ์คํฌ๋กค ๊ธฐ๋ฅ์ ๊ตฌํํ๋ค. ์ด๋ฅผ ์ํด react-query
์ useInfiniteQuery
๋ฅผ ํ์ฉํด ๋น๋๊ธฐ ๋ฐ์ดํฐ ํ๋ฆ์ ๊ด๋ฆฌํ๊ณ ๋ธ๋ผ์ฐ์ API์ธ Intersection Observer
๋ก ํ๋จ ์์๊ฐ ๋ทฐํฌํธ์ ์ง์
ํ๋์ง๋ฅผ ๊ฐ์งํ์ฌ ์ถ๊ฐ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ๋ ๊ตฌ์กฐ๋ก ๊ตฌ์ฑํ๋ค.
Intersection Observer๋?
Intersection Observer API๋ ๋ธ๋ผ์ฐ์ ๋ทฐํฌํธ ๋๋ ์ง์ ๋ ๋ฃจํธ(root
)์ ํน์ ์์๊ฐ ๊ต์ฐจํ๋ ์์ ์ ๊ฐ์งํด ์ฝ๋ฐฑ์ ์คํํ ์ ์๋ ๋ธ๋ผ์ฐ์ API๋ค. ์ฃผ๋ก ๋ฌดํ ์คํฌ๋กค, ์ด๋ฏธ์ง Lazy Loading, ๊ด๊ณ ๊ฐ์์ฑ ์ธก์ ๋ฑ์ ํ์ฉ๋๋ค.
Target
: ๊ด์ฐฐํ DOM ์์Observer
: ๋์ ์์๊ฐ ๊ต์ฐจ ์ํ์ผ ๋ ์ฝ๋ฐฑ์ ์คํํ๋ ์ฃผ์ฒดCallback
: ์ํ๊ฐ ๋ณ๊ฒฝ๋์์ ๋ ์คํ๋๋ ํจ์Options
:root
,rootMargin
,threshold
๋ฑ ๊ฐ์ง ์กฐ๊ฑด ์ค์
๐ ๋ฌดํ ์คํฌ๋กค ๊ตฌํ ์์
1. ๊ธฐ์กดfindMany()
๋ก ์ ๋ถ ๋ถ๋ฌ์ค๋ ๋ฐฉ์์์ ํ์ด์ง๋ค์ด์ ๊ธฐ๋ฐ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ก ๋ณ๊ฒฝ
2.useInfiniteQuery
๋ฅผ ์ฌ์ฉํด ํ์ด์ง ๋จ์๋ก ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ๊ณ ,getNextPageParam
์ ์ ์ํด ๋ค์ ์์ฒญ ์กฐ๊ฑด์ ์ค์
3.Intersection Observer
๋ก ํ๋จ ๊ฐ์ ์์(ref
)๋ฅผ ๋ฑ๋กํ๊ณ , ํด๋น ์์๊ฐ ๋ทฐํฌํธ์ ๋ค์ด์ค๋ฉดfetchNextPage()
ํธ์ถ
4.isFetching
,isFetchingNextPage
,hasNextPage
๊ฐ์ ๊ธฐ๋ฐ์ผ๋ก ๋ก๋ฉ ์ํ UI๋ฅผ ์ ์ดํ์ฌ ์ฌ์ฉ์ ํ๋ฆ์ ์์ฐ์ค๋ฝ๊ฒ ์ ์ง
Next.js 13์ App Router์์๋ error.js
ํ์ผ์ ํตํด ๊ฒฝ๋ก๋ณ๋ก ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ๊ตฌ์ฑํ ์ ์๋ค. ์ด ํ์ผ์ ํด๋น ์ธ๊ทธ๋จผํธ์ React Error Boundary
๋ฅผ ์๋ ์์ฑํ๋ฉฐ ์ค์ฒฉ๋ ๊ฒฝ๋ก ํ์์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ ๋ ํด๋น ๋ธ๋ก๋ง ๋ถ๋ฆฌํด ๋ ๋๋ง์ ์ ์ดํ๋ค.
์ค๋ฅ๊ฐ ๋ฐ์ํด๋ ์ ์ฒด ์ ํ๋ฆฌ์ผ์ด์
์ด ์ค๋จ๋์ง ์๊ณ , ๋ฌธ์ ๊ฐ ์๊ธด ๋ถ๋ถ๋ง ๊ฒฉ๋ฆฌ๋์ด ๋๋จธ์ง ํ๋ฉด์ ์ ์์ ์ผ๋ก ๋์ํ๊ณ reset()
ํจ์๋ฅผ ํ์ฉํด ์ฌ์ฉ์๊ฐ ์ง์ ๋ค์ ์๋ํ ์ ์๋ ๋ณต๊ตฌ UI๋ฅผ ๊ตฌํํ ์ ์๋ค.
์ค์ฒฉ ๋ผ์ฐํธ์์์ ์๋ฌ ์ฒ๋ฆฌ
Next.js App Router์์๋ ์ค์ฒฉ ๊ตฌ์กฐ๋ฅผ ๋ฐ๋ฅด๊ธฐ ๋๋ฌธ์ rror.js ํ์ผ ์ญ์ ์ค์ฒฉ๋ ๊ฒฝ๋ก๋ง๋ค ์ ์ํ ์ ์๋ค. ์ด๋ฅผ ํตํด ๊ฒฝ๋ก๋ณ๋ก ์๋ฌ UI๋ฅผ ๋ค๋ฅด๊ฒ ๊ตฌ์ฑํ๊ฑฐ๋, ๋ ์ธ๋ฐํ ์ค๋ฅ ํธ๋ค๋ง์ ๊ตฌํํ ์ ์๋ค.
app/products/error.tsx
: ์ํ ํ์ด์ง ๊ด๋ จ ์๋ฌ ์ ์ฉ UIapp/global-error.tsx
: ์ฑ ์ ์ญ์์ ๋ฐ์ํ๋ ์์ธ ์ฒ๋ฆฌ
๐ก
error.js
๋ React Error Boundary์ฒ๋ผ ํด๋ผ์ด์ธํธ์์ ๋์ํด์ผ ํ๋ฏ๋ก, ๋ฐ๋์'use client'
์ง์์ด๋ฅผ ์ ์ธํด์ผ ์ ์์ ์ผ๋ก ์๋ํ๋ค.
์ ์ฒด ํ๋ฉด์ด ํฐ ํ๋ฉด(Fallback UI)์ผ๋ก ๋ฐ๋๋ ๊ฒ์ ์ฌ์ฉ์ ์
์ฅ์์ ํฐ ์ดํ ์์ธ์ด ๋ ์ ์๋ค. Next.js 13์ error.js
๋ฅผ ํ์ฉํ๋ฉด ๊ฒฝ๋ก๋ณ๋ก ์ ์ ํ ์๋ฌ ์ฒ๋ฆฌ UI๋ฅผ ์ ๊ณตํ ์ ์์ด, ์๋น์ค ์ ๋ขฐ๋์ ์ฌ์ฉ์ ๊ฒฝํ ๋ชจ๋๋ฅผ ์งํค๋ ์๋ฌ ๋์์ด ๊ฐ๋ฅํ๋ค.