수정기능 만들기
수정기능 또한 Dynamic Route를 이용. 각 글에 대한 수정 페이지로 이동한 뒤, 그 페이지에 해당 글의 내용을 채워준다. 글을 채우는 것은 next.js에서는 태그 내에 defaultValue 속성을 쓰면 된다.
mongoDB를 수정할 때에는 updateOne 함수를 사용한다.
서버 측에 _id를 전달해야 하는데, 나는 이를 사용자에게는 보이지 않는 input을 하나 만들어서 그 안에 defaultValue로 원래 글의 _id를 넣어주었다.
import { connectDB } from '@/util/database';
import { ObjectId } from 'mongodb';
export default async function Edit(props) {
const db = (await connectDB).db('board')
const resolvedParams = await props.params
let origin = await db.collection('post').findOne({ _id: new ObjectId(String(resolvedParams.seq)) })
return (
<div className="p-20">
<h4>글 수정하기</h4>
<form action="/api/board/edit" method="POST">
<input name="title" placeholder="글제목" defaultValue={origin.title} />
<input name="content" placeholder="글내용" defaultValue={origin.content} />
<input name="seq" defaultValue={String(resolvedParams.seq)} style={{ display: 'none' }}></input>
<button type="submit">수정</button>
</form>
</div>
)
}
import { connectDB } from '@/util/database';
import { ObjectId } from 'mongodb';
export default async function handler(request, response) {
if (request.method == 'POST') {
const body = request.body;
let updated_title = body.title
let updated_content = body.content
let post_seq = body.seq
if (updated_title.trim() == '') {
return response.status(500).json('제목을 입력해주세요!')
} else if (updated_content.trim() == '') {
return response.status(500).json('내용을 입력해주세요!')
}
const db = (await connectDB).db('board')
const result = await db.collection('post').updateOne(
{ _id: new ObjectId(String(post_seq)) },
{ $set: { title: updated_title, content: updated_content } })
response.status(200).redirect('/list')
}
}
삭제기능 만들기 (Ajax)
자식 컴포넌트에 데이터를 전달할 때 props로 전달할 수도 있지만, 구조가 복잡해질 경우 DB에 해당 데이터가 있다고 했을 때 이를 DB로부터 가져오는 것이 더 효율적일 수 있다. 이때에 useEffect를 사용하며, 직접 DB에서 가져오는 것이 아니라 서버로부터 GET 요청 등을 통해 해당 데이터를 가져오게 된다.
다만 useEffect의 경우, 빈 HTML이 로딩된 후 실행되기 때문에 검색 노출에는 불리할 수 있다. 따라서 검색 노출을 신경써야 하는 경우 props를 통해 데이터를 전달하는 것이 더 낫다.
또한 props를 이용하는 경우 중괄호({ }) 안에 전달한 변수명 자체를 구조분해해서 명시할 수 있는데, 이 경우 자식 컴포넌트에서 'props.' 을 생략할 수 있게 된다.
서버에게 요청을 보내는 방법은, form 태그를 이용하는 방법도 있지만 Ajax를 사용하는 방법도 있다. 보통 form 태그가 응답을 받아온 후 페이지가 전환된다면, Ajax는 페이지 전환 없이 데이터 전송 및 요청을 하고 응답을 받아올 수 있다. ajax를 이용하기 위해서는 fetch 함수를 사용한다.
const resp = await fetch('/api/comment', {
method: 'POST',
body: JSON.stringify({ text: '내용' }),
headers: { 'Content-Type': 'application/json' }
})
이후 해당 resp를 json으로 변환해 응답 정보들을 활용할 수 있다.
<span onClick={async () => {
let isDelete = confirm('정말 삭제하시겠습니까?');
if (isDelete) {
const resp = await fetch('/api/board/delete', {
method: 'POST',
body: JSON.stringify({ _id: data._id }),
headers: {
'Content-Type': 'application/json',
}
});
const json = await resp.json();
if (json.success) {
alert('삭제 완료!');
router.refresh();
} else {
alert('삭제 실패: ' + json.message);
}
}
}}> 삭제</span>
/* /api/board/delete */
import { connectDB } from '@/util/database';
import { ObjectId } from 'mongodb';
export default async function handler(request, response) {
const db = (await connectDB).db('board')
const result = await db.collection('post').deleteOne({ _id: new ObjectId(String(request.body._id)) });
if (result.deletedCount == 1) {
return response.status(200).json({ success: true, message: '삭제 완료' });
}
return response.status(500).json({ success: false, message: '삭제 실패' });
}
주의할 점으로는 현재 Pages Router를 사용하고 있기 때문에 자동으로 request.body의 JSON이 파싱되는데, 만약 App Router를 사용하는 경우 JSON.parse 함수로 파싱해줘야 전달받은 자료형을 사용할 수 있다.
그런데 axios를 통해, Ajax를 훨씬 더 간편하게 사용할 수 있다. header를 지정할 필요가 없고, 응답으로 온 resp를 자동으로 JSON 파싱해주기 때문에 resp.json() 등을 하지 않아도 된다.
npm install axios
// 설치 이후
import axios from 'axios';
<span onClick={async () => {
let isDelete = confirm('정말 삭제하시겠습니까?');
if (isDelete) {
try {
const resp = await axios.delete('/api/board/delete', {
data: { _id: data._id }
});
if (resp.data.success) {
alert('삭제 완료!');
router.refresh();
} else {
throw new Error('서버 오류: ' + resp.data.status);
}
} catch (err) {
console.log(err);
}
}
}}> 삭제</span>
이때 axios 요청 시 data로 보낸 Object 객체는 request.body로 확인할 수 있고(JSON 자동 파싱),
이후 서버에서 '.json(~~)'으로 보낸 Object 객체는 resp.data 에서 확인할 수 있다.
유의사항으로, Next.js 13 이후에서는 서버 컴포넌트 한정으로, fetch 함수가 자동 캐싱 및 최적화 기능을 포함한 특별한 형태로 동작하기 때문에, 웬만하면 fetch에 익숙해지면 좋다. (클라이언트 컴포넌트는 지원 X)
참고) 애니메이션 주는 방법
1. 요소에 애니메이션 동작 전 스타일 넣기
- transition 속성도 넣기 (CSS 스타일 변화 시 서서히 바뀌는 시간 간격)
2. 원하는 시점에 애니메이션 동작 후 스타일 넣기
Query String / URL Parameter
fetch 함수를 통해서는 body에, form 태그를 통해서는 input에 넣으면 되는데 또 다른 방식으로는 Query String이 있다. 이는 url에 ?를 쓴 뒤 '데이터이름=값'의 형태로 데이터를 넘기는 방법이다. 서버에서는 이렇게 받은 데이터를 request.query를 통해 활용할 수 있다. 그리고 여러 데이터를 동시에 넘기고 싶다면 &로 구분한다.
Query String은 무엇보다도 간단하고, GET 요청으로도 데이터를 전송할 수 있다는 것이 장점이다. 다만 데이터가 많으면 더러워지고, url은 클라이언트에게 노출되기 때문에 민감 정보를 집어넣을 수 없다는 단점이 있다.
그리고 이전에 배운 Dynamic Route에서 api에 대괄호([ ])를 붙여 데이터를 전달된 값을 URL Parameter라고 한다. 이를 통해서도 서버에 데이터를 전송할 수 있다.
서버에 데이터를 전송하는 방법
- fetch 함수 (Ajax방식, response.body에 데이터를 전달)
- form 태그 (response.body에 데이터를 전달)
- Query String (response.query에 데이터를 전달)
- URL Parameter (response.query에 데이터를 전달. 단, Pages Router 한정)
글 삭제를 URL Parameter 방식으로 바꾸기
기존에는 board 폴더에 delete.js가 이를 처리했고, 삭제할 글의 _id까지 Request body 안에 넣어서 보냈다면, 이제는 board 폴더의 [post_id].js에 이 로직을 옮긴 뒤, 삭제할 글의 _id를 URL Parameter 방식으로 Request query 안에서 꺼내서 쓰게 바꾼다.
if (request.method == 'DELETE') {
const db = (await connectDB).db('board')
const result = await db.collection('post').deleteOne({ _id: new ObjectId(String(request.query._id)) });
if (result.deletedCount == 1) {
return response.status(200).json({ success: true, message: '삭제 완료' });
}
return response.status(500).json({ success: false, message: '삭제 실패' });
}
그리고 클라이언트 단의 요청 url에는 `/api/board/delete?_id=${data._id}`로 삭제할 글의 _id는 따로 보내준다.
성능 향상: 렌더링과 캐시
참고) 프로젝트 배포하기
1. 터미널에서 npm run build
- O 기호: Static Rendering
- 기본적인 페이지 렌더링 방식
- 만든 페이지를 그대로 클라이언트에게 전달
- 미리 페이지 완성본을 만들어 놓기 때문에 전송이 빠르다
- λ 기호: Dynamic Rendering
- 클라이언트가 페이지를 접속할 때마다 HTML을 새로 만들어서 전달
- fetch, dynamic route 등등을 사용하면 자동으로 이 방식으로 렌더링된다
- 서버/DB 부담이 커질 수 있다. 이를 보완하기 위해 캐싱을 활용한다.
2. 프로젝트 폴더를 클라우드 등 배포 환경에 두기
3. npm run start로 서버 실행
만약 Dynamic Rendering이 필요한 페이지가 Static Rendering 상태로 되어 있다면, 변경 사항이 발생하더라도 페이지가 바뀌지 않는다. 때문에 강제로 Dynamic Rendering으로 바꿔줄 필요가 있다. 문제 있는 페이지에 다음 코드를 추가한다.
export const dynamic = 'force-dynamic'
반대의 경우에는 다음 코드를 추가한다.
export const dynamic = 'force-static'
페이지 캐싱
페이지 완성본을 잠깐 저장해두고 재사용하는 방식이다. 페이지 뿐만 아니라 GET 요청 결과 등도 캐싱이 가능하다. 이를 통해 서버 부담을 줄일 수 있다. 이 캐시를 활용하는 방법이 바로 fetch이다. (서버 컴포넌트 한정)
await fetch('/URL', {cache: 'force-cache'})
fetch에서 자동 캐싱 기능은 기본값으로, 저렇게 적지 않아도 잘 동작한다. 다만 캐싱을 이용하고 싶지 않은 경우 'no-store' 로 설정하면 매번 서버로 요청해서 새 응답을 가져오도록 할 수 있다. (실시간 데이터가 중요한 경우)
그리고 캐싱된 데이터를 일정 시간만큼만 재사용하고, 그 이후에는 새롭게 갱신해 줄 수도 있다.
await fetch('/URL', {next : {revalidate : 60})
참고로 캐싱된 데이터는 하드 용량을 차지한다.
이를 활용하기 위해서 바로 DB에서 데이터를 바로 가져오지 않고, fetch를 통해 DB에서 가져오는 요청을 통해 데이터를 가져오는 방법을 취할 수 있다. 다만 그냥 간단하게 DB에서 데이터를 직접 가져오면서도 캐싱을 활용하고 싶다면, revalidate 변수를 이용한다.
export const revalidate = 60;
결론적으로 서버 컴포넌트에서 캐싱을 활용하고 싶다면 fetch를 쓰거나, revalidate를 명시하면 된다. 참고로 revalidate를 설정하는 방식을 ISR(Incremental Static Regeneration) 방식 캐싱이라고 한다.
참고자료
코딩애플 온라인 강의 <Next.js로 웹서비스 만들기>
'크래프톤 정글 > Equipped in 정글(나만무)' 카테고리의 다른 글
[나만무] 06.17 TIL - Next.js 학습 by 코딩애플 (0) | 2025.06.17 |
---|---|
[나만무] Jungle-Board 기본 CRUD 구현하기 (0) | 2025.06.17 |
[나만무] 06.15 TIL - Next.js 학습 by 코딩애플 (0) | 2025.06.15 |
[나만무] 06.14 TIL - Next.js 학습 by 코딩애플 (0) | 2025.06.14 |
[나만무] 06.13 TIL - Next.js 학습 by 코딩애플 (0) | 2025.06.13 |