
이번 단원에서는 양식(form)을 다루는 법에 대해 배운다.
1. 양식을 다룰때 무엇이 어려운지
2. 양식 제출을 관리하고 사용자 입력을 검증할 수 있는 방법
3. 브라우저가 제공하는 양식과 검증 기능이 어떻게 개발자의 업무기능을 완화 해줄 수 있는지
4. 사용자 입력과 검증 과정을 다루는 리액트만의 커스텀 해결법
양식은 뭐가 어렵지 라고 생각 할 수 있다.
반은 맞고 반은 틀리다고 할 수 있다.
확실히 폼을 제출하는쪽은 쉬운것이 맞다. 상태를 통해 데이터를 관리하거나, 값을 추출하기 위해 참조(ref)를 사용하거나, 브라우저에서 제공하는 빌트인 함수들을 사용하여 사용자가 입력한 데이터를 추출하여 fromData 객체를 통해 양식 필드로 옮길 수도 있다. 보는것과 같이 매우 직관적인 내용이라 어렵지는 않다.
하지만, 문제는 검증문제다. 특히 좋은 사용자 경험까지 제공하려한다면 더욱 어렵다. 예를들어보자, 사용자가 입력한 데이터를 검증하려할때 사용자가 잘못된 타이핑을 했다면 언제 오류창을 띄워야할까?
키를 누를때마다 검증을하여 올바르지않으면 바로 오류메시지를 띄워야할까?
사용자가 모두 입력한뒤 넘어가면 그때가서야 오류메시지를 띄워도될까?
앱의 요구사항에 따라 절충방법을 배워서 활용 할 수 있게끔 배워보자.
이제 Form에 대해서 제대로 알아보도록 하자.
이메일과 비밀번호를 입력하여 로그인하는 양식이 있다.
해당 로그인 폼의 코드는 다음과 같다.
Login.jsx
export default function Login() {
return (
<form>
<h2>Login</h2>
<div className="control-row">
<div className="control no-margin">
<label htmlFor="email">Email</label>
<input id="email" type="email" name="email" />
</div>
<div className="control no-margin">
<label htmlFor="password">Password</label>
<input id="password" type="password" name="password" />
</div>
</div>
<p className="form-actions">
<button className="button button-flat">Reset</button>
<button className="button">Login</button>
</p>
</form>
);
}
짤막 지식 : React에서는 for가 아니라 htmlfor으로 사용한다.
class를 className이라고 하듯이 HTML에서 for는 예약어로 사용되기 때문에
중복현상을 피하기 위하여 React는 for가 아니라 htmlfor로 사용한다
근데 for가 뭐였지?
주로 라벨에서 쓰이며 해당 라벨이 어떤 입력 요소와 연결되어있는지를 나타내는데 자주 사용된다.
// htmlfor : id가 "email"인 요소와 연결시키겠습니다.
<label htmlFor="email">Email</label>
<input id="email" type="email" name="email" />
이제 로그인 버튼에 새로운 함수를 추가해보자!
Login.jsx 일부코드
function handleSubmit() {
console.log("제출완료.");
}
<p className="form-actions">
<button className="button button-flat">Reset</button>
<button className="button" onClick={handleSubmit}>Login</button>
</p>
실행해보면..

“제출완료”가 유지되지 않고 나오자마자 사라지는것을 볼 수 있다. 왜이런 현상이 나오는것일까?
Login 버튼 누름 => “제출완료”콘솔 => 페이지 새로고침 발생
여기서 페이지 새로고침 때문에 나오는 현상인데 페이지 새로고침의 현상이 생기는 이유는
양식요소(form)에있는 버튼은 기본적으로 양식을 제출하는 용도로 사용된다.
즉 양식요소에있는 버튼은 기본적으로 HTTP 요청이 발생하며 웹사이트의 서버로 전송된다는것이다. 이러한 HTTP 요청이 발생하면 서버에의해 렌더링이 되고나서 다시 클라이언트에게 전송이된다. 이것이 새로고침 현상이 생기는 이유다.
이러한 기본 구성이 지금은 문제가 된다. 웹 주소의 리액트 웹사이트를 관리하는 서버가 순수 개발 서버라서 이 서버는 양식 제출을 위해 관리 할 수 있는 코드를 갖추지 않았다.
이 리액트 앱을 실제 사용자를 위해 배포하더라도 index.html 파일 또는 자바스크립트 파일들만 관리 할 수 있기 떄문이다. 즉 새로 들어오는 양식 요청을 관리 할수는 없다.
따라서 기본 구성을 막는 방법을 배워보자.
1.type 속성에서 기본 유형이 submit으로 되어있는것을 바꾸는 방법
아무런 type 속성을 입력하지 않으면 submit 유형이된다.
<button className="button" onClick={handleSubmit}>Login</button>
명시적으로 type 속성을 바꿔서 기본 구성을 막아보자.
<button className="button" onClick={handleSubmit} type="button">Login</button>
2.form 양식에 onSubmit 속성을 추가하여 submit 이벤트가 발생할때마다 기본구성을막는 함수를 실행시키는 방법
폼 양식에 submit이 발생할때 함수를 실행하여 기본 구성을 막자.
click, change, submit 이벤트 이 모든게 suubmit을 유발한다.
또한 이러한 이벤트들은 event 객체를 얻을 수 있는데, 이 event 객체로
preventDefault 메소드를 사용할 수 있다.
// 기본구성을 막는 handleSubmit 함수
function handleSubmit(event) {
event.preventDefault();
}
// submit을 유발하는 모든 이벤트는 해당 handleSubmit함수를 거치게된다.
<form onSubmit={handleSubmit}>
첫번째 방법과 두번째 방법 모두 잘 작동하는것을 볼 수 있다.

Login 폼에 사용자가 입력한것을 저장하는 방법은 두가지다.
첫번째로 각각 1:1로 상태를 만들어 저장하는 방법
두번째로 객체 상태 하나를 만들어 관리하는 방법
// 이메일과 비밀번호 상태를 각각 만들어 관리하는 방법
const [enteredEmail, setEnteredEmail] = useState("");
const [enteredPassword, setEnteredPassword] = useState("");
// 폼 제출(Login)시 실행될 함수
function handleSubmit(event) {
event.preventDefault();
console.log("유저이메일은" + enteredEmail + "입니다");
console.log("유저비밀번호는" + enteredPassword + "입니다");
}
// 이메일 타이핑 감지시 실행될 함수
function handleEmailChange(event) {
setEnteredEmail(event.target.value);
}
// 비밀번호 타이핑 감지시 실행될 함수
function handlePasswordChange(event) {
setEnteredPassword(event.target.value);
}
return (
<form onSubmit={handleSubmit}>
<h2>Login</h2>
<div className="control-row">
<div className="control no-margin">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
name="email"
// handleEmailChange와 연결시키기
// input값이 변경될때 handleEmailChange의 함수가 작동이된다.
onChange={handleEmailChange}
// 초기값은 '' 공백일것이다. 사용자 입력시 변경됨
value={enteredEmail}
/>
</div>
<div className="control no-margin">
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
name="password"
// handleEmailChange와 연결시키기
// input값이 변경될때 handlePasswordhange의 함수가 작동이된다.
onChange={handlePasswordChange}
// 초기값은 '' 공백일것이다. 사용자 입력시 변경됨
value={enteredPassword}
/>
</div>
</div>
<p className="form-actions">
<button className="button button-flat">Reset</button>
<button className="button">Login</button>
</p>
</form>
);
}
import React, { useState } from "react";
export default function Login() {
// 이메일과 비밀번호 상태를 객체 하나로 만들어 관리하는 방법
const [enteredValues, setEnteredValues] = useState({
email: '',
password: ''
});
// 폼 제출(Login)시 실행될 함수
function handleSubmit(event) {
event.preventDefault();
console.log("유저이메일은" + enteredValues.email + "입니다");
console.log("유저비밀번호는" + enteredValues.password + "입니다");
}
// 비밀번호 or 이메일 타이핑 감지시 실행될 함수
// identifier와 event 매개변수는 객체의 key와 값을 설정하기위한 매개변수다.
function handleInputChange(identifier, event) {
setEnteredValues( (preValues) => ({
...preValues,
[identifier]:event.target.value
}))
}
return (
<form onSubmit={handleSubmit}>
<h2>Login</h2>
<div className="control-row">
<div className="control no-margin">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
name="email"
// onChange={handleInputChage}로 사용할경우
// 우리에게 필요한 identifier를 줄 수 없다.
// 그리고 익명함수로 넣으면 입력값으로 이벤트 객체를 얻을수있다.
onChange={(event) => handleInputChange('email',event)}
value={enteredValues.email}
/>
</div>
<div className="control no-margin">
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
name="password"
// onChange={handleInputChage}로 사용할경우
// 우리에게 필요한 identifier를 줄 수 없다.
// 그리고 익명함수로 넣으면 입력값으로 이벤트 객체를 얻을수있다.
onChange={(event) => handleInputChange('password', event)}
value={enteredValues.password}
/>
</div>
</div>
<p className="form-actions">
<button className="button button-flat">Reset</button>
<button className="button">Login</button>
</p>
</form>
);
}
첫번째 방법과 두번째 방법 모두 잘 작동하니 코드에 맞게 선택하면 될것같다.
useState를 사용하여 사용자 입력을 수집하는것 이외에도 다른 방법이 있다.
DOM 요소를 직접 참조할 수 있게 하는 Refs를 사용하는것이다.
Refs : DOM요소에 직접적으로 접근할때 유용한 문법이다.
Login.js
import React, { useState } from "react";
export default function Login() {
const email = useRef();
const password = useRef();
// 폼 제출(Login)시 실행될 함수
function handleSubmit(event) {
event.preventDefault();
// 참조객체에 있는 current 속성에 반드시 접근해야 실제로 연결된 값을 가질 수 있다.
const enteredEmail = email.current.value;
const enteredPassword = password.current.value;
console.log("유저이메일은" + enteredEmail + "입니다");
console.log("유저비밀번호는" + enteredPassword + "입니다");
}
// 비밀번호 or 이메일 타이핑 감지시 실행될 함수
function handleInputChange(identifier, event) {
setEnteredValues( (preValues) => ({
...preValues,
[identifier]:event.target.value
}))
}
return (
<form onSubmit={handleSubmit}>
<h2>Login</h2>
<div className="control-row">
<div className="control no-margin">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
name="email"
// 참조 속성을 추가하여 DOM 요소간에 연결 관계를 만들기
ref = {email}
/>
</div>
<div className="control no-margin">
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
name="password"
// 참조 속성을 추가하여 DOM 요소간에 연결 관계를 만들기
ref= {password}
/>
</div>
</div>
<p className="form-actions">
<button className="button button-flat">Reset</button>
<button className="button">Login</button>
</p>
</form>
);
}
잘 작동하는것을 볼 수 있다.
복잡한 양식을 다룰때는 좋은 방법이 있을까?
브라우저 네이티브에 내장된 기능인 FormData를 사용 해보자.
FormData는 양식에 입력된 각기 다른 값들을 쉽게 얻을 수 있도록 도와주는 “객체”다.
밑에 JSX 부분보다 위에 주석처리된곳 위주로 공부하자.
Signup.js
export default function Signup() {
function handSubmit(event) {
event.preventDefault();
// event.target은 양식 그 자체를 뜻한다.
// 또한 양식에 있는 input에 추가된 모든 데이터로 접근 할 수 있게 해준다.
// 그렇지만 제대로 작동하려면 input 요소는 name 속성을 가져야만 한다.
const allFormData = new FormData(event.target);
// 체크박스도 가져오자. 모든 체크박스(name이 acquisition)을 얻으려면 getAll
const acquisitionChannel = allFormData.getAll('acquisition');
// 입력된 값들을 한번에 객체로 얻어오는 방법을 쓰자.
// 객체(allFormData)를 키-값 쌍의 배열을 가지는 이터러블 객체로 만들고 => entries()
// 좀더 가독성 높고 쉽게 조작할 수 있게 일반 객체로 만들자 => fromEntries
const data = Object.fromEntries(allFormData.entries());
// 체크박스에대한 데이터를 프로퍼티에 추가하기
data.acquisition = acquisitionChannel;
console.log(data);
}
JSX 코드부분 ( 여기보다 위쪽을 중심으로 보자 )
return (
<form onSubmit={handSubmit}>
<h2>Welcome on board!</h2>
<p>We just need a little bit of data from you to get you started 🚀</p>
<div className="control">
<label htmlFor="email">Email</label>
<input id="email" type="email" name="email" />
</div>
<div className="control-row">
<div className="control">
<label htmlFor="password">Password</label>
<input id="password" type="password" name="password" />
</div>
<div className="control">
<label htmlFor="confirm-password">Confirm Password</label>
<input
id="confirm-password"
type="password"
name="confirm-password"
/>
</div>
</div>
<hr />
<div className="control-row">
<div className="control">
<label htmlFor="first-name">First Name</label>
<input type="text" id="first-name" name="first-name" />
</div>
<div className="control">
<label htmlFor="last-name">Last Name</label>
<input type="text" id="last-name" name="last-name" />
</div>
</div>
<div className="control">
<label htmlFor="phone">What best describes your role?</label>
<select id="role" name="role">
<option value="student">Student</option>
<option value="teacher">Teacher</option>
<option value="employee">Employee</option>
<option value="founder">Founder</option>
<option value="other">Other</option>
</select>
</div>
<fieldset>
<legend>How did you find us?</legend>
<div className="control">
<input
type="checkbox"
id="google"
name="acquisition"
value="google"
/>
<label htmlFor="google">Google</label>
</div>
<div className="control">
<input
type="checkbox"
id="friend"
name="acquisition"
value="friend"
/>
<label htmlFor="friend">Referred by friend</label>
</div>
<div className="control">
<input type="checkbox" id="other" name="acquisition" value="other" />
<label htmlFor="other">Other</label>
</div>
</fieldset>
<div className="control">
<label htmlFor="terms-and-conditions">
<input type="checkbox" id="terms-and-conditions" name="terms" />I
agree to the terms and conditions
</label>
</div>
<p className="form-actions">
<button type="reset" className="button button-flat">
Reset
</button>
<button type="submit" className="button">
Sign up
</button>
</p>
</form>
);
}
보기좋게 객체로 보여진것을 볼 수 있다.
궁금한점
const allFormData = new FormData(event.target);
const data = Object.fromEntries(allFormData.entries());
console.log(data);
이코드를 바꿔서 이런식으로 되는지 궁금했다.
allFormData도 객체니까 그냥 바로 이런식으로 사용해도 되는거아닌가??
const data = Object.fromEntries(allFormData);
안된다. allFormData자체로는 객체 자체를 얻을 수 없다. 야생인 상태라고 보면된다.
그래서 allFormData.entries()메소드로 반복가능한 이터레이터를 얻은뒤 다시 보기좋은 객체상태로 만들기위해 fromEntreis()메소드를 사용하여 일반객체(data)를 얻는것이다.
사용자가 Form에 입력한 값들을 초기화를 하고 싶다면 어떻게 해야할까?
특정 태그에 type속성의 reset을 추가하면 간단하게 초기화를 구현 할 수 있다.
<button type="reset" className="button button-flat">
Reset
</button>
set함수를 이용해서 상태를 업데이트하여 공백문자로 만들어 줄 수 있다.
function resetHandler() {
setEnteredValues({
email: '',
password: ''
});
<button className="button button-flat" onClick={resetHandler} >Reset</button>
이벤트 함수를 가져와서 type이 reset을 호출한다.
function handSubmit(event) {
event.preventDefault();
...
event.target.reset();
}
return (
<form>
<button type="reset" className="button button-flat">
Reset
</button>
</form>
)
DOM을 직접 건들여서 업데이트하는 방식이다.
하지만 해당 방식은 권장하지 않는다. 보통 DOM을 건들이는것은 리액트가 담당할부분이기 때문이다.
function handSubmit(event) {
email.current.value ='';
}
이메일의 상태(enteredValues.email)로 키보드 입력에 따른 유효성 검사를 해보자.
// 이메일 유효성 검사
// @를 포함하면 false, @를 포함하지 않으면 true
const emailIsInvalid = !enteredValues.email.includes("@");
// 오류 메시지 조건부 출력
// @를 포함하지않으면 오류메시지 출력.
{emailIsInvalid &&<p>Please enter a valid email address.</p>}
하지만 해당 코드는 문제점이 있다.
이메일을 입력하지도 않았는데 처음부터 제대로된 이메일을 입력하라고 오류메시지를 출력한다. 이는 사용자 입장에서 불쾌한 경험이 될 수 있다.
(아직 타이핑도안했는데 제대로 입력하라고 압박한다..)
해당 문제점을 해결해보자
이메일이 최소한 공백문자는 아니해야한다는 로직을 추가해보자.
수정전
const emailIsInvalid = !enteredValues.email.includes("@");
수정후
const emailIsInvalid = enteredValues.email !== "" &&!enteredValues.email.includes("@");
공백일때는 출력이 안되게끔 해결이되었다! 하지만 해당 솔루션도 두 가지 문제점이 있다.
1.사용자가 모두 타이핑을 하기전에도 오류메시지를 출력한다.

// 해당 변수가 false로 고정이되버린다.
const emailIsInvalid = enteredValues.email !== '' &&!enteredValues.email.includes("@");

해당 문제점을 다음 단원에서 해결해보자.
Blur 상태와 관련한 onBlur란??

위의 사진처럼 EMAIL 입력칸에 마우스를 두면 포커스 상태가 되었다가, 마우스를 바깥에다 클릭하게 되면 EMAIL 입력칸이 포커스 상태가 해제되어 Blur 상태라고 한다.
이를 onBlur 속성을 사용하여 여러가지로 활용 할 수 있다.
가장먼저 입력이 수정되었는지, 즉 blur 상태가 되었는지의 상태를 저장 해야한다.
// 입력의 포커스를 기록하는 데에 쓰이는 상태
const [didEdit, setDidEdit] = useState({
email : false,
password : false
});
포커스를 기록하는 상태를 유동적으로 바꾸는 함수를 만들자.
// email의 포커스가 해제될경우 해당 didEdit 상태는 true로 변경.
// true는 곧 포커스가 해제되었음을 의미한다.
// 이 true의 상태를 활용하여 유효성검사 로직에 추가할 수 있다.
function handleInputBlur(identifer) {
setDidEdit(prevEdit => ({
...prevEdit,
[identifer]: true
}))
}
그리고 JSX코드에 이메일에 해당하는 부분에 onBlur 속성을 추가하자.
// 식별자인 email을 보내기위한 방식으로, 인수를 쓰기위해 함수형태로 감싸야한다.
// 포커스가 해제된다면 handleinputBlur 함수를 실행하면서 email로 인수를 보낼게요.
onBlur={() => handleInputBlur('email')}
이메일 유효성 검사를 바꿔보자.
// 이메일 유효성 검사
// didEdit.email가 true라면 && 해당 문을 실행하여라.
const emailIsInvalid = didEdit.email &&!enteredValues.email.includes("@");

blur 상태( 포커스가 해제될때)가 되기 전까지 유효성검사를 안하다가 blur 상태가 되고난 후 유효성검사를 하는 모습이다. @를 포함하지 않으므로 오류메시지를 출력했다.

유효성검사에 문제가없는 이메일이므로 blur 상태가 되더라도 오류메시지를 출력하지 않는다.
하지만 여기서도 문제점이 한가지 더 있다.
blur 상태를 기준으로 유효성 검사를 한다는 로직을 만들때
1. 유효하지않게 타이핑한뒤
2. blur 상태로 두고 유효성 검사 시작
3. 오류메시지가 출력
이 상황에서 다시 올바르게 타이핑하려고 할때,
유효성검사의 로직인 “@”가 있는지의 여부를 만날때까지 오류메시지가 출력이되버린다.

해당 문제점은 한번 blur 상태가 true 되었던것이 유지되는것이 문제이므로
타이핑을 시작할때 blur 상태를 다시 false로 만들어버리면 된다.
// 비밀번호 or 이메일 타이핑 감지시 실행될 함수
function handleInputChange(identifier, event) {
setEnteredValues((preValues) => ({
...preValues,
[identifier]: event.target.value,
}));
// 새로 추가된 로직 blur 상태를 false로 만들자.
setDidEdit( (prevEdit) => ({
...prevEdit,
[identifier]: false
}));
}

마우스커서를 바깥쪽에 클릭하여 blur 상태가 true 되었다가(오류메시지출력),
다시 마우스커서를 이메일쪽에 클릭하여 타이핑할때 blur 상태가 false 되어 오류메시지를 출력하지 않는다.
정리하면 로직을 다음과 같은 패턴을 가져야한다는것을 알 수 있다.
이 두가지를 결합하고..
추가로 해당 로직까지 추가하자.
유효성 검사를 실시간으로 하는것이 아니라 Form을 제출할때 실시하는것도 가능하다.
이는 종종 있는 인기있는 패턴이다.
세가지를 알아보도록 하자.
useRef를 사용한시점에서는 키 입력에 따른 유효성 검사가 사실상 어렵다.
별개의 이벤트 리스너를 설정해줘야하는데, 그럴경우 상태를 이용해 관리해주어야한다.
따라서 useRef(참조)를 이용하여 검증을 하고싶으면 사용자가 양식을 제출했을 때에만 검증하는 방식을 이용하자.
Login.jsx
import React, { useRef,useState} from "react";
export default function Login() {
const [emailIsInvalid, setEmailIsInvalid] = useState(false);
const email = useRef();
const password = useRef();
// 폼 제출(Login)시 실행될 함수
function handleSubmit (event) {
event.preventDefault();
// 참조객체에 있는 current 속성에 반드시 접근해야 실제로 연결된 값을 가질 수 있다.
const enteredEmail = email.current.value;
// 들어온 이메일 내용이 유효한가?
const emailIsValid = enteredEmail.includes('@');
// 유효하지않다면 EmailIsInvalid 상태를 true로 설정한다.
// 그리고 return을 반환하여 다음문이 실행되지 않도록 한다.
if (!emailIsValid) {
setEmailIsInvalid(true);
return;
}
// 들어온 이메일 내용이 유효할경우 EmailIsInvalid 상태를 false로 설정한다.
// 이렇게 설정해야지만 한번 EmailIsinValid 상태가 true로 고정되어있는것을 바꿔질 수 있다.
setEmailIsInvalid(false);
console.log('Sending HTTP requset....');
}

Form 제출할때(Login) 유효성 검사를 하는것을 볼 수 있다.
Form 유효성 검사를 빌트인 속성을 사용하여 훨씬 쉽게 사용할 수 있다.
단지 속성에 "required"를 추가해주면 된다.
Signup.jsx
<form onSubmit={handSubmit}>
<h2>Welcome on board!</h2>
<p>We just need a little bit of data from you to get you started 🚀</p>
<div className="control">
<label htmlFor="email">Email</label>
<input id="email" type="email" name="email" required/>
</div>
<div className="control-row">
<div className="control">
<label htmlFor="password">Password</label>
// minLength도 required 속성과 마찬가지로 브라우저에서 제공하는 빌트인 속성이다.
<input id="password" type="password" name="password" required minLength={6} />
</div>
// .....(이하생략, 아래 코드들도 모두 required 속성을 추가했다.)
</form>

Form 내장 빌트인 함수인 required만으로는 부족하다고 느낄 상황이 있을것이다.
이럴 경우 당연하게도 나만의 커스텀 로직을 만들어서 추가하여 사용할 수 있다.
예를들어 비밀번호와 비밀번호 확인 두개 각각의 인풋이 일치해야지만 통과할 수 있게끔 커스텀 로직을 추가해보자.
Signup.jsx
// 비밀번호 일치여부 상태
const [passwordsArenotEqual, setPasswordAreNotEqual] = useState(false);
function handSubmit(event) {
// (생략...)
// 비밀번호가 일치하지않다면 일치여부 상태를 true로 설정한다.
if(data.passowrd !== data['confirm-password']) {
setPasswordAreNotEqual(true);
}
// (생략...)
}
return (
<form onSubmit={handSubmit}>
// (생략...)
<div className="control">
<label htmlFor="confirm-password">Confirm Password</label>
<input
id="confirm-password"
type="password"
name="confirm-password"
required
/>
// 비밀번호가 일치하지않을때만 p태그 조건부 출력
<div>{passwordsArenotEqual &&<p>비밀번호가 일치해야 합니다.</p>}</div>
</div>
// (생략...)
</form>
)

공통되는 부분이 많고, JSX부분이 굉장히 복잡하고 가독성이 좋지않을때 사용 할 수 있는것이 재사용 가능한 입력 컴포넌트를 구축하는것이다.
즉 입력 양식도 공통되는 로직을 추출하여 컴포넌트를 따로 만들어서 관리 및 사용하는것이다.
다음 코드를 보고 공통된 로직을 추출하여 컴포넌트를 만들어보자.
이메일입력에 해당하는 입력양식
StateLogin.jsx
<div className="control no-margin">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
name="email"
onBlur={() => handleInputBlur("email")}
onChange={(event) => handleInputChange("email", event.target.value)}
value={enteredValues.email}
/>
<div className="control-error">
{emailIsInvalid &&<p>이메일을 제대로 입력해주세요.</p>}
</div>
비밀번호입력에 해당하는 입력양식
StateLogin.jsx
<div className="control no-margin">
<label htmlFor="password">{password}</label>
<input
id="password"
type="password"
name="password"
onBlur={() => handleInputBlur("password")}
onChange={(event) => handleInputChange("password", event.target.value)}
value={enteredValues.password}
/>
<div className="control-error">
{passowrdIsInvalid &&<p>비밀번호를 제대로 입력해주세요.</p>}
</div>
공통되는 구조를 파악했으면 컴포넌트를 만들자.
재사용가능한 입력 컴포넌트 만들기
Input.jsx
export default function Input({ label, id, error, ...props }) {
return (
<div className="control no-margin">
<label htmlFor={id}>{label}&</label>
<input id={id} {...props} />
<div className="control-error">{error &&<p>{error}</p>}</div>
</div>
);
}
이제 이러한 틀을 이용하여 활용하자. 알맞게 인수를 보내주면 된다.
email과 password의 재사용가능한 컴포넌트를 활용하여 JSX 코드에 서술.
StateLogin.js
<Input
label="email"
id="email"
type="email"
name="email"
onBlur={() => handleInputBlur("email")}
onChange={(event) => handleInputChange("email", event.target.value)}
value={enteredValues.email}
error={emailIsInvalid && "이메일을 제대로 입력해주세요."}
/>
<Input
label="password"
id="password"
type="password"
name="password"
onBlur={() => handleInputBlur("password")}
onChange={(event) =>
handleInputChange("password", event.target.value)
}
value={enteredValues.password}
error={passwordIsInvalid && "비밀번호를 제대로 입력해주세요."}
/>
매개변수를 서술하는 기준이 무엇인지? 어떤건 단지 따로 써주고, 어떤건 스프레드 문법을 사용한다.
Q. label, id , error는 왜 스프레드 문법을 사용하지 않았나?
같은 태그에서 사용하는것인지로 구분이 된다.
Q. 그럼 type, name ,onBlur, onChange, value 속성은 input 태그에만 쓰이니까 스프레드문법 으로 지정해도 되는가?
맞다. 따로 지정한것 이외에는 모두 스프레드처럼 흩뿌려버린다. 가독성과 간결해지는면이 엄청 크다.
추가적으로 유효성 검사의 로직을 아웃소싱하는것도 가능하다.
util 폴더에 validation.js 파일을 만들어서 재사용 가능한 유효성 검사 로직을 만들자.
validation.js
export function isEmail(value) {
return value.includes('@');
}
export function isNotEmpty(value) {
return value.trim() !== '';
}
export function hasMinLength(value, minLength) {
return value.length >= minLength;
}
export function isEqualsToOtherValue(value, otherValue) {
return value === otherValue;
}
해당 로직부분을 로직을 아웃소싱하여 바꿔보자.
StateLogin.jsx
아웃소싱 전
const emailIsInvalid = didEdit.email &&!enteredValues.email.includes('@');
const passwordIsInvalid = didEdit.password &&enteredValues.password.trim().length <6;
StateLogin.jsx
아웃소싱 후
const emailIsInvalid = didEdit.email &&!enteredValues.email.includes("@");
const passwordIsInvalid =
didEdit.password &&enteredValues.password.trim().length <6;
StateLogin.jsx
아웃소싱 후
const emailIsInvalid =
didEdit.email &&
!isEmail(enteredValues.email) &&
!isNotEmpty(enteredValues.email);
const passwordIsInvalid =
didEdit.password &&!hasMinLength(enteredValues.password, 6);
모두 문제없이 작동한다!
해당 코드들을 보자.
StateLogin.jsx
// 이메일과 비밀번호 상태를 객체 하나로 만들어 관리하는 방법
const [enteredValues, setEnteredValues] = useState({
email: "",
password: "",
});
// 입력의 포커스를 기록하는 데에 쓰이는 상태
const [didEdit, setDidEdit] = useState({
email: false,
password: false,
});
// 이메일 유효성 검사
// didEdit.email 의 blur 상태가 true라면 && 해당 문을 실행하여라.
const emailIsInvalid =
didEdit.email &&
!isEmail(enteredValues.email) &&
!isNotEmpty(enteredValues.email);
const passwordIsInvalid =
didEdit.password &&!hasMinLength(enteredValues.password, 6);
// email or password의 포커스가 해제될경우 해당 didEdit 상태는 true로 변경
// true는 곧 포커스가 해제되었음을 의미한다.
// 이 true의 상태(blur 상태)를 활용하여 유효성검사 로직에 추가할 수 있다.
function handleInputBlur(identifer) {
setDidEdit((prevEdit) => ({
...prevEdit,
[identifer]: true,
}));
}
// 폼 제출(Login)시 실행될 함수
function handleSubmit(event) {
event.preventDefault();
}
// 비밀번호 or 이메일 타이핑 감지시 실행될 함수
function handleInputChange(identifier, value) {
setEnteredValues((preValues) => ({
...preValues,
[identifier]: value,
}));
setDidEdit((prevEdit) => ({
...prevEdit,
[identifier]: false,
}));
}
function resetHandler() {
setEnteredValues({
email: "",
password: "",
});
}
솔직히 말해서 코드들이 클린하게 되어있다고 생각하기 힘들다.
그래서 유효성 검사 로직을 아웃소싱 했던것처럼 커스텀 Hook을 생성해보자.
커스텀 Hook을 사용한다고 했을때부터 눈치 챘을수도 있지만, 일반 함수로는 만들 수 없다. 상태들을 사용하기 떄문에 커스텀 Hook으로 만들어야한다.
커스텀 Hook 사용하기
userInput.js
import { useState } from "react";
// 유효성 검사로직부분을 하드코딩하는것이 아니라 매개변수로 받아와야한다.
export function useInput(defaultValue, validationFn) {
// 두 상태를 묶는 객체로 관리하는것이 아니라, 하나의 값에 대한 관리만을 해보자.
const [enteredValue, setEnteredValue] = useState(defaultValue);
// 여러 입력의 상태를 결합하는 객체를 관리하는 마찬가지로 단 하나의 값으로만 관리해보자.
const [didEdit, setDidEdit] = useState(false);
// 유효성 검사 로직 부분도 커스텀 훅에 의탁하자.
const valueIsValid = validationFn(enteredValue);
// 비밀번호 or 이메일 타이핑 감지시 실행될 함수
// identifier, value 였던 매개변수를 하나(event)의 이벤트객체만 받도록 하자.
function handleInputChange(event) {
setEnteredValue(event.target.value);
setDidEdit(false);
}
function handleInputBlur() {
setDidEdit(true);
}
// 객체가아니라 배열로 해도된다. 상황에맞게!
return {
value : enteredValue,
handleInputChange,
handleInputBlur,
hasError : didEdit &&!valueIsValid
}
}
커스텀 훅 사용하기
StateLogin.jsx
export default function Login() {
// 객체 구조 분해할당으로 좀더 간결하고 가독성 높게 표현하자.
const {
value: emailValue,
handleInputChange: handleEmailChange,
handleInputBlur: handleEmailBlur,
hasError:emailHasError
} = useInput("", (value) => isEmail(value) &&isNotEmpty(value));
// 객체 구조 분해할당으로 좀더 간결하고 가독성 높게 표현하자.
const {
value: passwordValue,
handleInputChange: handlePasswordChange,
handleInputBlur: handlePasswordBlur,
hasError : passwordHasError
} = useInput("", (value) => hasMinLength(value, 6));
// 폼 제출(Login)시 실행될 함수
function handleSubmit(event) {
event.preventDefault();
if (emailHasError || passwordHasError) {
return;
}
console.log(emailValue, passwordValue);
}
function resetHandler() {
setEnteredValues({
email: "",
password: "",
});
}
return (
<form onSubmit={handleSubmit}>
<h2>Login</h2>
{/* export default function Input({ label, id, error, ...props }) { */}
<div className="control-row">
<Input
label="email"
id="email"
type="email"
name="email"
// () => handleInputBlur("email") 이런식으로 더이상 감싸지않아도된다.
// () => (event) => handleInputChange("email", event.target.value) 이것도 마찬가지!
onBlur={handleEmailBlur}
onChange={handleEmailChange}
value={emailValue}
error={emailHasError &&"이메일을 제대로 입력해주세요."}
/>
<Input
label="password"
id="password"
type="password"
name="password"
onBlur={handlePasswordBlur}
onChange={handlePasswordChange}
value={passwordValue}
error={passwordHasError &&"비밀번호를 제대로 입력해주세요."}
/>
</div>
<p className="form-actions">
<button className="button button-flat" onClick={resetHandler}>
Reset
</button>
<button className="button">Login</button>
</p>
</form>
);
}
해당 파트는 헷갈리는 부분이 많아서 따로 정리해야겠음.
객체 구조 분해할당, 콜백함수 보내는 등 강의로 만들어져있는 코드가아니라 따로 복습하는 시간을 가져야할것같다.