-
2019-09-10 개발일지개발일지 2019. 9. 10. 17:40
어제 마무리한 채팅 프로그램을 리뷰해보자.
데이터 베이스는 사용하지 않았다.
가볍고 빠르게 만들기 위해서 변수에 저장하여 사용하는 방식으로 만들었다.
(굳이 간단한 프로그램 디비 붙여서 자원낭비 하기 싫다...)
우선 로그인 및 회원가입 화면을 만들어보자.
최초로 사이트를 접속했을 때 로그인 폼을 보여준다.
이후 좌측에서 메뉴를 변경하면 그에 해당하는 폼을 보여준다.
공통적으로 input 태그에 대한 데이터 검증을 시도해야하고, 로그인에서 존재하지 않는 계정으로 로그인을 시도할 경우 회원가입 폼으로 이동시켜주자.
템플릿을 하나 만들고 아래의 코드를 작성하자.
html head title script(src = '/js/socket.io.js') body div(id = 'wrapper') div(id = 'user-wrap') nav span(id = 'nav-header') Chat Application div(id = 'menu-list') div(id = 'menu-header') 메뉴 div(class = 'button menu-btn active' id = 'login-btn') LOGIN div(class = 'button menu-btn' id = 'join-btn') JOIN form(id = 'login-form') h1 LOGIN p input(type = 'text' id = 'login-id-txt' placeholder = 'id' autocomplete = 'off') p input(type = 'password' id = 'login-password-txt' placeholder = '********' autocomplete = 'off') p input(type = 'submit' class='btn' id = 'login-submit' value='로그인') form(id = 'join-form') h1 JOIN p input(type = 'text' id= 'join-id-txt' placeholder = 'id' autocomplete = 'off') p input(type = 'password' id = 'join-password-txt' placeholder = '********' autocomplete = 'off') p input(type = 'submit' class = 'btn' id = 'join-submit' value = '회원가입')
우선 socket.io는 반드시 사용할 것이니 미리 헤드 태그에서 로드해주자.
이후 디자인을 입혀주자.
/* 공통 CSS */ html, body { width: 100%; height: 100%; margin: 0; padding: 0; } form input[type=text], form input[type=password] { width: 70%; height: 30px; padding-left: 15px; font-size: 17px; border: 1px solid #aaa; } input[type=button], input[type=submit], button { cursor: pointer; } nav { width: 100%; min-width: 1280px; height: 50px; background: #0084ff; color: #fff; padding: 0px 20px; box-sizing: border-box; display: flex; justify-content: space-between; align-items: center; font-size: 17px; } /* 로그인, 회원가입 CSS */ #wrapper::after { clear: both; content: ''; display: block; } #user-wrap { display: block; } .btn { background: #fff; padding: 7px 40px; border: 1px solid RGB(0, 126, 243); color: RGB(0, 126, 243); font-size: 17px; } .btn:hover { background: #0084ff; color: #fff; } #menu-list { float: left; width: 200px; border: 1px solid #0084ff; margin-top: 20px; border-radius: 5px; } #menu-header { background: #0084ff; color: #fff; height: 40px; font-size: 18px; line-height: 40px; text-align: center; border-radius: 5px; } .menu-btn { text-align: center; border-bottom: 1px solid #f0f0f0; padding: 10px 0px; } .menu-btn:hover { background: #f0f0f0; } .menu-btn.active { background: #f0f0f0; } #login-form { float: left; display: block; width: 400px; text-align: center; margin: 20px; border: 1px solid #0084ff; border-radius: 5px; } #join-form { float: left; display: none; width: 400px; text-align: center; margin: 20px; border: 1px solid #0084ff; border-radius: 5px; }
이제 동적인 기능을 위해 자바스크립트를 작성해보자.
// 로그인 로그아웃 회원가입 영역 DOM const userWrap = document.querySelector('#user-wrap'); const loginForm = document.querySelector('#login-form'); const loginBtn = document.querySelector('#login-btn'); const loginSubmit = document.querySelector('#login-submit'); const joinForm = document.querySelector('#join-form'); const joinBtn = document.querySelector('#join-btn'); const joinSubmit = document.querySelector('#join-submit'); const logoutBtn = document.querySelector('#logout-btn'); loginBtn.addEventListener('click', e => { e.preventDefault(); this.menuHeaderChange(loginBtn, joinBtn); this.displayChange(loginForm, joinForm); }); joinBtn.addEventListener('click', e => { e.preventDefault(); this.menuHeaderChange(joinBtn, loginBtn); this.displayChange(joinForm, loginForm); }); /** * 메뉴 헤더 활성 상태 변경 함수 * @author Johnny * @param activeEl 활성상태로 바꿀 엘리먼트 * @param inactiveEl 비활성상태로 바꿀 엘리먼트 */ function menuHeaderChange(activeEl, inactiveEl) { if (activeEl && inactiveEl) { activeEl.removeAttribute('class'); activeEl.setAttribute('class', 'button menu-btn active'); inactiveEl.removeAttribute('class'); inactiveEl.setAttribute('class', 'button menu-btn'); } } /** * 화면 전환 함수 * @author Johnny * @param showEl 화면에서 표현할 엘리먼트 * @param hideEl 화면에서 숨길 엘리먼트 */ function displayChange(showEl, hideEl) { if (showEl && hideEl) { showEl.style.display = 'block'; hideEl.style.display = 'none'; } }
- 당장 사용하지 않지만 추후에 스크립트 동작에서 사용하므로 모든 셀렉터들을 변수로 잡아주자.
- 로그인 메뉴 버튼과 회원가입 메뉴 버튼을 클릭했을 때에 대한 이벤트 리스너를 바인딩하고 메뉴의 view를 변경하는 함수와 화면 폼을 변경하는 함수를 작동시켜준다.
- 메뉴를 변경하는 함수는 클릭한 메뉴가 활성화된 것 처럼 표현할 수 있도록 active 클래스를 컨트롤한다.
- 화면을 변경하는 함수는 선택한 메뉴에 맞는 폼을 display css 속성을 이용해 컨트롤한다.
그럼 이제 메뉴를 클릭해보자.
메뉴를 변경하면 선택한 메뉴에 맞는 폼으로 전환이 된다.
이제 먼저 회원가입 기능을 만들어보자.
const socket = io.connect('http://localhost:5000/chat2'); // 채팅방 아이디 let roomId = 1; // 채널 아이디 let socketId = ''; /** * 회원가입 액션 * @author Johnny */ joinSubmit.addEventListener('click', e => { e.preventDefault(); let id = document.querySelector('#join-id-txt'); let pw = document.querySelector('#join-password-txt'); const joinData = { id: id.value, pw: pw.value }; if (!joinData.id || !joinData.pw) { alert('아이디와 패스워드를 입력해주세요.'); id.focus(); return false; } /** * 회원가입 요청 이벤트 발생 * @author Johnny * @param joinData.id 사용자 아이디 * @param joinData.pw 사용자 비밀번호 * @callback response.msg 시스템 메시지 내용 * @callback response.result 처리 결과 플래그 */ socket.emit('join user', joinData, (response) => { if (response) { alert(response.msg); id.value = ''; pw.value = ''; if (response.result) { loginBtn.click(); } } }); });
- 회원가입 버튼에 대한 클릭 이벤트 리스너를 등록한다.
- 회원가입 버튼을 클릭하면 가장 먼저 input 태그의 아이디와 패스워드의 값이 존재하는지 체크한다.
- 태그에 값이 없으면 알림과 함께 id input 태그에 포커스를 둔다.
- 값이 모두 있다면 join user 이벤트를 소켓 연결된 서버로 발생시킨다.
- 콜백으로 서버로부터 시스템 메시지와 회원가입 처리 결과를 받는다.
- 회원가입이 정상적으로 처리된 경우 로그인 폼을 표현하기 위해 로그인 메뉴 버튼에 클릭 이벤트를 발생시킨다.
이제 서버 코드를 작성해보자.
import socketIO from 'socket.io'; import http from 'http'; interface User { id: string; pw: string; } export function chat2ChannelOpen(server: http.Server) { const io = socketIO(server); const chat2Channel = io.of('/chat2'); const users: any = {}; chat2Channel.on('connection', socket => { console.log('>>>>> chat2 channel opend!'); /** * 회원가입 이벤트 * @author Johnny * @param joinData.id 사용자 아이디 * @param joinData.pw 사용자 비밀번호 * @callback cb 콜백 */ socket.on('join user', (joinData: User, cb) => { console.log('>>>>> join user event catch'); if (joinCheck(joinData)) { cb({ result: false, msg: '이미 존재하는 회원입니다.' }); return false; } const { id: userId, pw: userPw } = joinData; users[userId] = { id: userId, pw: userPw }; cb({ result: true, msg: '축하합니다. 회원가입이 완료되었습니다.' }); }); /** * 회원가입 체크 * @author Johnny * @param joinData.id 사용자 아이디 */ function joinCheck(joinData: User) { const { id: userId } = joinData; if (users.hasOwnProperty(userId)) { return true; } else { return false; } } }); }
- 클라이언트로 받는 객체의 타입을 정하기 위해 User 인터페이스를 정의했다.
인터페이스만 봐도 알 수 있듯이 클라이언트로 받는 User 객체는 id와 pw밖에 없다. - chat2ChannelOpen()이라는 함수를 정의하고 모듈로 내보내기 설정을 했다.
'왜 이전 채팅 프로토타입을 만들때와 다르게 클래스로 만들지 않았는가?'에 대한 이유는 클래스에는 멤버변수와 초기화만을 진행하고 외의 이벤트들은 다른 파일에 정의해서 작성해야한다는 점이 귀찮았다. (...)
그래서 함수로 정의하고 모듈화하여 함수 호출만으로 소켓 연결과 이벤트들이 활성화되도록 작성했다. - io.of('/chat2')는 네임스페이스를 적용하는 함수다.
네임스페이스를 적용하지 않을 경우 디폴트 네임스페이스는 루트(/)인데, 'localhost:port/'로 소켓 연결할 수 있는 것이다.
위의 코드에서 나는 '/chat2'로 네임스페이스를 적용했으므로, 'localhost:port/chat2' 경로로 소켓을 연결할 수가 있게 된다. - users 객체는 사용자의 데이터를 담는 변수다.
해당 프로젝트는 데이터 베이스 연결을 하지 않으니 해당 변수에 회원 정보를 저장하고 필요할 때 마다 사용할 것이다. - join user 이벤트에 대한 캐치 이벤트를 작성한다.
- 사용자로부터 입력받은 아이디를 기준으로 users 객체에 존재하는 아이디인지 체크하여 가입된 회원인지 검증한다.
- 이미 가입된 사용자면 result는 false로 메시지와 함께 콜백을 보낸다.
- users 객체에 존재하지 않는 사용자라면 users객체에 id와 pw를 저장한다.
- 콜백으로 result는 true로 가입 축하 메시지를 전달한다.
위의 내용으로 작동하는지 확인을 해보자.
회원가입에 성공하는 경우 축하 알림과 함께 로그인 폼으로 이동시켜준다.
이미 존재하
는 사용자인 경우 알림을 띄워준다.
이제 로그인을 해보자.
로그인을 했을 때 채팅방 화면으로 전환해주도록 만들 것이다.
가장 먼저 템플릿과 디자인을 작업하자.
body div(id = 'wrapper') div(id = 'user-wrap') ... div(id = 'content-wrap') nav span(id = 'nav-header') Chat Application span(id = 'logout-btn') 로그아웃 div(id = 'content-cover') div(id = 'room-wrap') div(id = 'room-list') div(id = 'room-header') 목록 div(id = 'room-select') div(class = 'room-el active' data-id = '1') 자유 div(class = 'room-el' data-id = '2') 뷰 공부방 div(class = 'room-el' data-id = '3') 노드 공부방 div(class = 'room-el' data-id = '4') 자바 공부방 div(id = 'chat-wrap') div(id = 'chat-header') 자유 div(id = 'chat-log') //- div(class = 'other-msg') //- span(class = 'other-name') 죠니 //- span(class = 'msg') 안녕하세요. //- div(class = 'my-msg') //- span(class = 'msg') 넹 ㅎㅇㅎㅇ form(id = 'chat-form') input(type = 'text' id = 'message' autocomplete = 'off' size = '30' placeholder = '메시지를 입력하세요.') input(type = 'submit' id = 'send-message-submit' value='보내기') div(id = 'member-wrap') div(id = 'member-list') div(id = 'member-header') 참여자 div(id = 'member-select') //- div(class = 'member-el') 이스코 //- div(class = 'member-el') 호날두 //- div(class = 'member-el') 모드리치
user-wrap 태그 밑에 content-warp부터 추가하면 된다.
/* 채팅방 CSS */ #content-wrap { width: 100%; min-width: 1280px; display: none; } #logout-btn { cursor: pointer; } #content-cover { width: 1280px; margin: 0 auto; padding-top: 20px; display: flex; justify-content: space-around; } #room-wrap { width: 200px; } #room-list { border: 1px solid #0084ff; border-radius: 5px; } #room-header { background: #0084ff; color: #fff; height: 40px; font-size: 18px; line-height: 40px; text-align: center; border-radius: 5px; } .room-el { text-align: center; border-bottom: 1px solid #f0f0f0; padding: 10px 0px; cursor: pointer; } .room-el:hover { background: #f0f0f0; } .room-el.active { background: #f0f0f0; } /* 채팅 로그 CSS */ #chat-wrap { width: 600px; border: 1px solid #ddd; } #chat-header { height: 60px; text-align: center; line-height: 60px; font-size: 25px; font-weight: 900; border-bottom: 1px solid #ddd; } #chat-log { height: 700px; overflow-y: auto; padding: 10px; } .my-msg { text-align: right; } .other-msg { text-align: left; margin-bottom: 5px; } .msg { display: inline-block; border-radius: 15px; padding: 7px 15px; margin-bottom: 10px; margin-top: 5px; } .other-msg > .msg { background: #f1f0f0; } .my-msg > .msg { background: #0084ff; color: #fff; } .other-name { font-size: 12px; display: block; } #chat-form { display: block; width: 100%; height: 50px; border-top: 2px solid #f0f0f0; padding: 0; } #message { width: 85%; height: calc(100% - 1px); border: none; padding-bottom: 0; } #message:focus { outline: none; } #chat-form > input[type=submit] { outline: none; border: none; background: none; color: #0084ff; font-size: 17px; } .notice { text-align: center; color: #8e8e8e, font-size: 13px; } /* 멤버 목록 CSS */ #member-wrap { width: 200px; } #member-list { border: 1px solid #aaaaaa; border-radius: 5px; } #member-header { height: 40px; font-size: 18px; line-height: 40px; padding-left: 15px; border-bottom: 1px solid #f0f0f0; font-weight: 600; } .member-el { border-bottom: 1px solid #f0f0f0; padding: 10px 20px; font-size: 14px; }
디자인은 요로코롬
그럼 이제 화면에 대한 스크립트 작업을 시작하자.
// 채팅 영역 DOM const contentWrap = document.querySelector('#content-wrap'); const roomsEl = document.querySelectorAll('.room-el'); const chatHeader = document.querySelector('#chat-header'); const chatForm = document.querySelector('#chat-form'); const chatLog = document.querySelector('#chat-log'); const sendMessageSubmit = document.querySelector('#send-message-submit'); // 참여자 영역 DOM const memberSelect = document.querySelector('#member-select'); /** * 로그인 액션 * @author Johnny */ loginSubmit.addEventListener('click', e => { e.preventDefault(); let id = document.querySelector('#login-id-txt'); let pw = document.querySelector('#login-password-txt'); const loginData = { id: id.value, pw: pw.value }; if (!loginData.id || !loginData.pw) { alert('아이디와 패스워드를 입력해주세요.'); id.focus(); return false; } /** * 로그인 요청 이벤트 발생 * @author Johnny * @param loginData.id 사용자 아이디 * @param loginData.pw 사용자 비밀번호 * @callback response.msg 시스템 메시지 내용 * @callback response.result 처리 결과 플래그 */ socket.emit('login user', loginData, (response) => { if (response) { alert(response.msg); id.value = ''; pw.value = ''; if (response.result) { socketId = socket.id; roomId = 1; this.displayChange(contentWrap, userWrap); // 채팅 로그 클리어 chatLog.innerHTML = ''; // 자유방 입장 roomsEl[0].click(); } else { joinBtn.click(); } } }); }); /** * 채팅방 참여자 목록 갱신 이벤트 * @author Johnny * @param users.user.socketId 채널 아이디 * @param users.user.name 사용자 아이디 */ socket.on('user list', users => { let memberHTML = ''; users.forEach(user => { if (user.socketId === socketId) { memberHTML += `<div class='memberEl'>${user.name} (me)</div>`; } else { memberHTML += `<div class='memberEl'>${user.name}</div>`; } }); memberSelect.innerHTML = memberHTML; }); /** * 채팅방 나가기 이벤트 * @author Johnny * @param userId 사용자 아이디 */ socket.on('leaved room', userId => { const noticeDivHTML = document.createElement('div'); noticeDivHTML.classList.add('notice'); noticeDivHTML.innerHTML = `<strong>${userId}</strong> 님이 나가셨습니다.`; chatLog.append(noticeDivHTML); }); /** * 채팅방 입장 이벤트 * @author Johnny * @param userId 사용자 아이디 */ socket.on('joined room', userId => { const noticeDivHTML = document.createElement('div'); noticeDivHTML.classList.add('notice'); noticeDivHTML.innerHTML = `<strong>${userId}</strong> 님이 들어오셨습니다.`; chatLog.append(noticeDivHTML); });
- 로그인 버튼을 클릭했을 때 이벤트를 등록해주자.
- 아이디와 패스워드를 검증한다.
- 아이디와 패스워드를 입력하지 않은 경우 id input 태그에 포커스를 맞추고 함수를 종료한다.
- 검증이 완료되면 login user 이벤트를 발생시키고 서버에 아이디와 패스워드 데이터를 전달한다.
- 콜백이 오면 input을 초기화한다.
- 서버에서 문제없이 처리되어 응답이 오면 소켓 아이디와 채팅방 아이디를 자유 채팅방 아이디로 설정한다.
- 로그인에 성공하면 자동으로 '자유' 채팅방에 접속이 된다.
- 화면을 로그인 회원가입 화면에서 채팅 화면으로 전환시킨다.
- 채팅창 로그를 초기화한다.
- 만약 서버에서 false가 전달되면 회원가입 메뉴에 클릭 이벤트를 발생시킨다.
- 아이디와 패스워드를 검증한다.
- 서버에서 보내는 채팅방 접속자를 갱신하는 이벤트인 user list 이벤트를 캐치한다.
- 인자로 전달되는 사용자 객체를 반복문을 통하여 div 태그로 그린다.
- 만약 내가 접속하고 있는 방인 경우엔 (me)를 붙여서 나라는 것을 표현한다.
- 인자로 전달되는 사용자 객체를 반복문을 통하여 div 태그로 그린다.
- 서버에서 보내는 채팅방 나가기 이벤트 leaved room을 캐치한다.
- 채팅방 로그에 어떤 유저가 나간 것인지 기록한다.
- 서버에서 보내는 채팅방 접속 이벤트 joined room을 캐치한다.
- 채팅방 로그에 어떤 유저가 접속한 것인지 기록한다.
서버 코드를 작성해봅시다.
interface LoginUser extends User { roomId: number; } const onlineUsers: any = {}; /** * 로그인 이벤트 * @author Johnny * @param loginData.id 사용자 아이디 * @param loginData.roomId 사용자 참여 채팅방 아이디 * @callback cb 콜백 */ socket.on('login user', (loginData: LoginUser, cb) => { console.log('>>>>> login user event catch'); if (loginCheck(loginData)) { const { id: userId, roomId } = loginData; const socketId = socket.id; onlineUsers[userId] = { roomId: 1, socketId }; socket.join(`room${roomId}`); cb({ result: true, msg: '로그인에 성공하였습니다.' }); updateUserList('0', '1', userId); } else { cb({ result: false, msg: '회원가입을 진행해주세요.' }); return false; } }); /** * 로그인 체크 * @author Johnny * @param loginData.id 사용자 아이디 * @param loginData.pw 사용자 패스워드 */ function loginCheck(loginData: LoginUser) { const { id: userId, pw: userPw } = loginData; if (users.hasOwnProperty(userId) && users[userId].pw === userPw) { return true; } else { return false; } } /** * 접속자 리스트 갱신 * @author Johnny * @param prevRoomId 이전 채팅방 아이디 * @param nextRoomId 다음 채팅방 아이디 * @param userId 사용자 아이디 */ function updateUserList(prevRoomId: string, nextRoomId: string, userId: string) { if (prevRoomId !== '0') { chat2Channel .in(`room${prevRoomId}`) .emit('user list', getUsersByRoomId(prevRoomId)); socket.broadcast .in(`room${prevRoomId}`) .emit('leaved room', userId); console.log('prev :', prevRoomId); } if (nextRoomId !== '0') { chat2Channel .in(`room${nextRoomId}`) .emit('user list', getUsersByRoomId(nextRoomId)); chat2Channel .in(`room${nextRoomId}`) .emit('joined room', userId); console.log('next :', nextRoomId); } } /** * 채팅방 접속자 리스트 구하기 * @author Johnny * @param roomId 채팅방 아이디 */ function getUsersByRoomId(roomId: string): Array<Object> { const userstemp: Array<Object> = []; Object.keys(onlineUsers).forEach(user => { if (onlineUsers[user].roomId === roomId) { userstemp.push({ socketId: onlineUsers[user].socketId, name: user }); } }); return userstemp; }
- User 인터페이스를 확장하는 인터페이스를 하나 만든다.
로그인 된 사용자가 어떤 채팅방에 참여하고 있는지를 서버에서 판단해야하므로 roomId라는 이름으로 멤버변수를 하나 만든다. - 로그인 된 온라인 상태의 사용자 정보를 담을 onlineUsers 객체를 만든다.
- login user에 대한 캐치 이벤트를 작성한다.
- loginData가 유효한지 검증을 진행한다.
- 객체에 아이디가 있는지 검증하고, 패스워드가 회원가입할 때 작성했던users 객체에 담겨있는 패스워드와 동일한지 검증한다.
- 검증에 성공하면 onlineUser 객체에 사용자 아이디를 키로 채팅방 아이디와 소켓 아이디를 객체로 저장한다.
- socket.join()을 통해 채팅방에 접속시켜준다. 인자로 들어가는 값은 채팅방 alias다
- 콜백으로 result는 true로 메시지와 함께 전달한다.
- 채팅방에 접속한 사용자 목록을 갱신시키기 위해 접속자 리스트 갱신 함수를 수행한다.
- 이전 채팅방에서 나간 경우 (내가 다른 채팅방으로 넘어간 경우) 해당 채팅방의 접속자 목록을 갱신하는 user list 이벤트를 발생시킨다.
그리고 사용자가 나갔다는 내용을 전달하기 위해 사용자 아이디를 전달하며 leaved room 이벤트를 발생시킨다. - 다음 채팅방에 접속하는 경우 접속자 목록을 갱신하고, 사용자 아이디를 전달하며 joined room 이벤트를 발생시킨다.
- 이전 채팅방에서 나간 경우 (내가 다른 채팅방으로 넘어간 경우) 해당 채팅방의 접속자 목록을 갱신하는 user list 이벤트를 발생시킨다.
- 검증에 실패하면 result는 false로 실패 메시지와 함께 콜백으로 전달한다.
- loginData가 유효한지 검증을 진행한다.
이제 정상적으로 작동하는지 확인해보자.
회원가입했던 계정으로 로그인을 하면 자유 채팅방에 자동으로 접속이 되고, 채팅방 채팅 로그에 입장한 사용자가 누구인지 쌓인다.
다른 사용자가 내가 접속하고 있는 채팅방에 들어오면 시스템 알림이 채팅로그에 쌓인다.
나 혹은 다른 사용자가 채팅방에서 떠나면 시스템 알림이 채팅로그에 쌓인다.
나머지는 너무 길어지니까 내일 마무리하도록 하자.
'개발일지' 카테고리의 다른 글
2019-09-16 개발일지 (0) 2019.09.16 2019-09-11 개발일지 (0) 2019.09.11 2019-09-09 개발일지 (0) 2019.09.09 2019-09-06 개발일지 (0) 2019.09.06 2019-09-05 개발일지 (0) 2019.09.05