๐Ÿก ์ˆ™๋ฐ• ์˜ˆ์•ฝ ํ”Œ๋žซํผ: ๊ณตํ†ต ๋ ˆ์ด์•„์›ƒ๊ณผ ๋”๋ฏธ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ, ๋ฉ”์ธ ํŽ˜์ด์ง€ ๊ตฌํ˜„ (2)

ฮต( ฮต ห™ยณห™)ะท โ—‹ยบยท2025๋…„ 7์›” 13์ผ
0
post-thumbnail

๋ณธ ๊ธ€์€ ํŒจ์ŠคํŠธ์บ ํผ์Šค โ€“ Next.js ์‹ค๋ฌด ๊ฐ•์˜ ์ค‘ Part 8. Next.js 13์œผ๋กœ ์ˆ™๋ฐ• ์˜ˆ์•ฝ ํ”Œ๋žซํผ ๋งŒ๋“ค๊ธฐ๋ฅผ ์ˆ˜๊ฐ•ํ•˜๋ฉฐ ํ•™์Šตํ•œ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๐Ÿ™๐Ÿป


๐Ÿ’จ ์ „์—ญ ๋ ˆ์ด์•„์›ƒ ๊ตฌ์„ฑํ•˜๊ธฐ (Tailwind CSS)

  • Tailwind: HTML, CSS, JavaScript๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ˜์‘ํ˜• ๋””์ž์ธ์„ ์‰ฝ๊ฒŒ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ๋Š” CSS ํ”„๋ ˆ์ž„์›Œํฌ

์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” 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์—์„œ ๊ด€๋ฆฌํ•จ์œผ๋กœ์จ ์œ ์ง€๋ณด์ˆ˜์„ฑ๊ณผ ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ๋‹ค.

โ–ช๏ธ ์ „์—ญ Provider ๊ตฌ์กฐ ๋ถ„๋ฆฌ

์ƒํƒœ ๊ด€๋ฆฌ(Recoil, Redux ๋“ฑ), ํ…Œ๋งˆ, ๋‹คํฌ๋ชจ๋“œ, ์„ธ์…˜ ๋“ฑ๊ณผ ๊ฐ™์ด ์ „์—ญ์ ์œผ๋กœ ์ ์šฉ๋˜๋Š” ์„ค์ •์ด๋‚˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ดˆ๊ธฐํ™” ์ฝ”๋“œ๋Š” layout.tsx์— ์ง์ ‘ ์ž‘์„ฑํ•˜๊ธฐ๋ณด๋‹ค app/provider.tsx๋กœ ๋ถ„๋ฆฌํ•ด์„œ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ผ๋ฐ˜์ ์ด๋‹ค.

  • layout.tsx์˜ ์—ญํ• ์ด ๋ช…ํ™•ํ•ด์ง€๊ณ  ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์ด ํ–ฅ์ƒ๋œ๋‹ค.

โ–ช๏ธ React Icons๋ฅผ ํ™œ์šฉํ•œ ์•„์ด์ฝ˜ ์ ์šฉ

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 ๊ธฐ์ดˆ ๋ฌธ๋ฒ•๊ณผ Mock ๋ฐ์ดํ„ฐ ์„ธํŒ…

Prisma Client๋Š” ํƒ€์ž… ์•ˆ์ •์„ฑ๊ณผ ์ž๋™ ์™„์„ฑ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ORM(Object-Relational Mapping) ๋„๊ตฌ๋กœ SQL ์ฟผ๋ฆฌ๋ฅผ ์ง์ ‘ ์ž‘์„ฑํ•˜์ง€ ์•Š๊ณ ๋„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ์•ˆ์ „ํ•˜๊ฒŒ ์ƒํ˜ธ์ž‘์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

โ–ช๏ธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ(Create) ๋ฐ ์กฐํšŒ(Read)

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 ๋“ฑ)์˜ ๋ฐ์ดํ„ฐ๋„ ํ•จ๊ป˜ ์กฐํšŒ ๊ฐ€๋Šฅ

โ–ช๏ธ ๋ฐ์ดํ„ฐ ์ˆ˜์ •(Update) ๋ฐ ์‚ญ์ œ(Delete)

// โœ… ์‚ฌ์šฉ์ž ์ •๋ณด ์ˆ˜์ •
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() ์กฐ๊ฑด์— ํ•ด๋‹นํ•˜๋Š” ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ ๋ฒˆ์— ์‚ญ์ œํ•  ๋•Œ ์‚ฌ์šฉ

โ–ช๏ธ Pagination(ํŽ˜์ด์ง€๋„ค์ด์…˜) ๋ฐ ์ •๋ ฌ

const paginatedUsers = await prisma.user.findMany({
  skip: 0,
  take: 10,
  orderBy: {
    username: 'asc',
  },
})
  • skip: ๊ฑด๋„ˆ๋›ธ ํ•ญ๋ชฉ ์ˆ˜ (์˜ˆ: 0์ด๋ฉด ์ฒ˜์Œ๋ถ€ํ„ฐ)
  • take: ๊ฐ€์ ธ์˜ฌ ํ•ญ๋ชฉ ์ˆ˜ (ํŽ˜์ด์ง€๋‹น 10๊ฐœ ๋“ฑ)
  • orderBy: ์ •๋ ฌ ๊ธฐ์ค€ ์„ค์ • (asc: ์˜ค๋ฆ„์ฐจ์ˆœ, desc)

โ–ช๏ธ Seed ํŒŒ์ผ๋กœ ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ์‚ฝ์ž…

๊ฐœ๋ฐœ ์ดˆ๊ธฐ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ๋”๋ฏธ ๋ฐ์ดํ„ฐ๋ฅผ ์ž๋™์œผ๋กœ ์‚ฝ์ž…ํ•˜๊ณ  ์‹ถ์„ ๋•Œ๋Š” 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 ๋ช…๋ น์–ด๋กœ ํ•ด๋‹น ์‹œ๋“œ ๋ฐ์ดํ„ฐ๋ฅผ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.


โ–ช๏ธ Faker ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ๋”๋ฏธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ

์‹ค์ œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์— 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์— ๋งคํ•‘ํ•ด ํ™”๋ฉด์— ์ถœ๋ ฅ๋œ๋‹ค.

๐Ÿ“Š ๋ฉ”์ธ ํŽ˜์ด์ง€ ๊ฐœ๋ฐœํ•˜๊ธฐ

โ–ช๏ธ ๊ฒ€์ƒ‰ ํ•„ํ„ฐ ๊ตฌ์„ฑ

์—ฌํ–‰์ง€, ์ฒดํฌ์ธ/์ฒดํฌ์•„์›ƒ ๋‚ ์งœ, ๊ฒŒ์ŠคํŠธ ์ˆ˜๋ฅผ ์ž…๋ ฅ๋ฐ›๋Š” ํ•„ํ„ฐ UI๋กœ ๊ฐ ํ•„ํ„ฐ ํ•ญ๋ชฉ์€ useState๋ฅผ ํ†ตํ•ด ๊ฐœ๋ณ„ ์ƒํƒœ๋กœ ๊ด€๋ฆฌํ•˜๋ฉฐ, ํ˜„์žฌ ์–ด๋–ค ํ•„ํ„ฐ๊ฐ€ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋Š”์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” detailFilter, ์ž…๋ ฅ๊ฐ’์„ ์ €์žฅํ•˜๋Š” filterValue ์ƒํƒœ๋ฅผ ํ•จ๊ป˜ ๊ตฌ์„ฑํ–ˆ๋‹ค. (์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด Recoil๋กœ ๋ฆฌํŒฉํ† ๋ง ์˜ˆ์ •)


โ–ช๏ธ ๋‹ฌ๋ ฅ ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„ (react-calendar)

์ฒดํฌ์ธ/์ฒดํฌ์•„์›ƒ ๋‚ ์งœ ์„ ํƒ์„ ์œ„ํ•ด react-calendar์™€ dayjs๋ฅผ ํ™œ์šฉํ•ด ์บ˜๋ฆฐ๋” UI๋ฅผ ๊ตฌ์„ฑํ–ˆ๋‹ค. ๋‚ ์งœ ํฌ๋งท์€ dayjs๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ , react-calendar์˜ ๊ธฐ๋ณธ ์Šคํƒ€์ผ์€ global.css์— ๊ฐ€์ ธ์™€ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•˜์—ฌ ์›ํ•˜๋Š” ํ˜•ํƒœ๋กœ ์ˆ˜์ •ํ–ˆ๋‹ค.

๐Ÿ’ก react-calendar๋Š” ๋ฆฌ์•กํŠธ ํ™˜๊ฒฝ์—์„œ ๊ฐ„๋‹จํ•œ ์„ค์ •๋งŒ์œผ๋กœ ๋‚ ์งœ ์„ ํƒ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ์บ˜๋ฆฐ๋” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ, ๊ธฐ๋ณธ์ ์ธ API๋งŒ์œผ๋กœ๋„ ๋น ๋ฅด๊ฒŒ ์ ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค.

  • defaultValue: ์ดˆ๊ธฐ ์„ ํƒ ๋‚ ์งœ
  • formatDay: ๋‚ ์งœ ์ถœ๋ ฅ ํฌ๋งท
  • minDate: ์„ ํƒ ๊ฐ€๋Šฅํ•œ ์ตœ์†Œ ๋‚ ์งœ ์ œํ•œ

โ–ช๏ธ Recoil๋กœ ํ•„ํ„ฐ ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ

๊ฒ€์ƒ‰ ํ•„ํ„ฐ์˜ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ๊ธฐ์กด 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 13์˜ Data Fetching ์ „๋žต ์ •๋ฆฌ (SSR / SSG / ISR)

Next.js 12๊นŒ์ง€๋Š” getStaticProps, getServerSideProps, getStaticPaths ๋“ฑ ํŠน์ˆ˜ํ•œ ํ•จ์ˆ˜ ๊ธฐ๋ฐ˜์œผ๋กœ SSG/SSR/ISR์„ ๊ตฌํ˜„ํ–ˆ์ง€๋งŒ,
Next.js 13(App Router)๋ถ€ํ„ฐ๋Š” fetch() ํ•จ์ˆ˜์— ์บ์‹œ ์„ค์ •(cache)๊ณผ ๋ฆฌ๋ฐธ๋ฆฌ๋ฐ์ด์…˜(next.revalidate) ์˜ต์…˜์„ ์กฐํ•ฉํ•ด ๋ Œ๋”๋ง ์ „๋žต์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋‹ค.

  • SSR(Server-Side Rendering) ๋ฐฉ์‹
    ์š”์ฒญ ์‹œ๋งˆ๋‹ค ์„œ๋ฒ„์—์„œ HTML์„ ์ƒˆ๋กœ ๋ Œ๋”๋งํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ตœ์‹  ๋ฐ์ดํ„ฐ ๋ณด์žฅ์ด ํ•„์š”ํ•œ ํŽ˜์ด์ง€์— ์ ํ•ฉํ•˜๋‹ค
const res = await fetch('https://api.example.com/rooms', {
  cache: 'no-store',
});
  • SSG(Static Site Generation)
    ๋นŒ๋“œ ์‹œ์ ์— ์ •์  HTML๋กœ ๋ Œ๋”๋งํ•˜๊ณ  ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ์บ์‹œํ•ด ์ตœ๊ณ ์˜ ์„ฑ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ 'force-cache'๋กœ ๋ช…์‹œ์ ์œผ๋กœ ์ž‘์„ฑํ•˜์ง€ ์•Š์•„๋„ ์ ์šฉ๋œ๋‹ค.
const res = await fetch('https://api.example.com/rooms', {
  cache: 'force-cache', // ์ƒ๋žต ๊ฐ€๋Šฅ
});
  • ISR(Incremental Static Regeneration)
    ์ •์ ์œผ๋กœ ์ƒ์„ฑ๋œ ํŽ˜์ด์ง€๋ฅผ ์ผ์ • ์ฃผ๊ธฐ๋กœ ์ž๋™ ๊ฐฑ์‹ ํ•ด SSG์˜ ์„ฑ๋Šฅ๊ณผ ์ตœ์‹ ์„ฑ์„ ๋ชจ๋‘ ๋งŒ์กฑํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.
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๋Š” ๋ฐ์ดํ„ฐ ์บ์‹ฑ, ๋ฆฌํŽ˜์น˜, ์˜ค๋ฅ˜ ์žฌ์‹œ๋„ ๋“ฑ ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ๊ธฐ๋ณธ์œผ๋กœ ์ œ๊ณตํ•ด ๋ณด๋‹ค ์•ˆ์ •์ ์ธ ์ƒํƒœ ๊ด€๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค.

โ–ช๏ธ Intersection Observer ๊ธฐ๋ฐ˜ ๋ฌดํ•œ ์Šคํฌ๋กค ๊ตฌํ˜„

์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œํ‚ค๊ธฐ ์œ„ํ•ด, ํŽ˜์ด์ง€ ํ•˜๋‹จ์— ๋„๋‹ฌํ•˜๋ฉด ์ž๋™์œผ๋กœ ๋‹ค์Œ ์ˆ™์†Œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฌดํ•œ ์Šคํฌ๋กค ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ–ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด 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 Error Handling ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

Next.js 13์˜ App Router์—์„œ๋Š” error.js ํŒŒ์ผ์„ ํ†ตํ•ด ๊ฒฝ๋กœ๋ณ„๋กœ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด ํŒŒ์ผ์€ ํ•ด๋‹น ์„ธ๊ทธ๋จผํŠธ์— React Error Boundary๋ฅผ ์ž๋™ ์ƒ์„ฑํ•˜๋ฉฐ ์ค‘์ฒฉ๋œ ๊ฒฝ๋กœ ํ•˜์œ„์—์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ํ•ด๋‹น ๋ธ”๋ก๋งŒ ๋ถ„๋ฆฌํ•ด ๋ Œ๋”๋ง์„ ์ œ์–ดํ•œ๋‹ค.

์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ด๋„ ์ „์ฒด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ค‘๋‹จ๋˜์ง€ ์•Š๊ณ , ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธด ๋ถ€๋ถ„๋งŒ ๊ฒฉ๋ฆฌ๋˜์–ด ๋‚˜๋จธ์ง€ ํ™”๋ฉด์€ ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•˜๊ณ  reset() ํ•จ์ˆ˜๋ฅผ ํ™œ์šฉํ•ด ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ๋‹ค์‹œ ์‹œ๋„ํ•  ์ˆ˜ ์žˆ๋Š” ๋ณต๊ตฌ UI๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.

์ค‘์ฒฉ ๋ผ์šฐํŠธ์—์„œ์˜ ์—๋Ÿฌ ์ฒ˜๋ฆฌ
Next.js App Router์—์„œ๋Š” ์ค‘์ฒฉ ๊ตฌ์กฐ๋ฅผ ๋”ฐ๋ฅด๊ธฐ ๋•Œ๋ฌธ์— rror.js ํŒŒ์ผ ์—ญ์‹œ ์ค‘์ฒฉ๋œ ๊ฒฝ๋กœ๋งˆ๋‹ค ์ •์˜ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๊ฒฝ๋กœ๋ณ„๋กœ ์—๋Ÿฌ UI๋ฅผ ๋‹ค๋ฅด๊ฒŒ ๊ตฌ์„ฑํ•˜๊ฑฐ๋‚˜, ๋” ์„ธ๋ฐ€ํ•œ ์˜ค๋ฅ˜ ํ•ธ๋“ค๋ง์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • app/products/error.tsx: ์ƒํ’ˆ ํŽ˜์ด์ง€ ๊ด€๋ จ ์—๋Ÿฌ ์ „์šฉ UI
  • app/global-error.tsx: ์•ฑ ์ „์—ญ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

๐Ÿ’ก error.js๋Š” React Error Boundary์ฒ˜๋Ÿผ ํด๋ผ์ด์–ธํŠธ์—์„œ ๋™์ž‘ํ•ด์•ผ ํ•˜๋ฏ€๋กœ, ๋ฐ˜๋“œ์‹œ 'use client' ์ง€์‹œ์–ด๋ฅผ ์„ ์–ธํ•ด์•ผ ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•œ๋‹ค.

์ „์ฒด ํ™”๋ฉด์ด ํฐ ํ™”๋ฉด(Fallback UI)์œผ๋กœ ๋ฐ”๋€Œ๋Š” ๊ฒƒ์€ ์‚ฌ์šฉ์ž ์ž…์žฅ์—์„œ ํฐ ์ดํƒˆ ์š”์ธ์ด ๋  ์ˆ˜ ์žˆ๋‹ค. Next.js 13์˜ error.js๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๊ฒฝ๋กœ๋ณ„๋กœ ์ ์ ˆํ•œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ UI๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์–ด, ์„œ๋น„์Šค ์‹ ๋ขฐ๋„์™€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๋ชจ๋‘๋ฅผ ์ง€ํ‚ค๋Š” ์—๋Ÿฌ ๋Œ€์‘์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

profile
์ฐจ๊ณก์ฐจ๊ณก ์Œ“์•„๋‘๊ธฐ ๐Ÿ’ญ

0๊ฐœ์˜ ๋Œ“๊ธ€