Node 강의/주특기 플러스

2-4 프로토 파일 로드

kagan-draca 2024. 10. 22. 14:10

1) protobufjs 간단 사용법

1. 프로토콜 버퍼 정의 파일 작성하기

 

먼저, 최상위 폴더에 protobuf 파일을 만들고, request와 response 폴더를 만듭니다.

request 폴더에는 common.proto 파일을 생성해주고, 

syntax = 'proto3';

package common;

message Package{
    uint32 handlerId =1;
    string userId = 2;
    string clientVersion =3;
    uint32 sequence = 4;
    byte payload = 5;
}

위의 내용은 클라이언트가 서버에 내용을 보내기 위한 내용들로

 

어떠한 이벤트가 "어떠한 handlerId로 유저가 clientVersion과 sequence를 가지고 요청을 보낼 것인데

이 handerId에서 payload를 처리하기 위해서는 위와 같은 데이터가 필요하다." 라는 내용입니다.

 

response 폴더에는 response.proto 파일을 생성해주고,

syntax = 'proto3'

package response;

message Response{
    uint32 handlerId = 1;
    uint32 responseCode =2;
    int64 timestamp = 3;
    byte data = 4;
    uint32 sequence = 5;
}

위의 내용으로 서버가 클라이언트에게 내용을 보내기 위한 내용들을 작성해줍니다. 

 

위와 같이 작성하는 이유는 서버와 클라이언트가 packet을 주고 받을 때 어떤 내용들이 올 것이라는 걸 미리 알아서 혼선이 없도록 하기 위해서 입니다.

 

이제 두 파일을 JavaScript 상에서 편하게 읽기 위해 packetName.js라는 매핑 파일을 만들겠습니다.

packetName.js 파일 내부에는 아래와 같이

export const packetNames = {
  common: {
    Packet: 'common.Packet',
  },
  response: {
    Response: 'response.Response',
  },
};

위에 작성된 'common' 패키지의 message인 Packet을 나타내는 'common.Package'와 'response' 패키지의 message인 Response를 나타내는 'response.Response'를 작성해줍니다. 만약 어떤 다른 패킷들이 추가 된다면, 위와 같이 파일을 만들어주고, packetName.js 파일에 넣어주면 됩니다.

 

그 후, init 폴더에 loadProtos.js 파일을 만들어주고, 

 

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
 
const protoDir = path.join(__dirname, '../protobuf');

 

protobuf가 있는 폴더를 경로로 지정해줍니다. 그후,

const getAllProtoFiles = (dir, fileList = []) => {
  //../protobuf 폴더 내에 .proto로 끝나는 파일들을 읽어오기 위한 함수
  // 매개변수로는 dir로 디렉토리 경로와 fileList라는 배열로 향후 확장성을 위해 존재하는 중 입니다.
  const files = fs.readdirSync(dir);
  // 해당 폴더의 파일들을 읽어온다.
  files.forEach((file) => {
    // 읽어온 파일들에서
    const filePath = path.join(dir, file);
    // 파일의 확장자와 위치를 붙여 경로를 만들어줍니다.

    // 만약 읽어온 무언가가 디렉토리이면
    if (fs.statSync(filePath).isDirectory()) {
      // 재귀 함수 형태로 다시 그 읽어온 디렉토리를 넣어 파일을 읽어오게 만들어줍니다.
      getAllProtoFiles(filePath, fileList);
    } else if (path.extname(file) === '.proto') {
      // 만약 읽어온 무언가가 파일이고, .proto 파일이면
      fileList.push(filePath);
      // 리스트에 추가해줍니다.
    }
  });

  return fileList;
};

const protoFiles = getAllProtoFiles(protoDir);

const protoMessages = {};

로 ".proto" 파일로 된 파일들을 가져와 protoFiles 변수 안에 저장해줍니다. 

 

파일들을 읽어는 왔으니 외부에서 사용할 수 있도록 해보겠습니다.

export const loadProts = async () => {
  try {
    const root = new protobuf.Root();

    await Promise.all(protoFiles.map((file) => root.load(file)));

    for (const [packageName, types] of Object.entries(packetNames)) {
      console.log(packageName);
      console.log(types);
    }

    console.log('Protobuf 파일이 로드 됐습니다.');
  } catch (error) {
    console.error('Protobuf 파일 로드 중 오류가 발생했습니다 : ', error);
  }
};

 

우선,

import protobuf from 'protobufjs';

protobufjs 패키지를 import 받아주고,

 

그 다음,

const protoMessages = {};
export const loadProts = async () => {
 
};

로 외부에서 사용할 수 있도록 함수를 만든 후,

export const loadProts = async () => {
  try {
    const root = new protobuf.Root();
    // protobuf에서 new 연산자로 Root() 객체를 만들어줍니다.

    console.log('Protobuf 파일이 로드 됐습니다.');
  } catch (error) {
    console.error('Protobuf 파일 로드 중 오류가 발생했습니다 : ', error);
  }
};

 

try-catch 문으로 protobuf.Root() 객체를 생성하고,

    await Promise.all(protoFiles.map((file) => root.load(file)));
    // Promise.all로 파일들을 한 번에 읽어옵니다.

비동기 방식으로 파일을 모두 읽어 들입니다.

 

    // protoMessages를 사용하기 위해서 protoMessages에 내용물을 맵핑하기 위해 packetNames를 가져옵니다.
    for (const [packageName, types] of Object.entries(packetNames)) {
      protoMessages[packageName] = {};
      for (const [type, typeName] of Object.entries(types)) {
        protoMessages[packageName][type] = root.lookup(typeName);
      }
    }

그리고,  protoMessages객체에 packageName이라는 key에 따른 객체를 생성해준 후,

      for (const [type, typeName] of Object.entries(types)) {
        protoMessages[packageName][type] = root.lookupType(typeName);
      }

그 객체에 type 이라는 key에 따른 root.lookupType으로 타입 이름을 받아와줍니다.

 

파일을 사용하는 위치는 index.js의 initServer 함수로

import { loadProts } from './loadProtos.js';

를 import 해주고,

 

const initServer = async () => {
  try {
    await loadGameAssets();
    await loadProts();
  } catch (e) {
    console.error(e);
    process.exit(1);
  }
};

게임 에셋 load와 함께 Prots도 load해줍니다.

 

하지만, 이럴 경우 파일 원본 데이터를 조작할 위험이 있기 때문에 loadProtos.js에서

protoMessages를 얕은 복사한 함수를 사용하게 만들겠습니다.

 

export const getProtoMessages = ()=>{
    return {...protoMessages}
}

위와 같이 protoMessages는 객체인데 spread연산자와 다시 {}객체를 만들어줘

얕은 복사를 수행합니다.

 

이유 : 얕은 복사를 통해 원본 객체를 복사한 객체를 반환할 경우 객체 내부의 값이 바뀌더라도 원본 객체는 바뀌지 않기 때문이다.