Vuetify + Socket Io를 사용하여 채팅 웹 사이트 만들기 3번째 파트입니다
저번 포스팅까지는 사용자가 생성한 채팅방에 입장하는 부분까지 구현을 진행하였고 현재까지 구현이 완료된 기능 리스트는 아래와 같습니다
1. 로그인이 가능해야 된다 - 완료
2. 회원가입이 가능해야 하며 각 사용자들을 구분할 수 있는 닉네임이 필요하다 - 완료
3. 로그인한 사용자는 현재 생성되어있는 채팅방 리스트를 볼 수 있으며 언제든지 본인의 채팅방을 생성할 수 있다
4. 어떠한 사용자든 현재 생성되어있는 방에 입장이 가능하다
5. 방에 입장한 사용자든 입장한 방에 있는 다른 사용자들과 실시간 채팅이 가능하다
6. 방에 입장한 사용자들은 언제든지 입장한 방을 나갈 수 있다
그러면 이번 포스팅에서는 사용자가 방에 입장한 후 실제 채팅을 입력하는 부분과 상대방에 채팅을 실시간으로 확인할 수 있도록 기능을 구현해 보도로 하겠습니다
우선 실제 채팅을 진행하려면 채팅 화면을 사용자에게 보여줘야 됩니다
이러한 채팅 화면은 사용자가 방에 입장을 했을 때 보여주면 되고 사용자가 방에 나갔을 경우 채팅 화면을 숨기는 형태로 되어야 합니다
저번 포스팅 마지막 부분에 아래와 같이 방 입장 성공 이벤트를 받아 오고 있었는데 해당 부분을 통해서 채팅 화면을 사용자가에 보여줄 수 있도록 수정할 예정입니다
this.$socket.on("joinRoomSuccess",data =>{
this.currentRoomIdx = data.roomIdx
})
그러면 저번에 개발했었던 roomList html 부분을 다시 보도록 하겠습니다
<template>
<v-container fluid fill-height>
<v-row justify="center">
<v-col cols="12" sm="8" md="12" lg="12">
<v-card>
<div class="wrap clearfix">
<div class="roomList" :class="{'roomDp':isSidebar}" ref="room" v-click-outside="onClick">
<div class="searchBar clearfix">
<div class="searchWrap">
<input type="text" class="search" placeholder="검색어를 입력해주세요." v-model="searchRoom" @keyup.enter="getRoomList()" @blur="getRoomList()"/>
</div>
<div class="addRoom">
<!--방생성 컴포넌트-->
<addRoom @roomListChange="getRoomList()"></addRoom>
</div>
</div>
<ul>
<li v-for="(item,index) in roomList" :key="index" @click="joinRoom(item)">
<h1>{{item.roomName}}</h1>
<p>현재 참가 인원 : {{item.currentUser}}명</p>
<span class="time">{{item.lastChat}}</span>
<div class="bar"></div>
</li>
</ul>
</div>
<div class="chat">
<div class="titleBar clearfix" ref="titlebar">
<img src="@/assets/list.svg" @click="isSidebar = true"/>
<h1 v-if="currentRoomIdx">{{currentRoomTitle}}</h1>
<h1 v-else>채팅방을 선택해주세요</h1>
<p @click="outRoom()" v-if="currentRoomIdx">나가기</p>
</div>
<chat :roomIdx="currentRoomIdx"></chat>
</div>
</div>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
roomList 화면에서 실제 채팅 컴포넌트가 동작하는 부분은 아래 부분입니다
<div class="chat">
<div class="titleBar clearfix" ref="titlebar">
<img src="@/assets/list.svg" @click="isSidebar = true"/>
<h1 v-if="currentRoomIdx">{{currentRoomTitle}}</h1>
<h1 v-else>채팅방을 선택해주세요</h1>
<p @click="outRoom()" v-if="currentRoomIdx">나가기</p>
</div>
<chat :roomIdx="currentRoomIdx"></chat>
</div>
'chat'이라는 컴포넌트를 한 개 생성을 하여 어떠한 방이든 동일한 화면이 보일 수 있도록 하였으며, 채팅방 같은 경우는 서로 다른 채팅방에 채팅 내역이 공유가 되면 안 되기에 현재 입장한 방에 idx값을 받아서 컴포넌트로 전달하는 구조입니다
chat 컴포넌트를 보도록 하겠습니다
우선 chat 컴포넌트에 html 부분과 화면입니다
<template>
<div>
<div class="emptyChat" v-if="!roomIdx">
<p>채팅방을 클릭하여 채팅을 시작해보세요!</p>
</div>
<div class="chatWrap" v-else>
<div class="chatList" ref="chatList">
<div v-for="(item,index) in chatList" :key="index">
<div class="chatItem clearfix" v-if="item.isQuit == 'N'"
:class="{'meChat' : $store.state.userIdx == item.userIdx, 'otherChat' : $store.state.userIdx != item.userIdx}">
<div class="chatWrap">
<p v-if="$store.state.userIdx != item.userIdx">{{item.nickname}}</p>
<div class="content">
{{item.content}}
</div>
</div>
</div>
<div v-else class="joinOther">
<p>{{item.content}}</p>
</div>
</div>
</div>
<div class="chatSend clearfix">
<textarea v-model="content"></textarea>
<div class="sendBtn" @click="sendMessage()">
<v-icon color="white">
mdi-send
</v-icon>
</div>
</div>
</div>
</div>
</template>
일반적인 채팅 화면 레이아웃은 채팅 리스트 화면과 하단에 채팅 내용 입력창이 존재하게 되며
채팅 리스트 화면 같은 경우는 내가 입력한 채팅과, 상대방이 입력한 채팅이 구분이 되어야 됩니다. 또한 채팅방에서 발생하는 기타 이벤트들도 표시를 해준다면 사용자들 채팅방에 상황을 쉽게 알 수 있을 것입니다
그래서 저는 사용자 본인이 입력한 채팅은 현재 로그인한 사용자의 idx와 소켓으로 불러온 채팅 리스트 중 동일한 idx를 가지고 있는 채팅 리스트는 'meChat'이라는 class으로 선언하고, 그 외에 채팅 리스트는 'otherChat'이라는 class로 선언하여 본인, 상대방에 채팅을 구분하였습니다
또한 채팅 리스트중 isQuit이라는 값이 존재하는 이는 어떠한 사용자가 나갔는지 체크를 하는 상태 값입니다 이 값을 이용해서 채팅 리스트 인지 아니면 기타 이벤트인지 구분하여 표시될 수 있도록 구현하였습니다
그러면 이제 chat 컴포넌트에 스크립트와 실제 채팅을 입력하는 함수를 보도록 하겠습니다
<script>
export default {
name : "chat",
props:{
roomIdx:{
type:[String,Number],
default:0
}
},
data:()=>({
content:"",
chatList:[]
}),
watch: {
roomIdx: function(value, oldValue) {
this.content = "";
}
},
mounted(){
},
methods:{
/**
* @description 메세지 전송
*/
sendMessage(){
let params = {
content : this.content,
roomIdx : this.roomIdx,
userIdx : this.$store.state.userIdx
}
this.$socket.emit("sendMessage",{
input : params
})
this.content = "";
}
}
}
</script>
위에 roomList 화면에서 선언한 거와 같이 roomIdx 값을 props를 통해서 전달받도록 기본 구성을 하였습니다
그리고 실제 채팅을 입력하는 함수는 sendMessage라는 함수를 구현하였으며
사용자가 입력한 채팅 내용, 현재 채팅 방에 idx 그리고 로그인한 사용자의 idx 값을 sendMessage 소켓에 전달하였습니다
그리고 서버에서는 다음과 같이 sendMessage 소켓을 받은 후 DB에 입력한 채팅을 insert, 그리고 다시 조회하도록 하였습니다
/**
* @description 메세지 전송 소켓
*/
socket.on("sendMessage", async (data)=>{
let input = data.input;
input.userSocket = socket.id;
const chat = new Chat();
let chatIdx = await chat.sendMessage(input,"N");
if(chatIdx){
let chatList = await chat.chatList(input.roomIdx);
console.log("chatList",chatList)
io.sockets.in(input.roomIdx.toString()).emit("getChatList",{
chatList : chatList
})
}
})
const mysql = require("../config/db");
class Chat{
constructor(){
}
/**
* @description 메세지 전송
* @param {*} params
* @returns
*/
sendMessage(params,isQuit){
return new Promise((resolve,reject)=>{
const sql = "insert into chat(roomIdx,content,userSocket,userIdx,isQuit) values (?,?,?,?,?)";
let param = [params.roomIdx,params.content,params.userSocket,params.userIdx,isQuit];
mysql.query(sql,param,(err,rows,fields)=>{
if(err){
reject(0)
}else{
resolve(rows.insertId);
}
})
})
}
/**
* @description 채팅 리스트
* @param {*} roomIdx
* @returns
*/
chatList(roomIdx){
return new Promise((resovle,reject)=>{
const sql = `select c.idx,roomIdx,content,userSocket,userIdx,createDate,u.nickname,c.isQuit from chat c
left join user u on u.idx = c.userIdx where c.roomIdx = ?`;
let param = [roomIdx]
mysql.query(sql,param,async(err,rows,fiedls)=>{
if(err){
reject(err)
}else{
resovle(rows)
}
})
})
}
}
module.exports = Chat
sendMessage 소켓 부분을 자세히 보면 아래와 같은 부분이 존재합니다
if(chatIdx){
let chatList = await chat.chatList(input.roomIdx);
console.log("chatList",chatList)
io.sockets.in(input.roomIdx.toString()).emit("getChatList",{
chatList : chatList
})
}
사용자가 채팅을 입력하고 DB에 insert 가 되면 다시 채팅 리스트를 조회하는 부분입니다
이는 만약 사용자가 채팅을 입력하였는데 내가 입력한 채팅 내용이 새로고침을 해야 확인을 할 수 있게 된다면 이는 대단히 UX적으로 안 좋은 경험을 사용자들에게 전달하게 될 것입니다
또한 내가 입력한 채팅을 현재 방에 입장한 본인 외에 다른 사용자들도 실시간으로 확인을 할 수 있어야 되기에 어떠한 사용자가 채팅 입력을 완료되면 그 방에 있는 사용자들에게 채팅 리스트를 전달하는 구조입니다
그러면 node js 쪽에서 getChatList라는 이벤트를 전달했으니 클라이언트 쪽에서 받아야 됩니다
chat 컴포넌트에 스크립트를 다음과 같이 변경해주도록 합니다
<script>
export default {
name : "chat",
props:{
roomIdx:{
type:[String,Number],
default:0
}
},
data:()=>({
content:"",
chatList:[]
}),
watch: {
roomIdx: function(value, oldValue) {
this.content = "";
}
},
mounted(){
this.getSocektChatList();
},
methods:{
/**
* @description 채팅 리스트 가져오기 (소켓)
*/
getSocektChatList(){
this.$socket.on("getChatList",data =>{
console.log("getChatList",data);
this.chatList = data.chatList
setTimeout(()=>{
this.$refs.chatList.scrollTop = this.$refs.chatList.scrollHeight
},100)
})
},
/**
* @description 메세지 전송
*/
sendMessage(){
let params = {
content : this.content,
roomIdx : this.roomIdx,
userIdx : this.$store.state.userIdx
}
this.$socket.emit("sendMessage",{
input : params
})
this.content = "";
}
}
}
</script>
node js에서 전달한 getChatList 소켓 이벤트를 받을 수 있도록 함수를 만들고 컴포넌트가 생성될 시 이벤트가 등록되도록 mounted에서 함수를 호출하였습니다
그리고 서버에서 전달받은 채팅 리스트를 받아서 뿌려주는 형태로 되어있습니다
그런데 여기서 다음과 같은 의문의 코드 부분이 하나 존재합니다
setTimeout(()=>{
this.$refs.chatList.scrollTop = this.$refs.chatList.scrollHeight
},100)
자 생각해보도록 하겠습니다
지금 서비스되고 있는 카카오톡이나, 라인 메신저를 보면 채팅 스크롤을 맨 위로 올렸다가 내가 채팅을 입력하게 되면 스크롤이 맨 아래로 이동되는 것을 확인할 수 있습니다
이는 채팅 리스트 특성상 가장 최신에 채팅이 맨 아래 있으면 UI적으로 사용자가 가장 최근에 입력한 채팅이라는 것을 알 수 있도록 UI를 구성한 것입니다
저 또한 마찬가지로 채팅 리스트를 가장 최신으로 유지하기 위에 위와 같은 코드를 작성하였습니다
chatList를 서버에서 가져오면 실제 화면에 바인딩이 되고 채팅 리스트는에 스크롤 높이 값을 가져와서 스크롤 값을 조절하는 형태입니다
하나 setTimeOut를 쓰지 못하면 채팅 리스트를 뿌려주기 전에 높이값을 가져오게 되는 이슈가 있어서 setTimeOut를 1초 정도 주었습니다
여기까지 하면 사용자가 방에 입장 후, 채팅 입력 그리고 실시간으로 채팅 리스트를 확인할 수 있도록 구현이 완료되가 되었습니다
그러나 아직 안 끝난 부분이 있습니다. 만약 이제까지 한 부분으로 마무리가 되면 어떠한 사용자가 처음 방에 입장했을 때 채팅 리스트를 확인하지 못합니다
getChatList 이벤트 같은 경우는 node js에서 이벤트를 전달해야만 실행이 되는 이벤트 여서 처음 방에 입장하면 채팅 리스트 확인이 불가능합니다
그래서 chat컴포넌트를 다음과 같이 변경해줍니다
<template>
<div>
<div class="emptyChat" v-if="!roomIdx">
<p>채팅방을 클릭하여 채팅을 시작해보세요!</p>
</div>
<div class="chatWrap" v-else>
<div class="chatList" ref="chatList">
<div v-for="(item,index) in chatList" :key="index">
<div class="chatItem clearfix" v-if="item.isQuit == 'N'"
:class="{'meChat' : $store.state.userIdx == item.userIdx, 'otherChat' : $store.state.userIdx != item.userIdx}">
<div class="chatWrap">
<p v-if="$store.state.userIdx != item.userIdx">{{item.nickname}}</p>
<div class="content">
{{item.content}}
</div>
</div>
</div>
<div v-else class="joinOther">
<p>{{item.content}}</p>
</div>
</div>
</div>
<div class="chatSend clearfix">
<textarea v-model="content"></textarea>
<div class="sendBtn" @click="sendMessage()">
<v-icon color="white">
mdi-send
</v-icon>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name : "chat",
props:{
roomIdx:{
type:[String,Number],
default:0
}
},
data:()=>({
content:"",
chatList:[]
}),
watch: {
roomIdx: function(value, oldValue) {
console.log("roomIdx",value);
this.content = "";
this.getChatList(value)
}
},
mounted(){
this.getSocektChatList();
},
methods:{
/**
* @description 채팅 리스트가져오기 (REST)
*/
getChatList(roomIdx){
this.axios.get(`/chatList/${roomIdx}`,{}).then((res)=>{
if(!res.data.err){
this.chatList = res.data.list
setTimeout(()=>{
this.$refs.chatList.scrollTop = this.$refs.chatList.scrollHeight
},100)
}
}).catch((err)=>{
console.log(err);
});
},
/**
* @description 채팅 리스트 가져오기 (소켓)
*/
getSocektChatList(){
this.$socket.on("getChatList",data =>{
console.log("getChatList",data);
this.chatList = data.chatList
setTimeout(()=>{
this.$refs.chatList.scrollTop = this.$refs.chatList.scrollHeight
},100)
})
},
/**
* @description 메세지 전송
*/
sendMessage(){
let params = {
content : this.content,
roomIdx : this.roomIdx,
userIdx : this.$store.state.userIdx
}
this.$socket.emit("sendMessage",{
input : params
})
this.content = "";
}
}
}
</script>
채팅 리스트를 가져올 수 있는 REST API를 한 개 만들어서 roomIdx가 변경된 시점을 watch로 추적하여 roomIdx가 변경되었을 시 해당 roomIdx에 채팅 리스트를 가져온 후 화면에 뿌릴 수 있도록 하였습니다
여기까지 하면 오늘 포스팅에 구현 목표는 달성하게 되었습니다
마치며
오늘은 채팅 웹 페이지에서 가장 중요한 기능인 실제 채팅을 하는 기능을 구현해보았습니다 평소에 카카오톡을 많이 사용을 하는데 완벽하지는 않지만 그래도 주요 기능을 제 손으로 직접 구현을 해보았다는 것이 정말 뿌듯했습니다 물론 중간중간 삽 집을 조금 해서 어떻게 해야 되지 싶었는데 그래도 방법을 찾아서 해결을 하였습니다(setTimeOut 부분은 리팩토링을 한번 해봐야 될 거 같습니다..) 다음 포스팅에서는 채팅방을 나가는 기능과 세부적인 디테일을 살리 수 있는 기능들을 구현해보도록 하겠습니다
'vue.js' 카테고리의 다른 글
Vue3의 새로운 상태 관리 라이브러리 Pinia (0) | 2023.07.06 |
---|---|
Vuetify + Socket Io 를 사용하여 채팅 웹 사이트 만들기-2 (0) | 2022.06.10 |
Vuetify + Socket Io 를 사용하여 채팅 웹 사이트 만들기-1 (0) | 2022.06.05 |
Vue Cli 설치 및 프로젝트 세팅 (0) | 2021.12.12 |