회원가입 같은 경우는 입력이 되는 동시에 사용자가 입력값이 유효한지에 대한 안내를 받을 수 있게 안내 문구를 출력해주었다.
+— src
+— Page
+— Component
+— Input.js
+— Signin.js (로그인 페이지)
+— Signup.js (회원가입 페이지)
+— Validation
+— Validation.js (유효 검사 자체 라이브러리)
+— App.js
import './App.css';
import Signin from './Page/Signin';
import Signup from './Page/Signup';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<div className="App">
<Routes>
<Route path="/" element={<Signin/>}/>
<Route path="/signup" element={<Signup/>}/>
</Routes>
</div>
</BrowserRouter>
);
}
export default App;
react-router-dom
을 이용해서 SPA(Single Page Application)를 구현해주었다.
import React,{useState} from 'react';
import './Signin.css';
import {Link} from 'react-router-dom';
import Validation from '../Validation/Validation.js'
function Signin(){
const [userId , setuserId] = useState(""); //ID 저장
const [userPasswd , setuserPass] = useState(""); //Pass 저장
function handleId(e){
setuserId(e.target.value);
}
function handlePasswd(e){
setuserPass(e.target.value);
}
function ValidPassword(){
if(userPasswd.length === 0){
console.log(userPasswd);
return(<div>Password 입력해주세요.</div>)
}
else if(userPasswd.length > 0){
console.log(userPasswd);
return(<div>입력 완료</div>)
}
}
return(
<div className="signin-form">
<div className="input-group">
<input className ="signin_userid" type="text" onChange={handleId} value={userId} placeholder="ID를 입력해주세요."></input>
</div>
<div className="input-group">
<input className ="signin_password" type="password" onChange={handlePasswd} value={userPasswd} placeholder="비밀번호를 입력해주세요."></input>
</div>
<div className="button-group">
<button>Login</button>
<Link to="/signup"><button>회원가입</button></Link>
</div>
</div>
)
}
export default Signin;
사실 로그인 페이지는 나와 같이 프로젝트를 진행한 동료가 작성해주었다. 여기서 설명해야 할 부분은 Link
를 이용하여 SPA를 구현하였다는 것이다.
import {useState} from 'react';
import {Link} from 'react-router-dom';
import "./Signup.css";
import Input from "./Component/Input";
import Validation from '../Validation/Validation';
function Signup(){
/*... 생략*/
const [userPassword, setUserPassword] = useState("");
const [userEmail, setUserEmail] = useState("");
const [userPhoneNumber, setUserPhoneNumber] = useState("");
const [validCheck] = useState(new Array(5).fill(false));
function handleInput(setter, validFunc ,order){
return (event)=>{
let value = event.target.value
setter(value);
if (order === 2) validCheck[order] = (userPassword === value);
else validCheck[order] = validFunc(value);
}
}
function handleSubmit(event){
if (validCheck.reduce((result, cur)=> !cur ? false : result )) alert("모두 유효하게 작성되었습니다.");
else alert("이상합니다.")
}
return (
<div className="signup-form">
/*생략*/
<Input
className="signup_password"
placeholder="비밀번호를 입력해주세요."
changeHandler={handleInput(setUserPassword, Validation.signupPasswordCheck ,1)}
value = {userPassword}
valid={validCheck[1]}
valid_message = "비밀번호는 특수문자 !@#$%^&*_- 와 영어 알파벳, 숫자가 각각 한 개씩 포함되게 8자 이상으로 작성해주세요."
type = "password" />
/*생략*/
<Input
className="signup_email"
placeholder="이메일을 입력해주세요."
changeHandler={handleInput(setUserEmail, Validation.signupEmailCheck, 3)}
value = {userEmail}
valid={validCheck[3]}
valid_message = "입력하신 이메일은 유효하지 않습니다." />
/*...생략*/
<div className="button-group">
<button onClick={handleSubmit}>가입하기</button>
<Link to="/"><button type="button">돌아가기</button></Link>
</div>
</div>
)
}
export default Signup;
각각의 input 값들을 Signin이 모두 관리, 통제할 수 있게 state로 저장하였다. 비밀번호를 입력하는 input
을 집중해서 살펴보자.
function Signup(){
const [userPassword, setUserPassword] = useState("");
const [validCheck] = useState(new Array(5).fill(false));
/*생략*/
function handleInput(setter, validFunc ,order){
return (event)=>{
let value = event.target.value
setter(value);
if (order === 2) validCheck[order] = (userPassword === value);
else validCheck[order] = validFunc(value);
}
}
return(
<div className="signup-form">
{/*생략*/}
<Input
className="signup_password"
placeholder="비밀번호를 입력해주세요."
changeHandler={handleInput(setUserPassword, Validation.signupPasswordCheck ,1)}
value = {userPassword}
valid={validCheck[1]}
valid_message = "비밀번호는 특수문자 !@#$%^&*_- 와 영어 알파벳, 숫자가 각각 한 개씩 포함되게 8자 이상으로 작성해주세요."
type = "password" />
{/*생략*/}
</div>
)
}
Signup
이라는 상위 컴포넌트에서 각 입력의 상태값을 관리하려고 했다. 그래서 useState
를 이용하여 Signup
함수 컴포넌트에서 상태 변수들을 만들어 사용하였다. 비밀번호에 해당하는 상태값은 userPassword
이다.
HTML에서 입력되는 값을 리액트 함수 컴포넌트의 상태값으로 저장하기 위해서는 이벤트 핸들러가 필요하다. onChange
라는 input
엘리먼트의 이벤트 리스너가 내가 작성한 이벤트 핸들러 handleInput
을 호출하게 만들었다. 이를테면 여기서 '상태값 끌어올리기'를 한 것이라고 생각하면 되겠다.
handleInput
은 사실 이벤트 핸들러 함수를 반환하는 고차함수다. 이벤트리스너에 등록되는 함수는 무조건 이벤트핸들러야 하는데, 나는 각 input
컴포넌트마다 다른 setter를 써줘야만 했다.(예를 들면, 비밀번호 같은 경우는 setUserPassword
를 사용하지만 id 같은 경우는 setUserId
가 사용되어야 했다.) 똑같은 명령을 다시 작성하여 사용하는 것을 피하기 위해 나는 고차함수로 해결했다.
심지어 handleInput
에는 validFunc
라는 콜백함수도 존재한다. 앞선 포스팅에서 살펴봤던 정규표현 함수를 이 파라미터에 넘겨 이벤트 핸들러가 실행될 때 사용되게 한다. 이렇게 작성하게 된 이유는 각 입력값의 종류에 따라 사용하는 정규표현도 다르기 때문이다.
맨 뒤 order
라는 인자가 존재하는 이유는 각 입력값에 대한 유효성을 validCheck
라는 배열 state에서 관리하기 때문이다. 따라서 각 input
컴포넌트를 번호로 구분해 마지막으로 입력된 값이 유효한 지에 대한 판별값을 validCheck
에 저장하여 관리하게 하였다.
State
를 배열로 사용할 때 생기는 이슈state를 배열로 사용하니 그 값을
useState
가 제공하는Setter
가 필요없게 되었다. 그렇다면 state를 굳이useState
를 사용하여 선언할 필요가 없어 보인다. 하지만 배열의 값이 바뀌었을 때 해당 값에 의존하는 컴포넌트에 렌더링이 안되는 것처럼 보였다. 결국useState
를 이용하여 렌더링 문제를 해결하였다.
이벤트 핸들러를 통해 상태값을 바꿔줄 때 주의할 점
이벤트핸들러
를 이용하여 상태값을 저장하게 구현을 하였다. 하지만 발생한 이벤트 내에서 현재 컴포넌트에 저장되어 있는 State를 확인을 하면 아직 이전의 상태값이 저장되어 있는 것을 확인할 수 있다.Setter
가 이미 호출이 되었어도 이 현상이 똑같이 발생한다. 이를 통해 추측할 수 있는 바이벤트핸들러
내Setter
로 바꾸는 상태값은 이벤트가 종료가 되고 난 이후에 저장된다고 생각해 볼 수 있다. 그래서 이번 프로젝트에서처럼 '비밀번호'와 '비밀번호 확인'이 일치하는지를 확인하려면 Event Target의 value로 비교하여야 바라는 구현을 할 수 있다.
function Input({placeholder, className, changeHandler, value, valid , valid_message, type}){
return (
<div className="input-group">
<input className={className} placeholder={placeholder} onChange={changeHandler} value={value} type={type === "password" ? "password" : "text"}/>
{value.length > 0 && !valid ? <p className="notice-error">{valid_message}</p>: undefined }
</div>
)
}
export default Input ;
부모(상위) 컴포넌트에서 넘겨준 props
에는 여러 값들이 있었다. 그 중에 먼저 살펴볼 것은 이벤트 핸들러다. 부모 컴포넌트에서 받은 이벤트 핸들러의 역할은 하위 컴포넌트에 의해 바뀐 상태값을 끌어올리기 위해서다. 예를 들어 여기서 구현한 비밀번호 입력값 같은 경우는 값의 타이핑이 input
엘리먼트에서 일어난다. 하지만 타이핑이 끝난 시점의 비밀번호 문자열은 Signup
컴포넌트의 userPassword
에 저장되어 관리하려한다. 때문에 이벤트와 이벤트에 해당하는 정보가 발생하는 곳은 input
-> 실제로 이벤트핸들러 안에 작성되어 있는 실행문이 실행되는 곳은 상위 컴포넌트 내부로 하여 '상태 끌어올리기'를 구현해낼 수 있다.
valid
값을 props
로 전해받아 안내문구를 출력해냈다. 삼항연산자의 조건문으로 쓰이는 것을 확인할 수 있다.