'바퀴를 재발명하지 마라'는 말이 있다. 개발을 할 때에는 직접 기능을 새로 개발하는 것보다, 원래 존재하는 좋은 기능을 가져다가 쓰라는 의미이다.
이는 프론트엔드 개발에도 적용되는데, 생각보다 기존에 이미 있는 좋은 기능을 우리의 부주의로 놓치고 지나갈 때가 많다.
이 중에서 특히 form에 대해 알아보면 좋겠다.
form 태그 사용하기
다음은 네이버의 회원가입 창이다. 정보들을 입력 받아, 회원 가입 api를 실행시키니 input을 받아 submit하는 form의 형태를 띄고 있다.
다음은 포지션, 제목 등을 입력 받아 글을 검색하는 필터 칸이다. 이도 어찌보면 정보들을 입력 받아 검색 api를 실행하니 form의 형태를 띈다고 해볼 수 있겠다.
function Component() {
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = () => {
axios.post("/join", { username, email, password });
};
return (
<div>
<input type="username" onChange={(e) => setUsername(e.target.value)} />
<input type="email" onChange={(e) => setEmail(e.target.value)} />
<input type="password" onChange={(e) => setPassword(e.target.value)} />
<button onClick={handleSubmit}>회원가입</button>
</div>
);
}
이런 형태의 일들을 깊게 생각해보지 않는다면 다음과 같이 작성하게 된다.
각각을 state로 만들어서 username, email, password를 저장한 후 버튼을 클릭할 때 api를 발사하는 것이다.
하지만 form이라는 이미 주어진 '바퀴'를 이용해서 구현해본다면 어떨까.
function Component() {
const handleSubmit = (e) => {
e.preventDefault();
const form = new FormData(e.currentTarget);
axios.post("/join", {
username: form.get("username"),
email: form.get("email"),
password: form.get("password"),
});
};
return (
<form onSubmit={handleSubmit}>
<input type="username" name="username" />
<input type="email" name="email" />
<input type="password" name="password" />
<button type="submit" />
</form>
);
}
다음과 같이 state의 사용 없이 더 깔끔하고 간결하게 코드를 작성할 수 있다. state의 변화로 인한 재렌더링이 없어지니 성능 향상도 꾀해볼 수 있고, html을 마주하는 브라우저의 입장에서도 더 직관성도 높아진다.
HTML 태그 속성들 이용해보기
autofocus
우리는 로그인 버튼을 눌렀을 때, 아이디 인풋에 바로 포커스가 가서 아이디를 입력할 수 있기를 바란다. 실제로 네이버 로그인도 동일하게 구현되어 있다.
이를 구현하라는 요구사항을 맞이한다면, 대부분이 useEffect
를 사용해야겠다는 생각이 들 것이다.
function Component() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return (
<form>
<input ref={inputRef} type="username" name="username" />
<input type="email" name="email" />
<input type="password" name="password" />
<button type="submit" />
</form>
);
}
하지만 <input>
태그의 속성 중 하나인 autofocus
를 활용해보자.
mdn에서는 autofocus를 다음과 같이 정의하고 있다. (deepL 번역)
페이지 로드가 완료되었을 때(또는 요소가 포함된
<dialog>
가 표시되었을 때) 입력에 자동으로 포커스를 가져야 함을 나타내는 부울 속성입니다(있는 경우).
다시 말해, 페이지 로드가 완료가 되었을 때 autofocus
가 있는 input에 바로 포커스가 이루어진다는 의미다. 따라서, 코드를 이렇게 개선해볼 수 있다.
function Component() {
return (
<form>
<input type="username" name="username" autoFocus />
<input type="email" name="email" />
<input type="password" name="password" />
<button type="submit" />
</form>
);
}
주의할 점: 스크롤을 좀 내려야 도달할 수 있는 input에
autofocus
가 존재할 경우 해당 input까지 바로 스크롤이 내려가버리므로, 시각 장애인 등은 그 위의 컨텐츠를 보지 못할 가능성이 있으므로 접근성에는 유의해야 한다.
reset
폼에 입력했던 내용들을 초기화해야 한다는 요구사항이 있다고 하자. 이 경우 각 form의 요소들을 하나 하나 값을 초기화해주는 함수를 짜게 된다면, 추후 input 요소들이 추가되거나 줄어들 경우 해당 함수가 수정되지 않을 경우 버그가 발생할 수 있다. 물론 코드도 길어지는 것은 덤이다.
이 때 <button>
태그의 type 중 하나인 reset을 활용해보자. 간단하고 단순하게 폼들의 내용을 초기화해준다.
function Component() {
return (
<form>
<input type="username" name="username" />
<input type="password" name="password" />
<button type="submit">제출</button>
<button type="reset">초기화</button>
</form>
);
}
React-Hook-Form과의 연동
하지만 이는 자바스크립트가 아닌, 순수 HTML의 힘으로 요소의 값을 변화시킨 것이다. 리액트에서 자주 사용되는 라이브러리인 React-Hook-Form
을 사용하고 있었을 경우 조금 애로사항이 있을 것이다.
type FormValues = {
username: string;
password: string;
};
export default function Form() {
const { register } = useForm<FormValues>();
return (
<form>
<input type="username" {...register("username")} />
<input type="password" {...register("password")} />
<button type="submit">제출</button>
<button type="reset">초기화</button>
</form>
);
}
다음과 같이 코드를 작성할 경우, 초기화 버튼을 눌러서 값을 초기화시키면 변경된 값이 React-Hook-Form
에는 반영되지 않게 된다.
그도 그럴 것이, 완전한 Uncontrolled Components (https://legacy.reactjs.org/docs/uncontrolled-components.html) 이기 때문이다. 조금의 코드를 추가하여 변경 사항을 알려주도록 하자.
type FormValues = {
username: string;
password: string;
};
export default function Form() {
const { register, reset } = useForm<FormValues>();
return (
<form onReset={() => reset({ username: "", password: "" })}>
<input type="username" {...register("username")} />
<input type="password" {...register("password")} />
<button type="submit">제출</button>
<button type="reset">초기화</button>
</form>
);
}
maxlength, minlength
폼에는 validation을 위해 문자열의 최대 길이, 최소 길이를 지정하는 요구사항이 들어오곤 한다. 물론 리액트 훅 폼 등의 validation들이 존재하지만, HTML 기본 옵션을 이용해보면 더욱 좋을듯 싶다.
export default function Form() {
return (
<form>
<input
type="username"
maxLength={10}
minLength={3}
/>
<input type="password" />
<button type="submit">제출</button>
<button type="reset">초기화</button>
</form>
);
}
다음과 같이 옵션을 추가하면, maxLength 초과의 길이로는 아예 입력이 되지 않으며 minLength 미만의 길이로는 submit이 되지 않는다.
마무리 하며
이렇게 기본적인 HTML 태그에도 써볼만한 기능이 많다. 리액트 사용법, 리액트 환경의 라이브러리들의 사용법을 학습하는 데에 매몰되는 것 보다, 근본을 다져보는 것도 어떨까 싶다.