본문 바로가기
C.W.K.
Stream
Lesson 02 of 08 · published

폼과 서버 액션 연결하기

~20 min · form, action, FormData

Level 0호기심
0 XP0/68 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

formaction에 함수를 건네

가장 단순한 형태는 <form action={serverAction}>이야. 제출하면 브라우저가 만든 FormData가 서버 액션에 전달되고, 액션이 revalidatePath로 무효화한 화면까지 함께 다시 렌더링할 수 있어.

FormData의 실제 타입을 좁혀

formData.get('name')의 결과는 문자열, 파일, 또는 null일 수 있어. 텍스트는 검증 뒤 문자열로, 업로드는 File로 좁혀. 단순한 type assertion이 입력 검증을 대신하지는 않아.

폼에 없는 문맥은 .bind()로 묶어

수정할 글의 ID처럼 화면에는 필요하지만 input으로 노출할 이유가 없는 값은 action.bind(null, id)로 묶을 수 있어. 묶인 값은 FormData보다 앞선 인자로 액션에 도착해. 프레임워크는 액션 참조를 클라이언트로 보내기 전에 그 값을 암호화해.

숨은 input과 권한 검사는 다른 문제야

묶거나 숨겨 보낸 ID도 사용자가 신뢰할 수 있는 권한 증거는 아니야. 액션 안에서 현재 세션을 다시 읽고, 그 사용자가 실제 자원을 바꿀 수 있는지 확인해야 해.

폼의 모든 필드와 묶인 인자를 신뢰할 수 없는 입력으로 취급해. FormData를 schema로 변환하고, 파일은 크기·형식까지 검사하며, 자원 ID는 세션의 소유권과 다시 대조해. 성공 뒤 이동할지 같은 화면을 재검증할지도 제품 흐름에 맞춰 액션 끝에서 결정해.

숨은 input보다 .bind()가 값을 덜 노출해 보여도 보안 차이는 권한 검사가 만들어. 둘 다 서버에 도착하는 요청 데이터야. 사용자가 수정할 필요 없는 문맥을 명확히 분리하는 데 bind를 쓰되, 암호화된 참조를 권한 증거로 착각하지 마. 폼 필드 이름은 서버 액션의 입력 계약이므로 화면만 바꾸고 액션을 그대로 두면 조용히 값이 빠질 수 있어. 제출 전후의 FormData 키를 시험에서 비교해 계약을 지켜. 텍스트·파일·묶인 자원 ID가 있는 폼을 JavaScript 켠 상태와 끈 상태에서 제출해. 액션이 받는 인자 순서와 타입을 기록하고, ID를 조작한 요청이 schema는 통과해도 소유권 검사에서 거부되는지 반드시 확인해. 파일 업로드는 FormData에 들어왔다고 저장 준비가 끝난 게 아니야. 스트리밍·크기 제한·악성 형식 검사·실패 시 임시 파일 정리까지 서버 경계의 일부로 다뤄야 해.

Code

폼과 서버 액션 연결하기·tsx
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';

export async function addTodo(formData: FormData) {
  const text = formData.get('text') as string;
  await db.todo.create({ data: { text, done: false } });
  revalidatePath('/todos');
}

// app/todos/page.tsx
import { addTodo } from '@/app/actions';

export default async function TodosPage() {
  const todos = await db.todo.findMany();
  return (
    <>
      <ul>{todos.map(t => <li key={t.id}>{t.text}</li>)}</ul>
      <form action={addTodo}>
        <input name="text" required />
        <button>Add</button>
      </form>
    </>
  );
}
추가 인자 묶기·tsx
'use server';
export async function updateTodo(id: string, formData: FormData) {
  const text = formData.get('text') as string;
  await db.todo.update({ where: { id }, data: { text } });
  revalidatePath('/todos');
}

function TodoItem({ todo }: { todo: { id: string; text: string } }) {
  const updateWithId = updateTodo.bind(null, todo.id);
  return (
    <form action={updateWithId}>
      <input name="text" defaultValue={todo.text} />
      <button>Save</button>
    </form>
  );
}

External links

Exercise

폼에 텍스트, 파일, .bind()로 묶은 자원 ID를 넣어. 정상 제출과 조작한 ID를 보내 입력 검증과 소유권 검사가 각각 작동하는지 확인해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.