처음 리액트를 사용하는 거기도 하고 예전에 배웠던 html css javascript 모두 까먹어서
로그인 페이지와 회원가입 페이지를 만드는데 2틀 정도 걸린 것 같아요 ㅎㅎ
제가 나중에 다시 까먹을 것 같아서 기록해두려고 합니다 !
초기 설정
우선 앞 선 글에서 알려드린 방법대로 프로젝트를 생성해주시고, 같이 생성된 쓸모 없는 나머지 파일들은 지우고 이렇게 남겨주세요!
src 폴더 속 파일들을 간단하게 설명해보자면,
1. index.js 에서 App.js 를 호출합니다.
2. App.js 에서 사용자의 컴포넌트를 호출합니다.
컴포넌트는 간단히 말하자면 하나의 기능을 구현한 함수라고 보시면 됩니다.
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
예를 들자면 Welcome 이라는 함수가 하나의 컴포넌트라고 이해하시면 됩니다.
이 프로젝트에서 저는 Login 과 Register, 총 두개의 컴포넌트를 생성했다고 볼 수 있겠죠?
이제 파일에서 지워야할 부분들을 모두 지우고 아래와 같이 준비해주세요
1. index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
2. index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
3. App.js
import React from 'react';
import Login from './Login';
function App() {
return (
<div>
<Login />
</div>
);
}
export default App;
4. index.css - 이 파일의 코드들은 모두 지워주세요
Login 페이지 작성
제가 구성한 로그인 페이지는 이렇습니다.
그래서 우선 구현하기 위해 src 폴더에 Login.jsx 파일을 만들어 줍시다.
파일 이름은 다르게 하셔도 됩니다.
(다르게 하시면 App.js 에서 <Login /> 태그도 같이 수정해주셔야해요)
그런 다음 이러한 형태로 작성해주세요.
import React, { useEffect, useState } from 'react';
export default function Login() {
return (
);
}
이제 화면을 구성해봅시다 !
아까 App.js 작성할 때, return 문 안에 <Login /> 태그 쓰셨던 것 생각 나시죠?
해당 태그를 호출하면 Login.jsx 에서 Login 함수의 반환 값을 App.js <div> 의 태그 안으로 갖고 오게 되면서 화면에 작성했던 코드를 볼 수 있게됩니다. 그러므로 우선 Login 함수의 return 문 안에 html을 작성해봅시다.
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
const User = {
email: 'abc@naver.com',
pw: 'System2000!!'
}
맨 위쪽에는 useEffect와 useState를 임포트 해주고, 페이지 이동을 위해 Link도 임포트 해줍니다.
아마 처음 리액트를 배우시는 분들은 Link 컴포넌트를 임포트할 때 아마 인식이 안되실거에요 !
react-router-dom 이 설치되지 않으셔서 그러신 거니,
당황하지 마시고 파워쉘에 다음 명령어를 입력해주시면 이제 vscode가 해당 함수를 인식합니다.
npm i react-router-dom
그 다음 정상적으로 작동하는지 확인하기 위해 User 라는 값을 생성해서 더미데이터를 만들어 줍시다.
export default function Login() {
const [email, setEmail] = useState('');
const [pw, setPw] = useState('');
const [emailValid, setEmailValid] = useState(false);
const [pwValid, setPwValid] = useState(false);
const [notAllow, setNotAllow] = useState(true);
갑자기 뭐가 많긴 한데 간단히 아래와 같은 기능을 하는 변수들이라고 생각하시면 됩니다.
- email : 이메일을 저장하는 변수
- pw : 비밀번호를 저장하는 변수
- emailValid : 이메일의 유효성 테스트 값을 저장하는 변수
- pwValid : 비밀번호의 유효성 테스트 값을 저장하는 변수
- notAllow : 로그인 버튼의 비활성화 여부를 저장하고 있는 변수
각 변수 옆의 set~~~ 이런건 해당 변수에 값을 변경하거나 지정할 때 사용하는 함수라고 생각하시면 됩니다.
이메일과 비밀번호는 유효성 체크 전엔 초기값을 false로 설정하고,
로그인 버튼은 입력이 완료되기 전까지 활성화 되면 안되니 true 로 설정해줍니다.
useState는 간단히 말해서 새로고침을 하지 않아도 동적으로 페이지가 변하도록 도와주는 컴포넌트라고 보시면 됩니다.
const handleEmail = (e) => {
setEmail(e.target.value);
const regex =
/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i;
if (regex.test(email)) {
setEmailValid(true);
} else {
setEmailValid(false);
}
}
const handlePw = (e) => {
setPw(e.target.value);
const regex =
/^(?=.*[a-zA-z])(?=.*[0-9])(?=.*[$`~!@$!%*#^?&\\(\\)\-_=+])(?!.*[^a-zA-z0-9$`~!@$!%*#^?&\\(\\)\-_=+]).{8,20}$/;
if (regex.test(pw)) {
setPwValid(true);
} else {
setPwValid(false);
}
}
const onClickConfirmButton = () => {
if (email === User.email && pw === User.pw) {
alert('로그인에 성공했습니다.')
} else {
alert('등록되지 않은 회원이거나 입력한 값이 일치하지 않습니다.')
}
}
useEffect( () => {
if (emailValid && pwValid) {
setNotAllow(false);
return;
}
setNotAllow(true);
}, [emailValid, pwValid]);
함수는 간단하게 4개 정도만 추가했는데요, 사실 저도 유튜브 보고 따라한거라 이해하기만 했는데 설명 드리자면, 이메일과 비밀번호의 유효성을 체크하는 함수 두개와 더미데이터를 테스트해볼 수 있는 로그인 성공 여부 체크 함수 한개, 마지막으로 버튼의 활성화 여부를 체크하는 함수 한개 로 구성되어있습니다.
regex는 이메일이나 비밀번호 조건에 부합하도록 검사하는 정규화 표현식인데요,
만약 regex 에 부합하지 않는 값이 들어온다면 false를 반환하도록 되어있습니다.
useEffect는 React 의 Hook으로 컴포넌트가 렌더링될 때 특정 사이드 이펙트를 수행할 수 있게 해줍니다
useEffect는 두 가지 주요 인수(arguments)를 받는데요,
- 첫 번째 인수: 함수 (컴포넌트가 렌더링될 때 실행됨)
- 두 번째 인수: 배열 (useEffect가 의존하는 값들이 들어가며, 배열에 있는 값이 변경될 때마다 useEffect 내부의 함수가 실행됨. 만약 이 배열을 생략하면, 컴포넌트가 렌더링될 때마다 해당 함수가 실행됨)
그래서 제가 작성한 함수를 봤을 때, emailValid 와 pwValid가 변경될 때 마다 제가 임의로 작성한 함수가 작동하게 되는겁니다. 저는 두 유효성 체크를 모두 통과했을 때에만 버튼을 활성화 시키도록 작성했습니다.
return (
<div className="page">
<div className='titleWrap'>
<br/>
로그인
</div>
<div className="contentWrap">
<div className="inputTitle">이메일 주소</div>
<div className="inputWrap">
<input
type="text"
className="input"
placeholder="you@example.com"
value={email}
onChange={handleEmail}/>
</div>
<div className="errorMessageWrap">
{
!emailValid && email.length > 0 && (
<div>올바른 이메일을 입력해주세요.</div>
)}
</div>
<div style={{ marginTop: "26px" }} className="inputTitle">비밀번호</div>
<div className="inputWrap">
<input
type="password"
className="input"
placeholder='영문, 숫자, 특수문자 포함 8자 이상'
value={pw}
onChange={handlePw}/>
</div>
<div className="errorMessageWrap">
{
!pwValid && pw.length > 0 && (
<div>영문, 숫자, 특수문자 포함 8자 이상 입력해주세요.</div>
)}
</div>
</div>
<div className='buttonWrap'>
<button onClick={onClickConfirmButton} disabled={notAllow} className="bottomButton">
로그인
</button>
</div>
<hr nonshade/>
<div className='registerWrap'>
<div className='registerTitle'>
계정이 없으신가요? <Link to="/register">가입하기</Link>
</div>
</div>
</div>
);
}
나머지 실질적인 부분들은 크게 이런 구조으로 작성했습니다
- page : 로그인 페이지
- titleWrap : 제목 부분
- contentWrap : 나머지 부분들
- inputTitle
- inputWrap
- errormessageWrap : 정규화 식 체크 부분
- buttonWrap : 로그인 버튼 부분
- registerWrap : 회원가입 페이지 이동 부분
- registerTitle
마지막의 가입하기 부분은 'Link' 라는 컴포넌트를 사용했는데요,
a 태그의 href는 새로운 페이지를 직접 이동하여 시간이 오래걸리는 반면,
Link는 전체 페이지를 새로고침 하지 않고도 페이지를 이동할 수 있기 때문에 많이 사용한다고 합니다.
그래서 <Link to="/register"> 태그를 쓰기 위해서 라우터도 작성해주어야 하는데요,
저는 App.js 파일을 다음과 같이 수정해주었습니다. Router.js 도 추가해보고 index.js 도 수정해보고 했지만
아래와 같은 방법이 제일 대중적이더라구요! 참고로 현재 상황에서 index.js 는 수정하지 않았습니다.
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Login from './Login';
import Register from './Register';
function App() {
return (
<div>
<BrowserRouter>
<Routes>
<Route path='/' element={<Login />} />
<Route path='/register' element={<Register />} />
</Routes>
</BrowserRouter>
</div>
);
}
export default App;
간단하게 라우팅 테이블을 적어놓았다고 생각하시면 이해하시기 편하실 거에요.
저는 바로 로그인 페이지로 접속하도록 설정했기 때문에 path를 '/' 로만 입력했는데,
'/login' 이런식으로 바꾸고 </BrowserRouter> 태그 밑에 <Login /> 태그를 넣으셔도 무방합니다😊
그래서 완성된 Login.jsx 의 전체 코드는 아래와 같습니다.
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
const User = {
email: 'abc@naver.com',
pw: 'System2000!!'
}
export default function Login() {
const [email, setEmail] = useState('');
const [pw, setPw] = useState('');
const [emailValid, setEmailValid] = useState(false);
const [pwValid, setPwValid] = useState(false);
const [notAllow, setNotAllow] = useState(true);
const handleEmail = (e) => {
setEmail(e.target.value);
const regex =
/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i;
if (regex.test(email)) {
setEmailValid(true);
} else {
setEmailValid(false);
}
}
const handlePw = (e) => {
setPw(e.target.value);
const regex =
/^(?=.*[a-zA-z])(?=.*[0-9])(?=.*[$`~!@$!%*#^?&\\(\\)\-_=+])(?!.*[^a-zA-z0-9$`~!@$!%*#^?&\\(\\)\-_=+]).{8,20}$/;
if (regex.test(pw)) {
setPwValid(true);
} else {
setPwValid(false);
}
}
const onClickConfirmButton = () => {
if (email === User.email && pw === User.pw) {
alert('로그인에 성공했습니다.')
} else {
alert('등록되지 않은 회원이거나 입력한 값이 일치하지 않습니다.')
}
}
useEffect( () => {
if (emailValid && pwValid) {
setNotAllow(false);
return;
}
setNotAllow(true);
}, [emailValid, pwValid]);
return (
<div className="page">
<div className='titleWrap'>
<br/>
로그인
</div>
<div className="contentWrap">
<div className="inputTitle">이메일 주소</div>
<div className="inputWrap">
<input
type="text"
className="input"
placeholder="you@example.com"
value={email}
onChange={handleEmail}/>
</div>
<div className="errorMessageWrap">
{
!emailValid && email.length > 0 && (
<div>올바른 이메일을 입력해주세요.</div>
)}
</div>
<div style={{ marginTop: "26px" }} className="inputTitle">비밀번호</div>
<div className="inputWrap">
<input
type="password"
className="input"
placeholder='영문, 숫자, 특수문자 포함 8자 이상'
value={pw}
onChange={handlePw}/>
</div>
<div className="errorMessageWrap">
{
!pwValid && pw.length > 0 && (
<div>영문, 숫자, 특수문자 포함 8자 이상 입력해주세요.</div>
)}
</div>
</div>
<div className='buttonWrap'>
<button onClick={onClickConfirmButton} disabled={notAllow} className="bottomButton">
로그인
</button>
</div>
<hr nonshade/>
<div className='registerWrap'>
<div className='registerTitle'>
계정이 없으신가요? <Link to="/register">가입하기</Link>
</div>
</div>
</div>
);
}
css 는 아래와 같이 작성하였습니다. 참고만 해주세요 ㅎㅎ
.page {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
max-width: 500px;
padding: 0 20px;
left: 50%;
transform: translate(-50%, 0);
background-color: #f7f7f7;
overflow: hidden;
display: flex;
flex-direction: column;
}
.titleWrap {
margin-top: 87px;
font-size: 26px;
font-weight: 700;
color: #262626;
}
.contentWrap {
margin-top: 26px;
flex: 1 0 1;
}
.inputTitle {
font-size: 12px;
font-weight: 600;
color: #262626;
}
.inputWrap {
display: flex;
border-radius: 8px;
padding: 16px;
margin-top: 8px;
background-color: white;
border: 1px solid #e2e0e0;
}
.inputWrap:focus-within {
border: 1px solid #e854cc;
}
.input {
width: 100%;
outline: none;
border: none;
height: 17px;
font-size: 14px;
font-weight: 400;
}
.input::placeholder {
color: #dadada;
}
.errorMessageWrap {
margin-top: 8px;
color: #ef0000;
font-size: 12px;
}
.buttonWrap {
flex: 1 0 1;
margin-top: 20px;
}
.bottomButton {
background-color: #e854cc;
width: 100%;
height: 48px;
border: none;
font-weight: 700;
border-radius: 64px;
color: white;
cursor: pointer;
}
.bottomButton:disabled {
background-color: #dadada;
color: white;
}
hr {
background-color: #dadada;
margin-top: 16px;
border: none;
height: 0.3px;
width: 100%;
}
.registerWrap {
flex: 1 0 1;
margin-top: 5px;
}
.registerTitle {
font-size: 14px;
font-weight: 400;
color: #919191;
text-align: center;
}
a {
text-decoration: none;
font-weight: 800 !important;
color: #e854cc !important;
cursor: pointer;
}
개인적으로 css 를 작성하면서 까다로웠던 점은 flex 의 여백 조절이 어려웠다는 것인데요,
이 부분은 추후에 따로 포스팅할 예정입니당 😙
Register 페이지 작성
Register 페이지는 Login 페이지와 유사하여 변경된 부분만 설명 드리겠습니다.
가입 완료 버튼을 눌렀을 때 다시 로그인 페이지로 이동하게 하기 위해서 useNavigate 라는 컴포넌트를 사용했는데요,
import { useNavigate } from 'react-router-dom';
const navigate = useNavigate();
const onClickConfirmButton = () => {
alert('회원가입이 완료되었습니다.')
navigate('/');
}
이동하고 싶은 컴포넌트가 Login 부분이기 때문에 아까 설정했던 경로인 '/'를 navigate 함수의 인자로 사용해주면
회원가입 완료 알람이 뜨고 난 후, 로그인 페이지로 이동하게 됩니다.
Link 와는 다른 점은 , Link는 조건 없이 바로 해당 컴포넌트로 이동하게 된다면,
Navigate 는 일정 조건이 발생해야 지정한 컴포넌트로 이동할 수 있다는 것입니다.
그래서 사용하기 전에 useNavigate 를 임포트 하고, navigate 객체를 만든 후,
사용하고 싶은 함수 내부에 사용해주시면 됩니당 🤩
그렇게 해서 완성하게 된 Register.jsx의 전체 코드는 아래와 같습니다 !
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
export default function Register() {
const [email, setEmail] = useState('');
const [pw, setPw] = useState('');
const [emailValid, setEmailValid] = useState(false);
const [pwValid, setPwValid] = useState(false);
const [notAllow, setNotAllow] = useState(true);
const navigate = useNavigate();
const handleEmail = (e) => {
setEmail(e.target.value);
const regex =
/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i;
if (regex.test(email)) {
setEmailValid(true);
} else {
setEmailValid(false);
}
}
const handlePw = (e) => {
setPw(e.target.value);
const regex =
/^(?=.*[a-zA-z])(?=.*[0-9])(?=.*[$`~!@$!%*#^?&\\(\\)\-_=+])(?!.*[^a-zA-z0-9$`~!@$!%*#^?&\\(\\)\-_=+]).{8,20}$/;
if (regex.test(pw)) {
setPwValid(true);
} else {
setPwValid(false);
}
}
const onClickConfirmButton = () => {
alert('회원가입이 완료되었습니다.')
navigate('/');
}
useEffect( () => {
if (emailValid && pwValid) {
setNotAllow(false);
return;
}
setNotAllow(true);
}, [emailValid, pwValid]);
return (
<div className="page">
<div className='titleWrap'>
이메일과 비밀번호를
<br/>
입력해주세요
</div>
<div className="contentWrap">
<div className="inputTitle">이메일 주소</div>
<div className="inputWrap">
<input
type="text"
className="input"
placeholder="you@example.com"
value={email}
onChange={handleEmail}/>
</div>
<div className="errorMessageWrap">
{
!emailValid && email.length > 0 && (
<div>올바른 이메일을 입력해주세요.</div>
)}
</div>
<div style={{ marginTop: "26px" }} className="inputTitle">비밀번호</div>
<div className="inputWrap">
<input
type="password"
className="input"
placeholder='영문, 숫자, 특수문자 포함 8자 이상'
value={pw}
onChange={handlePw}/>
</div>
<div className="errorMessageWrap">
{
!pwValid && pw.length > 0 && (
<div>영문, 숫자, 특수문자 포함 8자 이상 입력해주세요.</div>
)}
</div>
</div>
<div className='buttonWrap'>
<button onClick={onClickConfirmButton} disabled={notAllow} className="bottomButton">
가입
</button>
</div>
<hr nonshade/>
<div className='registerWrap'>
<div className='registerTitle'>
계정이 있으신가요? <Link to="/">로그인하기</Link>
</div>
</div>
</div>
);
}
완성된 회원가입 페이지는 이렇게 생겼습니다 ㅎㅎ
실제로는 이렇게 작동합니다