vue.js 기본 익히고 나니 간단한 어플을 만들어보고 싶어졌다. 그래서 최소한의 기능을 갖춘 채팅 어플리케이션을 만들어보기로 했다. 일단 먼저 큰 그림을 잡았는데 전에 사용해봤던 DBeaver를 사용해서 MYSQL 데이터베이스 구성하고 사용자, 메세지, 채팅방 정보를 관리하고 요청에 따라서 정보 보여주고 등록 할 계획이다.
데이터베이스 이름은 chat으로 설정했고, 채팅방 정보 담을 Chats 테이블, 메세지 정보 담을 Messages 테이블 그리고 마지막으로 사용자들 정보 담을 Users 테이블 이렇게 3개로 했고 필드는 아래와 같이 구성했다.
그리고 관계 설정은 다음과 같이 했다
데이터베이스 설계는 일단 이렇게 끝내고 혹시 후에 필요한 필드가 생기거나 하면 그때 또 수정해야겠다. 이제 회원가입이랑 로그인 기능을 구현해보자!
클라이언트에서 사용자에게 입력받은 아이디와 패스워드를 서버에 전송하고, 새로운 사용자면 데이터베이스에 등록할거다.
사용자가 회원가입 화면에서 아이디와 비밀번호를 입력한 후 회원가입을 클릭하거나 엔터를 치면, register() 메서드가 호출된다.
<input v-model="username" placeholder="Username" class="form-control form-control-lg mb-3" required @keydown.enter="register"/>
<input type="password" v-model="password" placeholder="Password" class="form-control form-control-lg" required @keydown.enter="register"/><br/>
<button class="btn btn-primary w-100" @click="register">회원가입</button>
async register() {
try {
const response = await axios.post('http://localhost:5000/register', {
username: this.username,
password: this.password,
});
alert(response.data.msg);
this.$router.push('/mainchat/chatlist');
} catch (error) {
console.error(error.response ? error.response.data : error);
alert('이미 회원가입한 사용자입니다. 로그인 해주세요.');
}
},
register() 메서드는 axios를 사용해서 서버의 '/register' 엔드포인트에 POST 요청을 보낸다.
그럼 서버에서 회원가입 요청을 처리하는 부분을 살펴보면
@app.route('/register', methods=['POST'])
def register():
username = request.json.get('username') # 요청에서 사용자 이름 가져오기
password = request.json.get('password') # 요청에서 비밀번호 가져오기
# 사용자 이름 중복 확인
existing_user = User.query.filter_by(username=username).first()
if existing_user:
return jsonify({"msg:" "이미 사용중인 아이디입니다."}), 400
hashed_password = bcrypt.generate_password_hash(password).decode('utf-8') # 비밀번호 해시 처리
new_user = User(username=username, password=hashed_password) # 새로운 사용자 객체 생성
db.session.add(new_user) # 데이터베이스 세션에 추가
db.session.commit() # 세션 커밋(저장)
return jsonify({"회원가입 성공!"}), 201 # 성공 메시지 반환
요청에서 사용자 아이디와 비밀번호를 가져온 후에 사용자가 입력한 아이디와 일치하는 사용자 정보를 데이터베이스에서 조회한 다음에 만약 존재한다면 중복된 아이디라는 메시지를 400 상태 코드와 함께 반환한다.
나는 지금 flask를 사용하고 있는데 flask 자체는 데이터베이스에 대한 직접적인 지원을 제공하지 않는다고해서 처음에는 당황했는데 찾아보니까 라이브러리를 사용하면 MYSQL과 통신할 수 있다고 했다. Flask-SQLAlchemy라는 라이브러리인데 이름에서도 느껴지지만 Flask와 SQLAlchemy를 결합한 확장으로 ORM(Obejct Relational Mapping)을 통해 데이터베이스와 상호작용할 수 있게 해준다고 한다. 후 다행이다😮💨
여기서 잠깐❗❗
사용하는 방법은 일단
1. from flask_sqlalchemy import SQLAlchemy로 SQLAlchemy를 import 해야한다.
2. app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:*******@localhost/chat' 데이터베이스 URL 설정해서 MYSQL과 연결해주면 된다.
=> 여기서 mysql+pymysql은 MYSQL 데이터베이스를 사용하고 pymysql 모듈을 통해 연결한다는 의미이다. root는 데이터베이스 사용자명이고 별표자리에는 mariadb 비밀번호를 적어주면 된다. localhost/chat은 데이터베이스 서버 주소와 데이터베이스 이름을 뜻한다.
4. db = SQLAlchemy(app)으로 SQLAlchemy 객체를 초기화한다.
만약에 Flask에서 직접 SQL 문을 직접 작성하고 실행하고 싶으면 'mysql-connector-python'이라는 라이브러리를 사용할 수도 있는데 나는 이번에 사용하지 않았다.
그리고 Flask-SQLAlchemy를 사용해서 데이터베이스와 상호 작용을 하기 위해서는 ORM(Object-Relational Mapping)을 설정해야한다. 이를 위해 테이블 모델을 정의해야하는데 모델을 정의하면 데이터베이스 테이블을 Python 클래스와 매핑하고, Python 객체를 사용해서 데이터베이스 레코드를 생성, 조회, 업데이트, 삭제할 수 있다. 나는 DBeaver로 설계해둔 테이블을 참고해서 아래와 같이 테이블 모델을 정의했다.
from python import db
from datetime import datetime, timezone
# User 모델 정의
class User(db.Model):
__tablename__ = 'Users'
user_id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False)
password = db.Column(db.String(255), nullable=False)
def to_dict(self):
return {
'user_id': self.user_id,
'username': self.username
}
# Messages 모델 정의
class Messages(db.Model):
__tablename__ = 'message'
message_id = db.Column(db.Integer, primary_key=True)
chat_id = db.Column(db.String(36), nullable=False)
sender_id = db.Column(db.Integer, nullable=False)
sender_name = db.Column(db.String(36), nullable=False)
receiver_id = db.Column(db.Integer, nullable=False)
receiver_name = db.Column(db.String(36), nullable=False)
text = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc))
def to_dict(self):
return {
'chat_id': self.chat_id,
'sender_id': self.sender_id,
'sender_name': self.sender_name,
'receiver_id': self.receiver_id,
'receiver_name': self.receiver_name,
'text': self.text,
'created_at': self.created_at.isoformat()
}
# Chat 모델 정의
class Chats(db.Model):
__tablename__ = 'chats'
chat_id = db.Column(db.String(36), primary_key=True)
user1_id = db.Column(db.Integer, nullable=False)
user2_id = db.Column(db.Integer, nullable=False)
def to_dict(self):
return {
'chat_id': self.chat_id,
'user1_id': self.user1_id,
'user2_id': self.user2_id,
}
이제 SQLAlchemy를 사용해서 데이터베이스의 특정 사용자를 조회해보자!
existing_user = User.query.filter_by(username=username).first() User.query는 User 모델에 대한 쿼리 객체를 생성하고 filter_by를 통해서 조건을 설정하고 first()하면 조건에 맞는 첫 번째 결과를 반환하게 된다.
비밀번호는 bcrypt를 사용해서 안전하게 해시 처리했다. 비밀번호를 해시 처리한 이유는 비밀번호는 중요한 개인 정보이기 때문에 보안을 위해 안전하게 저장되어야하기 때문이다. bcrypt가 특히나 비밀번호 해싱에 최적화되어있다고 해서 사용했고 단방향 해시 함수라서 원래 비밀번호를 복원할 수 없기 때문에 데이터베이스에 비밀번호를 평문으로 저장하는 것보다 훨씬 안전하다고 한다.
그리고 이렇게 해시된 비밀번호를 사용해서 새로운 사용자 객체를 생성한다. db.session.add(new_user)로 세션에 새로운 사용자 객체를 추가하고 db.session.commit()을 통해서 변경 사항을 데이터베이스에 영구적으로 저장한다.
회원가입이 성공하면 성공 메시지를 클라이언트에 반환한다. Postman으로 테스트해봐도 잘된다😄

로그인은 클라이언트와 서버 간의 통신하고 인증 토큰을 사용하는 것까지 구현해보았다. 사용자가 로그인을 하면 토큰을 생성하고, 이후 같은 요청을 받았을 때 해당 토큰으로 사용자를 확인하는 인증 기능이다.
사용자가 로그인 화면에서 아이디와 비밀번호를 입력하고 엔터를 누르거나 로그인을 클릭하면 login() 메서드가 호출된다.
<input v-model="username" placeholder="Username" class="form-control form-control-lg mb-3" required @keydown.enter="login"/>
<input type="password" v-model="password" placeholder="Password" class="form-control form-control-lg" required @keydown.enter="login"/><br/>
<button class="btn btn-primary w-100" @click="login">로그인</button>
async login() {
try {
const credentials = { username: this.username, password: this.password };
const response = await this.$store.dispatch('login', credentials);
console.log(response);
alert('로그인 성공! Access Token: ' + response.access_token || response.token);
this.$router.push('/mainchat/chatlist');
} catch (error) {
console.error(error);
alert('로그인에 실패하였습니다. 아이디와 비밀번호를 확인해주세요.');
}
},
login 메서드는 사용자에게 입력받은 아이디와 비밀번호로 credentials라는 객체를 만들고 이 credentials 객체를 store.js의 login() 액션을 호출할 때 같이 전달한다.
// 로그인
async login({ commit, dispatch }, credentials) {
try {
const response = await axios.post('http://localhost:5000/login', {
username: credentials.username,
password: credentials.password,
});
console.log('로그인 응답 정보', response.data);
const accessToken = response.data.access_token || response.data.token;
if (!accessToken) {
throw new Error('No access token found in login response');
}
commit('SET_USER', response.data.user);
commit('SET_TOKEN', accessToken);
return response.data;
} catch (error) {
console.error('Login failed:', error);
throw error;
}
},
store 액션의 login 메서드는 axios를 사용해서 전달받은 credentials의 정보와 함께 /login 엔드포인트에 POST 요청을 보낸다.
# 사용자 로그인 API
@app.route('/login', methods=['POST'])
def login():
username = request.json.get('username') # 요청에서 사용자 이름 가져오기
password = request.json.get('password') # 요청에서 비밀번호 가져오기
user = User.query.filter_by(username=username).first() # 사용자 조회
if user and bcrypt.check_password_hash(user.password, password):
access_token = create_access_token(identity=user.user_id) # JWT 토큰 생성
return jsonify(access_token=access_token), 200 # 토큰 반환
return jsonify({"msg": "Invalid username or password"}), 401 # 실패 메시지 반환
그럼 서버에서 로그인 요청을 처리할 때, 요청받은 username과 password를 사용해서 사용자를 요청하고 query.filter_by를 통해 데이터베이스에서 사용자를 조회한 다음, check_password_hash를 사용해서 입력된 비밀번호가 해시된 비밀번호와 일치하는지 확인하는 과정을 거친다.
해시된 비밀번호와 일치해서 인증에 성공하면, create_access_token(identity=user.user_id) 호출을 통해 JWT를 생성한다. 이때 flask_jwt_extended에서 check_password_hash랑 create_access_token를 import 해와야지 사용할 수 있고 JWT를 안전하게 서명하기 위해 자기만의 비밀키인 JWT_SECRET_KEY도 필요하다. app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY')와 jwt = JWTManager(app)를 통해 비밀 키 설정하고 JWT 관리 객체 생성할 수 있도록 했다.
나는 .env 파일에 비밀키를 만들어두었고 환경 변수로 불러와서 사용하도록 설정했다. JWT_SECRET_KEY는 JWT의 무결성과 보안을 유지하는데 필수이기 때문에 이 키가 없으면 JWT를 안전하게 서명할 수 없다.
이렇게 생성된 access_token는 클라이언트에 반환되어서, 클라이언트는 이후에 로그인 요청에서 이 토큰을 사용해서 인증을 수행할 수 있다.
다시 액션 login 메서드로 올라가서 성공적으로 로그인이 되고 access_token을 찍어보면 토큰을 무사히 받아온 것을 확인할 수 있다. 이 토큰은 앞으로 클라이언트와 서버간의 인증에 사용될 것이다.

그리고 'SET_USER'와 'SET_TOKEN' 뮤테이션을 호출해서 사용자 정보와 토큰을 상태에 각각 저장한다. 포스트맨으로 잘 동작하는지 확인해보자!!

access_token이 잘 받아와진다😚 데이터베이스도 확인해보니 회원가입, 로그인한 사용자들이 성공적으로 저장되었고 password도 해시 처리가 된 상태로 저장된 것을 볼 수 있다!

그리고 회원가입은 등록만 하는 함수이기 때문에 등록이 완료되면 로그인이 되도록 구현한 로그인 함수가 실행되도록 코드를 추가했다.
async register() {
try {
const response = await axios.post('http://localhost:5000/register', {
username: this.username,
password: this.password,
});
alert(response.data.msg);
await this.$store.dispatch('login', { => 🤠 로그인 코드 추가
username: this.username,
password: this.password,
});
await this.$store.dispatch('fetchUserData');
this.$router.push('/mainchat/chatlist');
} catch (error) {
console.error(error.response ? error.response.data : error);
alert('이미 등록된 사용자입니다. 로그인 해주세요.');
}
},