가끔은 sudo 키워드를 인생에도 적용할 수 있으면 좋겠다는 생각을 한다.
의지와 상관없이 거부할 수 없는 명령같은거.
이상한 생각 금지.
최근들어 벨로그가 자주 먹통이 되는게 참 안타깝다.
넥스트 CRUD 세번째.
업데이트 로직은 리액트에서 업데이트 자체가 어렵다기 보다는
업데이트 레이아웃을 컨트롤 하는 부분이 어려웠던 것 같다.
사실 CRUD api 로직은 전부 비슷비슷해서
명령을 보낸다 -> 수행한다 의 로직을 따르는데 (프로그래밍이 대부분 이렇지만)
리액트에서의 업데이트는 기존의 데이터를 불러와서 바인딩 하고,
그 데이터를 수정하고 업데이트 로직을 수행할 때 기존의 데이터를 어떤식으로 업데이트 할 지.
이 말로는 간단해보이는 로직이 정말 수많은 방법이 있고,
예외사항이나 (예를들어 이미지나 캔버스) 시행착오도 많이 겪었던 것 같다.
넥스트는 어떻게 다른지 알아보자.
//update/[postId]/page.js
import postCollectionAPI from "@/util/postCollectionAPI";
import { ObjectId } from "mongodb";
export default async function Update({ params }) {
const data = await postCollectionAPI("findOne", {
_id: new ObjectId(params.postId),
});
return (
<div>
<h4>수정페이지</h4>
<form action="/api/update" method="POST">
<input name="title" defaultValue={data.title} />
<input name="content" defaultValue={data.content} />
<input
style={{ display: "none" }}
name="_id"
defaultValue={data._id.toString()}
/>
<button type="submit">수정하기</button>
</form>
</div>
);
}
list 페이지에서 버튼을 누르면 해당 페이지로 이동한다.
post 페이지와 동일한 ui 에 서버 컴포넌트로 데이터를 바인딩했다.
_id 값은 유동적으로 변해야 하기때문에 dynamic route 를 적용했다.
params.postId 에 _id 값을 담은 컴포넌트.
mongo db 의 고유 id는 new ObjectId 형식이기 때문에 저런식으로 바인딩 해줘야 한다.
주의할 점은 form elements 는 DELETE, UPDATE 등의 method 를 인식할 수 없으므로
POST 메소드로 설정해야 한다는 점.
굉장히 보기에 불편하지 않을 수 없다.
그리고 submit 되었을 때 서버에서 id 로 검색을 해야 하기 때문에
_id 값을 input 에 담아서 같이 보내주고 화면에는 보이지 않게 display : none 을
설정해준 모습.
import { ObjectId } from "mongodb";
import { connectDB } from "./db";
export default async function postCollectionAPI(
method,
query = {},
options = {}
) {
const client = await connectDB;
const db = client.db("forum");
const postCollection = db.collection("post");
let result;
switch (method) {
case "find":
result = postCollection.find(query, options).toArray();
break;
case "findOne":
result = postCollection.findOne(query, options);
break;
case "insertOne":
postCollection.insertOne(query);
break;
case "updatePost":
const { _id, title, content } = query;
postCollection.updateOne(
{ _id: new ObjectId(_id) },
{ $set: { title, content } }
);
break;
default:
throw new Error("잘못된 요청입니다.");
}
return result;
}
이제 postCollectionAPI 함수에 updatePost case 를 추가해줬다.
사실, 해당 함수의 method case 를 find, findOne 처럼 그대로 해버리면
함수의 확장성이 떨어진다.
연습삼아 만든 api 라서 그대로 차용하고 있지만
update 기능은 차후에 확장가능성이 다분하므로 updatePost 로 명명해줬다.
이런 패턴이 옳은 패턴인지는 다른 개발자분들의 프로젝트를 봐야겠지만,
현재까지는 크게 나쁘다고 생각하지 않아 적용하고 있다.
다른 개발자분들이라고 해서
const client = await connectDB;
const db = client.db("forum");
const postCollection = db.collection("post");
이런 코드가 모든 컴포넌트에 존재하는걸 좋아할 리가 없으므로.
mongo DB의 updateOne 메소드는 특정조건을 설정해 해당 조건을 만족하는 데이터 중
가장 먼저 찾을 수 있는 데이터를 가져와 원하는 값으로 수정해준다.
첫번째 인자에 조건, $set key 값의 value 가 수정할 내용이 된다.
위의 조건은 id로 게시물을 찾는 예시.
// pages/api/update.js
import postCollectionAPI from "@/util/postCollectionAPI";
export default function update(req, res) {
if (req.method === "POST") {
const { _id, title, content } = req.body;
postCollectionAPI("updatePost", { _id, title, content });
res.redirect(302, "/list");
}
}
그러면 api 코드에서 전달받은 데이터를 알맞게 넣어주기만 하면 완성.
응답으로 list 페이지로 redirect 될 수 있도록 해줬다.
연습용이므로 크게 예외처리를 신경쓰지는 않았지만
try catch 문을 이용해서 예외처리를 해주는 것이 좋다.
delete 기능은 훨씬 간단하다.
"use client";
import { useRouter } from "next/navigation";
import DetailLink from "./DetailLink";
import UpdateLink from "./UpdateLink";
export default function Listitem({ listitem }) {
const handlePostDelete = (id) => {
fetch(`/api/remove?id=${id}`, {
method: "DELETE",
})
return (
<>
{listitem.map((item) => (
<div className="list-item" key={item._id}>
<DetailLink postId={item._id.toString()}>
<h4>{item.title}</h4>
<p>{item.date}</p>
</DetailLink>
<UpdateLink postId={item._id.toString()} />
<button onClick={() => handlePostDelete(item._id.toString())}>
삭제하기
</button>
</div>
))}
</>
);
}
fetch 등의 기능은 서버 컴포넌트에서 사용할 수 없으니까
클라이언트 컴포넌트로 listItem 을 따로 만들어줬다.
위의 컴포넌트는 list 서버 컴포넌트에서 props 로 데이터를 받아 바인딩하고,
버튼에 id를 담아 handlePostDelete 함수로 삭제기능을 구현한 예시.
가장 주의해야 할 점.
"DELETE" 메소드는 body 를 사용하지 않는 것이 좋다.
대부분의 HTTP 클라이언트가 DELETE 메소드에 body 를 포함하는 것을 허용하지 않는다.
따라서 쿼리 스트링이나 URL 파라미터로 보내는 방법을 선택했다.
`/api/remove?id=${id}`
이런 식으로 ? 기호 뒤에 key=value 형식으로 넣어주면 된다.
이렇게 보낸 쿼리 스트링은
import postCollectionAPI from "@/util/postCollectionAPI";
export default async function remove(req, res) {
if (req.method === "DELETE") {
await postCollectionAPI("deleteOne", req.query.id);
res.status(200).json();
}
}
req.query 메소드로 받아올 수 있다.
body 와 query 의 차이에 주의할 것.
import { ObjectId } from "mongodb";
import { connectDB } from "./db";
export default async function postCollectionAPI(
method,
query = {},
options = {}
) {
const client = await connectDB;
const db = client.db("forum");
const postCollection = db.collection("post");
let result;
switch (method) {
case "find":
result = postCollection.find(query, options).toArray();
break;
case "findOne":
result = postCollection.findOne(query, options);
break;
case "insertOne":
postCollection.insertOne(query);
break;
case "updatePost":
const { _id, title, content } = query;
postCollection.updateOne(
{ _id: new ObjectId(_id) },
{ $set: { title, content } }
);
break;
case "deleteOne":
postCollection.deleteOne({ _id: new ObjectId(query) });
break;
default:
throw new Error("잘못된 요청입니다.");
}
return result;
}
deleteOne 메소도 위의 update 와 비슷하게 조건을 설정해 해당 조건을 만족하는
데이터를 삭제하면 된다.
넥스트를 공부하다 보면
새로운 기술을 배운다! 라는 느낌보다는
리액트를 구조적으로 어떻게 설계하면 조금 더 효율적인 컴포넌트를 만들 수 있다!
를 배워나가는 것 같다.
물론 추가된 멋지고 유용한 기능들도 많지만
디렉토리 기반 라우팅 같은 것들로 미루어보아 조금 더 리액트를 우아하게 다룰 수 있도록
해준다는 느낌이 강하다.
서버 쪽 코드는 지금과 같이 얕은 수준에서는 크게 어려움이 없는 수준이고
오히려 직접 설계할 수 있으니까 구조적으로 조금 더 생각하게 되는 느낌.