[Next.js + TS 실전] 1. 메모 수정과 삭제
입문 시리즈(1~8편)에서 추가까지 했다면, 이번 편은 수정·삭제로 CRUD를 마칩니다.
목표:updateMemo/deleteMemo· Server Action 확장 · 수정 페이지.
이 편에서 할 일
lib/memos.ts에 수정·삭제 함수 추가actions.ts에 Server Action 2개 추가/memos/[id]/edit수정 페이지 만들기- 상세 페이지에 수정 링크 · 삭제 버튼 붙이기
7편에서 만든 추가(create) 흐름을 그대로 확장합니다.
새 개념보다 같은 패턴 반복이 목표입니다.
CRUD 중 지금 어디까지?
| 상태 | 이번 편 | |
|---|---|---|
| Create | ✅ 7편 | — |
| Read | ✅ 4~6편 | — |
| Update | ❌ | ✅ |
| Delete | ❌ | ✅ |
Create를 이미 Server Action으로 했으니, Update/Delete도 폼 + Server Action으로 가면 됩니다.
1) 데이터 함수: 수정·삭제
lib/memos.ts 하단에 추가합니다.
export async function updateMemo(
id: string,
input: { title: string; content: string }
): Promise<Memo | undefined> {
await delay(300);
const index = memos.findIndex((memo) => memo.id === id);
if (index === -1) {
return undefined;
}
memos[index] = {
...memos[index],
title: input.title,
content: input.content,
};
return memos[index];
}
export async function deleteMemo(id: string): Promise<boolean> {
await delay(300);
const index = memos.findIndex((memo) => memo.id === id);
if (index === -1) {
return false;
}
memos.splice(index, 1);
return true;
}
포인트:
updateMemo→ 못 찾으면undefineddeleteMemo→ 못 찾으면falsecreatedAt,id는 그대로 두고 제목·내용만 바꿉니다
2) Server Action 확장
app/memos/actions.ts에 두 함수를 추가합니다.
"use server";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { addMemo, deleteMemo, updateMemo } from "@/lib/memos";
export async function createMemo(formData: FormData) {
const title = String(formData.get("title") ?? "").trim();
const content = String(formData.get("content") ?? "").trim();
if (!title || !content) {
throw new Error("제목과 내용을 모두 입력하세요");
}
await addMemo({ title, content });
revalidatePath("/memos");
redirect("/memos");
}
export async function updateMemoAction(formData: FormData) {
const id = String(formData.get("id") ?? "").trim();
const title = String(formData.get("title") ?? "").trim();
const content = String(formData.get("content") ?? "").trim();
if (!id || !title || !content) {
throw new Error("제목과 내용을 모두 입력하세요");
}
const updated = await updateMemo(id, { title, content });
if (!updated) {
throw new Error("메모를 찾을 수 없습니다");
}
revalidatePath("/memos");
revalidatePath(`/memos/${id}`);
redirect(`/memos/${id}`);
}
export async function deleteMemoAction(formData: FormData) {
const id = String(formData.get("id") ?? "").trim();
if (!id) {
throw new Error("삭제할 메모 id가 없습니다");
}
const deleted = await deleteMemo(id);
if (!deleted) {
throw new Error("메모를 찾을 수 없습니다");
}
revalidatePath("/memos");
redirect("/memos");
}
7편과 비교:
| create | update | delete | |
|---|---|---|---|
| 입력 | title, content | id, title, content | id |
| 성공 후 | /memos |
/memos/[id] |
/memos |
| revalidate | /memos |
/memos, /memos/[id] |
/memos |
수정은 목록 + 상세 둘 다 갱신하는 게 안전합니다.
3) 수정 페이지: /memos/[id]/edit
폴더와 파일을 만듭니다.
app/memos/[id]/edit/page.tsx
import { notFound } from "next/navigation";
import { getMemoById } from "@/lib/memos";
import { updateMemoAction } from "@/app/memos/actions";
type EditMemoPageProps = {
params: Promise<{ id: string }>;
};
export default async function EditMemoPage({ params }: EditMemoPageProps) {
const { id } = await params;
const memo = await getMemoById(id);
if (!memo) {
notFound();
}
return (
<main>
<h1>메모 수정</h1>
<form action={updateMemoAction}>
<input type="hidden" name="id" value={memo.id} />
<p>
<label>
제목
<br />
<input
type="text"
name="title"
defaultValue={memo.title}
required
/>
</label>
</p>
<p>
<label>
내용
<br />
<textarea
name="content"
rows={5}
defaultValue={memo.content}
required
/>
</label>
</p>
<button type="submit">저장</button>
</form>
</main>
);
}
포인트:
- 새 메모와 거의 같은 폼,
defaultValue로 기존 값 채움 hiddeninput으로id를 Action에 전달'use client'없음 → 서버 컴포넌트 + Server Action 조합
/memos/1/edit 에서 제목을 바꾸고 저장하면 상세 페이지 내용이 바뀌어야 합니다.
4) 상세 페이지에 수정·삭제 UI
app/memos/[id]/page.tsx를 아래처럼 확장합니다.
import Link from "next/link";
import { notFound } from "next/navigation";
import { deleteMemoAction } from "@/app/memos/actions";
import { getMemoById } from "@/lib/memos";
type MemoDetailPageProps = {
params: Promise<{ id: string }>;
};
export default async function MemoDetailPage({ params }: MemoDetailPageProps) {
const { id } = await params;
const memo = await getMemoById(id);
if (!memo) {
notFound();
}
return (
<main>
<h1>{memo.title}</h1>
<p>{memo.content}</p>
<time dateTime={memo.createdAt}>{memo.createdAt}</time>
<p>
<Link href={`/memos/${memo.id}/edit`}>수정</Link>
</p>
<form action={deleteMemoAction}>
<input type="hidden" name="id" value={memo.id} />
<button type="submit">삭제</button>
</form>
</main>
);
}
- 수정 → Link로 edit 페이지 이동
- 삭제 → 별도
<form>+deleteMemoAction
삭제는 onClick + fetch가 아니라 폼 제출로 처리합니다.
7편 추가와 같은 Server Action 패턴입니다.
동작 흐름
수정
/memos/1/edit 에서 저장
↓
updateMemoAction
↓
updateMemo (배열에서 해당 항목 교체)
↓
revalidatePath("/memos", "/memos/1")
↓
redirect("/memos/1")
삭제
/memos/1 에서 [삭제] 클릭
↓
deleteMemoAction
↓
deleteMemo (배열에서 splice)
↓
revalidatePath("/memos")
↓
redirect("/memos")
왜 삭제도 form + action인가
클릭 핸들러로도 할 수 있습니다.
"use client";
// onClick → fetch('/api/...') ...
하지만 지금 앱은 Server Action 중심입니다.
- API 라우트를 따로 안 만들어도 됨
- create/update/delete가 같은 파일·같은 패턴
- 서버에서만 데이터 변경
실무에서는 “삭제 전 확인(confirm)”을 넣을 때 클라이언트 버튼이 필요해질 수 있습니다.
입문+실전 1편에서는 가장 단순한 form 삭제로 충분합니다.
자주 하는 실수
1. 수정 폼에 id hidden을 빼먹음
→ Action에서 어떤 메모를 고칠지 모릅니다
2. value 대신 controlled input을 서버에서 쓰려 함
→ 서버 폼은 defaultValue가 맞습니다 (value + onChange는 Client)
3. update 후 revalidatePath('/memos/1')만 하고 목록은 안 함
→ 목록 카드 제목이 옛날로 남을 수 있습니다
4. action={deleteMemoAction()} 처럼 호출
→ action={deleteMemoAction} (함수 참조)
5. 삭제 후 상세 URL에 그대로 머무름
→ redirect('/memos')로 보내는 게 자연스럽습니다
이 편에서 가져갈 것
- CRUD는 Create 패턴을 Update/Delete로 확장하면 된다
- 수정·삭제도 Server Action +
FormData+ hiddenid - 수정 페이지는
/memos/[id]/edit+defaultValue revalidatePath는 바뀌는 경로를 모두 호출- 지금은 메모리 배열, 나중에 DB로 함수만 교체
다음 편 예고
[Next.js + TS 실전] 2. Prisma + SQLite로 DB 붙이기
- 서버 재시작해도 메모가 남게
lib/memos.ts의 배열을 DB 쿼리로 교체- CRUD를 “진짜 저장”으로 바꾸기
시리즈 목차 (실전)
- 메모 수정과 삭제 ← 지금
- Prisma + SQLite로 DB 붙이기 (예정)
- Vercel 배포 (예정)
입문 시리즈와의 연결
| 입문 | 실전 1편 |
|---|---|
addMemo |
updateMemo, deleteMemo |
createMemo |
updateMemoAction, deleteMemoAction |
/memos/new |
/memos/[id]/edit |
| 추가 후 목록 | 수정 후 상세, 삭제 후 목록 |
입문 7편을 이해했다면, 이번 편은 복붙·수정 수준으로 따라올 수 있습니다.
'frontend > NextJs' 카테고리의 다른 글
| # [Next.js + TS 실전] 3. Vercel에 배포하기 (0) | 2026.07.30 |
|---|---|
| # [Next.js + TS 실전] 2. Prisma + SQLite로 DB 붙이기 (1) | 2026.07.30 |
| # [Next.js + TS] 8. 정리 + 다음 학습 로드맵 (0) | 2026.07.29 |
| # [Next.js + TS] 7. 폼과 서버 액션 입문 (0) | 2026.07.29 |
| # [Next.js + TS] 6. 데이터 가져오기 (0) | 2026.07.29 |
