Introduction
지금까지 React로 Frontend를 만들고, fetch를 통해 React와 Express가 서로 통신하도록 만들었습니다. 이렇게 서로 통신이 가능하게 되었으니 express-session과 Firebase Realtime Database를 가지고 React에서 유저 인증을 할 수 있도록 해 보겠습니다. *내용이 조금은 길어질 수 있습니다.
Session
이 부분에 대한 정확한 정보는 사실 저도 잘 모르겠습니다만, 우선 제가 파악 내용대로 설명을 드리자면, Session은 유저에 대한 인증 정보를 서버에 저장합니다. 유저가 페이지에 접속했을 때, Express는 유저의 디바이스에 저장된 쿠키 값을 불러옵니다. 이 쿠키 값이 서버에 저장된 값과 인증 정보가 같다면 유저의 세션을 유효한 것으로 간주합니다. 보안 측면에서 이게 쓸만한 기능인지는 모르겠습니다. 보안이 중시되는 경우, Passport.js, JWT등의 다른 인증관련 미들웨어를 알아보시는 것도 좋습니다. 여기에서는 일단 단순 기초적인 세션 구현을 위해 express- session을 사용하도록 하겠습니다.
Installation
$npm i express-session session-file-store
패키지가 저장된 폴더에서 터미널을 열어 npm i express-session session-file-store를 입력합니다. 여기서 express-session은 말 그대로 세션을 불러오고, 저장하는 역할을 합니다. 그러나 express-session은 기본적으로 세션 데이터를 메모리에 저장합니다. 서버가 죽으면 모두 초기화되어버립니다. 따라서, sesion-file-store를 설치해 추가되어 있는 세션의 정보가 서버 디렉터리에 파일 형태로 저장되도록 합니다.
Usage In Project:
유저 인증을 다음과 같이 구현할 예정입니다. DB 부분은 코드 주석에도 언급했겠지만 추후 Firebase Realtime Database를 사용해서 구현하도록 하겠습니다.
다이어그램 형태로 로직에 대한 숙지가 끝났다면, 이제 코드에 이 로직을 구현해보겠습니다.
App.js
App.js에 몇 가지 api를 추가했습니다. 서버에서는 세션 확인, 입력된 전화번호, 방문목적의 유효성 확인, SMS로 전송된 인증번호 유효성 확인을 수행할 수 있게 되었습니다. 이 때 React와는 json으로 통신하게 되는데, 여기서 app이라는 이름의 express 미들웨어를 선언한 직후 express.json() 미들웨어 함수를 로드해야 합니다. 그렇지 않으면 express에서 json을 인식하지 않습니다.
// app.js
const express = require("express");
const path = require("path"); // react build 파일에 접근하기 위해 필요함
const session = require("express-session"); // 세션 처리
var FileStore = require("session-file-store")(session);
const port = process.env.PORT || 5000;
// express 객체 생성
const app = express();
//Express가 json을 파싱할 수 있도록 미들웨어 추가.
//!!!이게 없으면 json 관련해서 아무리 잘 해도 undefined가 떠버립니다!
app.use(express.json());
var sessionOptions = {
store: new FileStore(fileStoreOptions),
secure: false,
secret: "helloexpress",
resave: false,
saveUninitialized: false,
name: "my.connect.sid",
// expires: // day, sec, min, hour, millisecond
cookie: {
maxAge: 86400, // day, sec, min, hour, millisecond // 쿠키의 수명을 하루로 유지합니다.
},
};
//Session 설정
app.use(session(sessionOptions));
var fileStoreOptions = {
retries: 0,
reapInterval: 10,
path: "./session", // 세션 관련 파일을 서버 ./session 경로에 저장합니다.
logFn: function () {},
}; // 세션 저장 관련 처리
// 세션 확인, 클라이언트가 초기에 페이지에 접속할 때 이 부분을 먼저 참조하도록 하여,
// 세션이 있는걸로 확인된다면 바로 메인페이지로 Redirect되도록 설정할 수 있습니다.
app.post("/api/session_check", function (req, res, next) {
if (req.session.phone) {
console.log("session exists : " + req.session.phone);
res.json({
msg: "session exists",
code: 202,
session: req.session.phone,
expires: req.session.valid,
});
} else {
console.log("sess NOT exists");
res.json({ msg: "session not exists", code: 401, session: req.session });
}
});
// 일단 전화번호와 방문 목적을 받습니다.
app.post("/api/info_check", function (req, res, next) {
var isPhoneValid,
isPurposeValid = false;
// 우선 DummyData를 추가합니다. 이 부분은 추후 Firebase Realtime Database로 대치될 예정입니다.
var phone = "01010101010";
var purpose = "As a Owner";
/////////////////// Firebase Database 관련 코드 추가 예정 ///////////////////
// 전화번호 유효성 여부를 검증합니다.
if (req.body.phone === phone) {
isPhoneValid = true;
// 방문목적 유효성 여부를 검증합니다.
if (req.body.purpose === purpose) {
isPurposeValid = true;
// 둘 다 유효한 값이라면, SMS 전송해 인증번호 확인합니다.
if (isPhoneValid && isPurposeValid) {
/////////////////// SOLAPI 문자인증 영역 ///////////////////
res.json({ code: 202, message: "Approved_User" });
//이 응답이 전송된 시점에서 React에서는 인증번호를 입력하는 창이 뜨도록 합니다.
} else {
res.json({ code: 401, message: "Wrong Phone or / and purpose" });
}
} else {
res.json({ code: 401, message: "Purpose Not Matched" });
}
} else {
res.json({ code: 401, message: "Phone Not Matched" });
}
});
app.post("/api/code_check", function (req, res, next) {
// 우선 DummyCode를 추가합니다. 추후 랜덤한 값을 생성하도록 추가될 예정입니다.
/////////////////// CodeGenerator 관련 코드 추가 예정 ///////////////////
var code = 1234;
//받은 코드 유효성 검사 합니다.
if (code === Number(req.body.code)) {
// 유효한 코드가 입력되었다면, 유저를 세션에 추가하도록 합니다.
var sess = req.session;
sess.phone = req.body.phone;
sess.purpose = req.body.purpose;
sess.valid = +new Date() + 86400;
req.session.cookie.expires = 86400;
res.json({ code: 200, msg: "Login_Success!! " });
} else {
res.json({ code: 401, msg: "Login Failed. " });
}
});
app.use(express.static(path.join(__dirname, "client/build")));
app.use("/", function (req, res, next) {
res.sendFile(path.join(__dirname + "/client/build", "index.html"));
});
app.listen(port, function () {
console.log("server works on port :" + port);
});
./client/src/components/Login.js
Login.js는 서버와 통신하는 부분을 추가했습니다. expandAuthField state를 추가해서 LoginForm 부모 컴포넌트인 Login에서 LoginForm의 Collapse를 제어하도록 한 부분을 눈여겨 보시면 되겠습니다. /api/code_check 경로로 POST 가 이루어지는 부분에서 굳이 phone과 purpose를 또 보내준 이유는, 결국 code_check에서 인증 절차가 끝나게 되는데, info_check에서 받은 phone과 purpose를 App.js 내부 전역변수로 저장하기 좀 꺼려져서였습니다. 이 부분이 문제가 되지 않는다면 굳이 또 보내줄 필요 없이 express에서 전역변수로 데이터를 저장하는 방법도 좋습니다. 어디로 가도 목적지에만 도달할 수 있으면 됩니다.
//client/src/pages/Login.js
import React, { useState } from "react";
import LoginForm from "../components/LoginForm";
import Card from "react-bootstrap/Card";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
const Login = () => {
const [phone, setPhone] = useState("");
const [purpose, setPurpose] = useState("");
const [code, setCode] = useState("");
const [expandAuthField, setExpandAuthField] = useState(false);
const handlePhone = (e) => {
setPhone(e.target.value);
};
const handleSelect = (e) => {
setPurpose(e.target.value);
};
const handleCode = (e) => {
setCode(e.target.value);
};
const handleSubmit = (e) => {
const rOptions = {
// 데이터 통신의 방법과 보낼 데이터의 종류, 데이터를 설정합니다.
method: "POST", // POST는 서버로 요청을 보내서 응답을 받고, GET은 서버로부터 응답만 받습니다.
headers: { "Content-Type": "application/json" }, // json형태의 데이터를 서버로 보냅니다.
body: JSON.stringify({
// 이 데이터를 서버가 받아서 처리합니다.
phone: phone,
purpose: purpose,
}),
};
fetch("/api/info_check", rOptions)
.then((res) => res.json()) // Result를 JSON으로 받습니다.
.then((res) => {
console.log(res); // 결과를 console창에 표시합니다.
res.code === 202 && setExpandAuthField(true); // 인증번호를 입력할 수 있도록 UI를 변경시킵니다.
});
};
const handleCodeCheck = (e) => {
const rOptions = {
// 데이터 통신의 방법과 보낼 데이터의 종류, 데이터를 설정합니다.
method: "POST", // POST는 서버로 요청을 보내서 응답을 받고, GET은 서버로부터 응답만 받습니다.
headers: { "Content-Type": "application/json" }, // json형태의 데이터를 서버로 보냅니다.
body: JSON.stringify({ code: code, phone: phone, purpose: purpose }),
};
fetch("/api/code_check", rOptions)
.then((res) => res.json()) // Result를 JSON으로 받습니다.
.then((res) => {
console.log(res); // 결과를 console창에 표시합니다.
});
};
return (
<Row>
<Col xs={1} md={3}></Col>
<Col xs={10} md={6}>
<Card body style={{ marginTop: "1rem", borderRadius: "10px" }}>
<h3>Cranberry</h3>
<h5>Home IoT System</h5>
{/* UI가 백엔드와 연동될 수 있도록 Collapse를 추가합니다. */}
<LoginForm
phone={phone}
purpose={purpose}
showAuthCode={expandAuthField}
handlePhone={handlePhone}
handleSelect={handleSelect}
handleSubmit={handleSubmit}
handleCode={handleCode}
handleConfirm={handleCodeCheck}
/>
</Card>
</Col>
<Col xs={1} md={3}></Col>
</Row>
);
};
export default Login;
Done!
이상적인...환경에서는 큰 이상 없이 성공적으로 작동하는 것을 확인할 수 있습니다. 여기서 보안과 입력 예외처리만 추가한다면 적당히 쓸만한 무언가가 나올 것 같습니다. 다음 포스팅에서는 Firebase Realtime Database를 써서 Express 서버에 DB를 추가하겠습니다.
긴 글 읽어주셔서 감사합니다. 도움이 되셨다면 페이지 하단 좋아요와 광고 클릭 부탁드립니다. 고마움을 표현하는 가장 쉬운 방법입니다.