[웹사이트] 로그인 및 회원가입 만들어보기 (Docker + Node.js + MySQL)

 

환경

- Rocky Linux 9.5

- Docker

- Node.js

- MySQL

 

1. 리눅스 및 도커 설치

 

록키 리눅스 설치

 

리눅스 기초 다지기 사전준비 - VirtualBox Rocky 9 설치

개요호스트 기반 가상화 프로그램 중 VirtualBox를 사용하여 실습환경을 구축한다. VirtualBox Download - 링크 Rocky Linux Download - 링크   VirtualBox NetworkVirtualBox는 여러 가상 네트워크 환경을 제공한다.

openstack.tistory.com

 

록키 리눅스 기본 환경구성

 

 

[Rocky Linux] 기본 환경구성

글쓴이가 해당 블로그 내 Rocky Linux 구축 후 기본적으로 설정하는 환경 설정이다. 기대하는 효과로 글쓴이와 동일한 환경으로 구성하여 조금 더 편안하게 실습을 따라올 수 있다. - SELinux, 방화

openstack.tistory.com

 

도커 설치

 

Docker 설치하기 [Rocky 9.4]

도커 설치 EPEL 저장소 추가dnf install -y epel-release Docker Repository 추가dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo Docker 패키지 설치dnf install -y docker-ce docker-ce-cli containerd.io Doc

openstack.tistory.com

 

2. Node.js 및 MySQL 컨테이너 설치

 

Node.js 및 MySQL 이미지 다운로드

docker pull node:22-alpine
docker pull mysql

 

 

Node.js 컨테이너 실행

docker run -it -d -p 80:3000 --name=app node:22-alpine

 

MySQL 컨테이너 실행

docker run -d --name=db -e MYSQL_ROOT_PASSWORD=password mysql:latest

 

실행확인

etc-image-0

 

 

3. MySQL 회원가입 데이터베이스 및 테이블 생성

 

MySQL 컨테이너 접속

docker exec -it db bash

 

MySQL 접속

mysql -uroot -p
password

 

 

etc-image-1

 

 

회원가입 데이터베이스 및 테이블 생성

CREATE DATABASE login_db;
USE login_db;

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

 

 

테이블 생성 확인

etc-image-2

 

 

nodejs 에서 mysql 접근 위한 계정 생성

create user node@'%' identified by 'password';
grant all privileges on login_db.* to node@'%';
flush privileges;

 

 

mysql 컨테이너 IP 확인

docker inspect db | grep IPAddress

 

etc-image-3

 

 

4. Node.js 회원가입 구현

 

Node.js 컨테이너 접속

docker exec -it app sh

 

Node.js 프로젝트 폴더 생성 및 초기화

mkdir -p /app/login-system
cd /app/login-system
npm init -y

 

etc-image-4

 

etc-image-5

 

 

필요 패키지 설치

npm install express mysql2 bcrypt express-session body-parser ejs

 

 

설치 확인

vi package.json

etc-image-6

 

 

필요 디렉터리 생성

mkdir config
mkdir -p public/css
mkdir views

 

각 코드를 폴더 안에 넣어준다.

 

app.js

const express = require('express');
const session = require('express-session');
const bodyParser = require('body-parser');
const bcrypt = require('bcrypt');
const db = require('./config/database');

const app = express();

// 미들웨어 설정
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(session({
  secret: 'your_secret_key',
  resave: false,
  saveUninitialized: true
}));

// View engine 설정
app.set('view engine', 'ejs');
app.use(express.static('public'));

// 라우트 설정
app.get('/', (req, res) => {
  res.render('home', { user: req.session.user });
});

app.get('/login', (req, res) => {
  res.render('login');
});

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

// 회원가입 API
app.post('/register', async (req, res) => {
  try {
    const { username, password, email } = req.body;
    
    // 비밀번호 해시화
    const hashedPassword = await bcrypt.hash(password, 10);
    
    // 사용자 등록
    const query = 'INSERT INTO users (username, password, email) VALUES (?, ?, ?)';
    db.query(query, [username, hashedPassword, email], (err, results) => {
      if (err) {
        console.error('회원가입 에러:', err);
        return res.status(500).json({ error: '회원가입 실패' });
      }
      res.json({ message: '회원가입 성공!' });
    });
  } catch (error) {
    console.error('서버 에러:', error);
    res.status(500).json({ error: '서버 에러' });
  }
});

// 로그인 API
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  
  const query = 'SELECT * FROM users WHERE username = ?';
  db.query(query, [username], async (err, results) => {
    if (err) {
      console.error('로그인 에러:', err);
      return res.status(500).json({ error: '로그인 실패' });
    }
    
    if (results.length === 0) {
      return res.status(401).json({ error: '사용자를 찾을 수 없습니다' });
    }
    
    const user = results[0];
    const validPassword = await bcrypt.compare(password, user.password);
    
    if (!validPassword) {
      return res.status(401).json({ error: '비밀번호가 일치하지 않습니다' });
    }
    
    req.session.user = {
      id: user.id,
      username: user.username,
      email: user.email
    };
    
    res.json({ message: '로그인 성공!' });
  });
});

// 로그아웃 API
app.post('/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) {
      return res.status(500).json({ error: '로그아웃 실패' });
    }
    res.json({ message: '로그아웃 성공!' });
  });
});

// 서버 시작
const port = 3000;
app.listen(port, () => {
  console.log(`서버가 포트 ${port}에서 실행 중입니다.`);
});

 

config/database.js

const mysql = require('mysql2');

const connection = mysql.createConnection({
  host: '172.17.0.3',
  user: 'node',           // MySQL 사용자 이름
  password: 'password',       // MySQL 비밀번호
  database: 'login_db'    // 사용할 데이터베이스 이름
});

connection.connect((err) => {
  if (err) {
    console.error('데이터베이스 연결 실패:', err);
    return;
  }
  console.log('데이터베이스 연결 성공');
});

module.exports = connection;

 

위에 host IP 는 우리가 확인한 mysql 컨테이너의 IP 로 설정해야 한다.

 

views/home.ejs

<!DOCTYPE html>
<html>
<head>
    <title>홈페이지</title>
    <link rel="stylesheet" href="/css/style.css">
</head>
<body>
    <div class="container">
        <h1>환영합니다!</h1>
        <% if (typeof user !== 'undefined' && user) { %>
            <p>안녕하세요, <%= user.username %>님!</p>
            <button onclick="logout()" class="btn">로그아웃</button>
        <% } else { %>
            <div class="button-group">
                <a href="/login" class="btn">로그인</a>
                <a href="/register" class="btn">회원가입</a>
            </div>
        <% } %>
    </div>

    <script>
        async function logout() {
            try {
                const response = await fetch('/logout', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    }
                });

                const data = await response.json();
                
                if (response.ok) {
                    alert('로그아웃 성공!');
                    window.location.href = '/';  // 홈페이지로 리다이렉트
                } else {
                    alert(data.error || '로그아웃 실패');
                }
            } catch (error) {
                alert('서버 오류가 발생했습니다.');
            }
        }
    </script>
</body>
</html>

 

 

views/login.ejs

<!DOCTYPE html>
<html>
<head>
    <title>로그인</title>
    <link rel="stylesheet" href="/css/style.css">
</head>
<body>
    <div class="container">
        <h1>로그인</h1>
        <form id="loginForm" class="form">
            <div class="form-group">
                <label for="username">아이디:</label>
                <input type="text" id="username" name="username" required>
            </div>
            <div class="form-group">
                <label for="password">비밀번호:</label>
                <input type="password" id="password" name="password" required>
            </div>
            <button type="submit" class="btn">로그인</button>
        </form>
        <p>계정이 없으신가요? <a href="/register">회원가입</a></p>
    </div>

    <script>
        document.getElementById('loginForm').addEventListener('submit', async (e) => {
            e.preventDefault();
            
            const formData = {
                username: document.getElementById('username').value,
                password: document.getElementById('password').value
            };

            try {
                const response = await fetch('/login', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify(formData)
                });

                const data = await response.json();
                
                if (response.ok) {
                    alert('로그인 성공!');
                    window.location.href = '/';
                } else {
                    alert(data.error || '로그인 실패');
                }
            } catch (error) {
                alert('서버 오류가 발생했습니다.');
            }
        });
    </script>
</body>
</html>

 

 

views/register.ejs

<!DOCTYPE html>
<html>
<head>
    <title>회원가입</title>
    <link rel="stylesheet" href="/css/style.css">
</head>
<body>
    <div class="container">
        <h1>회원가입</h1>
        <form id="registerForm" class="form">
            <div class="form-group">
                <label for="username">아이디:</label>
                <input type="text" id="username" name="username" required>
            </div>
            <div class="form-group">
                <label for="email">이메일:</label>
                <input type="email" id="email" name="email" required>
            </div>
            <div class="form-group">
                <label for="password">비밀번호:</label>
                <input type="password" id="password" name="password" required>
            </div>
            <button type="submit" class="btn">회원가입</button>
        </form>
        <p>이미 계정이 있으신가요? <a href="/login">로그인</a></p>
    </div>

    <script>
        document.getElementById('registerForm').addEventListener('submit', async (e) => {
            e.preventDefault();
            
            const formData = {
                username: document.getElementById('username').value,
                email: document.getElementById('email').value,
                password: document.getElementById('password').value
            };

            try {
                const response = await fetch('/register', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify(formData)
                });

                const data = await response.json();
                
                if (response.ok) {
                    alert('회원가입 성공!');
                    window.location.href = '/login';
                } else {
                    alert(data.error || '회원가입 실패');
                }
            } catch (error) {
                alert('서버 오류가 발생했습니다.');
            }
        });
    </script>
</body>
</html>

 

 

public/css/style.css

body {
    font-family: Arial, sans-serif;
    margin: 0;
    padding: 0;
    background-color: #f5f5f5;
}

.container {
    max-width: 500px;
    margin: 50px auto;
    padding: 20px;
    background-color: white;
    border-radius: 5px;
    box-shadow: 0 0 10px rgba(0,0,0,0.1);
}

h1 {
    text-align: center;
    color: #333;
}

.form {
    display: flex;
    flex-direction: column;
    gap: 15px;
}

.form-group {
    display: flex;
    flex-direction: column;
    gap: 5px;
}

label {
    font-weight: bold;
    color: #555;
}

input {
    padding: 8px;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 16px;
}

.btn {
    background-color: #007bff;
    color: white;
    padding: 10px 15px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 16px;
    text-align: center;
    text-decoration: none;
}

.btn:hover {
    background-color: #0056b3;
}

.button-group {
    display: flex;
    gap: 10px;
    justify-content: center;
    margin-top: 20px;
}

p {
    text-align: center;
    margin-top: 20px;
}

a {
    color: #007bff;
    text-decoration: none;
}
    
a:hover {
    text-decoration: underline;
}

 

 

서버 실행

/app/login-system # node app.js

 

etc-image-7

 

 

이제 로컬OS 서버 IP의 80으로 접속해보자

 

etc-image-8

 

etc-image-9

 

etc-image-10

 

회원가입 후 MySQL 테이블 확인

etc-image-11