프로젝트, 기대의 시작
"오늘 점심 뭐 먹지..?"
아마 직장을 다니는 분이라면 공감할텐데, 중요하진 않은것 같지만 모두가 매일같이 고민하고 또 고민하는 그 질문. 오늘 뭐먹지.
점심에만 하면 다행인데 하루에 최소 두번은 하는 고민이지 싶다. 그래서 조금이나마 이런 고민을 덜어주고 오히려 이런 고민을 단순하지만 재밌게 게임처럼 메뉴를 골라볼 수 있도록 런치고고를 만들었다. 서버와 클라이언트 모두 자바스크립트 기반의 node.js 와 리액트를 사용했다. 전체적인 플로우는 다음과 같다.
- 메인페이지에서 새로운 룸이 생성되면 사용자의 위치를 확인한다.
- 동의시 사용자의 현재 위치를 기준으로 투표결과에 따라 가까운 거리순으로 식당 목록을 알려준다.
=> 만약 미동의시, 사용자가 직접 입력할 수 있도록 주소 검색 컴포넌트를 추가했다.
실제로 어떻게 동작하는지 살펴보자.
간단히 클라이언트에서는 이렇게 동작을 하고 그에 따라서 서버는 어떻게 구성이 되고 설계가 되었는지 알아보자.
데이터베이스 스키마는, MongoDB Atlas 로!
MongoDB, 그리고 mongoose ORM
실시간으로 받아오는 식당 정보는 그때그때 생성된 투표방의 투표 결과에 따라 즉시 저장하고 내려주기 때문에 잘 짜여진 테이블에 데이터를 넣기를 선택하기 보다 원하는 정보를 그때그때 수정하고 넣을 수 있도록 mongoDB 를 활용했다. 처음 아이디어를 구상할 때는 DB를 사용하지 않고 진행하는걸 고려했었다. 이유는 투표가 이루어 질때마다 DB에 저장할 것이라고 생각해서 분명 딜레이가 생길거라고 예상했다. 그리고 실시간으로 딜레이 없이 논스탑 클릭킹을 구현하려면 서버와 DB 소통에 무리가 있을것이라 생각했었다. 사실 이 부분은 오히려 서버는 없어도 DB는 있어야 중간이나 투표 종료 후에 입장하는 유저에게 보여줄 수 있는 정보가 기본적으로 저장되어 있어야 한다고 결론 지어졌다. MongoDB는 기본적으로 객체 형태이기 때문에 아주 손쉽게 사용할 수 있었고, mongoose 모듈을 사용하여 데이터의 CRUD 가 자유로웠다. 아주 익숙한 모양이지 않은가? 하지만 체감상 SQL 데이터베이스보다 약간 딜레이되는 느낌이 들기도 했다. 그렇게 스키마를 작성하고, MVC 모델을 이용하여 데이터와 서버의 플로우를 구성했고 비동기적인 처리로 서버와 DB 간의 딜레이를 최대한 줄였다. 이 부분은 socket.io 부분에서 다시 한번 설명하려고 한다. 그리고 mongoDB 에서 제공하는 Atlas 클라우드 서비스를 활용하여 시작부터 데이터베이스를 띄워놓고 바로바로 테스트하며 서버를 만들 수 있어서 굉장히 편리했다. 그리고 무료로 제공하는 DB의 크기가 넉넉해 배포까지도 무리 없이 진행할 수 있었다.
1
2
3
4
5
6
7
8
9
10
11
12
|
const Room = require("roomSchema");
const saveRoomData = data => {
const room = new Room({
room_name: data.room_name,
creator: data.creator,
location: data.location,
number_people_joined: data.number_peeple_joined
});
room.save().catch(err => console.error(err));
}
module.exports = saveRoomData;
|
cs |
데이터를 저장하는 것도 어렵지 않게 .save() 를 하면 된다. 그리고 비동기 처리와 같은 방법으로 .catch() 로 발생하는 에러를 처리할 수 있다. *roomSchema 를 가져오는 것과 DB에 저장하는 함수를 exports 하는 것도 잊지말자.
Socket.io, 실시간 투표시스템
실시간이라는 개념이 빠지면 안되는 기능은 바로 채팅기능이다. 온라인에서 소통이라는게 0.몇초의 딜레이는 있겠지만 우리는 이것을 실시간이라고 부른다. 채팅 뿐만아니라 이러한 실시간 기능이 들어갈때면 socket.io를 활용한다. polling 이라던지 long-polling 같이 일정시간마다 지속적으로 요청을 날려서 사용자로부터 어떤 요청이 있는지 확인하거나 타임아웃을 걸어놓고 요청이 일어나길 기다리는 것과는 다르다.
그럼 실시간 투표기능은 어떻게 만들 수 있을까?
1. 투표방 만들기
그럼 먼저 투표방을 만들어 보자. 클라이언트에서 /create 로 요청을 날리면 express 에 빌트인 되어있는 crypto 모듈을 활용해서 새로운 방 주소를 만들어 클라이언트에 응답해준다. 그러면 사용자는 새로운 방의 주소를 복사해 투표에 참여할 사용자들에게 전달하게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
const express = require("express");
const router = express.Router();
const db = require("../models/db");
const crypto = require("crypto");
// create room
router.post("/create", (req, res) => {
let random = Math.random().toString();
let room_name = crypto
.createHash("sha1")
.update(random)
.digest("hex")
.slice(0, 5);
let newRoom = {
room_name: room_name,
creator: room_name,
location: req.body.location,
number_people_joined: 0
};
db.saveRoomData(newRoom);
res.status(200).json(room_name);
});
|
cs |
어? 왜 포스트로 받았지? 이유는 일정 거리내의 식당목록을 가져오기 위해 새로운 방을 만들때 사용자의 위치가 추가되어야 했다.
2. 4가지 상태와 액션
처음 설계할때는 투표방에 입장한 사용자가 2명 이상이 되면 투표대기 카운트다운이 시작되고 카운트다운이 끝나면 투표가 시작되도록 구상했었다. 투표 또한 일정한 시간내에서만 가능하도록 설계했다. 사실 카운트다운 부분에서도 고민이 많았다. 사용자 기준(클라이언트)에서 카운트다운을 하면 접속 시점마다 다른 카운트다운이 될 것이기 때문에 서버에서 모두 같은 값의 카운트다운을 내려줘야 했다. 서버에서 아무런 요청이 없어도 내려줄 수 있을까? 가장 고민된 부분이기도 했다. 하지만 setInterval() 과 socket 의 emit() 의 조합은 모든걸 가능하게 했다. 이 부분은 각 상태에 대한 코드에서 살펴보자.
첫번째 상태: wait
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
app.get("/room/:room_name", async (req, res, next) => {
try {
// Status: wait
let theRoom = await db.getByRoom_name(req.params.room_name).then(res => {
if (res) {
return res;
}
});
let joined_num = theRoom.number_people_joined + 1;
db.getByRoom_nameAndUpdatePeopleNumber(
req.params.room_name,
joined_num
).then(res => {
io.sockets.in(req.params.room_name).emit("joined", joined_num);
});
} catch (err) {
console.error(err)
}
|
cs |
처음 방을 만든 사용자는 아직 새롭게 입장한 투표자가 없기 때문에 wait 상태에서 방 주소를 복사해 참여시키고자 하는 사용자들에게 전달하게 된다. *async 와 await 를 적절히 사용해야 한다.
두번째와 세번째 상태: ready / vote
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
// Status: ready
if (joined_num === 2) {
await db
.getByRoom_nameAndUpdateRoom_Status(req.params.room_name, "ready")
.then(res => {
io.sockets.in(req.params.room_name).emit("status", "ready");
});
let count = 10;
let countdown = setInterval(() => {
io.sockets.in(req.params.room_name).emit("countdown", count);
count--;
// Status: vote
if (count === -1) {
clearInterval(countdown);
db.getByRoom_nameAndUpdateRoom_Status(
req.params.room_name,
"vote"
).then(res => {
io.sockets.in(req.params.room_name).emit("status", "vote");
liveCountdown(15);
});
}
}, 1000);
}
|
cs |
이제 새로운 사용자가 입장하게 되면 투표자가 2명이 되는 시점부터 투표대기 카운트다운이 시작된다. 여기서 setInterval() 을 활용해서 1초마다 카운트다운 값을 socket 에서 해당 투표방으로 .emit("countdown", count) 내려주게 된다. 카운트가 -1 되는 시점에서 setInterval() 을 종료시키고 투표가 시작되었다는 의미의 "vote" 상태를 각 투표자가 있는 방으로 내려준다. LiveCountdown(15) 는 투표 중 카운트다운을 실행하는 함수이다.
네번째 상태: result
이제 마지막 상태로 전환된다. 투표가 끝난 상황. 이전의 "vote" 상태에서 실행했던 LiveCountdown(15) 함수 안에 상태를 전환하는 코드를 포함하고 있어 카운트다운이 종료되면 상태를 전환시킨다. 투표 결과를 종합해서 가장 투표를 많이 받는 메뉴를 카카오맵 API 를 활용해 거리순으로 데이터에 저장 후 socket 을 통해 결과값을 응답해준다. 카카오맵이 주변 식당목록과 거리순으로 자동 정렬된 json 형태로 응답해주기 때문에 이번 프로젝트에서 활용도가 높았다.
3. 실시간 투표
1
|
socket.broadcast.to(vote.room_name).emit("vote", vote.category);
|
cs |
투표자가 투표를 하게 되면 어떤 카테고리를 클릭했는지 socket 을 통해서 날려준다. 그리고 broadcast 라는 기능을 이용하면 해당 투표자를 제외한 투표방에 있는 다른 투표자들에게 내가 어떤 카테고리를 투표했는지 알려줄 수 있다. 그렇게 되면 클라이언트 입장에서는 카테고리가 클릭될 때 액션을 각각의 투표자 화면에서 실행시켜주면 되는 것이다. 이 프로젝트에서 포인트는 게임처럼 몇번을 클릭하던 상관없이 최대한 많은 클릭을 해서 투표에서 이길 수 있도록 만드는 것이었다.
만약 클릭에 따라 DB 에 저장을 하게 되면 1초에 수십에서 수백개의 lookup 과 update 가 이루어져야 했다. 두가지 선택지를 생각했다. 첫번째로 서버 메모리를 활용해서 투표결과를 서버메모리에 저장 후 투표가 끝나는 시점에서 DB 에 저장하는 것과 두번째로 Redis 라는 캐싱메로리 데이터베이스를 활용해 투표방의 투표상태를 들고 있다가 투표가 끝나는 시점에서 DB 에 저장하는 것이었다. 결론부터 얘기하자면, 첫번째 방법을 사용했다. Redis 를 사용해보고 싶었지만 1차적으로 프로젝트 개발 수준에서는 서버 메모리로 충분히 테스트와 어느정도의 메모리내에서 문제 없을거라고 판단했다. 프로덕션 레벨로 진행되게 되면 redis 를 활용계획에 있다.
결론
이렇게 socket.io 를 활용하게 되면 실시간으로 소통이 필요한 기능에 아주 유용하게 사용할 수 있고, namespace 기능을 이용하게 되면 새롭게 만들어진 방만 활용하는게 아니라 라우팅을 통해서 조금 더 넓은 범위에서 활용될 수 있다. 특히 broadcast 기능은 실제 사용자가 체감할 수 있는 딜레이 느낌을 최소화 시켜줄 수 있었다. 이제는 사용자의 경험이 굉장히 중요하기 때문에 socket 과 같은 모듈을 적극적으로 활용하고 응용할 필요가 있다. 대부분의 프로젝트나 서비스에서는 사용자와의 소통이 이루어지기 때문에 socket.io 공식 문서와 어떤식으로 사용될 수 있는지 그 예시들을 찾아서 공부해 보는 것도 도움이 될 것 같다.
참고
'Programming > JavaScript tips' 카테고리의 다른 글
React 로 포트폴리오 만들기 (0) | 2019.10.26 |
---|---|
AWS S3 - React app 배포하기 (0) | 2019.10.23 |
AWS 시대 (0) | 2019.08.23 |
[TIL] node.js 서버 구성 (0) | 2019.08.13 |
Node.js 서버사이드 (0) | 2019.08.11 |