김서버의 프론트엔드 일기
HTML Form 그리고 web-form-helper 본문
개요
프론트엔드 개발은 서버에 요청을 보내서 필요한 콘텐츠를 받아 와서 화면에 그려주는 작업들을 많이 하게 됩니다. 또한 사용자의 입력을 화면에서 받아서, 서버에 넘기는 작업도 하게 됩니다.
그 중에서 사용자의 입력을 받아서 서버에 넘기는 것을 form이라고 부릅니다.
대표적으로 로그인 화면, 검색창, 필터, 그리고 더 나아가서는 페이지 네이션까지 형태는 정말 다양한 컴포넌트들이 있습니다.
위 컴포넌트들이 form인가에 대해서 의문이 들 수 있지만, 사용자에게 입력을 받아서 서버에게 요청을 보내는 큰 틀에서는 form이라고 부를 수 있습니다.
React 공식문서에는 html form을 다음과 같이 설명을 했습니다.
HTML 폼 엘리먼트는 폼 엘리먼트 자체가 내부 상태를 가지기 때문에, React의 다른 DOM 엘리먼트와 다르게 동작합니다. 예를 들어, 순수한 HTML에서 이 폼은 name을 입력받습니다.
그래서 그 form이 어떤 방식으로 내부 상태를 가지고 있는지가 궁금해서 공부를 하게 되었고, 결국 web-form-helper라는 라이브러리를 만들기까지 도달한 과정을 정리하는 내용입니다.
1. 기존의 사용하는 form들의 예시와 문제점 (with React)
React를 사용 하시면 form을 사용하는 패턴은 보통 다음과 같은 두 가지 패턴을 사용합니다. 이런 방식은 vue나 angular, svelt 같은 것을 사용해도, React와 비슷한 패턴을 가지고 있고 또한 비슷한 문제를 가지게 됩니다.
1-1. 제어 컴포넌트
function FormExample({ handleSubmit }) {
const [value, setValue] = useState()
const onSubmit = (e) => {
e.preventDefault();
handleSubmit({ value });
}
return (
<form onSubmit={onSubmit}>
<input value={value} onChange={(e) => setValue(e.target.value)} />
<button type="submit">서브밋</button>
</form>
)
}
input의 value를 state와 bind하고 input에서 change event가 발생할 때마다 그 state를 변화시켜서 value를 바꾸는 방식을 제어 컴포넌트라고 부릅니다.
React에서의 제어 컴포넌트의 방식으로 form submit을 하기까지의 과정은 다음과 같습니다.
- input에 change event가 발생할 경우, 그 값을 state로 저장한다.
- form이 submit을 하게 될 경우 위에서 저장한 state 들로 다시 데이터를 가공해서 사용하게 된다.
React 같은 상태 기반 라이브러리를 사용 하게 되면, form을 구성할 때, 제일 먼저 떠오르고 가장 쉬운 방법이지만, 제일 큰 치명적인 단점을 가지게 됩니다.
그것은 바로, input에 입력을 할 때 마다 계속 setState를 발생시키게 됩니다.
작은 단위에서는 큰 문제를 일으키지 않은 것으로 보이지만, 만약 FormExample이 return 하는 dom의 수가 1000 개 1만 개가 되는 경우라면, 사용자의 입력이 생길 때마다 가상 돔을 비교하고 업데이트를 하는 과정이 매번 발생하기 때문에 입력할 때 끊기는 현상을 겪게 될 것이고, 심하면 Input에 키보드 타건을 할 때마다, 1초 혹은 상당한 시간이 흐른 뒤에 입력이 되는 모습을 볼 수가 있습니다.
1-2. 비제어 컴포넌트
function FormExample({ handleSubmit }) {
const inputRef = useRef()
const onSubmit = (e) => {
e.preventDefault();
handleSubmit({ value: inputRef.current?.value });
}
return (
<form onSubmit={onSubmit}>
<input ref={inputRef} />
<button type="submit">서브밋</button>
</form>
)
}
위의 제어 컴포넌트에서 setState가 자주 발생해서 나타나는 문제를 inputElement에 ref라는 것을 이용해서 해결 한 방식을 비제어 컴포넌트라고 React 공식문서에서는 부릅니다.
제어 컴포넌트에 비교 했을 때, 성능상 문제는 해결이 되었지만 다음과 같은 단점을 가지고 있습니다.
- form과 input 컴포넌트가 다른 곳에 있으면 form에서 input까지 상태가 아닌 ref 변수를 props로 넘겨야 하는 상황이 생깁니다.
- form에 있는 Input의 개수가 정확하게 정해져 있지 않고, 동적으로 늘어나는 형태일 경우, 사용하는 것이 어려워지는 문제가 발생합니다.
하지만 여기서의 한가지 깨달을 수 있었던 점은 사용자가 입력하는 값을 React의 상태에서 관리를 전혀 하지 않아도 된다는 점을 알게 되었습니다.
제어 컴포넌트의 성능 문제, 비제어 컴포넌트의 input 개수 관련되는 문제를 해결하는 방식이 여러 가지가 있습니다.
위 문제를 해결하기 위해 보통은 Formik이나 react-hook-form 같이 form과 관련된 외부 라이브러리를 사용하는 방법이 있지만, 이런 라이브러리는 오직 React를 위한 것이기 때문에, 다른 프레임워크에서는 전혀 사용을 할 수 없습니다.
이런 부분 때문에, html form의 내부 상태만 가지고, form을 더 쉽게 핸들링할 수 있는 라이브러리를 만드는 것이 좋을 것 같다는 생각이 들어서 web-form-helper를 만들게 되었습니다.
2. web-form-helper의 시작
처음 시작은 form을 사용할 때, 가장 필요한 submit event에 대한 로직을 작성하기 시작했습니다.
HTML 폼 엘리먼트는 폼 엘리먼트 자체가 내부 상태를 가지기 때문에, React의 다른 DOM 엘리먼트와 다르게 동작합니다. 예를 들어, 순수한 HTML에서 이 폼은 name을 입력받습니다.
HTML의 formElement가 자체적으로 상태를 가지고 있고, FormData 자체가 htmlFormElement를 이용해서 생성을 할 수 있기 때문에, 가장 쉽게 FormData의 객체를 이용해서 submit 이벤트를 작성을 해보았습니다.
function FormExample({ handleSubmit }) {
const onSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target)
const entires = formData.entries();
const obj = {};
for (const item of entries) {
obj[item[0]] = item[1];
}
console.log(obj);
}
return (
<form onSubmit={onSubmit}>
<input name="value" />
<button type="submit">서브밋</button>
</form>
)
}
이런 방식으로 사용을 할 경우에, 제어컴포넌트의 단점인 input change마다 rerender를 하지 않습니다.
그리고 비제어 컴포넌트에서 해결 해야하는 단점인 input의 개수가 동적으로 늘어나도, form.entries에 그 추가된 값들이 전부 들어 있어서 확인이 가능하고 또한, 자식 컴포넌트에서 input을 form이 Submit 할 때 값을 사용해야 한다면, input에 name만 추가하면 되기 때문에, ref를 props로 넘겨야 한다는 단점도 해결이 되었습니다.
하지만 단점은 모든 값들의 value가 오직 문자열로만 출력이 된다는 단점이 있고, 해당 값을 가지고 있는 input의 타입을 바로 알 수 있는 방법이 없을뿐더러, form.entries는 IE에서는 지원하지 않는 문제가 있습니다.
3. onSubmit과 FormElement.elements
위의 방식을 만족 할 수 없었던 이유는 front 화면을 jsp나 php 등으로 작성을 했을 때, 오직 html의 form으로 action과 method를 작성하고, 내부에 input element에 name을 넣어서 사용을 했고, 서버에서 바로 이런 부분을 읽을 수 있었기 때문입니다.
그렇기에 브라우저의 종류에는 관계없이 form에 들어있는 입력들을 JS로 확인 할 수 있을 것이라는 확신이 들었습니다.
//w3c school에 있는 form 예시
<form action="/action_page.php" method="get">
<label for="fname">First name:</label>
<input type="text" id="fname" name="fname"><br><br>
<label for="lname">Last name:</label>
<input type="text" id="lname" name="lname"><br><br>
<input type="file" name="image" />
<input type="submit" value="Submit">
</form>
그래서 하나씩 submit의 이벤트를 받았을 때, 나오는 event.target을 console.log로 하나씩 하나씩 IE와 크롬 브라우저에서 동시에 체크를 하면서 공부를 했습니다.
그 중에서 event.target.elements라는 attribute를 발견하게 되었고, 이곳에 form의 child input들이 전부 들어가 있다는 점을 발견하게 되었습니다.
그래서 이 attribute를 mdn을 통해서 확인해보니, form이 관리하는 상태에 해당하는 양식들을 DOMElement의 배열로 가지고 있고, 이때 관리하는 DOMElement들은 기본적으로 잘 알고 있는 input, button, select, textarea과 추가적으로 fieldset, object, output 가 있었습니다.
그리고 form이 관리 하는 DOMElement들이 사라지거나 추가가 되면, 이 elements에 즉시 반영이 되는 점도 있어서, 가장 적합하다고 판단이 되었습니다.
우선 가장 많이 사용 되는 input, button, select, textrea를 이용해서 submit 함수를 작성하던 도중 input의 type이 file일 경우, value가 C:\fakepath\....로 나타나는 점을 확인이 되었습니다. input에 file을 넣는 경우가 많기도 하고, 서버에 저런 이상한 경로로 file을 보내는 것은 말이 안 되기 때문에, 꼭 해결해야 하는 부분이었습니다.
우선 원인은 크게 두 가지였습니다.
- FormElement.elements들에 있는 value는 문자열일 수밖에 없습니다. 왜냐하면 html은 결국 문자열로 이루어진 것을 브라우저를 통해서 그려주는 방식이기 때문입니다.
- 그럴 경우 file은 사용자 피씨에 path를 찍어야 할 텐데, 이럴 경우, 악성 소프트웨어가 사용자의 파일 구조를 알 수 있기 때문에 fakepath라는 것으로 이 문제를 해결했다고 mdn 문서에 적혀있었습니다.
그렇기 때문에, Input의 type이 file인 경우에는 value를 직접 사용하는 것은 불가능이었고, input.fileList를 사용해야 비로소 사용자가 입력한 file을 알 수 있다는 점을 알게 되었습니다.
이렇게 web-form-helper 의 onSubmit 함수를 type에 맞게 value를 조정해서 저장할 수 있도록 했고, 이 저장한 값을 onSubmit을 실행할 때, 콜백 함수의 매개변수로 실행할 수 있도록 했습니다.
4. onInvalid와 HTML Validate
html의 invalid 이벤트는 form의 submit의 요청을 할 때, validation의 과정을 거치면서 만족을 하지 못할 경우에 발생하는 이벤트 입니다.
이 이벤트가 발생 할 경우에는 다음과 같은 화면이 나오게 됩니다.
form의 submit 이벤트가 발생할 때, 자식 input들의 validation 과정을 하게 되는데, 이때 validation을 하는 것은 태그 html에 적혀 있는 규칙대로 validation을 하게 됩니다.
만약 오류가 날 경우, event.target은 오류를 발생시킨 inputElement가 됩니다. 특이한 점을 예로 들면 2개의 inputElement에서 오류가 발생하게 되면, 이벤트는 각각 한 번씩 일어나게 됩니다.
<form action="/action_page.php" method="get" id="form">
<label for="fname">First name:</label>
<input type="text" id="fname" name="fname" required minLength="4" maxLength="10"><br><br>
<label for="lname">Last name:</label>
<input type="text" id="lname" name="lname" required minLength="3" maxLength="12"><br><br>
<input type="submit" value="Submit">
</form>
const form = document.getElementById("form");
form.addEventListener("invalid", (event) => {
console.log(event.target) // html validate가 실패한 elements
console.log(event.target.validity) // 실패한 elements의 사유로 객체 형태로 되어있다.
})
invalid 이벤트가 발생 할 경우에는 event.target은 inputElement가 되지만 추가적으로 validation 오류가 발생한 사유와 메시지까지 알 수가 있습니다.
그리고 invalid가 발생한 inputElement의 참조 값으로 알 수 있기 때문에, 그 inputElement를 조작하는 것도 가능하게 되고, invalid 가상 클래스가 생성이 되어서 css 스타일을 조정할 수가 있습니다.
web-form-helper에서 onInvalid는 바로 이런 input들에서 invalid 오류가 발생한 inputElement와 오류가 발생한 사유를 매개 변수로 콜백 함수를 실행하게 됩니다.
결론
처음의 시작은 회사에서 사용 중인 antd의 form을 사용하다가 어떻게, 이 값들을 읽어가는 거지?부터 시작을 했었고, 위에서 예시를 든 것처럼 php나 jsp에서 form을 사용할 때, 그 값들을 어떻게 인식을 했었을까? 여기까지 의문점이 생각이 들어서 하나씩 하나씩 공부를 하게 되었습니다.
그러다 이렇게 공부한 것을 잘 사용을 하면, 기존에 react, vue, angular 등 라이브러리에 종속되었던 기존 form 라이브러리들 보다 더 좋은 방식으로 form을 핸들링할 수 있는 것을 만들 수 있지 않을까라는 생각을 하게 되어서, web-form-helper라는 오픈 소스를 만들게 되었습니다
비록 기존에 종속되어있는 라이브러리보다는 각 UI framework에 맞춤형 기능들을 제공할 수는 없지만, 라이브러리에 종속되지 않은 범용성이 있는 것이 좋다고 생각을 했었습니다.
https://www.npmjs.com/package/web-form-helper
참조
'공부하는거 > Web' 카테고리의 다른 글
WYSIWYG 에디터 개발 - 1 (Selection으로 폰트 적용하기) (0) | 2022.04.02 |
---|---|
MediaRecorder와 FileSystem으로 실시간 녹화 저장 프로그램 만들기 (0) | 2022.03.06 |
Service Worker란? (with PWA) (0) | 2022.02.07 |