Vuetify + Socket Io를 사용하여 채팅 웹 사이트 만들기 2번째 파트입니다
우선 필요한 기능 리스트 및 완료된 기능은 아래와 같습니다
1. 로그인이 가능해야 된다 - 완료
2. 회원가입이 가능해야 하며 각 사용자들을 구분할 수 있는 닉네임이 필요하다 - 완료
3. 로그인한 사용자는 현재 생성되어있는 채팅방 리스트를 볼 수 있으며 언제든지 본인의 채팅방을 생성할 수 있다
4. 어떠한 사용자든 현재 생성되어있는 방에 입장이 가능하다
5. 방에 입장한 사용자든 입장한 방에 있는 다른 사용자들과 실시간 채팅이 가능하다
6. 방에 입장한 사용자들은 언제든지 입장한 방을 나갈 수 있다
이번에 저희는 3,4번을 진행하게 될 것이며
3,4번을 진행하기 위해 클라이언트 단에서 퍼블리싱 및 프런트 엔드 개발 작업부터 시작하여
실제 소켓 통신을 이용하여 방에 입장하는 로직을 구현하게 됩니다
일단 프론트 화면입니다
레이아웃 구조는 왼쪽에 방 리스 트을 확인할 수 있는 사이드 메뉴와 중앙에 채팅을 직접 입력할 수 있는 칸으로 구분이 되어있습니다
해당 페이지 같은 경우는 쉽게 작업을 하려면 한 페이지에 모든 기능을 다 넣을 수도 있지만
추후 유지보수나 소스 가독성, 컴포넌트 재사용성을 생각하여 방생 성, 채팅 창 화면을 컴포넌트로 분리하였습니다
roomList.vue
<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>
방 리스트 페이지 html 소스입니다 addRoom이라는 방생 성 컴포넌트 한 개와, chat이라는 채팅 컴포넌트로 분리를 하였으며
실제 방 리스트 같은 경우는 소켓 통신으로 받아온 데이터를 뿌리게 됩니다
스크립트 부분은 연관된 부분이 많기 때문에 제가 개발할 때 개발한 순서대로 코드를 작성하도록 하겠습니다
우선 addRoom 컴포넌트입니다
<template>
<div>
<v-dialog v-model="dialog" width="500">
<template v-slot:activator="{ on, attrs }">
<img v-bind="attrs" v-on="on" src="@/assets/add.svg"/>
</template>
<v-card>
<v-toolbar
color="primary"
dark
>채팅방 생성</v-toolbar>
<v-card-text>
<div>
<v-form v-model="input.valid">
<v-text-field label="방 이름" type="text" v-model="input.roomName" :rules="input.roomRules" required></v-text-field>
</v-form>
</div>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="warning" text @click="dialog = false">
취소
</v-btn>
<v-btn color="primary" text @click="roomCreate()">
생성
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script>
export default {
name : "addRoom",
data:()=>({
dialog : false,
input:{
valid:false,
roomName:"",
roomRules:[
v => !!v || '방이름을 입력해주세요.'
]
}
}),
methods:{
/**
* @description 방생성
*/
roomCreate(){
console.log(this.input);
if(this.input.valid){
const params = {
roomName : this.input.roomName
}
this.axios.post("/addRoom",params).then((res)=>{
// console.log(res);
if(res.data.err == 0){
alert("방이 생성되었습니다.");
this.$emit("roomListChange")
this.dialog = false;
}else{
alert(res.data.errMsg);
return false;
}
}).catch((err)=>{
console.log(err);
});
}else{
alert("방이름을 입력해주세요.");
return false
}
}
}
}
</script>
방생 성 컴포넌트는 vuetify에 v-dialog를 이용하였으며 방 리스트 페이지에서 '+'버튼과 실제 모달 화면이 하나의 컴포넌트로 구성이 되어있습니다
<template v-slot:activator="{ on, attrs }">
<img v-bind="attrs" v-on="on" src="@/assets/add.svg"/>
</template>
해당 부분이 '+' 버튼의 html 구조이며 vuetify 문법을 사용하여 '+'버튼 클릭 시 아래에 v-card가 나오게 됩니다
방생 성 같은 경우는 생성 버튼 클릭 시 addRoom이라는 API를 호출해 실제 방을 생성하게 되며 방생 성이 완료가 되면
v-dialog를 닫으면서 방 리스트를 뿌릴 수 있도록 되어있습니다
이제 소켓 통신하는 부분입니다
roomList.vue
<script>
import addRoom from "@/components/addRoom.vue"
import chat from "@/components/chat.vue"
export default {
name :"roomListPage",
data: () => ({
isSidebar:false,
dialog: false,
searchRoom:"",
roomList : [],
currentRoomIdx:0,
currentRoomTitle:""
}),
components:{
addRoom,
chat
},
mounted(){
this.getRoomList();
this.$socket.on("sendRoomList",(data)=>{
this.roomList = data.list;
});
},
methods:{
/**
* @description 방 리스트
*/
getRoomList(){
this.$socket.emit("roomList",{
roomName : this.searchRoom
})
},
/**
* @description 모바일 메뉴 클릭 이벤트
*/
onClick(e){
if(e.target.parentNode !== this.$refs.titlebar){
this.isSidebar = false;
}
}
},
directives:{
clickOutSlide : vClickOutSlide.directives
}
}
</script>
방 리스트 데이터를 가져올 때 중요하게 봐야 될 부분은 mount 부분에 sendRoomList와 method에 getRoomList입니다
/**
* @description 방 리스트
*/
getRoomList(){
this.$socket.emit("roomList",{
roomName : this.searchRoom
})
},
해당 부분이 getRoomList 함수호 출시 전역 변수로 설정한 소켓에 함수로 emit이라는 함수를 사용하게 됩니다
emit 같은 경우는 데이터를 보낼 때 사용하는 함수이며 일반 RestAPI와 달리 첫 번째 인자로 '이벤트 명'을 입력하게 되고, 두 번째 인자로는 소켓에 전달할 데이터를 입력하게 됩니다
그러면 위의 코드는 서버에 roomList라는 이벤트에 roomName이라는 변수를 전달하면서 호출하겠다는 의미가 됩니다
그러면 서버단은 어떻게 구성이 되어있을까요?
서버는 아래와 같이 socket.on이라는 함수를 쓰게 됩니다
io.on('connection', (socket) => {
console.log('a user connected');
/**
* @description 방 리스트 소켓
*/
socket.on("roomList",async (data)=>{
const room = new Room();
let roomList = await room.roomList(data.roomName);
io.to(socket.id).emit("sendRoomList",{
list : roomList.list
})
})
});
서버단에서 roomList라는 이벤트에서 전달받은 데이터를 받을 수 있으며 해당 소켓이 호출이 되면 호출된 상황에 맞는 로직을 처리하게 됩니다
방 리스트 같은 경우는 현재 방 리스트를 DB에서 조회하는 로직이 진행하고 있습니다
여기서 의문점은 클라이언트에서 요청해서 DB 조회하는 로직까지는 되었는데 이 조회한 데이터를 다시 클라이언트에게 전달을 해야 됩니다
아까 제가 위에 emit은 어떠한 데이터를 보낼 때 사용한다고 하였습니다 위에서는 클라이언트에서 서버로 데이터를 보냈는데
반대로 서버에서 클라이언트로 데이터를 보낼 수 있습니다
그래서 DB 조회가 끝난 후 다시 클라이언트에게 조회한 데이터를 보내게 됩니다 여기서 다시 주의해야 될 부분은 클라이언트 화면을 보면
검색창이 있습니다 이 검색창에서 제가 'test'라고 입력을 했을 경우 저한테만 test로 검색된 방 리스트가 보여야 되고 다른 접속자들은 전체 리스트가 보여야 합니다
즉 검색을 요청한 소켓만이 검색 결과를 볼 수가 있어야 됩니다 이때 특정 소켓에게만 데이터를 보내야 되는데 그 부분이 io.to(socket.io)이며 해당 부분을 통해 검색 결과를 요청한 소켓에게만 sendRoomList의 이벤트명으로 데이터를 전달하게 됩니다
그러면 다시 클라이언트에서 sendRoomList라는 이벤트명으로 데이터를 받으면 됩니다
그 부분이 mount에 있는 sendRoomList입니다
this.$socket.on("sendRoomList",(data)=>{
this.roomList = data.list;
});
이렇게 되면 방생 성, 방 리스트 조회가 끝나게 됩니다
다음으로는 방 입장 부분입니다
우선 클라이언트입니다
<ul>
<li v-for="(item,index) in roomList" :key="index" @click="joinRoom(item)">
<h1>{{item.roomName}}</h1>
</li>
</ul>
/**
* @description 방입장
*/
joinRoom(item){
this.$socket.emit("joinRoom",{
roomIdx : item.idx,
userIdx : this.$store.state.userIdx
})
},
아까 서버에서 전달받은 방 리스트 데이터를 html에 뿌리고 사용자가 특정 방을 선택했을 때 joinRoom이라는 함수가 실행이 됩니다
자 이제부터는 방 리스트 부분과 비슷합니다
emit를 이용하여 joinRoom이라는 이벤트에 현재 사용자가 선택한 방에 인덱스 값과 사용자의 idx값을 데이터를 보내게 됩니다
그러면 서버에서는 socket.on("joinRoom") 이벤트로 받게 됩니다
/**
* @description 방 입장 소켓
*/
socket.on("joinRoom",async (data)=>{
if(socketToRoom[data.roomIdx]){
socketToRoom[data.roomIdx].push({socket : socket.id})
}else{
socketToRoom[data.roomIdx] = [
{socket : socket.id}
]
}
socket.join(data.roomIdx.toString())
io.to(socket.id).emit("joinRoomSuccess",{
roomIdx : data.roomIdx
})
});
그러면 이제 room이라는 개념이 생기게 됩니다
소켓에서 room은 쉽게 말하면 방(채널)이라고 생각하시면 됩니다
room이라는 개념을 사용하게 되면 room에 들어있는 socket들에게만 데이터가 전달이 가능하며 이를 통해 채팅방을 구현할 수 있게 됩니다
우선 소켓을 room에 입장시키기 전에 현재 room에 어떠한 소켓들이 들어와 있는지를 체크하기 위해 socketToRoom이라는 배열에 roomIdx에 키값으로 socketId를 저장시킵니다
그 이후에 socket.join이라는 함수가 나오게 되는데 이 부분이 실제 논리적으로 현재 소켓을 특정한 room에 입장시키겠다는 부분입니다
해당 함수는 무조건 string값만 받기 때문에 정수형으로 되어있는 값이면 toString으로 변환을 시켜줘야 됩니다
자 그러면 여기까지 하면 방 입장까지 다 되었는데 아직 클라이언트는 방에 입장이 된 건지, 안된 건지를 알 수가 없습니다
그래서 위에 방 리스트 소켓 때와 같이 io.to(socket.id). emit를 사용하여 해당 소켓이 선택한 방에 입장을 완료되었다는 데이터를 보냅니다
그러면 클라이언트는 아래와 같이 socket.on으로 받으면 됩니다
this.$socket.on("joinRoomSuccess",data =>{
this.currentRoomIdx = data.roomIdx
})
마치며
오늘은 실제 소켓 IO 통신에서 자주 사용하는 emit과 on 그리고 room 관련하여 포스팅을 해보았습니다 실제 작업할 때는 못 느꼈는데 포스팅을 하다 보니 클라이언트와 서버를 계속 번갈아가면서 작업을 하게 된 거 같고 처음 하시는 분들이라는 조금 헷갈리수도 있을 거 같다고 생각하였습니다(제가 글을 잘못 써서..) 작업은 번갈아가면서 하느라 조금 힘들었지만 emit과 on 개념을 명확해서 이해하는데 쉬웠고 실제 방 입장하는 함수인 join함수도 이해가 되어서 앞으로 잘 사용할 수 있을 거 같습니다 다음 포스팅에서는 방 입장 후 실제 채팅 화면이 나오는 부분과 실제 채팅을 하는 로직을 구현해보도록 하겠습니다
'vue.js' 카테고리의 다른 글
Vue3의 새로운 상태 관리 라이브러리 Pinia (0) | 2023.07.06 |
---|---|
Vuetify + Socket Io 를 사용하여 채팅 웹 사이트 만들기-2 (0) | 2022.06.19 |
Vuetify + Socket Io 를 사용하여 채팅 웹 사이트 만들기-1 (0) | 2022.06.05 |
Vue Cli 설치 및 프로젝트 세팅 (0) | 2021.12.12 |