# [Next.js + TS 실전] 1. 메모 수정과 삭제

[Next.js + TS 실전] 1. 메모 수정과 삭제

입문 시리즈(1~8편)에서 추가까지 했다면, 이번 편은 수정·삭제로 CRUD를 마칩니다.
목표: updateMemo / deleteMemo · Server Action 확장 · 수정 페이지.


이 편에서 할 일

  1. lib/memos.ts에 수정·삭제 함수 추가
  2. actions.ts에 Server Action 2개 추가
  3. /memos/[id]/edit 수정 페이지 만들기
  4. 상세 페이지에 수정 링크 · 삭제 버튼 붙이기

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 → 못 찾으면 undefined
  • deleteMemo → 못 찾으면 false
  • createdAt, 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로 기존 값 채움
  • hidden input으로 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 + hidden id
  • 수정 페이지는 /memos/[id]/edit + defaultValue
  • revalidatePath는 바뀌는 경로를 모두 호출
  • 지금은 메모리 배열, 나중에 DB로 함수만 교체

다음 편 예고

[Next.js + TS 실전] 2. Prisma + SQLite로 DB 붙이기

  • 서버 재시작해도 메모가 남게
  • lib/memos.ts의 배열을 DB 쿼리로 교체
  • CRUD를 “진짜 저장”으로 바꾸기

시리즈 목차 (실전)

  1. 메모 수정과 삭제 ← 지금
  2. Prisma + SQLite로 DB 붙이기 (예정)
  3. Vercel 배포 (예정)

입문 시리즈와의 연결

입문 실전 1편
addMemo updateMemo, deleteMemo
createMemo updateMemoAction, deleteMemoAction
/memos/new /memos/[id]/edit
추가 후 목록 수정 후 상세, 삭제 후 목록

입문 7편을 이해했다면, 이번 편은 복붙·수정 수준으로 따라올 수 있습니다.