Node 강의/주특기 플러스

2-9 유저 데이터 저장

kagan-draca 2024. 11. 1. 12:06

db 폴더 아래 user 폴더를 만들어줍니다.

 

user 폴더에는 user.queries.js 파일을 만들어 유저가 서버에 최초 접속할 경우 유저 정보를 DB에 저장하기 위한 쿼리

  CREATE_USER: 'INSERT INTO user (id, device_id) VALUES (?,?)',

와 생성 전 해당 유저가 이미 존재 했는지 조회하는 쿼리

  FIND_USER_BY_DEVICE_ID: 'SELECT * FROM user WHERE device_id = ?',

그리고 이미 존재하는 유저가 다시 서버에 접속 했다면 마지막으로 접속한 로그인 날짜, 시간을 갱신해줄 

  UPDATE_USER_LOGIN: 'UPDATE user SET last_login = CURRENT_TIMESTAMP WHERE id = ?',

쿼리를 하나의 객체로 묶어 만들어줍니다.

export const SQL_QUERIES = {
  FIND_USER_BY_DEVICE_ID: 'SELECT * FROM user WHERE device_id = ?',
  CREATE_USER: 'INSERT INTO user (id, device_id) VALUES (?,?)',
  UPDATE_USER_LOGIN: 'UPDATE user SET last_login = CURRENT_TIMESTAMP WHERE id = ?',
};

 

다시 user폴더에 user.db.js 파일을 생성해 위의 객체에 따른 Key로 해당 쿼리를 실행시켜줄 함수를 만들어줍니다.

  FIND_USER_BY_DEVICE_ID: 'SELECT * FROM user WHERE device_id = ?',

해당 쿼리가 실행될 함수는

export const findUserByDeviceID = async (deviceId) => {
  const [rows] = await pools.USER_DB.query(SQL_QUERIES.FIND_USER_BY_DEVICE_ID, [deviceId]);
  return toCamelCase(rows[0]);
};

으로 우리는 유저가 접속한 deviceId(그냥 id라고 생각해도 된다.)로 유저를 구분할 것이기 때문에 매개변수로 deviceId를 가져옵니다.

 

toCamelCase는 현재 DB의 Colum이 snake_case로 작성 돼 있기 때문에 CamelCase로 변형해주기 위한 함수로

import { toCamelCase } from '../../utils/transformCase.js';

utils의 transformCase.js 파일에 존재하고

import camelCase from 'lodash/camelCase.js';

export const toCamelCase = (obj) => {
  if (Array.isArray(obj)) {
    // 배열인 경우, 배열의 각 요소에 대해 재귀적으로 toCamelCase 함수를 호출
    return obj.map((element) => toCamelCase(element));
  } else if (obj !== null && typeof obj === 'object' && obj.constructor === Object) {
    // 객체인 경우, 객체의 키를 카멜케이스로 변환하고, 값에 대해서도 재귀적으로 toCamelCase 함수를 호출
    return Object.keys(obj).reduce((result, key) => {
      result[camelCase(key)] = toCamelCase(obj[key]);
      return result;
    }, {});
  }
  // 객체도 배열도 아닌 경우, 원본 값을 반환
  return obj;
};

위와 같이 작성 돼 있습니다. 코드를 살펴보면,

lodash를 이용해 snake_case를 CamelCase로 바꾸고 있는걸 볼 수 있습니다.

 

import pools from '../database.js';

pools 안에 우리가 사용하고자 하는 USER_DB가 존재하기 때문에 pools를 import 해주고

pools.USER_DB.query로 조회하고자 하는 deviceId가 존재하는지 rows에 담아줍니다.

 

  CREATE_USER: 'INSERT INTO user (id, device_id) VALUES (?,?)',

를 실행시킬 함수로는 

export const createUser = async (deviceId) => {
  const id = uuidv4();
  await pools.USER_DB.query(SQL_QUERIES.CREATE_USER, [id, deviceId]);
  return { id, deviceId };
};

같으며 우리는 DB의 PK(Primary Key)를 생성하기 위해

import { v4 as uuidv4 } from 'uuid';

uuid를 import 해주고 uuid에 존재하는 v4를 uuidv4라는 이름으로 사용할 것 입니다. 그 다음

const id = uuidv4()로 PK를 생성하고 실질적인 유저의 id가 될 deviceId를 가져와 

pools.User_DB.query로 id와 deviceId를 넣어줘 생성해줍니다.

 

  UPDATE_USER_LOGIN: 'UPDATE user SET last_login = CURRENT_TIMESTAMP WHERE id = ?',

를 실행시킬 함수로는 

export const updateUserLogin = async (id) => {
  await pools.USER_DB.query(SQL_QUERIES.UPDATE_USER_LOGIN, [id]);
};

으로 해당 유저의 id를 바탕으로 마지막 로그인 시간을 다시 기록하는 것을 볼 수 있습니다.

 

정리된 코드 :

import { v4 as uuidv4 } from 'uuid';
import pools from '../database.js';
import { SQL_QUERIES } from './user.queries.js';
import { toCamelCase } from '../../utils/transformCase.js';

export const findUserByDeviceID = async (deviceId) => {
  const [rows] = await pools.USER_DB.query(SQL_QUERIES.FIND_USER_BY_DEVICE_ID, [deviceId]);
  return toCamelCase(rows[0]);
};

export const createUser = async (deviceId) => {
  const id = uuidv4();
  await pools.USER_DB.query(SQL_QUERIES.CREATE_USER, [id, deviceId]);
  return { id, deviceId };
};

export const updateUserLogin = async (id) => {
  await pools.USER_DB.query(SQL_QUERIES.UPDATE_USER_LOGIN, [id]);
};

 

이제, 위의 함수들이 사용될 위치에 위의 함수들을 호출해보겠습니다.

src/handlers/user/inital.handler.js 파일의

import { addUser } from '../../session/user.session.js';
import { HANDLER_IDS, RESPONSE_SUCCESS_CODE } from '../../constants/handlerIds.js';
import { createResponse } from '../../utils/response/createResponse.js';
import { handleError } from '../../utils/error/errorHandler.js';
import { createUser, findUserByDeviceID, updateUserLogin } from '../../db/user/user.db.js';

const initialHandler = async ({ socket, userId, payload }) => {
  try {
    const { deviceId } = payload;


    addUser(socket, deviceId);

    // 유저 정보 응답 생성
    const initialResponse = createResponse(
      HANDLER_IDS.INITIAL,
      RESPONSE_SUCCESS_CODE,
      { userId: user.id },
      deviceId,
    );

    // 소켓을 통해 클라이언트에게 응답 메시지 전송
    socket.write(initialResponse);
  } catch (error) {
    handleError(socket, error);
  }
};

export default initialHandler;

은 유저가 처음 서버에 접속하면 동작하는 이밴트 처리 핸들러로

 

const initialHandler = async ({ socket, userId, payload }) => {
  try {
    const { deviceId } = payload;

    let user = await findUserByDeviceID(deviceId);

    if (!user) {
      user = await createUser(deviceId);
    } else {
      await updateUserLogin(user.id);
    }

위와 같이 내용을 추가해줍니다. 위의 코드를 요약하자면 deviceId로 DB에 유저의 존재 유무를 확인하고,

 

유저가 없으면 유저 정보를 DB에 생성해주고, 유저가 존재했다면 마지막 로그인 시간을 바꿔줍니다.

 

마지막으로 

    addUser(socket, user.id);

    // 유저 정보 응답 생성
    const initialResponse = createResponse(
      HANDLER_IDS.INITIAL,
      RESPONSE_SUCCESS_CODE,
      { userId: user.id },
      deviceId,
    );

으로 코드를 수정해줬는데 addUser는 기존에 deviceId로 넣어주고 있었으나

    let user = await findUserByDeviceID(deviceId);

으로 DB에 유저 존재 확인 및

      user = await createUser(deviceId);

존재하지 않을 경우 생성해주고 있기 때문에

    addUser(socket, user.id);

user.id로 바꿔줍니다.

export const addUser = (socket, uuid) => {
  const user = new User(uuid, socket);
  userSessions.push(user);
  return user;
};

addUser는 해당 user의 id를 바탕으로 해당 유저의 socket과 함께 유저의 객체를 생성해 

유저의 요청에 따른 세션을 관리할 수 있도록 해주는 함수 입니다.

 

마찬가지로 

    // 유저 정보 응답 생성
    const initialResponse = createResponse(
      HANDLER_IDS.INITIAL,
      RESPONSE_SUCCESS_CODE,
      { userId: deviceId },
      deviceId,
    );

응답으로 

 

{ userId : deviceId }로 유저의 응답해줄 userId를 만들어주고 있었는데,

DB에서 유저 정보를 보관 및 기록, 없다면 생성해주고 있어서

 

    // 유저 정보 응답 생성
    const initialResponse = createResponse(
      HANDLER_IDS.INITIAL,
      RESPONSE_SUCCESS_CODE,
      { userId: user.id },
      deviceId,
    );

{ userId : user.id } 로 변경해줍니다.

 

클라이언트와 서버의 동작을 확인하기 위해 서버와 클라이언트를 동작 시키기 전 client.js를 수정해주겠습니다.

import net from 'net';
import { getProtoMessages, loadProtos } from './src/init/loadProtos.js';

const TOTAL_LENGTH = 4; // 전체 길이를 나타내는 4바이트
const PACKET_TYPE_LENGTH = 1; // 패킷타입을 나타내는 1바이트

let userId;
let sequence;

const createPacket = (handlerId, payload, clientVersion = '1.0.0', type, name) => {
  const protoMessages = getProtoMessages();
  const PayloadType = protoMessages[type][name];

  if (!PayloadType) {
    throw new Error('PayloadType을 찾을 수 없습니다.');
  }

  const payloadMessage = PayloadType.create(payload);
  const payloadBuffer = PayloadType.encode(payloadMessage).finish();

  return {
    handlerId,
    userId: '1',
    clientVersion,
    sequence: 0,
    payload: payloadBuffer,
  };
};

const sendPacket = (socket, packet) => {
  const protoMessages = getProtoMessages();
  const Packet = protoMessages.common.Packet;
  if (!Packet) {
    console.error('Packet 메시지를 찾을 수 없습니다.');
    return;
  }

  const buffer = Packet.encode(packet).finish();

  // 패킷 길이 정보를 포함한 버퍼 생성
  const packetLength = Buffer.alloc(TOTAL_LENGTH);
  packetLength.writeUInt32BE(buffer.length + TOTAL_LENGTH + PACKET_TYPE_LENGTH, 0); // 패킷 길이에 타입 바이트 포함

  // 패킷 타입 정보를 포함한 버퍼 생성
  const packetType = Buffer.alloc(PACKET_TYPE_LENGTH);
  packetType.writeUInt8(1, 0); // NORMAL TYPE

  // 길이 정보와 메시지를 함께 전송
  const packetWithLength = Buffer.concat([packetLength, packetType, buffer]);

  socket.write(packetWithLength);
};

// 서버에 연결할 호스트와 포트
const HOST = 'localhost';
const PORT = 5555;

const client = new net.Socket();

client.connect(PORT, HOST, async () => {
  console.log('Connected to server');
  await loadProtos();

  const successPacket = createPacket(0, { deviceId: 'xxxxx' }, '1.0.0', 'initial', 'InitialPacket');

  sendPacket(client, successPacket);
});

client.on('data', (data) => {
  // 1. 길이 정보 수신 (4바이트)
  const length = data.readUInt32BE(0);
  const totalHeaderLength = TOTAL_LENGTH + PACKET_TYPE_LENGTH;

  // 2. 패킷 타입 정보 수신 (1바이트)
  const packetType = data.readUInt8(4);
  const packet = data.slice(totalHeaderLength, length); // 패킷 데이터

  if (packetType === 1) {
    const protoMessages = getProtoMessages();
    const Response = protoMessages.response.Response;

    try {
      const response = Response.decode(packet);

      if (response.handlerId === 0) {
        const responseData = JSON.parse(Buffer.from(response.data).toString());

        userId = responseData.userId;
        console.log('응답 데이터:', responseData);
      }
      sequence = response.sequence;
    } catch (e) {
      console.log(e);
    }
  }
});

client.on('close', () => {
  console.log('Connection closed');
});

client.on('error', (err) => {
  console.error('Client error:', err);
});

으로 수정해줍니다.

 

서버와 클라이언트를 동작 시켜 확인해보면 

정상 동작하는 것을 확인할 수 있습니다.

'Node 강의 > 주특기 플러스' 카테고리의 다른 글

2-8 DB 마이그레이  (0) 2024.11.01
2-7 DB 연동  (0) 2024.10.31
2-6 세션, 핸들러 추가  (0) 2024.10.31
2-5 패킷 파싱  (0) 2024.10.28
2-4 프로토 파일 로드  (0) 2024.10.22