빌드노트
화면에는 값이 있는데 FormData에는 필드가 없다
화면에 보이는 값이 FormData에서 빠질 때 name·disabled·체크박스 선택 상태를 확인한다. readonly 텍스트와 select의 차이, loopback echo 본문 대조, hidden 값을 서버에서 검증해야 하는 이유를 실제 브라우저 실험으로 설명한다.
입력값을 바꾸지 못하게 disabled를 붙였더니 서버로 보낸 데이터에서 그 필드만 빠질 수 있다. new FormData(form)은 화면에 보이는 값을 모두 복사하지 않는다. 비활성 컨트롤은 필드 목록에서 제외한다. 텍스트를 사용자가 편집하지 못하게 하면서 값은 보내야 한다면, disabled를 제거하고 name이 있는 입력에 readonly를 쓰는 방법부터 검토한다.
이 차이는 서버의 요청 파서에 도달하기 전에 생긴다. HTML 표준의 필드 목록 구성 절차 (새 탭에서 열림)는 disabled 컨트롤과 미선택 체크박스를 건너뛰고, 일반 입력의 name이 없거나 비어 있어도 항목을 만들지 않는다. 화면의 값과 전송할 필드 목록을 따로 확인해야 하는 이유다.
같은 폼에서 무엇이 남는지 확인했다
이 글을 위해 만든 합성 폼을 Windows 11의 headless Chromium 147.0.7727.15에서 실행했다. 실제 서비스나 사용자 데이터는 사용하지 않았다. 아래 내용을 새 demo.html 파일로 저장하고 브라우저에서 연 뒤 개발자 도구의 Console을 보면 같은 비교를 할 수 있다. 서버 없이 필드 목록만 확인하는 예제다.
<!doctype html>
<meta charset="utf-8">
<title>FormData 필드 비교</title>
<form id="demo">
<label>일반 <input name="enabled" value="alpha"></label><br>
<label>비활성 <input name="disabledText" value="beta" disabled></label><br>
<label>읽기 전용 <input name="readonlyText" value="gamma" readonly></label><br>
<label>name 없음 <input id="unnamed" value="delta"></label><br>
<label><input type="checkbox" name="checkedBox" value="yes" checked>선택</label>
<label><input type="checkbox" name="uncheckedBox" value="yes">미선택</label><br>
<label>색 <select name="shade" readonly>
<option value="pale">옅게</option><option value="deep">짙게</option>
</select></label>
<input type="hidden" name="marker" value="original">
</form>
<script>
const form = document.querySelector('#demo');
console.log(JSON.stringify([...new FormData(form)]));
</script>
처음 열었을 때 출력은 다음과 같았다.
[["enabled","alpha"],["readonlyText","gamma"],["checkedBox","yes"],["shade","pale"],["marker","original"]]
beta와 delta는 화면의 입력에 그대로 있었지만 출력에는 없었다. disabledText는 비활성 상태라서, unnamed는 id만 있고 name이 없어서 제외됐다. 체크하지 않은 uncheckedBox도 빠졌다. 이 체크박스의 value가 yes여도 선택되지 않으면 보내지지 않으며, 그 대신 false라는 값이 자동으로 들어가는 것도 아니다.
반면 읽기 전용인 readonlyText=gamma는 포함됐다. 같은 키보드 입력을 일반 텍스트와 읽기 전용 텍스트에 보내자 일반 텍스트만 바뀌었다. readonly는 이 텍스트의 사용자 편집을 막으면서 제출 대상에는 남겨 뒀다. MDN의 readonly 적용 범위 (새 탭에서 열림)도 이 차이를 설명한다.
select에 readonly를 붙여도 선택은 바뀐다
위 예제의 select readonly는 속성을 붙여도 선택을 고정하지 못한다. 직접 초점을 옮겨 아래 방향키와 Enter를 누르자 값이 pale에서 deep으로 바뀌었고, 새 FormData에도 shade=deep이 들어갔다. readonly가 적용되는 요소는 정해져 있으며 select와 체크박스는 여기에 포함되지 않는다. 텍스트 입력에서 통했던 수정을 그대로 옮길 수 없다.
선택값을 변경하지 못하게 표시하려면 설명 텍스트로 보여 주거나 disabled인 선택 상자로 보여 줄 수 있다. 다만 비활성 선택 상자 자체는 제출에서 빠진다. 서버가 이미 알고 있는 값이면 서버에서 다시 결정하고, 클라이언트가 값을 전달해야 하는 계약이면 별도의 hidden 입력을 둘 수 있다. 이때 전달된 값의 검증도 함께 정해야 한다.
전송 직전의 목록과 요청 본문을 비교한다
필드 목록과 실제 요청이 같은지도 별도로 확인했다. 127.0.0.1에만 열린 작은 echo 서버에 FormData를 POST했다. 이 서버는 받은 multipart 본문과 해석한 항목을 응답하고 출력할 뿐, 값을 저장하지 않는다. 처음 폼, 선택 변경 뒤, 입력 상태 변경 뒤의 세 요청에서 전송 직전 목록과 서버가 받은 이름·값·순서가 각각 일치했다.
첫 요청 본문의 일부는 다음과 같았다. multipart 구분선과 다른 필드는 생략했다.
Content-Disposition: form-data; name="readonlyText"
gamma
이 본문에도 disabledText 항목은 없었다. 이어 비활성 텍스트를 활성화하고 미선택 체크박스를 선택하자, 새 FormData와 echo 응답 양쪽에 disabledText=beta, uncheckedBox=yes가 추가됐다. 상태 변경 전에 만든 FormData에는 두 항목이 계속 없었다. 화면 상태를 고친 뒤에는 전송할 객체도 그 상태에서 새로 만들었는지 확인한다.
자기 폼에서는 전송 핸들러 안의 new FormData(실제폼) 바로 다음에 console.log([...data.entries()])를 두고, 개발자 도구 Network의 해당 요청 Payload와 비교한다. 목록부터 빠져 있으면 실제 DOM의 name, disabled, 체크박스 선택 상태를 확인한다. 상위 fieldset disabled도 살펴본다. 추가 실험에서는 자식 입력의 .disabled가 false인데도 matches(':disabled')는 true였고 필드는 빠졌다. 자식의 속성만 보면 놓칠 수 있다.
목록에는 있는데 Payload에 없다면, 방금 확인한 객체를 전송하는지와 중간에서 delete하거나 다른 요청 본문을 만드는 코드를 확인한다. Payload에도 있다면 서버가 받은 원문과 파싱·바인딩 결과를 나눠 조사한다. 위 실험은 fetch의 body에 FormData를 그대로 넣었다. 직접 전송 코드를 작성할 때는 Content-Type을 수동으로 지정하지 않아야 브라우저가 multipart 구분선에 맞는 헤더를 붙일 수 있다. MDN의 FormData 사용 설명 (새 탭에서 열림)
숨겨 보낸 값도 서버의 확정값은 아니다
hidden 입력은 누락을 피하는 전달 수단이 될 수 있지만, 사용자가 바꿀 수 없는 값이라는 뜻은 아니다. 위 실험에서 marker를 original에서 edited로 스크립트로 바꾼 뒤 만들자 echo 서버도 marker=edited를 받았다. readonlyText 역시 스크립트로 값을 바꿔 보내는 것이 가능했다. MDN의 hidden 입력 설명 (새 탭에서 열림)은 개발자 도구로 값을 바꿀 수 있음을 명시한다.
따라서 화면에서 변경을 막는 방법과 서버가 믿을 값의 근거를 분리한다. 표시만 할 값은 서버에서 유지하고, 받아야 하는 값은 허용된 형식과 범위인지 서버에서 검사한다. 어떤 항목이 빠졌을 때 기본값으로 바꿀지, 기존 값을 유지할지, 오류로 거절할지도 요청 계약으로 정한다. 빠진 체크박스를 모든 요청에서 무조건 false로 처리하면, 일부 필드만 수정하는 요청에서는 의도와 다를 수 있다.
직접 확인한 범위는 네이티브 HTML 폼과 Chromium 한 버전, loopback echo 요청이다. 실제 서비스의 서버 검증 로직, 프레임워크별 직렬화, Firefox·Safari·모바일 동작은 확인하지 않았다.