Node 강의/심화

1-5 서버 로직 개발(데이터 테이블 로드)( 점프 게임 )

kagan-draca 2024. 9. 26. 22:36

데이터 테이블 로드 :

 

기획 단계에서 만든 게임 테이블들을 서버 메모리에 로드하는 작업으로

'파일 시스템'을 사용하여 서버에서 필요한 데이터 테이블을 메모리에 로드할 수 있다.

 

1. fs(모듈) 파일 시스템이란?

-  Node.js의 fs(파일 시스템) 모듈은 모 파일 시스템에 접근하고,

   파일을 읽고 쓰는 기능을 제공합니다.

 

- 동기적, 비동기적 방식 모두로 파일 I/O  작업을 수행할 수 있습니다.

 

- 파일 생성, 읽기, 쓰기, 삭제, 수정 등의 작업을 할 수 있습니다.

 

- 다양한 형태의 파일 기반 작업을 가능하게 합니다.

 

 

데이터 테이블을 '로드'하기 이전 관리하는 방법

Data Table이 필요한 이유는 Client가 Stage 정보를 어느 정도 알고 있어야

서버에 요청(Request)을 보낼 수 있고, 요청을 받은 Server는 검증을 할 수 있기 때문입니다.

 

- 게임의 핵심이라고 할 수 있는 데이터 테이블의 관리 방법은 여러가지가 있습니다.

- DB, CDN, file 등으로 테이블을 관리합니다.

 

- 이 프로젝트에서는 file로 테이블을 관리하게 됩니다.

   (file로 관리하는 것이 가장 간편하다)

 

1) Data Table을 File로 관리 방식

그럼 Data Table을 File로 관리한다면, 클라이언트들은 1.0.1 버전과

함께 빌드가 된 클라이언트 유저가 가지고 있게 됩니다.

 

그리고, 서버는 1.0.1 버전의 파일들(정보)을 가지고 있는 것 입니다.

 

그래서, 클라이언트가 요청을 보낼 때 요청하는 목적과 Version을 함께 보내

서버가 검증을 할 수 있도록 만들어줘야 합니다.

 

2) Data Table을 DataBase로 관리 방식

File로 관리하는 방식과 유사하지만, 서버는 1.0.1 버전의

파일들(정보들)을 DB에 Data화 시켜 저장해 놓습니다.

 

만약, 기획팀에서 어떠한 파일들이 생성하면 기획팀에서는

DB에 파일들을 업로드 하게 됩니다.

 

클라이언트가 서버에 요청을 보내면 해당 요청을 처리하기 위해 DB에 접근해

파일들을 읽어오고, 유저의 요청을 처리하게 됩니다.

 

3) Data Table을 CDN로 관리 방식

1.0.1 버전의 파일들을 CDN에 저장해놓고,

 

클라이언트들이 CDN에 요청을 보내고

 

서버도 무언가를 처리하기 전 CDN에 접근해

정보를 읽고 처리합니다.

 

4) 복합적 방식 사용

 

위와 같이 하나만 사용하는 것이 아닌 복합적으로 사용하는 경우도 존재 합니다.

 

왜냐하면 CDN, DB, 서버가 가지고 있는 파일이 다를 수 있기 때문입니다.

 

ex)

 

서버가 가지고 있을 Data Table은 ID, Item, Monster 같이

핵심적인 파일들을 가질 것인데

 

클라이언트가 가질 Data Table은 ID, Item, Monster 뿐만 아니라

맵의 텍스쳐, 위치 정보 등등 서버에는 불 필요한 Data를 가지고,

서버에 요청을 보내게 될 것인데 이를 서버가 아닌

CDN이 처리할 수 있게 만들 수 있기 때문이다.

 

2. 본격적인 개발 시작

 

1) 기획 단계에 작성한 테이블들 .json 파일로 만들기

 

우리가 만든 item 테이블, stage 테이블, item_unlock 테이블을

최상위 폴더에 assets 폴더를 만들고 각각의 .json 파일로 만들어줍니다.

( 게임 데이터들은 소스코드가 아니기에 ‘src’ 폴더 안에 들어 가지않습니다. )

 

item.json에는 테이블을 바탕으로 작성한

{
  "name": "item",
  "version": "1.0.0",
  "data": [
    {
      "id": 1,
      "score": 10
    },
    {
      "id": 2,
      "score": 20
    },
    {
      "id": 3,
      "score": 30
    },
    {
      "id": 4,
      "score": 40
    },
    {
      "id": 5,
      "score": 50
    },
    {
      "id": 6,
      "score": 60
    }
  ]
}

 

이름이 item이고, version이 1.0.0 이면서

data로는 item를 구별하기 위한 id와 score가 있는 것을 볼 수 있고,

 

stage.json은

{
  "name": "stage",
  "version": "1.0.0",
  "data": [
    {
      "id": 1000,
      "score": 0
    },
    {
      "id": 1001,
      "score": 100
    },
    {
      "id": 1002,
      "score": 200
    },
    {
      "id": 1003,
      "score": 300
    },
    {
      "id": 1004,
      "score": 400
    },
    {
      "id": 1005,
      "score": 500
    },
    {
      "id": 1006,
      "score": 600
    }
  ]
}

stage를 구별하기 위한 id와 해당 스테이지 이동 충족 조건인 score가 존재합니다.

 

item_unlock.json은

{
  "name": "item_unlock",
  "version": "1.0.0",
  "data": [
    {
      "id": 101,
      "stage_id": 1001,
      "item_id": 1
    },
    {
      "id": 201,
      "stage_id": 1002,
      "item_id": 2
    }
  ]
}

해금 조건을 구별하기 위한 id와

어느 스테이지에서 해금을 할 것인지(stage_id),

어떤 아이템을 해금 시킬 것인지(item_id)로

표현 돼 있습니다.

 

우리는 저 파일들이 서버가 시작될 때

서버가 읽어올 수 있게 만들어줘야 합니다.

 

2) 서버 실행시 바로 .json 파일들 읽어오기

 

서버가 시동되면 다음 행동으로 바로 테이블들을 메모리에 로드해야하기 때문에

같은 성격인 init 폴더 안에서 관리해주겠습니다.

 

init/assets.js 파일 만들기

 

 

assets.js 파일 내부에는 

let gameAssets = {};

export const getGameAssets = () => {
  return gameAssets;
};

 

gameAssets 객체를 전역변수로 선언하고,

게임에셋에 접근할 수 있도록 get 함수도

같이 생성해줍니다.

 

그리고, 우리는 파일을 읽어올 수 있어야 하기 때문에

import fs from 'fs';
// 파일 시스템을 읽어오기 위해 사용

fs 패키지를 import 해줍니다. 또한,

 

import path from 'path'
// 우리가 만든 assets 폴더 아래 파일들을 읽어오기 위해
// Node.js에서 지원해주는 path를 import 해줍니다.

우리가 만든 assets 폴더 아래 파일들을 읽어오기 위해

Node.js에서 지원해주는 path 패키지를 import 해줍니다.

 

fs와 path를 사용해 파일을 읽어주기 위한 함수인

// 파일 읽는 함수
// 비동기 병렬 처리로 파일을 읽는다.
const readFileAsync = ()=>{

}

을 만들어줍니다.

 

비동기 병렬 처리를 사용해 파일들을 동시에 읽어오는 방식으로

3개의 파일을 동시에 읽어올 것 입니다.

 

 

 

그 결과 3개의 파일 중 가장 소요 시간이 긴 파일의 시간에 모든 파일을 읽어 올 수 있게 됩니다.

 

위와 같이 비동기 병렬 처리로 먼저 읽기가 끝난 파일이 다른 파일들을 기다리게 하기 위해

// 파일 읽는 함수
const readFileAsync = ()=>{
    return new Promise((resolve, reject)=>{
        // 3개의 파일을 한 번에 읽을 경우
        // 파일 읽기에 소요 시간이 다른데
        // Promise로 소요 시간이 가징 긴 파일의 시간까지
        // 다른 파일들을 기다리게 해줄 예정입니다. (Promise.all).
    })
}

 

new Promise((resolve, reject)=>{

 

})

객체를 사용해줍니다.

 

        fs.readFile(파일 경로,옵션, 콜백 함수()=>{})
        //readFile로 파일을 읽어온다.
        //매개변수로은 파일 경로, 옵션, 콜백 함수가 필요하다

 

파일 경로를 읽기 위해 

 

fs.readFile 메서드를 사용할 예정입니다.

 

readFile 메서드의 매개변수로는 

파일 경로, 옵션 , 콜백 함수가 있습니다.

 

현재까지 함수 내부 코드 : 

// 파일 읽는 함수
const readFileAsync = ()=>{
    return new Promise((resolve, reject)=>{
        // 3개의 파일을 한 번에 읽을 경우
        // 파일 읽기에 소요 시간이 다른데
        // Promise로 소요 시간이 가징 긴 파일의 시간까지
        // 다른 파일들을 기다리게 해줄 예정입니다. (Promise.all).
        fs.readFile(파일 경로,옵션, 콜백 함수()=>{})
        //readFile로 파일을 읽어온다.
        //매개변수로은 파일 경로, 옵션, 콜백 함수가 필요하다

       
        //이때 파일 경로를 정하기 위해서는 현재 파일(assets.js) 파일의 경로로 부터
        //찾고자 하는 파일의 위치를 찾아줘야 합니다.
        // 그래서 import {fileURLToPath} from 'url' 해줍니다.
    })
}

 

이때, 파일 경로를 정하기 위해서는 현재 파일(assets.js) 파일의

경로로부터 찾고자 하는 파일의 위치를 찾아줘야 합니다.

 

그래서 import { fileURLToPath } from 'url' 해줍니다.

 

현재까지 import 한 패키지 : 

import fs from 'fs';
// 파일 시스템을 읽어오기 위해 사용
import path from 'path';
// 우리가 만든 assets 폴더 아래 파일들을 읽어오기 위해
// Node.js에서 지원해주는 path를 import 해줍니다.
import { fileURLToPath } from 'url'
// Node.js에서 지원해주는 url 패키지

 

그리고 전역 변수로

const __filename = fileURLToPath(import.meta.url)
// import.meta.url은 현재 파일(assets.js)이 실행되고 있는 절대 경로를 나타냅니다.
// 형태는 file://C:/Users/~~~~/~~~/assets.js 형식을 출력 됩니다.
// fileURLToPath 함수로 C:/Users/~~~/~~~/assets.js로 만들어줍니다.

__filename을 만들고 fileURLToPath(import.meta.url) 해줍니다.

 

import.meta.url은 현재 파일(assets.js)이 실행되고 있는 절대 경로를

file://C/users/~~~/~~~/assets.js 형식으로 출력해줍니다.

 

이 형식의 url을 일반적인 url로 만들기 위해 

fileURLToPath함수를 사용해줍니다.

 

그 후 /assets를 버린 현재 파일를 찾아줍니다.

const __dirname = path.dirname(__filename)
// .js를 버린 디렉토리를 찾는다.

 

../assets로 상위 폴더를 2번 이동한 위치에 있는 assets 폴더에 들어갑니다.

const basePath = path.join(__dirname, "../../assets")
// 상위 폴더를 2번 이동한 위치에 있는 assets 폴더에 들어갑니다.

 

 

assets 폴드 안에 있는 파일을 가져오기 위해

const readFileAsync = ()=>{

readFileAsync 함수에 매개변수로   fileName을 주겠습니다.

const readFileAsync = (fileName)=>{

 

        fs.readFile()

그리고 fs.readFile의 매개변수들에 인자를 넣기 시작하겠습니다.

 

        fs.readFile(path.join(basePath,fileName),'utf8', ()=>{})

path.join(basePath, fileName)

으로 

 

basePath에 있는 assets 폴더 위치와 fileName의 매개변수로 파일 이름을 붙여줍니다.

 

options은 'utf8' 형식으로 줘 사람이 읽을 수 있는 파일 형식으로 해줍니다.

    fs.readFile(path.join(basePath, fileName), 'utf8', (err, data) => {
      if (err) {
        reject(err);
        return;
      }
      resolve(JSON.parse(data));
    });
    //readFile로 파일을 읽어온다.
    //매개변수로은 파일 경로, 옵션, 콜백 함수가 필요하다

    // fs.readFile(path.join(basePath, fileName),'utf8', ()=>{})
    // 경로는 basePath에 있는 assets 폴더 위치와 fileName의 매개변수로 파일 이름을 붙여줍니다.

    // options은 'utf8' 형식으로 줘 사람이 읽을 수 있는 파일 형식으로 해줍니다.

    // 콜백 함수로는 error가 날 경우 reject(err)와 return으로 함수를 거절하고
    // 정상 수행의 경우 resolve(JSON.parse(data)) 해줍니다.
    // JSON.parse로 data를 다시 JSON 형식으로 만들어줍니다.

 

함수 전체코드 : 

 

// 파일 읽는 함수
const readFileAsync = (fileName) => {
  return new Promise((resolve, reject) => {
    // 3개의 파일을 한 번에 읽을 경우
    // 파일 읽기에 소요 시간이 다른데
    // Promise로 소요 시간이 가징 긴 파일의 시간까지
    // 다른 파일들을 기다리게 해줄 예정입니다. (Promise.all).

    fs.readFile(path.join(basePath, fileName), 'utf8', (err, data) => {
      if (err) {
        reject(err);
        return;
      }
      resolve(JSON.parse(data));
    });
    //readFile로 파일을 읽어온다.
    //매개변수로은 파일 경로, 옵션, 콜백 함수가 필요하다

    // fs.readFile(path.join(basePath, fileName),'utf8', ()=>{})
    // 경로는 basePath에 있는 assets 폴더 위치와 fileName의 매개변수로 파일 이름을 붙여줍니다.

    // options은 'utf8' 형식으로 줘 사람이 읽을 수 있는 파일 형식으로 해줍니다.

    // 콜백 함수로는 error가 날 경우 reject(err)와 return으로 함수를 거절하고
    // 정상 수행의 경우 resolve(JSON.parse(data)) 해줍니다.
    // JSON.parse로 data를 다시 JSON 형식으로 만들어줍니다.

    //이때 파일 경로를 정하기 위해서는 현재 파일(assets.js) 파일의 경로로 부터
    //찾고자 하는 파일의 위치를 찾아줘야 합니다.
    // 그래서 import {fileURLToPath} from 'url' 해줍니다.
  });
};

 

우리가 현재 만든 함수는 파일 하나를 읽는 함수 입니다.

 

병렬로 읽어오기 위한 함수를 만들어보겠습니다.

 

// Promise.all()을 사용한
// 비동기 병렬 처리
const loadGameAssets = async () => {
  try {
    const [stages, items, itemsUnlocks] = await Promise.all([
      readFileAsync('stage.json'),
      readFileAsync('item.json'),
      readFileAsync('item_unlock.json'),
    ]);
    // readFileAsync로 assets 폴더 안 .json파일
    // 을 읽어오고, 배열에 값을 저장해줍니다.
    // 파일 이름을 따로 관리하는 방식으로
    // 하드 코딩을 안 하는 방법도 있다.

    gameAssets = { stages, items, itemsUnlocks };
    // 비동기 병렬 처리가 끝났으면 객체에 담아줍니다.
    return gameAssets;
  } catch (error) {
    throw new Error('Failed to load game assets :' + error.message);
  }
  // 파일을 동시에 읽는 중 하나의 파일에서 오류가 발생할 경우를 대비해
  // try-catch 문을 사용해줍니다.
};

 

위와 같은 과정을 거쳐 src/init/assets.js의 파일에서

assets의 여러 .json 파일들을 읽어 올 수 있게 만들어주었습니다.

 

전체 소스 코드 : 

import fs from 'fs';
// 파일 시스템을 읽어오기 위해 사용
import path, { join } from 'path';
// 우리가 만든 assets 폴더 아래 파일들을 읽어오기 위해
// Node.js에서 지원해주는 path를 import 해줍니다.
import { fileURLToPath } from 'url';
// Node.js에서 지원해주는 url 패키지
// fileURLToPath는 파일 형식의 url을
// 일반적인 url 형식으로 바꿔줍니다.

let gameAssets = {};

const __filename = fileURLToPath(import.meta.url);
// import.meta.url은 현재 파일(assets.js)이 실행되고 있는 절대 경로를 나타냅니다.
// 형태는 file://C:/Users/~~~~/~~~/assets.js 형식을 출력 됩니다.
// fileURLToPath 함수로 C:/Users/~~~/~~~/assets.js로 만들어줍니다.

const __dirname = path.dirname(__filename);
// /assets.js를 버린 디렉토리를 찾는다.
const basePath = path.join(__dirname, '../../assets');
// 상위 폴더를 2번 이동한 위치에 있는 assets 폴더에 들어갑니다.

// 파일 읽는 함수
// 비동기 별렬로 파일을 읽는다.
const readFileAsync = (fileName) => {
  return new Promise((resolve, reject) => {
    // 3개의 파일을 한 번에 읽을 경우
    // 파일 읽기에 소요 시간이 다른데
    // Promise로 소요 시간이 가징 긴 파일의 시간까지
    // 다른 파일들을 기다리게 해줄 예정입니다. (Promise.all).

    fs.readFile(path.join(basePath, fileName), 'utf8', (err, data) => {
      if (err) {
        reject(err);
        return;
      }
      resolve(JSON.parse(data));
    });
    //readFile로 파일을 읽어온다.
    //매개변수로은 파일 경로, 옵션, 콜백 함수가 필요하다

    // fs.readFile(path.join(basePath, fileName),'utf8', ()=>{})
    // 경로는 basePath에 있는 assets 폴더 위치와 fileName의 매개변수로 파일 이름을 붙여줍니다.

    // options은 'utf8' 형식으로 줘 사람이 읽을 수 있는 파일 형식으로 해줍니다.

    // 콜백 함수로는 error가 날 경우 reject(err)와 return으로 함수를 거절하고
    // 정상 수행의 경우 resolve(JSON.parse(data)) 해줍니다.
    // JSON.parse로 data를 다시 JSON 형식으로 만들어줍니다.

    //이때 파일 경로를 정하기 위해서는 현재 파일(assets.js) 파일의 경로로 부터
    //찾고자 하는 파일의 위치를 찾아줘야 합니다.
    // 그래서 import {fileURLToPath} from 'url' 해줍니다.
  });
};

// Promise.all()을 사용한
// 비동기 병렬 처리
const loadGameAssets = async () => {
  try {
    const [stages, items, itemsUnlocks] = await Promise.all([
      readFileAsync('stage.json'),
      readFileAsync('item.json'),
      readFileAsync('item_unlock.json'),
    ]);
    // readFileAsync로 assets 폴더 안 .json파일
    // 을 읽어오고, 배열에 값을 저장해줍니다.
    // 파일 이름을 따로 관리하는 방식으로
    // 하드 코딩을 안 하는 방법도 있다.

    gameAssets = { stages, items, itemsUnlocks };
    // 비동기 병렬 처리가 끝났으면 객체에 담아줍니다.
    return gameAssets;
  } catch (error) {
    throw new Error('Failed to load game assets :' + error.message);
  }
  // 파일을 동시에 읽는 중 하나의 파일에서 오류가 발생할 경우를 대비해
  // try-catch 문을 사용해줍니다.
};

const getGameAssets = () => {
  return gameAssets;
};

export { loadGameAssets, getGameAssets };

 

다시 app.js로 돌아와 서버가 실행될 경우

파일을 읽어올 수 있게 만들어줍시다.

 

import { loadGameAssets } from './init/assets.js';

우리가 만든 assets.js 파일의 loadGameAssets를 import하고

 

server.listen(PORT, async () => {
  console.log(PORT + '포트로 서버가 열렸습니다.');

  try {
    const assets = await loadGameAssets();
    console.log(assets);
    console.log('Assets loaded Successfull');
  } catch (error) {
    console.error('Failed to load game assets : ',e);
  }
  // assets의 병렬 처리에서 파일 읽기를 실패할 경우
  // try-catch 문으로 상위 함수(현재 여기)에 던져줬기 때문에
  // console.error()로 그냥 게임 assets들을 가져오는걸 실패했다
  // 문구로 띄워줍니다.
});

 

위와 같이 assets를 가져옵니다.

 

loadGameAssets가 비동기 함수이기 때문에

콜백함수 앞에 async를 붙여줍니다.

 

console.log로 읽은 파일이 잘 도착했는지 확인해줘야 합니다.