TypeScript 런타임 #

게임 서버에는 JavaScript 가상 머신(VM)이 내장되어 있어서 게임 프로젝트에서 사용자 지정 로직을 로드하고 실행할 때 사용할 수 있습니다. 이것은 GoLua와 함께 서버 코드를 작성할 때 지원되는 프로그래밍 언어로 추가됩니다.

이것은 클라이언트에서 실행을 원하지 않는 게임 코드를 실행하거나 클라이언트가 선택하지 않은 입력을 제공할 때 유용합니다. 이 Nakama 기능은 다른 시스템에서 람다 또는 클라우드 함수와 유사하다고 생각할 수 있습니다. 사용자가 게임을 플레이할 때마다 보상을 부여하려고 할 때 유용하게 사용할 수 있습니다.

TypeScript는 JavaScript 언어의 상위 집합입니다. 버그와 예상치 못한 런타임 행위를 줄이는데 도움이 되는 유형으로 코드를 작성할 수 있습니다. Nakama에서는 JavaScript가 지원되어 코드에서 TypeScript를 직접 사용할 수 있으며, JavaScript 코드 개발 시 사용하는 것이 좋습니다.

TypeScript에서 JavaScript 코드를 작성하는 방법에 대해서는 공식 문서를 참조하십시오.

필수 조건 #

프로젝트에서 TypeScript를 사용하려면 해당 도구를 설치해야 합니다.

  • Node v14 (액티브 LTS) 이상.
  • 기본 UNIX 도구 또는 Windows 장치에 대한 지식.

TypeScript 컴파일러와 다른 종속성은 NPM을 가져옵니다.

프로젝트 초기화 #

이 단계에서는 워크스페이스를 설정하여 모든 프로젝트 코드가 게임 서버에서 실행될 수 있도록 작성합니다.

프로젝트에서 워크스페이스로 사용하는 폴더 이름을 정의합니다. 이 케이스에서는 “ts-project"를 사용합니다.

1
2
mkdir -p ts-project/{src,build}
cd ts-project

NPM을 사용하여 프로젝트에서 노드 종속성을 설정합니다. TypeScript 컴파일러를 설치합니다.

1
2
npm init -y
npm install --save-dev typescript

프로젝트에 설치된 TypeScript 컴파일러를 사용하여 컴파일러 옵션을 설정합니다.

1
npx tsc --init

“tsconfig.json” 파일로 TypeScript 컴파일러에서 실행되는 사용 가능한 옵션을 설명합니다. 주석으로 처리된 항목을 제거하고 업데이트하면 최소화된 파일은 다음과 같이 표시됩니다:

1
2
3
4
5
6
7
8
9
{
  "compilerOptions": {
    "target": "es5",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

해당 구성 옵션을 "compilerOptions" 블록에 추가합니다:

1
"outFile": "./build/index.js",
롤업을 통한 TypeScript 번들링에서 Nakama에서 TypeScript 컴파일러에 의존하지 않고 TypeScript 코드를 다른 노드 모듈과 번들하는 방법의 예시를 살펴봅니다.

Nakama 런타임 유형을 프로젝트 종속성으로 추가하고 유형을 찾기 위해서 컴파일러를 구성합니다.

1
npm i 'https://github.com/heroiclabs/nakama-common'

해당 구성 옵션을 “tsconfig.json” 파일의 "compilerOptions" 블록으로 추가합니다:

1
2
3
"typeRoots": [
  "./node_modules"
],

이렇게 하면 설정이 완료되고 프로젝트는 이 레이아웃과 유사하게 표시됩니다:

1
2
3
4
5
6
7
8
9
.
├── build
├── node_modules
│   ├── nakama-runtime
│   └── typescript
├── package-lock.json
├── package.json
├── src
└── tsconfig.json

개발 코드 #

간단한 코드를 작성하여 게임 서버에서 실행할 수 있도록 JavaScript로 컴파일합니다.

모든 코드는 게임 서버 시작 시 글로벌 범위에서 찾는 함수에서 실행을 시작해야 합니다. 이 함수는 "InitModule"으(로) 불리며 RPC, 사전/사후 후크를 등록하는 방법이고, 다른 이벤트 함수는 서버에 의해서 관리됩니다.

아래의 코드는 "Logger"을(를) 사용하여 메시지를 작성하는 Hello World에 대한 간단한 예시입니다. “src” 폴더에서 소스 파일의 이름을 “main.ts"로 지정합니다. 즐겨찾기 편집기 또는 IDE에 작성할 수 있습니다.

1
2
3
4
let InitModule: nkruntime.InitModule =
        function(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
    logger.info("Hello World!");
}

이제 컴파일러 옵션에 파일을 추가하고 TypeScript 컴파일러를 실행할 수 있습니다.

1
2
3
4
5
6
7
8
{
  "files": [
    "./src/main.ts"
  ],
  "compilerOptions": {
    // ... etc
  }
}

코드베이스를 컴파일하려면 다음과 같이 합니다:

1
npx tsc

제한 사항 #

호환성 #

JavaScript 런타임은 현재 JavaScript ES5 사양을 지원하는 goja VM으로 제공됩니다. JavaScript 런타임은 ES5 사양에 포함되는 표준 라이브러리 함수에 대한 액세스를 제공합니다.

노드, 웹/브라우저 API, 네이티브 지원(예: 노드 활용)이 필요한 라이브러리를 지원하지 않습니다.

Go 런타임에서 TypeScript 함수를 호출하거나 TypeScript 런타임에서 Go 함수를 호출할 수 없습니다.

글로벌 상태 #

JavaScript 런타임 코드는 인스턴스된 컨텍스트(VM 풀)에서 실행됩니다. 글로벌 변수를 사용하여 메모리에 상태를 저장하거나 다른 JS 처리기 또는 함수 호출과 소통할 수 없습니다.

단일 스레드 #

JavaScript 런타임에서는 다중 스레드 처리가 지원되지 않습니다.

샌드박싱 #

JavaScript 런타임 코드는 전체 샌드박스로 적용되어 파일 시스템, 입력/출력 장치에 액세스할 수 없고, OS 스레드나 프로세스를 생성할 수 없습니다.

이렇게 하면 서버 내에서 JS 모듈로 인해 심각한 오류가 발생하지 않습니다. 런타임 코드는 예상치 못한 클라이언트 연결 해제를 발생시키거나 메인 서버 프로세스에 영향을 미치지 않습니다.

프로젝트 실행 #

Docker 사용 #

서버를 로컬로 실행할 수 있는 가장 쉬운 방법은 Docker를 사용하는 것입니다.

이렇게 하려면 Dockerfile(이)라는 파일을 생성합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
FROM node:alpine AS node-builder

WORKDIR /backend

COPY package*.json .
RUN npm install

COPY tsconfig.json .
COPY main.ts .
RUN npx tsc

FROM registry.heroiclabs.com/heroiclabs/nakama:3.13.1

COPY --from=node-builder /backend/build/*.js /nakama/data/modules/build/
COPY local.yml .

다음으로, docker-compose.yml 파일을 생성합니다. 자세한 내용은 Docker Compose로 Nakama 설치 문서를 참조하십시오.

 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
version: '3'
services:
  postgres:
    command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all
    environment:
      - POSTGRES_DB=nakama
      - POSTGRES_PASSWORD=localdb
    expose:
      - "8080"
      - "5432"
    image: postgres:12.2-alpine
    ports:
      - "5432:5432"
      - "8080:8080"
    volumes:
      - data:/var/lib/postgresql/data

  nakama:
    build: .
    depends_on:
      - postgres
    entrypoint:
      - "/bin/sh"
      - "-ecx"
      - >
        /nakama/nakama migrate up --database.address postgres:localdb@postgres:5432/nakama &&
        exec /nakama/nakama --config /nakama/data/local.yml --database.address postgres:localdb@postgres:5432/nakama        
    expose:
      - "7349"
      - "7350"
      - "7351"
    healthcheck:
      test: ["CMD", "/nakama/nakama", "healthcheck"]
      interval: 10s
      timeout: 5s
      retries: 5
    links:
      - "postgres:db"
    ports:
      - "7349:7349"
      - "7350:7350"
      - "7351:7351"
    restart: unless-stopped

volumes:
  data:

이제 명령을 사용하여 서버를 실행합니다.

1
docker compose up

Docker 사용하지 않음 #

Linux, Windows, macOS에 대한 Nakama 바이너리 스택을 설치합니다. 이 작업을 완료하고 나면 게임 서버를 실행하여 코드를 로드할 수 있습니다.

1
nakama --logger.level DEBUG --runtime.js_entrypoint "build/index.js"

위의 명령을 실행하기 전에 터미널에서 npx tsc을(를) 실행하여 build/index.js 파일을 만들어야 합니다.

서버가 실행 중인지 확인하기 #

서버 로그는 위에서 작성한 코드가 시작 시 로드되고 실행되었는지 나타내는 출력이나 유사한 항목으로 표시됩니다.

1
{"level":"info","ts":"...","msg":"Hello World!","caller":"server/runtime_javascript_logger.go:54"}

롤업을 통한 번들링 #

위 설정은 TypeScript 컴파일러에만 의존합니다. 이렇게 하면 도구 체인과 작업 흐름을 간단하게 유지할 수 있지만, TypeScript 코드를 추가적인 노드 모듈과 번들할 수 있는 기능을 제한합니다.

롤업은 Nakama 내에서 Node.js 런타임에 의존하지 않는 노드 모듈을 번들할 때 사용할 수 있는 옵션 중 하나입니다.

롤업 구성하기 #

TypeScript 프로젝트에서 롤업을 사용하도록 구성하는 경우, 위 단계에 따라 프로젝트에 추가 또는 수정해야 하는 몇 가지 단계가 있습니다.

첫 번째로 서버 런타임 코드에서 롤업을 실행할 수 있도록 추가적인 종속성을 설치하는 것입니다. 여기에는 Babel, 롤업, 여러 플러그인/프리셋 및 tslib이(가) 포함됩니다.

이렇게 하려면 종속성을 설치하는 아래의 명령을 터미널에서 실행하고 아래의 명령을 개발 종속성으로 package.json 파일에 추가합니다:

1
npm i -D @babel/core @babel/plugin-external-helpers @babel/preset-env @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-json @rollup/plugin-node-resolve @rollup/plugin-typescript rollup tslib

롤업을 프로젝트에서 개발 종속성으로 설치한 후, 이제 tsc 명령 대신에 rollup -c 명령을 실행하기 위해 package.json에서 build 스크립트를 수정해야 합니다. 빌드 파일을 내보내지 않고도 TypeScript 컴파일을 검증할 수 있도록 type-check 스크립트를 추가해야 합니다.

package.json

1
2
3
4
5
6
7
8
{
  ...
  "scripts": {
    "build": "rollup -c",
    "type-check": "tsc --noEmit"
  },
  ...
}

다음으로, 프로젝트에 아래의 rollup.config.js 파일을 추가해야 합니다.

rollup.config.js

 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
27
28
29
30
31
32
33
34
import resolve from '@rollup/plugin-node-resolve';
import commonJS from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import babel from '@rollup/plugin-babel';
import typescript from '@rollup/plugin-typescript';
import pkg from './package.json';

const extensions = ['.mjs', '.js', '.ts', '.json'];

export default {
  input: './src/main.ts',
  external: ['nakama-runtime'],
  plugins: [
    // Allows node_modules resolution
    resolve({ extensions }),

    // Compile TypeScript
    typescript(),

    json(),

    // Resolve CommonJS modules
    commonJS({ extensions }),

    // Transpile to ES5
    babel({
      extensions,
      babelHelpers: 'bundled',
    }),
  ],
  output: {
    file: 'build/index.js',
  },
};

이어서, 프로젝트에 babel.config.json 파일을 추가합니다.

babel.config.json

1
2
3
4
5
6
{
  "presets": [
    "@babel/env"
  ],
  "plugins": []
}

tsconfig.json 파일에 적용해야 할 변경 사항도 있습니다. 롤업을 사용하면 빌드 프로세스를 간소화하기 때문에 프로젝트에 새로운 *.ts 파일이 추가될 때마다 tsconfig.json 파일을 수동으로 업데이트하지 않아도 됩니다. 기존 tsconfig.json 파일의 컨텐츠를 아래의 예시로 대체합니다.

tsconfig.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "compilerOptions": {
    "noImplicitReturns": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "noUnusedLocals": true,
    "removeComments": true,
    "target": "es5",
    "module": "ESNext",
    "strict": false,
  },
  "files": [
    "./node_modules/nakama-runtime/index.d.ts",
  ],
  "include": [
    "src/**/*",
  ],
  "exclude": [
    "node_modules",
    "build"
  ]
}

다음으로, InitModule 함수를 참조하는 main.ts 파일 아래에 라인을 포함해야 합니다. 빌드 시 롤업에서 누락되지 않도록 하기 위한 것입니다.

main.ts

1
2
3
4
5
6
function InitModule(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
  logger.info('TypeScript module loaded.');
}

// Reference InitModule to avoid it getting removed on build
!InitModule && InitModule.bind(null);

Nakama에서 local.yml(이)라고 하는 구성을 생성해야 합니다. runtime.js_entrypoint 설정은 빌드된 JavaScript 코드를 읽도록 Nakama에 표시됩니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
console:
  max_message_size_bytes: 409600
logger:
  level: "DEBUG"
runtime:
  js_entrypoint: "build/index.js"
session:
  token_expiry_sec: 7200 # 2 hours
socket:
  max_message_size_bytes: 4096 # reserved buffer
  max_request_size_bytes: 131072

마지막으로, Dockerfile의 일부를 수정하여 rollup.config.jsbabel.config.json의 파일을 복제합니다. TypeScript 컴파일러를 직접 사용하지 않고 업데이트된 명령을 실행할 수 있도록 RUN 명령을 변경합니다. Dockerfile의 컨텐츠를 아래의 예시로 대체합니다.

Dockerfile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
FROM node:alpine AS node-builder

WORKDIR /backend

COPY package*.json .
RUN npm install

COPY . .
RUN npm run build

FROM registry.heroiclabs.com/heroiclabs/nakama:3.13.1

COPY --from=node-builder /backend/build/*.js /nakama/data/modules/build/
COPY local.yml /nakama/data/

모듈을 로컬로 빌드하기 #

모든 종속성을 설치합니다:

1
npm i

TypeScript 컴파일이 성공할 수 있도록 유형 확인을 실시합니다:

1
npm run type-check

프로젝트 빌드:

1
npm run build

Docker로 모듈 실행 #

사용자 지정 서버 런타임 코드로 Nakama를 실행하려면, 다음을 실행합니다:

1
docker compose up

모듈을 변경하고 다시 실행하려면, 다음을 실행하면 됩니다:

1
docker compose up --build nakama

이렇게 하면 최근 변경 사항으로 이미지가 다시 빌드됩니다.

오류 처리 #

JavaScript는 예외를 사용하여 오류를 처리합니다. 오류가 발생하면 예외가 발생됩니다. 사용자 지정 함수나 런타임에서 제공되는 함수에서 예외를 처리하려면 코드를 try catch 블록으로 감싸야 합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function throws(): void {
    throw Error("I'm an exception");
}

try {
    throws();
} catch(error) {
    // Handle error.
    logger.error('Caught exception: %s', error.message);
}

JavaScript에서 처리되지 않은 예외는 초기화 단계에서 처리되지 않는 경우(시작 시 런타임이 InitModule 함수 호출)를 제외하고는 런타임에 의해 로그되고 이로 인해 서버가 중단됩니다. 이 패턴을 사용하여 오류 처리 및 검사에 대한 모든 런타임 API 호출을 적용하는 것이 좋습니다.

1
2
3
4
5
6
try {
    // Will throw an exception because this function expects a valid user ID.
    nk.accountsGetId([ 'invalid_id' ]);
} catch(error) {
    logger.error('An error has occurred: %s', error.message);
}

클라이언트로 오류 반환 #

자체적으로 사용자 지정 런타임 코드를 작성하는 경우, 요청을 처리하는 과정에서 발생하는 오류가 클라이언트로 적절하게 전달되는지 확인해야 합니다. 클라이언트로 반환되는 오류에 명확한 정보를 전달하는 오류 메시지와 정확한 HTTP 상태 코드가 포함되어야 한다는 의미입니다.

내부적으로 Nakama 런타임은 클라이언트로 오류를 반환할 때 gRPC 오류 코드를 사용하여 정확한 HTTP 상태 코드로 전환합니다.

Nakama TypeScript 런타임은 nkruntime.Codes의 열거형에서 오류 코드를 정의합니다. 이를 통해 사용자 지정 nkruntime.Error 개체를 정의할 수 있습니다. 다음은 모듈에서 정의할 수 있는 오류의 예시입니다.

1
2
3
4
5
6
7
8
9
const errBadInput: nkruntime.Error = {
  message: 'input contained invalid data',
  code: nkruntime.Codes.INVALID_ARGUMENT
};

const errGuildAlreadyExists: nkruntime.Error = {
  message: 'guild name is in use',
  code: nkruntime.Codes.ALREADY_EXISTS
};

아래는 RPC 호출과 사전 후크에서 정확한 오류를 반환하는 방법에 대한 예시입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const createGuildRpc: nkruntime.RpcFunction = (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string): string | void  => {
  // ... check if a guild already exists and set value of `alreadyExists` accordingly
  const alreadyExists = true;

  if (alreadyExists) {
    throw errGuildAlreadyExists;
  }

  return JSON.stringify({ success: true });
};

const beforeAuthenticateCustom: nkruntime.BeforeHookFunction<nkruntime.AuthenticateCustomRequest> = (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, data: nkruntime.AuthenticateCustomRequest): void | nkruntime.AuthenticateCustomRequest => {
  const pattern = new RegExp('^cid-([0-9]{6})$');

  if (!pattern.test(data.account.id)) {
    throw errBadInput;
  }
  
  return data;
}

업그레이드 #

현재 버전 식별하기 #

Nakama 서버를 업그레이드하려면 사용 중인 현재 버전을 먼저 식별해야 합니다. nakama-runtime(Nakama Common이라고도 함)의 version에서 package.json(정확한 커밋 해쉬를 제공하는 최신 설치를 사용하는 경우 package-lock.json)을(를) 참조하거나 Dockerfile 또는 이미지 이름(예: heroiclabs/nakama:3.22.0)의 마지막 부분에 태그된 버전을 참조할 수 있습니다. 후자의 경우, 현재 nakama-runtime 버전을 식별한 후에 호환성 매트릭스를 참조하여 사용 중인 Nakama 바이너리 버전을 식별할 수 있습니다.

변경 사항 식별 #

현재 Nakama 버전이 설치된 상태에서 서버-런타임 릴리스 노트를 참조하여 현재 버전에서 어떤 변경 사항이 적용되었는지 확인합니다. 이 방법을 통해 사용자 지정 서버 런타임 코드에 영향을 주는 변경 사항이나 위반 사항을 식별할 수 있습니다.

최신 버전 설치 #

업그레이드하고자 하는 Nakama 버전을 확인하고 프로젝트에서 nakama-runtime의 버전을 업데이트해야 합니다. 호환성 매트릭스를 참조하여 설치해야 하는 nakama-runtime 패키지의 버전을 식별할 수 있습니다.

다음과 같이 설치할 수 있습니다(<version>이(가) v1.23.0와(과) 같은 github 태그인 경우):

1
npm i https://github.com/heroiclabs/nakama-common#<version>

Nakama 바이너리 업그레이드 #

nakama-runtime 패키지 버전이 업그레이드된 상태에서 서버가 사용 중인 Nakama 바이너리 버전을 업그레이드해야 합니다.

바이너리를 직접 사용하고 있는 경우, Nakama GitHub 릴리스 페이지에서 적절한 버전을 직접 다운로드할 수 있습니다.

Docker를 사용 중인 경우, 최종 FROM 구문에서 정확한 버전을 지정하여 Dockerfile을(를) 업데이트해야 합니다:

1
FROM registry.heroiclabs.com/heroiclabs/nakama:3.13.1

공통적인 문제점 #

TypeError: 개체에 구성원이 없음

위와 같은 오류 메시지가 나타나는 경우, 서버가 실행 중인 Nakama 버전에서 사용할 수 없는 Nakama 함수를 사용하고 있을 확률이 높습니다. 이러한 문제는 TypeScript 프로젝트에 설치된 nakama-runtime 패키지의 이후 버전이 현재 사용중인 Nakama 바이너리 버전과 호환되지 않을 경우에 발생합니다. 호환성 매트릭스를 참조하여 Nakama 및 Nakama Common(nakama-runtime)과 호환되는 버전을 사용하고 있는지 확인합니다.

샌드박싱 및 제한 사항 #

TypeScript 서버 런타임은 Goja Go 패키지를 통해 샌드박스가 적용된 JavaScript VM으로 제공됩니다. 서버에서 실행되는 모든 TypeScript/JavaScript 서버 런타임 코드는 Nakama를 통해 노출되는 특정 기능에만 액세스할 수 있습니다.

TypeScript를 사용하여 서버 런타임 코드를 개발할 때 유의해야 할 몇 가지 주요 제한 사항이 있습니다:

  • 모든 코드는 ES5 규격의 JavaScript로 컴파일되어야 함
  • 코드는 파일 시스템을 포함하여 어떠한 방식으로도 OS와 상호 작용할 수 없음
  • 코드가 Node 환경에서 실행되고 있지 않기 때문에 NodeJS 기능(예: crypto, fs 등)에 의존하는 모든 모듈을 사용할 수 없습니다.

Goja 내에서 발생하는 특정 호환성 문제에 대해서는 Goja에서 인식되는 비호환성 및 경고를 참조하십시오.

글로벌 상태 #

TypeScript 런타임은 글로벌 변수를 사용하여 메모리에 상태를 저장할 수 없습니다.

로거 #

JavaScript 로거는 서버 로거에 적용되는 래퍼입니다. 예시를 통해 출력 문자열에서 “동사”(예: “%s”)의 형식을 지정하고 인수로 대체되는 것을 살펴보았습니다.

JavaScript VM에서 사용되는 기본 Go 구조를 더 쉽게 로그하고 검사하기 위해서 “%#v"와 같은 동사를 사용할 수 있습니다. 전체 참조는 여기를 참조하십시오.

다음 단계 #

다음의 Nakama 기능을 포함하는 Nakama 프로젝트 템플릿을 살펴보십시오:

Related Pages