프로젝트를 진행하면서 로그인 컴포넌트를 구현하는 것은 서버와 통신하는 가장 첫번째 단계에 해당했다.
로그인을 어떻게 구현하는지 알고, 그 과정을 이해하는 것을 통해 백엔드와 프론트엔드를 어떻게 연결하는지에 대해서 탐구해 볼 수 있었다.
해당 블로그가 그 과정을 이해하는데 도움이 되길 바란다.
아래는 해당 프로젝트에서 사용한 로그인 컴포넌트의 코드이다.
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const Login = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loginCheck, setLoginCheck] = useState(false); // 로그인 상태 체크
const navigate = useNavigate();
const handleLogin = async (event) => {
// 로그인 처리 로직을 구현합니다.
event.preventDefault();
await new Promise((r) => setTimeout(r, 1000));
const response = await fetch(
"로그인 서버 주소",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: email,
password: password,
}),
}
);
const result = await response.json();
if (response.status === 200) {
setLoginCheck(false);
// Store token in local storage
sessionStorage.setItem("token", result.token);
sessionStorage.setItem("email", result.email); // 여기서 userid를 저장합니다.
sessionStorage.setItem("role", result.role); // 여기서 role를 저장합니다.
sessionStorage.setItem("storeid", result.storeId); // 여기서 role를 저장합니다.
console.log("로그인성공, 이메일주소:" + result.email);
navigate("/"); // 로그인 성공시 홈으로 이동합니다.
} else {
setLoginCheck(true);
}
};
return (
<div className="login-container">
<form className="login-form" onSubmit={handleLogin}>
<h1>On&Off</h1>
<label htmlFor="username">이메일</label>
<input
type="text"
id="username"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<label htmlFor="password">비밀번호</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{loginCheck && (
<label style={{color: "red"}}>이메일 혹은 비밀번호가 틀렸습니다.</label>
)}
<button onClick={handleLogin}>로그인</button>
<p className="signup-link">
아직 회원이 아니신가요? <Link to="/signup">회원가입</Link>
</p>
</form>
</div>
);
};
export default Login;
전체적인 로직을 설명하기 전에 서버와의 통신을 위해서 필수적인 지식이 필요하다. 그 지식은 다음과 같다.
- rest api
- rest api란 url주소와 메소드로 CRUD를 요청하는 것이다.
Create: POST
Read: GET
Update: PUT
Delete: DELETE
다음으로 해당프로젝트에서 사용한 fetch 함수에 대해서 설명하겠다.
해당 프로젝트에서는 fetch 함수를 통해서 서버와의 통신을 진행했다.
fetch 함수는 JavaScript에서 제공하는 API 중 하나로, 서버에서 데이터를 가져오기 위해 사용된다.
fetch 함수는 네트워크 요청을 보내고, 서버로부터 응답을 받는다. 응답은 promise 객체로 반환되며, 이를 통해 비동기적으로 데이터를 처리할 수 있다.
즉, fetch API는 백엔드 서버와 데이터를 주고받는데 사용한다.
fetch().then().then()
fetch() : 요청지 주소, method headers 등 요청정보, 데이터 정보(body)
then() : 첫번째 then. 받을 데이터 형태의 빈깡통으로 세팅. ---> 나머지 랜더링을 계속 진행
then() : 두번째 then. 실제 데이터가 담기는 곳. ---> 랜더링 완료 후 두번째 then에 데이터가 있으면 다시 랜더링이 일어남.
위와 같은 구조를 갖는다. 아래는 간단한 사용 예시들이다.
fetch('요청지 주소', { method : "GET" }) //메소드 방식 지정
.then(res => res.json()) //json으로 받을 것을 명시
.then(res => { //실제 데이터를 상태변수에 업데이트
console.log(1, res);
setValue(res);
});
fetch("요청지 주소", {
method : "POST", //메소드 지정
headers : { //데이터 타입 지정
"Content-Type":"application/json; charset=utf-8"
},
body: JSON.stringify(data) //실제 데이터 파싱하여 body에 저장
}).then(res=>res.json()) // 리턴값이 있으면 리턴값에 맞는 req 지정
.then(res=> {
console.log(res); // 리턴값에 대한 처리
});
const response = await fetch(
"요청지 주소",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: email,
password: password,
}),
}
);
const result = await response.json();
const response = await fetch(URL, options)
: 첫 번째 인수로는 요청을 보낼 URL을, 두 번째 인수로는 요청 옵션(메소드, 헤더, 바디 등)을 객체 형태로 전달method: "POST"
: HTTP 요청 방식을 POST로 설정한다. POST 방식은 주로 서버에 새로운 정보를 추가할 때 사용한다.headers: { "Content-Type": "application/json" }
: HTTP 요청 헤더를 설정한다. 여기서는 "Content-Type"이라는 헤더의 값을 "application/json"으로 설정하여, 요청 바디의 타입이 JSON임을 명시한다.body: JSON.stringify({ email: email, password: password })
: HTTP 요청 바디를 설정한다. 요청 바디는 서버에 보낼 데이터를 담는다. 여기서는 email과 password라는 두 개의 값을 JSON 형태로 변환하여 요청 바디로 전달한다.const result = await response.json()
: response.json() 메소드는 응답 바디를 JSON으로 해석하여 Promise를 반환한다. 따라서 await 키워드를 사용하여 이 Promise가 완료될 때까지 기다린 후, 그 결과를 result에 저장한다. 이 result는 서버로부터 받은 응답 데이터를 나타낸다.이 코드는 비동기식으로 작동하므로, await 키워드를 사용하는 부분은 반드시 async 함수 내부에서 호출되어야 한다.
if (response.status === 200) {
setLoginCheck(false);
// Store token in local storage
sessionStorage.setItem("token", result.token);
sessionStorage.setItem("email", result.email); // 여기서 userid를 저장합니다.
sessionStorage.setItem("role", result.role); // 여기서 role를 저장합니다.
sessionStorage.setItem("storeid", result.storeId); // 여기서 role를 저장합니다.
console.log("로그인성공, 이메일주소:" + result.email);
navigate("/"); // 로그인 성공시 홈으로 이동합니다.
} else {
setLoginCheck(true);
}
};
이 코드는 서버로부터 받은 응답(response)의 상태 코드가 200인지를 확인하여 로그인 성공 여부를 판단하고, 로그인 성공 시 사용자 정보를 세션 스토리지에 저장하고, 홈 화면으로 이동하는 등의 동작을 구현하는 JavaScript 코드이다.
if (response.status === 200) { ... }
: response.status는 서버로부터 받은 응답의 HTTP 상태 코드를 나타낸다. 200은 HTTP에서 'OK'를 의미하는 상태 코드로, 요청이 성공적으로 처리되었음을 나타낸다.setLoginCheck(false);
: setLoginCheck 함수를 호출하여 로그인 체크 상태를 false로 설정한다. (초기 로그인 상태는 false이다.)sessionStorage.setItem("key", value);
: 세션 스토리지는 웹 페이지의 세션 동안 데이터를 저장하는데 사용되는 저장소이다. 페이지를 새로 고침하거나 브라우저를 닫지 않는 한 데이터가 유지된다. setItem 메소드는 세션 스토리지에 특정 키와 값을 저장한다. 여기서는 "token", "email", "role", "storeid"라는 키에 각각 result.token, result.email, result.role, result.storeId 값을 저장한다.console.log("로그인성공, 이메일주소:" + result.email);
: 콘솔에 "로그인성공, 이메일주소:"라는 문자열과 result.email의 값을 출력한다.navigate("/");
: navigate 함수를 호출하여 홈 화면("/")으로 이동한다.else { setLoginCheck(true); }
: 서버로부터의 응답이 200이 아니라면, 즉 로그인이 성공하지 않았다면 setLoginCheck 함수를 호출하여 로그인 체크 상태를 true로 설정한다.아래 코드는 해당 프로젝트에서 사용한 회원가입 컴포넌트의 코드이다. 해당 프로젝트에서 사용한 로그인 컴포넌트에 대해서 소개하며 fetch와 sessionStorage에 대해서 설명을 하였기에 간략하게 설명하고 로그아웃 컴포넌트를 어떻게 구현했는지 소개하겠다.
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const Signup = () => {
const [email, setEmail] = useState("");
const [username, setUsername] = useState("");
const [userNickname, setUserNickname] = useState("");
const [phoneNumber, setPhoneNumber] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [role, setRole] = useState("");
const navigate = useNavigate();
const handleSignup = async (event) => {
event.preventDefault();
// 회원가입 처리 로직을 구현합니다.
// Check if passwords match
if (password !== confirmPassword) {
alert("비밀번호가 일치하지 않습니다.");
return;
}
// Create payload
const payload = {
email: email,
password: password,
nickname: userNickname,
name: username,
phone: phoneNumber,
role: role,
};
try {
const response = await fetch(
"요청지 주소",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
}
);
const data = await response.json();
if (response.status === 201) {
// Redirect to login.html
console.log("성공! 이메일주소: " + data.email);
navigate("/login"); // 로그인 성공시 홈으로 이동합니다.
} else if (response.status === 400) {
// Handle error
alert(`회원가입 실패: ${data.email}`);
}
} catch (error) {
console.error("오류 발생:", error);
}
};
return (
<div className="signup-container">
<form className="signup-form" onSubmit={handleSignup}>
<h1>On&Off</h1>
<label htmlFor="email">이메일</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<label htmlFor="username">사용자명</label>
<input
type="text"
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<label htmlFor="nickname">닉네임</label>
<input
type="text"
id="nickname"
value={userNickname}
onChange={(e) => setUserNickname(e.target.value)}
/>
<label htmlFor="phone-number">전화번호</label>
<input
type="text"
id="phone-number"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
/>
<label htmlFor="password">비밀번호</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<label htmlFor="confirm-password">비밀번호 확인</label>
<input
type="password"
id="confirm-password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
<div className="role-selection">
<label>회원 유형</label>
<div>
<input
type="radio"
id="customer"
value="user"
checked={role === "user"}
onChange={() => setRole("user")}
/>
<label htmlFor="customer">고객</label>
</div>
<div>
<input
type="radio"
id="seller"
value="seller"
checked={role === "seller"}
onChange={() => setRole("seller")}
/>
<label htmlFor="seller">판매자</label>
</div>
</div>
<button id="signup-button" onClick={handleSignup}>
회원가입
</button>
<p className="login-link">
이미 회원이신가요? <Link to="/login">로그인</Link>
</p>
</form>
</div>
);
};
export default Signup;
const payload = {
email: email,
password: password,
nickname: userNickname,
name: username,
phone: phoneNumber,
role: role,
};
try {
const response = await fetch(
" https://port-0-gateway-12fhqa2llofoaeip.sel5.cloudtype.app/auth/signup",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
}
);
const data = await response.json();
if (response.status === 201) {
// Redirect to login.html
console.log("성공! 이메일주소: " + data.email);
navigate("/login"); // 로그인 성공시 홈으로 이동합니다.
} else if (response.status === 400) {
// Handle error
alert(`회원가입 실패: ${data.email}`);
}
} catch (error) {
console.error("오류 발생:", error);
}
로그아웃의 로직은 간단하다. 아래는 해당 프로젝트에서 구현한 로그아웃 컴포넌트의 코드이다.
import { faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import "./UserProfile.css";
const UserProfile = () => {
const handleLogout = () => {
// 로그아웃 처리 로직을 구현합니다.
sessionStorage.removeItem("token");
sessionStorage.removeItem("email");
sessionStorage.removeItem("role");
sessionStorage.removeItem("storeid");
// 페이지 이동
navigate("/");
};
return (
<div className="user-profile">
<div className="user-logout-btn-container">
<button className="user-logout-btn" onClick={handleLogout}>
로그아웃
<div className="move-page-icon">
<FontAwesomeIcon icon={faChevronRight} />
</div>
</button>
</div>
</div>
);
};
onClick={handleLogout}
을 통해서 로그아웃 버튼을 클릭시 sessionStorage에 갖고있던 로그인 정보를 모두 버려주면 된다.sessionStorage.removeItem("token");
sessionStorage.removeItem("email");
sessionStorage.removeItem("role");
sessionStorage.removeItem("storeid");
로그인, 회원가입, 로그아웃을 구현하였다면 서버와의 통신에 대한 기초적인 이해가 생겼을 것이라 생각이 된다. 기술이 빠르게 발전하며 더 효율적이고, 좋은 구현방법이 나오고 있다. 현재는 이러한 방식으로 구현을 해놓은 상태이지만, 더 나은 방법에 대해서 탐구해보고 유지보수를 해보고자 한다.