폼은 쉬워 보이는데 실무에서 제일 많이 삽질하는 데가 또 폼이에요. 다 맞게 짠 것 같은데 값이 안 가고, 멀쩡하던 폼이 엔터 한 번에 날아가고요. 저도 이 글에 나오는 실수를 전부 직접 저질러 봤어요. 밤새 헤매다 원인이 글자 하나였던 적도 있고요. 오늘은 제가 피 본 흔한 폼 실수들을 증상, 원인, 해결 순서로 풀어볼게요. 미리 알면 여러분은 안 겪어도 되니까요.

14.1 분명 입력했는데 서버엔 왜 빈 값이 갈까요?

제가 처음 겪은 1등 삽질이에요. 회원가입 폼에서 이름, 이메일을 다 채우고 제출했는데, 서버 로그를 보니 이메일만 오고 이름은 없어요. 화면엔 분명히 홍길동이 찍혀 있는데요. 30분을 서버 코드만 뒤졌어요. 범인은 화면 쪽 input 태그였어요.


<input type="text" id="name">


보이세요? name 속성이 없어요. 브라우저는 폼을 제출할 때 name이 붙은 칸만 챙겨 보내요. id는 자바스크립트나 CSS가 칸을 찾는 이름표일 뿐, 제출과는 아무 상관이 없어요. name이 없으면 값이 아무리 차 있어도 서버에는 존재하지 않는 칸이에요.


<input type="text" id="name" name="name">


name="name"을 붙이자 바로 값이 갔어요. FormData(폼 데이터 묶음)나 서버의 req.body 모두 이 name을 열쇠로 값을 담거든요. 폼에서 값이 안 오면 제일 먼저 name부터 확인하세요. 십중팔구 여기예요.

14.2 제출하면 왜 화면이 홱 새로고침돼요?

자바스크립트로 예쁘게 처리하려 했는데, 보내기를 누르면 화면이 깜빡 새로고침되고 입력이 다 날아가요. 이건 폼의 기본 동작 때문이에요. form은 원래 제출되면 페이지를 통째로 다시 불러오게 돼 있어요. 우리 자바스크립트가 일하기도 전에 화면이 떠나버리는 거죠.


form.addEventListener("submit", (e) => {
e.preventDefault();
send(new FormData(form));
});


e.preventDefault() 한 줄이 그 기본 새로고침을 막아줘요. 이걸 빼먹으면 아무리 뒤에 좋은 코드를 써도 화면이 먼저 날아가요. 비슷한 함정이 하나 더 있어요. 폼 안의 버튼에 type을 안 적으면 기본값이 submit이라, 그냥 눌러도 폼이 제출돼요. 제출용이 아닌 버튼(예: 미리보기)에는 꼭 type="button"을 적어줘야 해요.

14.3 엔터만 쳤는데 왜 폼이 날아갔죠?

검색어 한 칸만 있는 폼에서, 사용자가 검색어 치고 엔터를 눌렀더니 폼이 제출되며 새로고침돼 버려요. 이것도 브라우저의 친절한 기본 동작이에요. 입력칸에서 엔터를 누르면 자동으로 폼을 제출하거든요. 칸이 하나일 때 특히 잘 튀어나와요.


이게 좋을 때도 있고 아닐 때도 있어요. 채팅 입력창처럼 엔터로 보내야 하면 그대로 두면 돼요. 반대로 여러 줄 메모 칸에서 줄바꿈하려고 엔터를 눌렀는데 폼이 제출돼 버리면 낭패죠. 이럴 땐 textarea(여러 줄 입력칸)를 쓰면 엔터가 줄바꿈으로 동작해요. 특정 키를 직접 다뤄야 하면 이렇게 걸러요.


input.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
send();
}
});


엔터는 보내기로 쓰고, 시프트+엔터는 줄바꿈으로 두는 흔한 패턴이에요. 핵심은 엔터가 기본 제출을 일으킨다는 걸 알고, 원하는 동작을 내가 정해주는 거예요.

14.4 왜 똑같은 글이 두 번 올라갔을까요?

이건 사용자 제보로 알았어요. 댓글이 두 개씩 달려요라고요. 재현해 보니, 인터넷이 느릴 때 보내기를 누르고 반응이 없으니 사용자가 한 번 더 눌렀던 거예요. 요청은 두 번 다 정상으로 서버에 도착했고요. 화면은 죄가 없어 보이지만, 버튼을 안 잠근 제 잘못이었죠.


button.disabled = true;
try {
await send(new FormData(form));
} finally {
button.disabled = false;
}


제출이 시작되면 버튼을 잠그고, 성공이든 실패든 finally에서 다시 풀어요. finally를 쓰는 이유는, 중간에 에러가 나도 버튼이 영영 잠긴 채 남지 않게 하려는 거예요. 여기에 더해 서버에서 같은 내용의 연속 요청을 걸러주면 이중으로 안전해요. 화면과 서버 양쪽에서 막는 습관을 들이세요.

14.5 검증이 왜 자꾸 헛돌까요?

검증에서도 두 번 크게 데였어요. 첫째는 순서 문제예요. 값을 서버로 보낸 다음에 검증하는 코드를 짰던 거죠. 잘못된 값이 이미 서버에 도착하고 나서 형식이 틀렸어요를 띄우니, 검증이 아무 소용이 없었어요. 검증은 무조건 보내기 전에 통과 여부를 확인하고, 통과할 때만 전송해야 해요.


둘째는 화면 검증만 믿은 실수예요. required(필수)나 type="email" 같은 HTML 검증은 편하지만, 브라우저에서만 도는 1차 안내일 뿐이에요. 개발자 도구로 속성을 지우거나 서버로 직접 요청을 쏘면 화면 검증은 가볍게 뚫려요. 그래서 검증은 화면에서 한 번(친절하게), 서버에서 한 번(진짜로) 이렇게 두 겹이 원칙이에요. 화면 검증은 사용자 편의, 서버 검증은 보안과 데이터 안전을 위한 거예요.

14.6 체크박스랑 셀렉트는 왜 값이 이상해요?

마지막 단골 삽질이에요. 약관 동의 체크박스 값을 서버에서 봤더니, 체크했을 땐 on이 오고 안 했을 땐 아무것도 안 와요. true나 false로 올 줄 알았는데 하고 당황하죠. 체크박스는 원래 체크됐을 때만 전송되고, 값을 안 정하면 기본값이 on이에요. 그래서 이렇게 값을 명시해요.


<input type="checkbox" name="agree" value="yes">


이러면 체크 시 agree=yes가 가고, 안 하면 아예 안 와요. 그러니 서버에서는 agree 값이 있으면 동의, 없으면 미동의로 판단해야 해요. 저는 여기서도 한 번 데였어요. 없을 때 값을 안 챙겨서 서버가 에러를 뱉었거든요. 없을 수도 있다는 걸 늘 염두에 둬야 해요. 셀렉트(select, 드롭다운 선택) 상자도 함정이 있어요. 여러 개를 고르는 multiple 상자는 name 하나에 값이 여러 개 담겨요. 서버에서 하나만 받게 짜두면 맨 마지막 값만 남거나 배열 처리에서 터지죠. multiple을 쓸 땐 서버가 여러 값을 배열로 받도록 미리 맞춰두세요.

14.7 오늘 정리

오늘은 제가 실제로 데인 흔한 폼 실수들을 모아봤어요. 값이 안 가면 name 속성부터, 화면이 새로고침되면 e.preventDefault버튼 type부터 의심하세요. 엔터가 폼을 날리는 건 기본 제출 동작 때문이고, 원하는 동작은 직접 정해주면 됐죠. 같은 글이 두 번 올라가는 건 버튼 잠금으로, 검증은 보내기 전에 그리고 서버에서 한 번 더가 원칙이었어요. 체크박스와 셀렉트는 전송 규칙이 달라서 값 이름을 명시하고 서버를 맞춰야 했고요. 이 목록만 머리에 넣어둬도 폼 삽질의 절반은 미리 피할 수 있어요. 제가 밤새운 만큼 여러분은 아끼시길 바라요.