회원가입이나 로그인, 혹은 기타 사용자가 입력한 데이터를 서버에 전달할 때 form을 다루게 된다.
이때 서버에 폼 데이터를 전달하는 방식으로
formData
생성자 함수를 활용react-hook-form
, formik
같은 폼 라이브러리로 핸들링방법이야 다양하다.
하지만 근본적인 접근 방식을 알아두면 나머지는 다 거기에서 파생된 개념이기에, 1과 2에 대한 내용을 간단히 정리해보자.
일반 폼 전송은 별도의 자바스크립트 코드 필요 없이 HTML로 폼을 전송할 수 있다. 폼 태그에 있는 action
, method
프로퍼티에 네트워크 메서드와 통신 결과를 받아올 경로를 지정해주고, type=submit
인 input이나 button을 명시하여 전송된다.
<form action="/submit" method="POST">
<input type="text" name="username" />
<button type="submit">제출</button>
</form>
브라우저에서 자동으로 데이터를 인코딩해주어서 별도의 형식 명시는 하지 않아도 된다. 기본적으로 application/x-www-form-urlencoded
형식으로 데이터를 전송한다. 단, POST 요청으로 파일을 전송하려면 enctype="multipart/form-data"
도 추가 명시해야 한다.
브라우저는 기본적으로 동기 방식이다. 서버와 통신이 원활히 이루어지면, 그 결과 데이터를 보여주기 위해 페이지 전체를 갱신한다. 그런데 폼 제출할 때마다 페이지 전체가 리로드 되는 웹사이트를 요즘엔 웬만해선 경험하기 어렵다.
only HTML 대신 자바스크립트로 핸들링하면 동적인 처리가 가능해서다.
폼데이터는 폼 필드와 해당 필드의 값을 key-value 쌍으로 나타낸 자바스크립트 객체이다. 이때 value에는 문자열, 숫자(자동으로 문자열 전환), blob 객체, 파일 등을 추가할 수 있다.
서버로 폼을 전송해주는 button 태그 역할을 네트워크 메서드(fetch
)가 이를 대신한다. HTTP 통신 headers에서 별도로 폼 전송 형식을 명시하지 않아도 자동으로 폼 데이터 타입에 맞게 인코딩 된다. 또한, 네트워크 통신은 비동기 처리방식이므로 페이지 전체 리로드가 발생하지 않아서 화면 깜빡임 따위가 없다.
new 연산자를 사용한 생성자 함수이기에 메서드를 통한 데이터 추가, 삭제 등이 간단하다.
const formData = new FormData();
formData.append('email', 'example@email.com');
formData.append('password', 'password123!');
formData.append('username', 'hey');
일부 메서드
append
: 객체에 이미 key가 있으면 그 키에 새 값 추가 (key 중복 가능)set
: 이미 동일한 key가 있으면 새 값으로 대체 (key 중복 불가)entries
: key value 쌍을 순회하는 iterator 반환delete
: 필드 삭제
🚨 이때 주의사항. value에 객체나 배열을 '바로' 넣을 수 없다.
const obj = { key: 'value' };
formData.append('objData', JSON.stringify(obj));
append
메서드는 동일한 필드명으로 값 추가가 가능하다는 점을 활용해 배열을 하나씩 순회해 추가할 수 있다.후자의 방법은 2개 이상의 파일을 핸들링할 때 유용하다. 아래 예시를 보자.
type이 file인 input으로 파일을 업로드할 수 있는데, 이때 데이터는 어떤 타입으로 담길까? 바로 유사배열 객체(Array-like Object)인 FileList이다.
유사배열 객체
'배열'처럼 인덱스로 접근 가능하고length
도 있지만,Array.prototype
을 상속받지 않아 배열 메서드 등의 사용이 불가한 객체.
ex) FileList를 콘솔에 찍어보면 이런 모양새다.FileList { 0: File, 1: File, length: 2 }
React나 Next.js 환경에서 간단하게 파일을 담는 상황을 만들어보면 이해가 좀더 쉽다.
const fileRef = useRef();
const handleFileUpload = () => {
const formData = new FormData();
const fileList = fileRef.current.files;
for (const file of fileList) {
formData.append('files[]', file); // 배열로
}
...
<input
type='file'
name='files'
ref={fileRef}
multiple={true}
onChange={handleFileUpload}
/>
파일을 업로드 하면 폼데이터 객체를 생성하고, ref로 타겟팅한 데이터를 가져와 files[]
라는 key name으로 각 값을 순회하며 넣어준다.
🚨 이때 주의사항이 있다. map
이나 forEach
같은 배열 메서드는 FileList에 사용할 수 없다. 앞서 언급한 대로 FileList는 배열이 아니라 유사배열 객체라서 Array.prototype
을 상속 받지 않았기 때문이다.
대신 iterable 객체인 점에 착안해 for문 등 다른 방법을 택하면 된다.
이제 서버로 폼 데이터를 전송하기 전, 제대로 값이 들어왔는지 확인해보자.
console.log(formData) // FormData {}
그런데 왜 빈 객체로 보이는 걸까? 브라우저에서의 디버깅은 toString
메서드로 객체를 직렬화해서 이루어지는데, formData는 자바스크립트 내장객체이다. 따라서 브라우저에서 문자열화 해도 객체 내부에는 접근할 수 없다.
그래도 iterable 객체인 건 마찬가지라 FileList에서 그러했듯 for문을, 혹은 formData의 메서드인 entries
를 활용할 수 있다.
for (const data of formData) {
console.log(data);
}
for (const [key, value] of formData.entries() {
console.log(`${key}:`, value)
}
이렇게 값이 잘 담기는 것도 확인했으니 fetch
로 서버에 통신을 보내고, 결과값을 핸들링 해주면 된다.
const fileRef = useRef();
const handleFileUpload = async () => {
const formData = new FormData();
const fileList = fileRef.current.files;
for (const file of fileList) {
formData.append('files[]', file);
}
// API 호출
try {
const res = await fetch('/api/upload', {
method: 'POST',
body: formData,
});
if (res.ok) {
const result = await res.json();
// 업로드 성공 핸들링
} else {
// 업로드 실패 핸들링
}
} catch (error) {
// 통신 에러 핸들링
}
};
...
<input
type='file'
name='files'
ref={fileRef}
multiple={true}
/>
위 예시코드에서는 프론트에서 files[]
라는 key name으로 전달했다. 고로 서버에서도 동일한 key name으로 받고, 타입을 배열로 명시해야 한다. 이런 내용은 백엔드와 협의 필수!