Lsiron

HELLO FOLIO(express, mongoose)- 3(bcrypt를 이용한 회원가입 구현) 본문

개발일지/HELLO FOLIO

HELLO FOLIO(express, mongoose)- 3(bcrypt를 이용한 회원가입 구현)

Lsiron 2024. 6. 27. 18:04

현재 폴더 구조

web_project/
├── db/
│   ├── model             # 각종 모델 폴더
│   ├── schemas           # 각종 스키마 폴더
│   └── index.js          # db 연결파일
├── middleware/           # 각종 middleware 폴더
├── passport/	          # 로그인 관련 passport 폴더
│   ├── strategies        # 전략 구성
│   └── index.js          # 전략 exports 파일
├── routes/               # 라우트 관련 폴더
│   └── index.js     	  # 라우트 exports 파일
├── services/             # 유저 관련 service 폴더
├── views/                # EJS 템플릿 폴더
├── public/               # static 파일 폴더
│   ├── css/              # CSS 파일들
│   ├── js/               # JS 파일들
│   └── images/           # 이미지 파일들
├── .env                  # 환경 변수 파일
├── .gitignore            # Git 무시 파일
├── package.json          # 프로젝트 메타데이터 및 종속성 목록
├── app.js                # 애플리케이션 진입점
├── index.js              # 서버 실행 파일
└── README.md             # 프로젝트 설명 파일

현재 설치한 모듈

  "dependencies": {
    "dotenv": "^16.4.5",
    "express": "^4.19.2",
    "mongodb": "^6.7.0",
    "mongoose": "^8.4.3",
  }

 

DB와 실행파일도 다 만들었으니, 이제 본격적으로 시작해보자. 

 

허나 시작하기에 앞서 터미널에 매번 $ npm start index.js 를 입력해주기는 번거로웠다.

 

때문에 파일을 저장만 하면 자동으로 서버를 실행시켜주는 nodemon 을 사용하도록 하자.

 

먼저 $ npm i -g nodemon 으로 라이브러리를 설치해준다. (-g 를 입력해주는 이유는 그냥 npm i nodemon 으로 설치하면 작동 하질 않는다.)

 

한번 설치해주고 터미널에 $ nodemon index.js 를 입력하면 터미널을 끄거나 오류가 발생하지 않는 이상 자동으로 서버가 실행된다.

 

또한 이제는 원격저장소에 올려야 하기 때문에 gitignore 파일에 무엇을 올리지 않을지 입력해 준다.

 

https://www.toptal.com/developers/gitignore

 

gitignore.io

Create useful .gitignore files for your project

www.toptal.com

 

위 사이트는 내가 사용하는 언어 등을 입력했을 때 무엇을 gitignore 파일에 입력해야하는지 알려준다 참고하도록 하자.

 

자 이제 로그인 및 회원가입을 만드려면 해당 HTML 화면이 필요할 것이다. 나는 지금 res.send('안녕하십니까')로 그저 글자만 보이게 해 놓았을 뿐이다. 

 

허나 HTML을 사용하지 않고 HTML + JS 로 사용하기 아주 간편한 템플릿 엔진 EJS를 사용하도록 하겠다.

 

EJS 를 사용하기 위해 먼저 터미널에 $ npm i ejs 를 입력하여 라이브러리를 설치 해 주도록 하자.

 

설치 했다면 바로 사용하기 위해 app.js 파일로 가서 아래 코드를 입력해주자.

app.set('view engine', 'ejs');

 

폴더 구조에서도 보면 알다시피 EJS 파일은 통상적으로 views 폴더에 보관한다.

 

views 폴더로 가서 home.ejs로 파일을 만들어주고 html 처럼 꾸며준다.

 

만든김에 회원가입을 위한 register.ejs 파일과 로그인에 성공시 접속할 myPage.ejs 또한 만들어주자.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="/css/home.css">
</head>
<body>
</body>
</html>
<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="/css/register.css">
</head>
<body>
</body>
</html>
<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="/css/myPage.css">
</head>
<body>
</body>
</html>

 

이제 app.js 파일로 가보자 첫 날엔 GET method만을 사용하여 조회만 가능하도록 하였고, res.send로 문자열을 보여주도록 했는데, 이제 ejs 파일도 있으니 res.render('home.ejs')로 바꾸고 ejs파일을 보여주도록 하자.

 

(render는 보통 ejs같은 템플릿 엔진을 보여줄 때 사용하며, ejs만 보여줄때는 인자를 하나만 적어도 되나, 나중에 ejs로 변수를 넘겨줄땐 첫 번째 인자에는 ejs 파일, 두 번째 인자에는 넘겨줄 변수를 적으면 된다.)

 

회원가입 페이지와 마이페이지도 만들었으니, 조회가 가능하도록 바꿔주자.

const express = require('express');                     
const app = express();                                  

app.get('/home', (req, res) => {						
	res.render('home.ejs')
})

app.get('/register', (req, res) => {						
	res.render('register.ejs')
})

app.get('/my-page', (req, res) => {						
	res.render('myPage.ejs')
})

module.exports = { app };

 

이제 /home 와 /register, /myPage 경로를 들어갔을때 home.ejs 와 register.ejs, myPage.ejs 에서 설정한 화면이 보여지게 된다. 

 

이제 로그인과 회원가입 화면을 만들었으니 기능을 만들어야겠다. (myPage는 나중에 작업할 예정)

 

데이터를 생성해주기 위해 POST method를 사용하여 API를 만들어보자

const express = require('express');                     
const app = express();                                  

app.get('/home', (req, res) => {						
	res.render('home.ejs')
})

app.post('/home', (req, res) => {
	console.log('로그인 완료')
})

app.get('/register', (req, res) => {						
	res.render('register.ejs')
})


app.post('/home', (req, res) => {
	console.log('회원가입 완료')
})

app.get('/my-page', (req, res) => {						
	res.render('myPage.ejs')
})

module.exports = { app };

 

이런식으로만 설정해 놓으면 당연히도 로그인과 회원가입이 안된다. 그저 method와 경로만 설정했을 뿐 console.log()에 입력한 문자열만 출력 될 것이다. 허나, 이 method와 경로를 설정했다는 것을 기억 해 두자.

 

로그인 과 회원가입 화면, 그리고 API 기본을 만들었으니 db폴더의 schemas 폴더로 가서 userSchema를 만들어야겠다. 

const mongoose = require('mongoose');

const UserSchema = new mongoose.Schema({
    email: {
        type: String,
        required: true,
        unique: true,
    },
    password: {
        type: String,
        required: true,
    },
    name: {
        type: String,
        required: true,
    },
{
    collection: "users",
    timestamps: true,
});

module.exports = UserSchema;

 

먼저 Schema 를 사용하기 위해 mongoose를 불러와주고, 유저와 관련한 UserSchema를 만들어준다. 회원가입을 위해서 name도 만들어보자.

 

type은 모두 String으로 해 주었고, 모두 문서가 저장될 때 필수 값이기 때문에 required로 해 주었으며, email은 중복되면 안되므로 unique를 true로 해 주었다. 

 

나는 Model별로 파일을 따로 만들 것 이기 때문에 UserSchema를 내보내 주도록 했다.

 

아래는 model 폴더 안의 UserModel이다. 

const mongoose = require('mongoose');
const UserSchema = require('../schemas/userSchema');  
const UserModel = mongoose.model('User', UserSchema); // 가져온 UserSchema 모델화

module.exports = UserModel;

 

UserModel을 내보내주었으니, 이 점을 잘 기억하고 있자.

 

이제 views 폴더로 가서 ejs파일에 로그인과 회원가입을 할 수 있도록 요소를 넣어주어야 한다.

 

먼저 home.ejs 이다.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="/css/home.css">
</head>
<body>
    <!-- 로그인 폼 구현 -->
      <form action="/login" method="post"> 
          <input name="email" type="text" placeholder="email">
          <input name="password" type="password" placeholder="password">
       	  <button type="submit">로그인</button>
      </form>
</body>
</html>

 

로그인 형식을 만들어 주기위해 form 태그를 이용하였다.

 

form 태그에 action 속성은 "/login" 으로 method 속성은 "post" 로 입력 해 주었다. action은 로그인을 위한 url경로이며 method는 로그인 방법이다. form태그를 이용하여 로그인을 하기 위해선 action과 method는 필수이다. 또한 input의 name 속성은 값을 서버로 전달 해 주기위한 속성이다.

 

로그인 화면을 만든김에 회원가입 화면도 같이 만들어보자 아래는 register.ejs 이다.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="/css/register.css">
</head>
<body>
    <!-- 회원가입 폼 구현 -->
      <form action="/register" method="post">
      	  <input name="name" type="text" placeholder="name">
          <input name="email" type="text" placeholder="email">
          <input name="password" type="password" placeholder="password">
          <input name="confirmPassword" type="password" placeholder="confirmPassword">
          <button type="submit">회원가입</button>
      </form>
</body>
</html>

 

confirmPassword는 비밀번호 확인 칸이다. 똑같이 form 태그에 action 속성은 "/register" 으로 method 속성은 "post" 로 입력 해 주었다.

 

이제 Schema와 Model을 만들었고 ejs에 요소까지 모두 넣어주었다. 그럼 이제 app.js로 API를 완성시키러 가보자.

 

그 전에 회원가입을 하면서 유저 정보를 저장 할 때 운영자가 DB에서 유저의 비밀번호를 볼 수 있다면 이게 해킹이 아닐까?

 

그럼 우리는 비밀번호를 암호화 시켜야 한다! 이를 위해서 라이브러리를 하나 설치해보자.

 

바로 비밀번호를 암호화 시켜주는 bcrypt 라이브러리 이다. $ npm i bcrypt 를 명령어로 입력하여 설치를 해준다.

 

그리곤 app.js에 bcrypt 라이브러리를 불러와보자.

const express = require('express');                     
const app = express();
const bcrypt = require('bcrypt');

app.get('/home', (req, res) => {						
	res.render('home.ejs')
})

app.post('/home', (req, res) => {
	console.log('로그인 완료')
})

app.get('/register', (req, res) => {						
	res.render('register.ejs')
})


app.post('/home', (req, res) => {
	console.log('회원가입 완료')
})

app.get('/my-page', (req, res) => {						
	res.render('myPage.ejs')
})

module.exports = { app };

 

이제 비밀번호를 암호화 시켜주는 bcrypt를 사용할 수 있게 되었다. 본격적으로 API를 만들어보자.

 

또한, ejs에서 보낸 body 값을 서버에서 다루기 위해서는 express에서 미들웨어를 사용해야한다.

 

바로 app.use(express.json());app.use(express.urlencoded({ extended: false })); 이다 이 코드를 app.js에 입력 해 주자.

 

const express = require('express');                     
const app = express();
const bcrypt = require('bcrypt');

app.use(express.json());                                // 유저가 데이터를 서버로 보내면 쉽게 꺼내 쓸 수 있도록 하는 코드 , 서버에서 req.body 쓰려면 필요함
app.use(express.urlencoded({ extended: false }));       // 유저가 데이터를 서버로 보내면 쉽게 꺼내 쓸 수 있도록 하는 코드 , 서버에서 req.body 쓰려면 필요함

app.get('/home', (req, res) => {						
	res.render('home.ejs')
})

app.post('/home', (req, res) => {
	console.log('로그인 완료')
})

app.get('/register', (req, res) => {						
	res.render('register.ejs')
})


app.post('/home', (req, res) => {
	console.log('회원가입 완료')
})

app.get('/my-page', (req, res) => {						
	res.render('myPage.ejs')
})

module.exports = { app };

 

외울 필요는 없다 그냥 클라이언트에서 보낸 값을 서버에서 처리하기 위해 이게 필요하다 만 알면되고 코드는 npm 사이트에 나와있다. 

 

그럼 이제 로직을 작성해보자.

const express = require('express');                     
const app = express();
const bcrypt = require('bcrypt');
const UserModel = require('./db/model/userModel');    // db파일의 models에서 User class 불러오기

app.use(express.json());                                // 유저가 데이터를 서버로 보내면 쉽게 꺼내 쓸 수 있도록 하는 코드 , 서버에서 req.body 쓰려면 필요함
app.use(express.urlencoded({ extended: false }));       // 유저가 데이터를 서버로 보내면 쉽게 꺼내 쓸 수 있도록 하는 코드 , 서버에서 req.body 쓰려면 필요함

app.get('/home', (req, res) => {						
	res.render('home.ejs')
})

app.post('/login', async (req, res, next) => {
        const { email, password } = req.body;

        const user = await UserModel.findOne({ email });
        if (!user) {
            console.log('등록된 사용자가 아닙니다.');
        }

        const isMatch = await bcrypt.compare(password, user.password);
        if (!isMatch) {
            console.log('비밀번호가 일치하지 않습니다.');
        }

        res.redirect('/my-page');
});

app.get('/register', (req, res) => {						
	res.render('register.ejs')
})

app.post('/register', async (req, res, next) => {
    const { email, password, name } = req.body;

      // 사용자가 이미 존재하는지 확인
      const checkingUser = await UserModel.findOne({ email });
      if (checkingUser) {
        console.log('이미 존재하는 사용자입니다');
      }

      // 비밀번호를 해싱함
      const hashedPassword = await bcrypt.hash(password, 10);  // 첫번째 인자는 password 변수 두번째 인자는 몇번 꼬을지 횟수

      // 새로운 사용자 생성
      const newUser = {
          email,
          password : hashedPassword,
          name,
      };

      await UserModel.create( newUser );

      res.redirect('/login');
});

app.get('/my-page', (req, res) => {						
	res.render('myPage.ejs')
})

module.exports = { app };

 

먼저 UserModel을 불러오기 위한 코드를 입력해준다. 변수를 UserModel로 선언한 뒤에 require()안으로 경로를 입력해준다.

 

회원가입부터 만져보자. input 태그에서 name 속성으로 인해 email과 password, name 값이, action 경로 및 method 로 인해 body값으로 받을 수 있게 되었다. 이 값을 다뤄보자.

 

(원래는 ejs에서 받아온 값을 표현하는 것으로, 

const email = req.body.email;
const password = req.body.password;
const name = req.body.name

 

이렇게 적는다.  req.body.email 여기서 email이 input태그의 name 속성 값이다.  

 

하지만, 구조분해할당으로 const { email, password, name } = req.body; 이렇게 한 줄로 줄여주었다.

 

먼저 이미 가입한 회원이 있는지 찾으려면 UserSchema에서 unique 키와 값을 주었던 email로 찾아보자. mongoose 함수 findOne() 을 통해, UserModel.findOne으로 찾을 수 있다. 

 

(findOne() 안에 { email } 이렇게 객체로 입력해준 이유는? 원래 { email : email } 이렇게 적어야 한다.

왼쪽은 UserSchema에서 설정한 email 필드, 오른쪽은 register.ejs에서 input 태그안의 name 값을 받아서 변수로 생성한 email. 허나 왼쪽과 오른쪽이 같을경우엔 { email } 이렇게 하나로 줄여서 쓸 수 있다. 그러니까 필드 값과 body 값이 일치하는지 찾는다는 뜻

 

조건문을 사용하여 만약 등록된 유저가 있다면 console.log()로 찍어준다. 이 후, 등록된 유저가 없다면 유저를 등록하자.

 

비밀번호를 암호화 시키지 않는다면 그냥 create함수를 통해 UserModel.create로 유저를 생성해주면 되지만, 우리는 암호화 시키는 과정을 추가해야한다.

 

const hashedPassword = await bcrypt.hash(password, 10);

 

bcrypt 라이브러리에 내장된 함수 hash를 이용하여 password를 암호화 해 준다. 첫번째 인자는 암호화 해줄 값 즉, password 변수 그리고 두 번째 인자는 비밀번호를 몇번 꼬와줄지 정하는 것이다. 

 

( 첫 번째 인자는 왜 password로 입력하지 않나요? 하면 bcrypt의 hash 함수에서 password 변수는 그 자체로 문자열이어야 한다. 때문에 객체가 아닌 password 변수만 넣어준 것. 그냥 mongoose 함수 사용할때만 객체형태로 쓴다고 알아두자.)

 

그리곤 newUser 변수를 생성해서 객체 값을 넣어준다. 위에서 말 했듯이 왼쪽은 필드, 오른쪽은 변수이다. create함수에 newUser 객체를 넣어주면 유저 생성이 완료되고, res.redirect('/login')을 통해 로그인 화면으로 이동한다. (redirect는 페이지를 이동시켜주는 것.)

 

이제 로그인을 살펴보자. 

 

회원가입과 똑같이 home.ejs에서 email과 password를 body 값으로 받아온다.

 

findOne({ email }) 을 통해 변수를 생성한뒤 데이터를 확인하고 , 이 데이터가 있는지 없는지 if(!user)조건문을 통해 확인 할 수 있다.

 

이 후, bcrypt에 내장된 compare 함수를 통해 UserSchema 의 password 필드 값과 body 값으로 받은 password가 일치하는지 조건문으로 확인 한다.

 

모두 이상이 없다면 res.redirect('/myPage')를 통해 myPage.ejs 화면이 보이도록 한다.

 

참고로 mongoose 내장함수와 bcrypt 내장함수를 사용할 땐 비동기 처리를 해주어야 하기 때문에 async await을 써준다. 

 

즉, 이 함수가 처리 되기까지 다음 코드를 실행하지 마세요 라는 뜻 이다.