로그인 기능 구현하기

345·2022년 12월 3일
2

유저가 사이트에 회원가입을 할 수 있도록 기능을 구현했습니다.
이제 유저가 사이트에 로그인하고, 로그인한 회원의 정보를 이용하여 유저에게 다양한 기능을 제공할 수 있도록 해봅시다.

구현해야 할 사항은 다음과 같습니다.

  1. 로그인 페이지 뷰 (View)
  2. 뷰를 로딩해줄 컨트롤러 (GET 요청 처리)
  3. 로그인 정보를 모델로 확인해줄 컨트롤러 (POST 요청 처리)

🔥 로그인 기능 구현하기

로그인 기능은 /login 라우트에서 처리합니다.

GET 요청을 하면 로그인 페이지를 로딩하고,
POST 요청을 하면 사용자가 form 으로 보낸 유저 정보를 받아와 DB 에 입력된 사용자와 일치하는지 확인합니다.

import express from "express";

import {
  getJoin,
  postJoin,
  getLogin,
  postLogin,
} from "../controllers/userController";

const rootRouter = express.Router();

.
.
.
rootRouter.route("/login").get(getLogin).post(postLogin);

export default rootRouter;

뷰 만들기

<!-- login.html -->
<form method="POST">
  <input placeholder="Username" type="text" name="username" required />
  <input placeholder="Password" type="password" name="password" required />
  <input type="submit" value="Login" />
</form>

<hr />
<div>
  <span>
    Don't have an account?
    <a href="/join">Create one now &rarr;</a>
  </span>
</div>

username 과 password 정보를 받아 POST 로 전송해주는 form 을 만들었습니다.
하단에는 링크를 걸어서, 미가입자일 경우 회원가입 페이지로 이동하게 합니다.

login | GET 요청 처리

사용자가 /login 으로 요청을 보냈을 때 그에 대한 응답을 처리합니다.
로그인 페이지 뷰를 렌더링하는 코드를 작성합니다.

// userController.js 파일
export const getLogin = (req, res) => 
	res.render("login.html", { pageTitle: "Login" });

login.html 페이지를 넘겨줍니다.

login | POST 요청 처리

사용자가 /login 라우트로 GET 요청을 보내서 받은 login.html 페이지에서,
form 에 유저 정보를 입력하고 제출하면 /login 으로 POST 요청이 들어옵니다.

이 요청의 body 에 포함된 유저 정보를 이용하여 로그인 서비스를 구현합니다.

  1. 유저가 실제 존재하는지 확인
  2. 유저의 비밀번호가 DB 상의 정보와 일치하는지 확인

위와 같은 확인 절차가 필요합니다.

// userController.js 파일
import User from "models/User";

export const postLogin = async (req, res) => {
  const { username, password } = req.body;
  const pageTitle = "Login";
  
  // username: username 인 유저가 있는지 확인
  // findOne 으로 조건에 일치하는 doc 을 불러옴
  const user = await User.findOne({ username });

  if (!user) {
    // 유저가 존재하지 않는다면 상태코드 변경 후 리턴
    return res.status(400).render("login.html", {
      pageTitle,
      errorMessage: "An account with this username does not exists.",
    });
  }

  /* bcrypt 를 이용하여 form 에 입력된 비밀번호와 
     DB 에 입력된 비밀번호가 일치하는지 확인
     
     bcrypt.compare( ) 을 이용하여 자동으로 해싱, 솔트 적용 후 비교해줌
*/  
  const ok = await bcrypt.compare(password, user.password);
  
  // 비밀번호가 일치하지 않으면 상태코드 변경 후 리턴
  if (!ok) {
    return res.status(400).render("login.html", {
      pageTitle,
      errorMessage: "Wrong password",
    });
  }
  
  // 유저가 존재하고, 비밀번호도 일치한다면 루트 페이지로 리다이렉트
  return res.redirect("/");
};

로그인 정보를 확인 후 오류가 있을 경우 다시 로그인 페이지로, 아무 오류도 없을 경우 메인 페이지로 돌아가도록 하였습니다.

✅ 로그인한 유저 정보

유저가 로그인을 했으니 사이트는 유저 정보를 기억해야 합니다.
보통 웹 사이트에서 로그인을 하면 일정 시간동안은 다시 로그인을 안 해도 브라우저가 내 로그인 정보를 기억합니다.

하지만... 이건 원래 자연스러운 일이 아닙니다.

브라우저와 서버의 통신 방식

HTTP 프로토콜이 가지는 특성은 무상태성비연결성 입니다.

브라우저는 서버에 요청 을 하고, 서버는 이에 응답 합니다.
한 번의 request-response 사이클이 끝나면 브라우저와 서버 간 연결도 끊어집니다. 연결을 계속 유지하지 않습니다.

이것이 비연결성 입니다.

또한, 다음 번 요청을 할 때 서버는 클라이언트의 상태를 기억하지 않습니다.
클라이언트가 로그인 정보를 보내고 서버가 로그인한 화면을 응답했다고 합시다.

다음 번 클라이언트의 요청에, 서버는

이 클라이언트는 아까 로그인한 애네? 이름이 홍길동 이었지...

하고 기억하지 않습니다.

이것이 무상태성 입니다.
즉, 로그인한 상태를 유지하고 싶다면 클라이언트는
매 요청에서 로그인 관련 정보를 보내야만 합니다.

매 화면마다 로그인을 해야 하는 건 불편합니다....
이를 해결하기 위해 쿠키세션 을 사용합니다.

✨ 쿠키와 세션

쿠키는 웹 브라우저가 가지는 클라이언트의 정보에 대한 문자열이고,
세션은 서버에서 관리되는 저장소라고 볼 수 있습니다.

세션은 쿠키를 이용하여 동작하는데, 쿠키에 포함된 세션 ID 정보로
세션에서 개별 클라이언트의 정보를 가져옵니다.
동작 과정은 다음과 같습니다.

  1. 클라이언트가 로그인
  2. 서버가 세션 생성 후 세션 ID 를 쿠키에 담아 클라이언트에 전송
  3. 클라이언트는 세션 ID 정보를 포함한 쿠키를 받게 됨
  4. 다음 번 요청부터 클라이언트는 서버에 쿠키를 전송

이렇게 브라우저는 매 요청마다 쿠키를 전송하고,
서버는 쿠키의 세션 ID 정보로 해당 클라이언트를 식별하는 것이죠.

더 자세한 정보는 아래 링크를 참고합시다.

What are session cookies? Do they need consent?


🟢 express-session

세션을 사용하기 위해 모듈을 설치해봅시다.

express-session - npm

위 링크에 express-session 모듈과 관련한 설명이 나와있습니다.
터미널에서 npm i express-session 명령어를 입력하여 모듈을 설치합니다.

express-session 모듈을 다루기 위한 설명입니다.

  • 클라이언트(브라우저)의 세션 관리
    • 접속 시 세션 생성 후 세션 아이디를 담은 쿠키 전송
    • 쿠키가 보유한 세션 아이디가 유효하지 않으면
      새로운 세션 생성 후 쿠키를 다시 보냄
      + ex) 세션이 종료되어 세션 정보가 삭제되었을 때

  • 생성된 세션은 Object
    • 언제든지 세션에 요소를 추가하여 사용 가능

express app 을 생성한 js 파일에서 세션 미들웨어를 추가합니다.

import session from "express-session";

const app = express();

.
.
.

// 미들웨어로 세션 생성
app.use(
  session({
    secret: "Hello!",
    resave: true,
    saveUninitialized: true,
  })
);

session 미들웨어를 사용하면, store 에 JSON 형태로 세션 객체가 생성되고 req.session 객체로 세션에 접근할 수 있습니다.

express-session 문서를 읽어보면,
resavesaveUninitialized 속성은 병렬상황과 메모리 효율을 위해
false 로 설정하길 추천하고 있지만, 일단 기본 설정인 true 로 합니다.
속성값은 추후 용도에 맞게 변경하면 되겠습니다.

위처럼 설정하면 브라우저가 서버에 요청시, 서버에서 그를 위한 세션을 생성하고 세션 아이디 정보를 담은 쿠키를 보내줍니다.
세션 정보는 sessionStore 에 저장되며, 세션 종료시 초기화됩니다.


로그인 정보 저장

로그인을 하면 세션에 유저 정보와 로그인 여부를 저장하도록 설정합니다.

export const postLogin = async (req, res) => {
  const { username, password } = req.body;
  
  const user = await User.findOne({ username });
  
  .
  .
  .

  // 세션에 로그인 여부와 유저 정보 저장
  // 세션에 loggedIn:true 와 user: {유저정보 object} 로 key:value 형태 정보 저장
  req.session.loggedIn = true;
  req.session.user = user;
  return res.redirect("/");
};

req.session 으로 접속한 브라우저의 세션을 사용합니다.
세션에서 loggedInuser 이라는 key 를 생성하고 value 를 넣어줍니다.


로그인 정보 미들웨어 설정

매 요청마다 세션에서 loggedIn 정보와 user 정보를 얻어올 수 있도록
미들웨어를 설정해봅시다.

import session from "express-session";

const app = express();

.
.
.

// 미들웨어로 세션 생성
app.use(
  session({
    secret: "Hello!",
    resave: true,
    saveUninitialized: true,
  })
);

// 로그인 정보를 설정하는 미들웨어
// 따로 파일을 생성해서 설정해줘도 OK
app.user(function(req, res, next) {
  // 세션에서 loggedIn 과 user 정보가 설정되었는지 확인
  // res 의 locals 에 값을 가져와 설정
  res.locals.loggedIn = Boolean(req.session.loggedIn);
  res.locals.loggedInUser = req.session.user;
  next();
 }
)

reqres 는 하나의 request-response 사이클이 끝나면 없어지지만, 세션은 서버 메모리에 저장되며 request-response 사이클 하나가 끝나도 세션 종료 전까지 유지됩니다.

따라서, 로그인한 유저라면 req.session 에 해당 브라우저가 입력한
loggedInuser 정보가 남아있습니다.


뷰에서 로그인 정보 사용

res.locals 를 사용하여 뷰 페이지에서 변수를 편하게 이용합니다.

res.locals ❓

  • request-response 사이클 동안 사용 가능
  • 뷰 페이지에서 변수를 이름만으로 간단히 사용 가능

res.locals 에서 선언한 변수는 별다른 과정 없이 이름만으로 뷰 페이지에서 사용할 수 있습니다.

  {% if loggedIn %}
  <li>
    <!-- 이름만 사용하여 변수 사용! -->
    <a href="/my-profile">{{loggedInUser.username}}'s Profile</a>
  </li>
  <li>
    <a href="/users/logout">Log Out</a>
  </li>
  {% else %}
  <li>
    <a href="/join">Join</a>
  </li>
  <li>
    <a href="/login">Log In</a>
  </li>
  {% endif %}

조건문을 사용하여 로그인했을 때와 아닐 때 보여줄 화면을 선택할 수 있습니다.

profile
기록용 블로그 + 오류가 있을 수 있습니다🔥

1개의 댓글

comment-user-thumbnail
2024년 1월 22일

잘 보고 갑니다 !

답글 달기