
๋ณธ ๊ธ์ ํจ์คํธ์บ ํผ์ค โ Next.js ์ค๋ฌด ๊ฐ์ ์ค Part 8. Next.js 13์ผ๋ก ์๋ฐ ์์ฝ ํ๋ซํผ ๋ง๋ค๊ธฐ๋ฅผ ์๊ฐํ๋ฉฐ ํ์ตํ ๋ด์ฉ์ ์ ๋ฆฌํ ๊ฒ์ ๋๋ค. ๐๐ป
์ฐํ๊ธฐ ๊ธฐ๋ฅ์ ๊ตฌํํ๊ธฐ ์ํด ์ฌ์ฉ์(User)์ ์์(Room) ๊ฐ์ N:M ๊ด๊ณ๋ฅผ ํํํ๋ Like ๋ชจ๋ธ์ ์ถ๊ฐ์ ์ผ๋ก ์ค๊ณํ๋ค. ์ฌ์ฉ์๊ฐ ์์์ ์ข์์๋ฅผ ๋๋ฅด๋ฉด ํด๋น ์กฐํฉ์ Like ๋ ์ฝ๋๊ฐ ์์ฑ๋๊ณ , ์ข์์๋ฅผ ํด์ ํ๋ฉด ํด๋น ๋ ์ฝ๋๊ฐ ์ญ์ ๋๋ค.
model Like {
id Int @id @default(autoincrement())
roomId Int
userId Int
createdAt DateTime @default(now())
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId, roomId])
}
roomId, userId: ์ด๋ค ์์๋ฅผ ์ด๋ค ์ฌ์ฉ์๊ฐ ์ฐํ๋์ง๋ฅผ ๋ํ๋ด๋ ๊ด๊ณ ํคcreatedAt: ์ฐํ ์์ ๊ธฐ๋กonDelete: Cascade: ์์ ๋๋ ์ฌ์ฉ์๊ฐ ์ญ์ ๋๋ฉด ๊ด๋ จ ์ฐ ๋ฐ์ดํฐ๋ ์๋์ผ๋ก ์ญ์ ๋๋๋ก ์ค์ @@index([userId, roomId]): ํ ์ฌ์ฉ์๊ฐ ๊ฐ์ ์์๋ฅผ ์ค๋ณต์ผ๋ก ์ฐํ๋ ๊ฒ์ ๋ฐฉ์งํ๊ฑฐ๋ userId + roomId ๊ธฐ๋ฐ์ ๋น ๋ฅธ ์กฐํ๋ฅผ ์ํ ์ธ๋ฑ์ค ์ค์ 

๐ก
POST /api/likes:roomId์userId์กฐํฉ์ผ๋ก ์ฐ ๋ฐ์ดํฐ๋ฅผ ์์ฑ ๋๋ ์ญ์ GET /api/rooms: ํด๋น roomId์ ๋ํ ์ฐ ๋ชฉ๋ก ํ์ธ- ํด๋ผ์ด์ธํธ์์๋
Like๋ฐ์ดํฐ์ ์ ๋ฌด์ ๋ฐ๋ผ UI ์ํ๋ฅผ ์กฐ๊ฑด ๋ถ๊ธฐํ์ฌ ํ์
roomId๋ฅผ props๋ก ์ ๋ฌ๋ฐ์ ํ์ฌ ์์ ์ ๋ณด๋ฅผ ์กฐํ/api/rooms?id=${roomId}๋ก GET ์์ฒญisLiked ๊ฐ์ ๊ธฐ์ค์ผ๋ก ์ปดํฌ๋ํธ์ UI ์ํ๋ฅผ ๋ค๋ฅด๊ฒ ๋ ๋๋งtoggleLike ํจ์๋ฅผ ์คํํ์ฌ /api/likes/route์ POST ์์ฒญ์ ์ ์ก

const toggleLike = async () => {
// ์ฐํ๊ธฐ / ์ฐ ์ทจ์ํ๊ธฐ ๋ก์ง
if (session?.user && room) {
try {
const like = await axios.post('/api/likes', {
roomId: room.id,
})
if (like.status === 201) {
toast.success('์์๋ฅผ ์ฐํ์ต๋๋ค.')
} else {
toast.error('์ฐ์ ์ทจ์ํ์ต๋๋ค.')
}
refetch()
} catch (e) {
console.log(e)
toast.error('์ฐ ๋ชฉ๋ก์ ์ถ๊ฐํ๋๋ฐ ์คํจํ์ต๋๋ค.')
}
} else {
toast.error('๋ก๊ทธ์ธ ํ ์๋ํด์ฃผ์ธ์.')
}
}
/api/likes/route ์ฐ ํ ๊ธ APIexport async function POST(req: Request) {
const { roomId } = await req.json();
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return new NextResponse('Unauthorized', { status: 401 });
}
// ํ์ฌ ์ ์ ๊ฐ ์ด roomId์ ๋ํด ์ด๋ฏธ ์ฐํ๋์ง ํ์ธ
const existingLike = await prisma.like.findFirst({
where: {
roomId,
userId: session.user.id,
},
});
if (existingLike) {
// ์ด๋ฏธ ์ฐํ ์ํ (์ญ์ ์ฒ๋ฆฌ)
const like = await prisma.like.delete({
where: { id: existingLike.id },
});
return NextResponse.json(like, { status: 200 });
} else {
// ์ฐํ ์ ์์ (์๋ก ์์ฑ)
const like = await prisma.like.create({
data: {
roomId,
userId: session.user.id,
},
});
return NextResponse.json(like, { status: 201 });
}
}
/api/likes๋ ๋จ์ผ API๋ก ์ฐ ๋ฑ๋ก ๋ฐ ํด์ ๋ฅผ ๋ชจ๋ ์ฒ๋ฆฌ- ๋ด๋ถ์์
roomId + userId์กฐํฉ์ผ๋ก ๊ธฐ์กดLike๋ ์ฝ๋๊ฐ ์๋์ง ํ์ธ
(์กด์ฌํ๋ฉด ์ญ์ , ์์ผ๋ฉด ์๋ก ์์ฑ)- ํ๋ก ํธ์๋๋ ๋ณต์กํ ๋ถ๊ธฐ ์์ด ํ๋์
toggleLike()ํจ์๋ก ์ฒ๋ฆฌ ๊ฐ๋ฅ
- ์ฐํ๊ธฐ ๊ธฐ๋ฅ์ ๋ก๊ทธ์ธํ ์ฌ์ฉ์๋ง ์ฌ์ฉํ ์ ์๊ธฐ ๋๋ฌธ์ API ์์ฒญ ์
next-auth์getServerSession()์ ํตํด ํ์ฌ ๋ก๊ทธ์ธ๋ ์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ์๋ฒ์์ ํ์ธํ๊ณ ์ธ์ฆ ์ฌ๋ถ๋ฅผ ๊ฒ์ฆํ๋ค.
/api/rooms/route: ์ฐ ์ฌ๋ถ ํ์ธ APIroomId์ userId๋ฅผ ๊ธฐ์ค์ผ๋ก ํด๋ผ์ด์ธํธ์์ ํน์ ์์๊ฐ ๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ์ฐ ๋ชฉ๋ก์ ํฌํจ๋์ด ์๋์ง๋ฅผ ํ๋จํ๊ธฐ ์ํด ์ฌ์ฉif (id) {
// ์์ ์์ธ ์ ๋ณด ์กฐํ
const room = await prisma.room.findUnique({
where: { id: parseInt(id) },
include: {
likes: {
where: session ? { userId: session?.user?.id } : {},
},
},
});
}
include.likes๋ฅผ ํตํดRoom๋ชจ๋ธ๊ณผLike๋ชจ๋ธ ๊ฐ์ ๊ด๊ณ๋ฅผ ํ์ฉํด ์ฐ ์ ๋ณด๋ฅผ ํจ๊ป ์กฐํํ๋ค. ์ด๋ ์กฐ๊ฑด์ผ๋กuserId === ํ์ฌ ๋ก๊ทธ์ธํ ์ฌ์ฉ์ ID๋ฅผ ์ ์ฉํ์ฌ ํด๋น ์์๋ฅผ ํ์ฌ ์ฌ์ฉ์๊ฐ ์ฐํ ๊ฒฝ์ฐ์๋งlikes๋ฐฐ์ด์ ๋ฐ์ดํฐ๊ฐ ํฌํจ๋๋ค. ๋ฐ๋ผ์room.likes.length > 0์ด๋ฉด ์ฐํ ์ํ, ๊ทธ๋ ์ง ์์ผ๋ฉด ์ฐํ์ง ์์ ์ํ๋ก ํ๋จํ ์ ์๋ค.
๋๊ธ ๊ธฐ๋ฅ์ ๊ตฌํํ๊ธฐ ์ํด ์ฌ์ฉ์(User)์ ์์(Room) ๊ฐ์ ๊ด๊ณ๋ฅผ ํํํ๋ Comment ๋ชจ๋ธ์ ์ค๊ณํ๋ค. Like ๋ชจ๋ธ๊ณผ ๊ฑฐ์ ๋น์ทํ๋ฉฐ, ์ฌ์ฉ์๊ฐ ๋๊ธ์ ์์ฑํ๋ฉด ํด๋น ์์์ ์ ์ ์ ์ฐ๊ฒฐ๋ ๋๊ธ ๋ฐ์ดํฐ๊ฐ ์์ฑ๋๋ฉฐ ๋ณธ๋ฌธ ๋ด์ฉ์ body ํ๋์ ์ ์ฅ๋๋ค.
model Comment {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
roomId Int
userId String
body String
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId, roomId])
}
roomId, userId: ๋๊ธ์ ์์ฑํ ์์์ ์ฌ์ฉ์ ๊ฐ์ ๊ด๊ณ ํคbody: ๋๊ธ ๋ณธ๋ฌธ ๋ด์ฉcreatedAt: ๋๊ธ ์์ฑ ์๊ฐ (์๋ ๊ธฐ๋ก)onDelete: Cascade: ์ฌ์ฉ์ ๋๋ ์์๊ฐ ์ญ์ ๋ ๊ฒฝ์ฐ ๊ด๋ จ ๋๊ธ๋ ํจ๊ป ์ญ์ @@index([userId, roomId]): ์ ์ /์์ ๊ธฐ์ค์ผ๋ก ๋น ๋ฅธ ์กฐํ๋ฅผ ์ํ ์ธ๋ฑ์ค ์ค์ 

๋๊ธ ๊ธฐ๋ฅ์ ์ ๋ ฅ๊ณผ ์กฐํ, ์ ์ฒด ๋ณด๊ธฐ๋ก ๋๋๋ฉฐ ๋ค์๊ณผ ๊ฐ์ ์ธ ๊ฐ์ง ์ปดํฌ๋ํธ๋ก ๊ตฌ์ฑ๋๋ค. ์ด๋ ๊ฒ ์ญํ ์ ๋ถ๋ฆฌํจ์ผ๋ก์จ ์ ๋ ฅ ์์ญ๊ณผ ์กฐํ ์์ญ์ ๋ ๋ฆฝ์ ์ผ๋ก ๊ตฌ์ฑํ ์ ์๋ค.
CommentForm: ๋๊ธ์ ์
๋ ฅํ๋ ์์ญ/api/comments์ POST ์์ฒญ์ ๋ณด๋ด ๋๊ธ์ ์์ฑ
CommentList: ์ต์ ๋๊ธ ๋ชฉ๋ก(์ต๋ 6๊ฐ)์ ๋ณด์ฌ์ฃผ๋ ์ปดํฌ๋ํธCommentListModal์ ์ด์ด ์ ์ฒด ๋๊ธ ํ์ธ ๊ฐ๋ฅ
CommentListModal: ๋ชจ๋ ๋๊ธ์ ๋ชจ๋ฌ ํํ๋ก ๋ณด์ฌ์ฃผ๋ ์ปดํฌ๋ํธ/api/comments์ GET ์์ฒญ์ ๋ณด๋ด ํ์ด์ง ๋จ์๋ก ๋๊ธ ๋ถ๋ฌ์ค๊ธฐIntersectionObserver์ useInfiniteQuery๋ฅผ ํ์ฉํด ๋ฌดํ ์คํฌ๋กค ๋ฐฉ์์ผ๋ก ๊ตฌํ
๋๊ธ ๊ธฐ๋ฅ์ /api/comments/route์์ ๋ค์ ์ธ ๊ฐ์ง ๋ฉ์๋๋ก ์ฒ๋ฆฌ๋๋ค.
/api/comments: ๋ก๊ทธ์ธ๋ ์ฌ์ฉ์ ์ธ์ฆroomId์ body(๋๊ธ ๋ด์ฉ)๋ฅผ ๋ณด๋ด๋ฉด ์๋ฒ์์๋ getServerSession์ผ๋ก ์ธ์ฆ๋ ์ ์ ์ ๋ณด๋ฅผ ํ์ธํ ํ, ํด๋น ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์๋ก์ด ๋๊ธ์ ์์ฑํ๋ค.export async function POST(req: Request) {
const session = await getServerSession(authOptions);
const formData = await req.json();
const { roomId, body }: { roomId: number; body: string } = formData;
const comment = await prisma.comment.create({
data: {
roomId,
body,
userId: session?.user.id,
},
});
return NextResponse.json(comment, { status: 200 });
}
getServerSession(authOptions)๋ก ์ธ์ฆ๋ ์ฌ์ฉ์ ์ฌ๋ถ ํ์ธ401 Unauthorized ๋ฐํroomId, body, userId๋ฅผ ๋ฐํ์ผ๋ก ๋๊ธ ์์ฑ/api/comments?roomId=1&limit=6: ํน์ ์์์ ๋๊ธ์ ์กฐํsearchParams๋ฅผ ๊ธฐ์ค์ผ๋ก ๋ถ๊ธฐํ์ฌ ๋ ๊ฐ์ง ์กฐํ ๋ฐฉ์์ ์ฒ๋ฆฌํ๋ค.page ํ๋ผ๋ฏธํฐ๊ฐ ์กด์ฌํ ๊ฒฝ์ฐ (๋ฌดํ ์คํฌ๋กค)if (page) {
const count = await prisma.comment.count({ where: { roomId } });
const comments = await prisma.comment.findMany({
where: { roomId },
orderBy: { createdAt: 'desc' },
take: limit,
skip: (page - 1) * limit,
include: { user: true },
});
return {
data: comments,
totalCount: count,
totalPage: Math.ceil(count / limit),
page,
};
}
skip, take๋ฅผ ํ์ฉํ offset ๊ธฐ๋ฐ paginationcount์ ํจ๊ป totalPage๋ ๋ฐํํ์ฌ ํด๋ผ์ด์ธํธ์์ ํ์ด์ง ์ข
๋ฃ ์กฐ๊ฑด ํ๋จ ๊ฐ๋ฅinclude: { user: true }๋ก ์ฌ์ฉ์ ์ ๋ณด๋ ํจ๊ป ํฌํจpage ํ๋ผ๋ฏธํฐ๊ฐ ์๊ณ limit๋ง ์กด์ฌํ ๊ฒฝ์ฐ (์ต์ ๋๊ธ 6๊ฐ)const comments = await prisma.comment.findMany({
where: { roomId },
orderBy: { createdAt: 'desc' },
take: limit,
include: { user: true },
});
CommentList์ ์ ํฉ๋ก๊ทธ์ธํ ์ฌ์ฉ์๊ฐ ์์ ์ ํ๋ ๋ด์ญ์ ํ์ธํ ์ ์๋๋ก, ๋ง์ดํ์ด์ง์๋ โ๋ด๊ฐ ์์ฑํ ๋๊ธโ๊ณผ โ์ฐํ ์์ ๋ฆฌ์คํธโ ํ์ด์ง๋ฅผ ๊ฐ๊ฐ ๊ตฌํํ๋ค.

app/(home)/users/comments/page.tsxapp/api/comments/route.tsAPI: GET /api/comments (userId๋ฅผ ์กฐ๊ฑด์ผ๋ก ํํฐ๋ง๋ ๋๊ธ๋ง ์กฐํ)
app/(home)/users/likes/page.tsxapp/api/likes/route.tsAPI: GET /api/likes (userId ๊ธฐ์ค์ผ๋ก Like ๋ชฉ๋ก ์กฐํ ๋ฐ ์์ ์ ๋ณด ํฌํจ)