5주간의 이야기 - 2

Dalbi·2021년 6월 3일
0
post-thumbnail
post-custom-banner

node.js

2021-05-10부터 2021-06-03까지 약 4주간 node.js로 프로젝트를 진행하게 되었다.

'여태까지 Python-Django 공부해놓고 왜 갑자기 node.js야?' 라고 물어본다면 Python과 node.js의 선택지가 있었는데 node.js를 선택했을 뿐이다.

node.js란 무엇인가? javascript runtime이다. 본래 javascript는 프론트엔드에서 사용하기 위해 만들어진 언어이다. 이를 백엔드에서 사용하기 위해 런타임으로 만든것이 node.js이다. 그리고 Python에서 Django 프레임워크를 사용하듯 node.js에서는 express 프레임워크를 사용할 예정이다.

node.js를 공부하며 느낀점은 Python-Django는 기본적으로 전반적인걸 지원해주고 node.js-express는 필요한걸 가져다 쓴다. Python-Django가 풀옵션 오피스텔 전세 입주라면 node.js-express는 신축 아파트를 분양받아 입주하는 느낌이다.(개인적인 견해임을 밝힌다.)

websocket

이번에 진행된 프로젝트에서 가장 핵심 기술은 websocket이다.

websocket이란 기존의 단방향 통신인 http통신과 다르게 Socket Connection을 유지해서 언제든 양방향 통신 또는 데이터 전송이 가능하도록 하는 기술이다. 주로 채팅 기능을 구현할때 사용된다.

이번 프로젝트에서는 websocket을 통해 유저는 퀴즈를 풀고 어드민은 퀴즈를 컨트롤하는 사이트를 만들었다. 전체적인 흐름은 다음과 같다.

어드민에서 버튼을 통해 websocket에 '유저입장'이라는 메세지를 보내면 websocket에 연결된 모든 유저에게 '유저입장'이라는 메세지를 전달하게 된다. 이때 유저는 퀴즈로 입장하는 버튼이 활성화되고 입장이 가능해진다.

이후 퀴즈가 시작되고 어드민에서 '보상확인'(퀴즈별로 보상이 존재한다.), '퀴즈시작', '정답확인'이라는 메세지를 순서에 맞춰 보내게되고 유저측에서는 메세지에 따라 보상을 화면에 띄워주거나, 퀴즈 문제를 풀고 정답을 확인하는 과정을 진행하게 된다. 이때 기존 http통신과 다르게 유저측에서는 특정 url로 접속하는 과정 없이 websocket으로 전반적인 퀴즈 흐름을 따라가게 된다.

아래는 어드민에서 보낸 메세지를 유저측으로 전달하는 코드이다.

var WebSocketServer = require("ws").Server;
var wss = new WebSocketServer({ port: 3000 });

wss.on("connection", function (ws) {
  ws.on("message", function (message) {
    wss.clients.forEach((client) => {
      client.send(message);
    });
  });
});

3000번 포트를 이용해 websocket 서버를 열고 서버에 입장 후, 메세지를 보내면 해당 메세지를 그대로 서버에 입장한 모든 유저에게 보내는 코드이다.

여기서는 ws라는 모듈을 사용했지만 websocket을 지원하지 않는 브라우저에서도 똑같은 기능을 사용할 수 있도록 보완한 socket.io라는 모듈도 존재하며 이를 사용하면 socket.io-redis 모듈을 이용해 redis의 pub/sub기능으로 채팅 서버를 만들수 있다.

sql문

이전 두 프로젝트에서는 Django를 활용하여 관계형 데이터베이스인 mysql을 사용하였다. 하지만 이번엔 node.js를 통해 postgresql을 사용하게 되었고 언어가 바뀌면 사용법또한 바뀌는 ORM방식이 아닌 직접 sql문을 작성하여 데이터베이스를 컨트롤 하기로 하였다.

아래는 이번 프로젝트에서 사용된 sql문 사용 예제이다.

예제 1) select, from, where

select password from admins where id=1;

간단한 예제이다. admins테이블에서 컬럼명이 id에서 값이 1인 row에서 password의 컬럼값을 출력해라. 라는 의미이다.

select로 필요한 컬럼명을 넣어주고 from으로 찾을 테이블명, where로 조건을 걸어준다. where에는 and로 추가적인 조건을 걸어둘수 있다.

예제 2) insert

insert into users (nickname, language_id, device_type) values (nickname, language_id, platform) returning *

users라는 테이블에 컬럼값 nickname, language_id, device_type에 해당하는 값을 nickname, language_id, platform에 있는 값으로 넣고 결과를 출력해라. 라는 의미이다.

insert로 table에 데이터를 넣는다는것을 명시하고 into 이후 테이블명(컬럼명1, 컬럼명2..)으로 작성하여 테이블과 컬럼명을 넣고 values(데이터1, 데이터2)로 넣을 데이터값을 명시한다.

예제 3) count(*)

select count(*) from users where language_id=1;

이번엔 첫번째 예제와 매우 흡사하다. 하지만 결과물은 다르다. select 뒤의 count는 해당 행의 숫자를 구해준다. count뒤의 *의 자리에는 컬럼명이 들어가고 *로 표기시에는 전체에 해당되는 행값을 구해준다. 때문에 이 경우에는 해당 숫자가 결과로 나온다.

예제 4) inner join, group by

select is_answer, count(*) from user_answers inner join quizes on quiz_id = quizes.id where quiz_seq=1 group by is_answer;

이번엔 좀 어렵다. is_answer의 컬럼에 있는 값에 대한 숫자를 구할건데 user_answers와 quizes의 inner join(교집합)을 통해서 user_answers의 quiz_id와 quizes의 id값이 같은 행 중에 quiz_seq의 값이 1인것을 is_answer의 값을 기준으로 나누어 구해라.

user_answers에서 그냥 group by를 통해 quiz_seq값이 1인것을 구하면 안될까 싶겠지만 user_answers에는 quiz_seq 컬럼이 없다. 그래서 inner join을 통해 user_answers와 quizes의 테이블을 합쳐 quiz_seq 컬럼을 기준으로 값을 출력한것이다.

is_answers 컬럼은 boolean 자료형이 들어가 있기 때문에 True, False, null 세가지 값을 기준으로 결과가 출력된다.

예제 5) drop schema, create schema

	DROP SCHEMA public CASCADE;
	CREATE SCHEMA public; 

mysql로 비유하자면 'drop database'와 같은 기능이다.

drop schema name <CASCADE | RESTRICT > 문법을 가지고 있고 CASCADE는 스키마에 포함된 개체를 전부 지우고 RESTRICT는 개체가 있을경우 스키마 삭제를 거부한다.(기본값은 RESTRICT이다.)

스키마를 따로 지정해주지 않았을경우 public이므로 public으로 전체 데이터 베이스를 삭제한다.

이후 다시 public 스키마를 생성하여 테이블이 생성될 수 있도록 한다.

Brocker

이번 프로젝트를 진행하며 나를 괴롭혔던 친구들이 몇몇 존재한다. 소개해 주도록 하겠다.

post요청에 body가 undefined에요.

node.js로 post요청에서 body의 값을 json으로 받아올때 보통 body-parser를 주로 사용한다.(express에 포함되어 설치된다.)

때문에 당연히 이번 프로젝트에서도 body-parser를 사용했고 postman으로 작동시켰을때 문제 없이 작동하는것을 확인했다. 하지만 문제는 프론트엔드와 통신을 했을때부터 시작됐다.

(현재는 수정된 코드이다. 코드가 예쁘진 않지만 이해 부탁바란다.)

위의 코드에서 정상적으로 body에 요청을 넣어 보냈음에도 지속적으로 KeyError가 발생했다. 확인해보니 data에 req.body(현재는 수정되어 rawBody로 변경되었다.)가 빈값이 담기고 있었던것이다.

해결책 1

nodejs post request body undefined 로 구글링을 하였다.

각종 사이트에서 body-parser를 이야기했고 body-parser를 재설치하고 재설정 해보았지만 문제가 해결되지 않았다.

해결책 2

프론트엔드에서 보내는 요청의 형식이 궁금했다. 그래서 요청의 headers를 열어서 확인해보았다. 결과는 충격적. Content-Type에 application/json가 아닌 text/plain으로 보내지고 있었던것이다.

이번엔 nodejs request post plain text 로 구글링을 하였다.

const express = require('express');
const app = express();
const parseRawBody = (req, res, next) => {
  req.setEncoding('utf8');
  req.rawBody = '';
  req.on('data', (chunk) => {
    req.rawBody += chunk;
  });
  req.on('end', () => {
    next();
  });
}
app.use(parseRawBody);
app.post('/test', (req, res) => {
  res.send(req.rawBody);
});
app.listen(3000);

위의 방법을 발견하게 되어 코드를 적용시켰고 body에 정상적으로 정보가 담겨오는것을 확인할 수 있었다.
[출처]

redis의 pub/sub기능을 사용하고 싶어요.

결론부터 말하자면 socket.io를 사용해야했다.

처음 프로젝트를 약 5만명이 동시에 사용하는 퀴즈 프로그램으로 계획하였고 때문에 nosql중에서도 연산속도가 빠르고 pub/sub기능이 있는 redis를 사용하기로 하였다.

socket.io의 경우 위에서도 언급했다시피 socket.io-redis 모듈을 이용하면 redis의 pub/sub기능을 사용하여 서버를 만들수 있다.

하지만 이번 프로젝트에서 본인은 node.js로 진행하고 다른 백엔드분은 python로 진행하기로하였다. 동기화 방식인 python을 비동기 방식으로 websocket을 사용할 수 있게 만들어주는 channels사용하였고 channels는 websocket으로 구성되어있다.

때문에 본인은 node.js임에도 socket.io가 아닌 ws모듈을 선택했고 ws와 redis의 pub/sub를 연결하기위한 몇몇 시도에도 전부 수포로 돌아갔다.

예를 들면 ws로 보낸 메세지를 redis에서 직접 받는 방법(redis에서 직접 안받아졌다.) 등등.

딱 한가지 방법으로는 성공하였었다. ws로 메세지를 받아 redis로 다시 전달하여 redis의 pub/sub기능을 활용해 subscribe한 유저를 대상으로 메세지를 ws로 보내는 방법을 사용할 수 있었지만 redis껍데기를 쓴 ws라는 느낌이 강해 그만두게 되었다.

6주차의 결과물

이 글을 작성하기 시작했을땐 분명 5주간의 이야기였는데 마무리 할 때 쯔음엔 6주가 되어버려 어느정도 결과물이 나오게 되었다.

좌측은 어드민, 우측은 유저 페이지이다. 어드민 페이지에서 버튼을 통해 입장할 수 있도록 버튼을 활성화하고 입장한 유저가 퀴즈페이지로 넘어가도록 컨트롤한다.

퀴즈풀이 과정 또한 기존의 http 방식과 다르게 정답을 입력 후 바로 통신을 하는것이 아닌 어드민에서 '정답확인' 버튼으로 정답여부를 확인한다.

이렇게 퀴즈를 진행한 후 각 유저에게 총 문제중 정답, 오답의 갯수를 알려주며 어드민에서는 통계자료를 볼 수 있다.

프로젝트 후기

우선 같이 프로젝트를 진행해준 팀원들에게 고맙다는 인사를 전하고싶다.(주말에도 열정적으로 임해주었던 모두에게 고맙다.) PL역할로 이번 프로젝트를 진행하였는데 부족한 부분이 많았다 생각한다.

나는 '어떤' 팀원이었나?

첫 프로젝트를 마치고 나에게 던진 질문이었다.

비롯 첫 프로젝트때에는 '첫 술에 배부르랴'라며 자기 자신을 합리화하듯 이야기 하였지만 이번에는 완벽한 PL은 아닐지라도 '다시 한번 함께 하고 싶은 팀원'이였다고 생각한다.
(아닌가요..? 맞다고 해주세요.)

한 달 동안 어려웠지만 즐거웠고 다시 한번 이런 기회가 온다면 다시 함께하고 싶다.

profile
백엔드..?
post-custom-banner

0개의 댓글