useFormStatus, useActionState, 이중 제출, 서버 액션 | 문의 버튼이 두 번 눌리면?
문의 폼 버튼이 두 번 눌리면 자식에서 폼 상태 훅의 대기로 잠근다. 액션 상태 훅은 서버가 돌려준 에러를 남기고, 같은 컴포넌트의 폼은 대기가 안 뜬다. React, Next.js, 서버 액션, 프론트엔드, 개발자, API, 폼데이터 기준으로 제출 중 칸만 보고 서버 액션 보안 글과 자리를 섞지 않는다. 2026년 8월 공식 문서.
문의가 같은 내용으로 두 통 쌓인 아침이 있습니다. 버튼 글자가 안 바뀌어서 한 번 더 누른 겁니다. 리액트 19의 폼 상태 훅은 부모 폼이 제출 중이면 대기가 참입니다. 같은 컴포넌트의 폼은 보지 않고, 자식 버튼에서만 읽습니다. 액션 상태 훅은 서버가 돌려준 에러 문구를 화면에 남깁니다. 훅을 붙인 액션의 첫 인자는 이전 상태이고, 폼데이터는 둘째입니다. 저는 12개 사이트 파트너십 폼에서 버튼을 자식으로 뺐습니다. 봇 칸은 턴스타일 글에, 쿠키는 세임사이트 글에, 캐시 무효화는 서버 액션 글에 있습니다. 여기는 제출 중 버튼과 에러 칸만 봅니다.
숫자는 2026년 8월 리액트 폼 상태 훅 문서와 넥스트 폼 가이드 기준입니다.
같은 문의가 두 통 쌓인 아침
한국 휴대폰에서 문의 보내기를 두 번 누르면, 서버는 두 건을 받습니다. 대기 표시가 없으면 손가락이 한 번 더 갑니다.
리액트 공식 문서는 폼 상태 훅을 리액트 돔에서 가져온다고 적습니다. 반환은 대기, 데이터, 메서드, 액션입니다. 대기가 참이면 부모 폼이 제출 중입니다. 같은 컴포넌트에 폼을 두고 훅을 부르면 대기는 끝까지 거짓입니다. 넥스트 폼 가이드는 검사 실패 문구를 남길 때 액션 상태 훅을 쓰라고 적습니다. 그때 서버 함수 첫 인자는 이전 상태가 됩니다.
파트너십 페이지를 열면 한국 사용자가 먼저 누릅니다. 카카오 로그인 쿠키 칸은 세임사이트 글입니다. 봇만 막을 때는 턴스타일입니다. 오늘은 사람 손이 두 번 가는 자리만 봅니다.
빠른 갈래 | 제출 중 버튼 글자와 비활성은 자식에서 폼 상태 훅을 읽습니다. 서버가 돌려준 문구는 액션 상태 훅입니다. 캐시 태그 무효화와 소유권 검사는 서버 액션 글입니다.
대기 표시와 에러 문구를 누가 맡나
폼 상태 훅은 지금 제출 중인지만 봅니다. 액션 상태 훅은 끝난 뒤에 남은 값을 봅니다.
항목
폼 상태 훅
액션 상태 훅
가져오는 곳
리액트 돔
리액트
읽는 위치
부모 폼의 자식
폼을 만든 클라이언트 컴포넌트
돌려주는 값
대기, 데이터, 메서드, 액션
상태, 전달 함수, 대기
잘 맞는 자리
보내기 버튼, 입력 잠금
검사 실패 문구, 이전 입력 유지
액션 인자
그대로 폼데이터
첫 인자는 이전 상태, 둘째가 폼데이터
연속 클릭
대기 동안 버튼을 잠근다
호출을 줄 세워 이전 결과를 다음 호출에 넘긴다
넥스트 폼 가이드는 검사 오류를 보여줄 때 폼을 클라이언트 컴포넌트로 바꾸고 액션 상태 훅을 붙이라고 적습니다. 리액트 문서는 폼의 액션 속성에 전달 함수를 주면 전환으로 감싼다고 적습니다. 클릭 핸들러에서 직접 부르면 전환 시작으로 감싸야 대기가 바뀝니다.
예전 이름은 폼 상태였습니다. 리액트 19 공개 글이 액션 상태 훅으로 바꿨다고 적습니다. 새 프로젝트는 새 이름만 씁니다.
버튼 잠금은 폼 상태 훅, 서버가 돌려준 문구는 액션 상태 훅이다
버튼을 폼 자식으로 빼는 순서
보내기 버튼은 폼 밖에 두지 않습니다. 훅은 부모 폼만 보고, 같은 파일의 폼은 무시합니다.
리액트 함정 칸이 그 실수를 예시로 둡니다. 폼 함수 안에서 훅을 부르면 대기는 끝까지 거짓입니다. 제출 컴포넌트를 만들고, 그 안에서 대기를 읽어 버튼을 잠급니다. 넥스트 앱 라우터면 폼을 만든 파일 위에 유즈 클라이언트를 둡니다. 서버 액션 자체는 유즈 서버 파일에 둡니다.
대기 동안 버튼을 꺼도 네트워크가 두 번 열리면 서버는 두 건을 받습니다. 화면 잠금과 서버에 한 줄만 남기는 칸은 자리가 다릅니다.
리액트는 액션 상태 훅의 연속 호출을 줄 세웁니다. 이전 반환이 다음 첫 인자입니다. 장바구니 수량을 더하는 예는 그렇게 동작합니다. 문의 저장은 더하기가 아닙니다. 같은 본문이 두 줄이면 중복입니다. 웹훅 재시도와 같은 멱등 키를 저장 칸에 둡니다. 그 칸은 웹훅 멱등 글입니다.
자바스크립트가 늦게 붙는 첫 페인트에서는 네이티브 제출이 먼저 갈 수 있습니다. 리액트는 퍼머링크 인자를 페이지 주소와 맞추라고 적습니다. 넥스트가 라우트를 맞춰 주는 경우가 많아서, 동적 주소만 따로 확인합니다. 테스트는 플레이라이트로 보내기를 두 번 눌러 한 건만 남는지 봅니다. 그 순서는 플레이라이트 글입니다.
버튼 비활성만으로는 부족하다. 같은 본문이 두 줄이면 저장 칸에서 한 번만 남긴다
잠금 한 줄 | 화면은 대기가 참일 때 버튼을 끕니다. 서버는 같은 세션과 같은 본문의 두 번째 저장을 거절합니다. 한쪽만 있으면 두 통이 남습니다.
액션 첫 인자가 폼데이터가 아닌 날
액션 상태 훅을 붙인 뒤에 이름이 비면, 첫 인자를 폼데이터로 읽은 겁니다. 둘째로 옮기면 값이 돌아옵니다.
리액트 문제 해결 칸이 그 순서를 적습니다. 훅 없이 액션만 쓰면 첫 인자가 폼데이터입니다. 훅을 씌우면 이전 상태가 앞에 끼어듭니다. 넥스트 폼 가이드도 같은 변경을 적습니다. 콘솔에 객체의 겟이 없다고 뜨면 이 칸부터 봅니다.
증상
먼저 볼 곳
대기가 끝까지 거짓
훅을 폼과 같은 컴포넌트에서 부름
이름 칸이 비어 저장됨
액션 첫 인자를 폼데이터로 읽음
대기가 안 바뀜
클릭에서 직접 부르면서 전환 시작을 안 감쌈
두 번째 클릭이 사라짐
앞 액션이 던져서 줄이 비워짐
문구가 성공 뒤에도 남음
성공 반환에 빈 메시지를 안 넣음
배포 직후 옛 클라이언트가 남으면 훅 이름이 어긋날 수 있습니다. 그 칸은 캐시 컨트롤입니다. 테일윈드 클래스만 갈아끼우는 자리는 테일윈드 v4입니다.
오늘 맞출 네 줄
문의 폼은 버튼 자식, 액션 인자 순서, 서버 한 번만, 검사 문구 네 줄이면 됩니다.
줄
맞출 것
1
보내기 버튼을 폼 자식으로 빼고, 대기면 비활성
2
액션 상태 훅을 쓰면 첫 인자는 이전 상태
3
같은 세션 같은 본문은 서버에서 한 줄만
4
예상 실패는 던지지 않고 문구로 돌림
저는 파트너십 폼을 이렇게 나눴습니다. 한국 모바일에서 보내기가 느리면 글자만 바꿔도 두 번째 탭이 줄었습니다. 봇과 쿠키와 캐시는 자리를 섞지 않습니다.
화면 잠금과 서버 한 줄을 같이 맞춘다. 훅만 넣고 저장을 두 번 받으면 메일이 두 통이다