[Next.js + TS] 7. 폼과 서버 액션 입문
6편까지는 메모를 읽기만 했습니다.
이번 편은 폼으로 메모를 추가합니다.
목표: Server Action ·FormData·revalidatePath로 목록 갱신.
이 편에서 할 일
- Server Action이 뭔지 짧게 이해하기
createMemo액션 만들기/memos/new작성 폼 페이지 만들기- 저장 후
/memos목록이 갱신되는지 확인하기
DB는 아직 없습니다.
메모리 배열에 넣고, 새로고침 없이 목록이 바뀌는 흐름만 익힙니다.
왜 Server Action인가
예전에 흔했던 흐름:
- 클라이언트에서
fetch('/api/memos', { method: 'POST' ... }) - API 라우트에서 저장
- 다시 목록 fetch
App Router에서는 더 단순한 길이 있습니다.
폼이 서버 함수를 직접 호출한다 → Server Action
- 별도의 API 라우트를 꼭 안 만들어도 됨
- 서버에서만 실행됨 (비밀키/DB 접근에 유리)
<form action={createMemo}>로 연결 가능
전부 Action으로 대체하라는 뜻은 아닙니다.
폼 제출·간단한 mutation부터 배우기 좋습니다.
1) 데이터 레이어에 “추가” 붙이기
lib/memos.ts에 생성 함수를 추가합니다.
import type { Memo } from "@/types/memo";
const memos: Memo[] = [
{
id: "1",
title: "Next.js 시작",
content: "App Router로 프로젝트 만들었다.",
createdAt: "2026-07-29",
},
{
id: "2",
title: "TypeScript props",
content: "MemoCard에 타입을 붙였다.",
createdAt: "2026-07-29",
},
];
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export async function getMemos(): Promise<Memo[]> {
await delay(800);
return memos;
}
export async function getMemoById(id: string): Promise<Memo | undefined> {
await delay(500);
return memos.find((memo) => memo.id === id);
}
export async function addMemo(input: {
title: string;
content: string;
}): Promise<Memo> {
await delay(300);
const memo: Memo = {
id: String(Date.now()),
title: input.title,
content: input.content,
createdAt: new Date().toISOString().slice(0, 10),
};
memos.unshift(memo); // 맨 앞에 추가
return memo;
}
id는 실습용으로Date.now()사용- 서버를 재시작하면 배열은 초기값으로 돌아갑니다 (메모리라서 정상)
2) Server Action 파일
경로: app/memos/actions.ts
"use server";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { addMemo } 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");
}
읽어볼 것:
| 코드 | 의미 |
|---|---|
"use server" |
이 파일의 함수는 서버에서 실행 |
FormData |
<form>이 보낸 값 묶음 |
formData.get("title") |
name="title" 입력값 |
revalidatePath("/memos") |
목록 캐시/화면을 다시 준비 |
redirect("/memos") |
저장 후 목록으로 이동 |
"use server"는 파일 맨 위에 둡니다.
(함수 단위로 쓸 수도 있지만, 입문에서는 파일 단위가 단순합니다.)
3) 작성 페이지: /memos/new
app/memos/new/page.tsx
import { createMemo } from "@/app/memos/actions";
export default function NewMemoPage() {
return (
<main>
<h1>새 메모</h1>
<form action={createMemo}>
<p>
<label>
제목
<br />
<input type="text" name="title" required />
</label>
</p>
<p>
<label>
내용
<br />
<textarea name="content" rows={5} required />
</label>
</p>
<button type="submit">저장</button>
</form>
</main>
);
}
포인트:
'use client'없음 → 서버 컴포넌트 페이지action={createMemo}로 서버 함수 연결- input의
name이FormData키와 같아야 함
http://localhost:3000/memos/new 에서 작성 → 저장하면 /memos로 이동하고 새 메모가 보여야 합니다.
4) 목록에서 “새 메모” 링크
app/memos/page.tsx에 링크를 하나 추가합니다.
import Link from "next/link";
import MemoCard from "@/components/MemoCard";
import { getMemos } from "@/lib/memos";
export default async function MemosPage() {
const memos = await getMemos();
return (
<main>
<h1>메모 목록</h1>
<p>
<Link href="/memos/new">새 메모 작성</Link>
</p>
<section>
{memos.map((memo) => (
<MemoCard key={memo.id} memo={memo} />
))}
</section>
</main>
);
}
원하면 layout 내비에도 /memos/new를 넣어도 됩니다.
동작 흐름 (한눈에)
/memos/new 에서 제출
↓
createMemo (Server Action)
↓
addMemo (메모리 배열에 push)
↓
revalidatePath("/memos")
↓
redirect("/memos")
↓
목록이 새 데이터로 다시 렌더
클라이언트에서 useState로 목록을 직접 고치지 않아도 됩니다.
서버가 데이터를 바꾸고, 해당 경로를 다시 그리게 합니다.
최소 검증만 넣은 이유
지금은:
if (!title || !content) {
throw new Error("...");
}
정도만 했습니다.
실서비스에서는:
- 필드별 에러 메시지 표시
useActionState같은 패턴- zod 등 스키마 검증
으로 확장합니다.
입문 편에서는 폼 → 서버 → 목록 갱신 한 바퀴가 목표입니다.
자주 하는 실수
1. input에 name을 안 줌
→ formData.get("title")이 비어 있습니다
2. "use server"를 클라이언트 컴포넌트 한가운데에 무작정 붙임
→ Action은 서버 파일(또는 서버 함수)로 분리하세요
3. revalidatePath를 빼먹음
→ 저장은 됐는데 목록이 옛 화면일 수 있습니다
4. 서버 재시작 후 메모가 사라짐
→ 메모리 배열이라 정상입니다. DB 붙이면 유지됩니다
5. action={createMemo()} 처럼 호출해 버림
→ action={createMemo} (함수 참조)여야 합니다
이 편에서 가져갈 것
- Server Action은 폼이 호출하는 서버 함수다
"use server"+FormData+name속성이 기본 세트다- 저장 후
revalidatePath로 목록을 갱신한다 redirect로 다음 화면으로 보낸다- 지금은 메모리, 나중엔 DB로
addMemo만 바꾸면 된다
다음 편 예고
[Next.js + TS] 8. 정리 + 다음 학습 로드맵
- 1~7편 한눈에 복습
- 지금 앱에서 부족한 점 (DB, 인증, 배포)
- 다음에 공부하면 좋은 순서
시리즈 마무리다운 체크리스트 + 로드맵으로 끝냅니다.
시리즈 목차
- 왜 Next.js + TypeScript인가
- 프로젝트 만들고 폴더 구조 이해하기
- TypeScript로 컴포넌트 타입 잡기
- 라우팅과 레이아웃
- Server Component vs Client Component
- 데이터 가져오기
- 폼과 서버 액션 입문 ← 지금
- 정리 + 다음 학습 로드맵
'frontend > NextJs' 카테고리의 다른 글
| # [Next.js + TS 실전] 1. 메모 수정과 삭제 (0) | 2026.07.30 |
|---|---|
| # [Next.js + TS] 8. 정리 + 다음 학습 로드맵 (0) | 2026.07.29 |
| # [Next.js + TS] 6. 데이터 가져오기 (0) | 2026.07.29 |
| # [Next.js + TS] 5. Server Component vs Client Component (0) | 2026.07.29 |
| # [Next.js + TS] 4. 라우팅과 레이아웃 (0) | 2026.07.29 |
