njs와 함께 node 모듈 사용

대부분, 개발자는 일반적으로 일종의 라이브러리로 사용할 수 있는 타사 코드를 사용하려고 합니다. JavaScript 영역에서 모듈의 개념은 상대적으로 새롭고 따라서 최근까지 표준이 없었습니다. 많은 플랫폼(브라우저)이 아직 모듈을 지원하지 않아서 코드를 재사용하기가 더 어렵습니다. 이 문서에서는 njs에서 Node.js 코드를 재사용하는 방법을 설명합니다.

이 문서의 예는 njs 0.3.8에 표시된 기능을 사용합니다

타사 코드가 njs에 추가된 경우 발생할 수 있는 문제는 다음과 같습니다.

  • 여러 파일이 서로 참조하고 다른 파일의 종속성을 참조함
  • 플랫폼별 API
  • 최신 표준 언어 구성

그러한 문제가 njs에 새로운 또는 njs에만 해당하는 문제가 아니라는 점은 희소식입니다. JavaScript 개발자가 매우 다른 속성을 지닌 다른 여러 플랫폼을 지원하려 할 때 매일 부딪히는 문제입니다. 위에 언급한 문제를 해결하도록 설계된 방법이 있습니다.

  • 여러 파일이 서로 참조하고 다른 파일의 종속성을 참조함

이 문제는 상호 의존하는 모든 코드를 단일 파일로 병합하여 해결할 수 있습니다. browserify 또는 webpack과 같은 도구가 전체 프로젝트를 받아서 코드와 모든 종속성을 포함하는 단일 파일을 생성합니다.

  • 플랫폼별 API

플랫폼과 무관한 방식으로 그러한 API를 구현하는 여러 라이브러리를 사용할 수 있습니다(그러나 성능에 영향이 있음). 특정한 기능은 polyfill 접근 방식을 사용하여 구현할 수도 있습니다.

  • 최신 표준 언어 구성

그러한 코드는 변환 컴파일할 수 있습니다. 즉, 기존 표준에 따라 최신 언어 기능을 다시 작성하는 여러 변환을 수행한다는 의미입니다. 예를 들어, babel 프로젝트를 이러한 목적에 사용할 수 있습니다.

이 가이드에서는 상대적으로 큰 2개의 npm 호스팅 라이브러리를 사용합니다.

  • protobufjsgRPC 프로토콜이 사용하는 protobuf 메시지를 생성하고 구문 분석하는 라이브러리
  • dns-packet — DNS 프로토콜 패킷을 처리하는 라이브러리

환경

이 문서에서는 대부분 일반적인 접근 방식을 사용하고 Node.js 및 JavaScript에 관한 특정한 모범 사례 조언은 피합니다. 여기 제안된 단계를 따르기 전에 해당 패키지의 설명서를 참조하십시오.

먼저(Node.js가 설치되고 작동한다고 가정), 비어 있는 프로젝트를 생성하고 몇 가지 종속성을 설치합니다. 아래 명령은 작업 디렉터리에 있다고 가정합니다.

$ mkdir my_project && cd my_project
$ npx license choose_your_license_here > LICENSE
$ npx gitignore node

$ cat > package.json <<EOF
{
  "name":        "foobar",
  "version":     "0.0.1",
  "description": "",
  "main":        "index.js",
  "keywords":    [],
  "author":      "somename <some.email@example.com> (https://example.com)",
  "license":     "some_license_here",
  "private":     true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  }
}
EOF
$ npm init -y
$ npm install browserify

Protobufjs

라이브러리는 .proto 인터페이스 정의에 대한 구문 분석기와 메시지 구문 분석 및 생성을 위한 코드 생성기를 제공합니다.

이 예에서는 gRPC 예제의 helloworld.proto 파일을 사용합니다. 목표는 다음 2개 메시지를 생성하는 것입니다. HelloRequest 및 HelloResponse. njs는 보안 고려 사항으로 인해 동적으로 새 함수를 추가하는 것을 지원하지 않기 때문에 클래스를 동적으로 생성하는 대신 protobufjs의 정적 모드를 사용합니다.

다음으로, 라이브러리가 설치되고 메시지 마샬링을 구현하는 JavaScript 코드가 프로토콜 정의에서 생성됩니다.

$ npm install protobufjs
$ npx pbjs -t static-module helloworld.proto > static.js

따라서 static.js 파일이 새로운 종속성이 되어 메시지 처리를 구현하는 데 필요한 모든 코드를 저장합니다. set_buffer() 함수에는 라이브러리를 사용하여 연속된 HelloRequest 메시지와 함께 버퍼를 생성하는 코드가 포함됩니다. 코드는 code.js 파일에 상주합니다.

var pb = require('./static.js');

// Example usage of protobuf library: prepare a buffer to send
function set_buffer(pb)
{
    // set fields of gRPC payload
    var payload = { name: "TestString" };

    // create an object
    var message = pb.helloworld.HelloRequest.create(payload);

    // serialize object to buffer
    var buffer = pb.helloworld.HelloRequest.encode(message).finish();

    var n = buffer.length;

    var frame = new Uint8Array(5 + buffer.length);

    frame[0] = 0;                        // 'compressed' flag
    frame[1] = (n & 0xFF000000) >>> 24;  // length: uint32 in network byte order
    frame[2] = (n & 0x00FF0000) >>> 16;
    frame[3] = (n & 0x0000FF00) >>>  8;
    frame[4] = (n & 0x000000FF) >>>  0;

    frame.set(buffer, 5);

    return frame;
}

var frame = set_buffer(pb);

코드가 작동하려면 다음과 같이 노드를 사용하여 코드를 실행합니다.

$ node ./code.js
Uint8Array [
    0,   0,   0,   0,  12, 10,
   10,  84, 101, 115, 116, 83,
  116, 114, 105, 110, 103
]

이렇게 하면 적절하게 인코딩된 gRPC 프레임을 얻을 수 있습니다. 이제 njs로 실행하겠습니다.

$ njs ./code.js
Thrown:
Error: Cannot find module "./static.js"
    at require (native)
    at main (native)

모듈이 지원되지 않으므로 예외가 발생했습니다. 이 문제를 극복하기 위해 browserify 또는 다른 유사한 도구를 사용하겠습니다.

기존 code.js 파일을 처리하려고 하면 브라우저에서, 즉 로딩 즉시 실행되어야 하는 여러 JS 코드가 생성됩니다. 실제로 원하는 결과가 아닙니다. 대신, 내보낸 함수가 nginx 구성에서 참조될 수 있어야 합니다.. 이에 따라 일부 래퍼 코드가 필요합니다.

이 가이드에서는 단순하게 하기 위해 모든 예에서 njs cli를 사용합니다. 실제로는 nginx njs 모듈을 사용하여 코드를 실행합니다.

load.js 파일에는 전역 네임스페이스에 핸들을 저장하는 라이브러리 로딩 코드가 포함됩니다.

global.hello = require('./static.js');

이 코드가 병합된 콘텐츠로 교체됩니다. 현재 코드는 “global.hello” 핸들을 사용하여 라이브러리에 액세스합니다.

다음으로, browserify로 처리하여 모든 종속성을 단일 파일로 가져옵니다.

$ npx browserify load.js -o bundle.js -d

그 결과 모든 종속성을 포함하는 거대한 파일이 됩니다.

(function(){function......
...
...
},{"protobufjs/minimal":9}]},{},[1])
//# sourceMappingURL..............

최종 “njs_bundle.js” 파일을 얻으려면 “bundle.js” 및 다음 코드를 연결합니다.

// Example usage of protobuf library: prepare a buffer to send
function set_buffer(pb)
{
    // set fields of gRPC payload
    var payload = { name: "TestString" };

    // create an object
    var message = pb.helloworld.HelloRequest.create(payload);

    // serialize object to buffer
    var buffer = pb.helloworld.HelloRequest.encode(message).finish();

    var n = buffer.length;

    var frame = new Uint8Array(5 + buffer.length);

    frame[0] = 0;                        // 'compressed' flag
    frame[1] = (n & 0xFF000000) >>> 24;  // length: uint32 in network byte order
    frame[2] = (n & 0x00FF0000) >>> 16;
    frame[3] = (n & 0x0000FF00) >>>  8;
    frame[4] = (n & 0x000000FF) >>>  0;

    frame.set(buffer, 5);

    return frame;
}

// functions to be called from outside
function setbuf()
{
    return set_buffer(global.hello);
}

// call the code
var frame = setbuf();
console.log(frame);

제대로 작동하는지 확인하기 위해 노드를 사용하여 파일을 실행하겠습니다.

$ node ./njs_bundle.js
Uint8Array [
    0,   0,   0,   0,  12, 10,
   10,  84, 101, 115, 116, 83,
  116, 114, 105, 110, 103
]

이제 njs를 사용하여 추가로 실행하겠습니다.

$ njs ./njs_bundle.js
Uint8Array [0,0,0,0,12,10,10,84,101,115,116,83,116,114,105,110,103]

마지막으로 njs별 API를 사용하여 어레이를 바이트 스트링으로 변환하므로 nginx 모듈에서 사용할 수 있습니다. return frame; } 행 앞에 다음 코드 조각을 추가할 수 있습니다.

if (global.njs) {
    return String.bytesFrom(frame)
}

최종적으로 다음과 같이 작동합니다.

$ njs ./njs_bundle.js |hexdump -C
00000000  00 00 00 00 0c 0a 0a 54  65 73 74 53 74 72 69 6e  |.......TestStrin|
00000010  67 0a                                             |g.|
00000012

이것은 의도한 결과입니다. 응답 구문 분석은 유사하게 구현할 수 있습니다.

function parse_msg(pb, msg)
{
    // convert byte string into integer array
    var bytes = msg.split('').map(v=>v.charCodeAt(0));

    if (bytes.length < 5) {
        throw 'message too short';
    }

    // first 5 bytes is gRPC frame (compression + length)
    var head = bytes.splice(0, 5);

    // ensure we have proper message length
    var len = (head[1] << 24)
              + (head[2] << 16)
              + (head[3] << 8)
              + head[4];

    if (len != bytes.length) {
        throw 'header length mismatch';
    }

    // invoke protobufjs to decode message
    var response = pb.helloworld.HelloReply.decode(bytes);

    console.log('Reply is:' + response.message);
}

DNS-패킷

이 예에서는 DNS 패킷의 생성 및 구문 분석을 위한 라이브러리를 사용합니다. 라이브러리와 라이브러리의 종속성은 아직 njs에서 지원하지 않는 최신 언어 구성을 사용하기 때문에 고려할 만한 사례입니다. 결국 추가 단계, 즉 소스 코드를 변환 컴파일하는 작업이 필요합니다.

다음 추가 노드 패키지가 필요합니다.

$ npm install @babel/core @babel/cli @babel/preset-env babel-loader
$ npm install webpack webpack-cli
$ npm install buffer
$ npm install dns-packet

구성 파일, webpack.config.js:

const path = require('path');

module.exports = {
    entry: './load.js',
    mode: 'production',
    output: {
        filename: 'wp_out.js',
        path: path.resolve(__dirname, 'dist'),
    },
    optimization: {
        minimize: false
    },
    node: {
        global: true,
    },
    module : {
        rules: [{
            test: /\.m?js$$/,
            exclude: /(bower_components)/,
            use: {
                loader: 'babel-loader',
                options: {
                    presets: ['@babel/preset-env']
                }
            }
        }]
    }
};

“production” 모드를 사용 중입니다. 이 모드에서 webpack은 njs에서 지원하지 않는 “eval” 구성을 사용하지 않습니다. 참조된 load.js 파일이 진입점입니다.

global.dns = require('dns-packet')
global.Buffer = require('buffer/').Buffer

라이브러리에 대한 단일 파일을 생성하여 동일한 방식으로 시작합니다.

$ npx browserify load.js -o bundle.js -d

다음으로 webpack으로 파일을 처리하고 이 자체가 babel을 호출합니다.

$ npx webpack --config webpack.config.js

이 명령은 dist/wp_out.js 파일을 생성합니다. 이 파일이 bundle.js의 변환 컴파일된 버전입니다. 이것을 코드를 저장하는 code.js와 연결해야 합니다.

function set_buffer(dnsPacket)
{
    // create DNS packet bytes
    var buf = dnsPacket.encode({
        type: 'query',
        id: 1,
        flags: dnsPacket.RECURSION_DESIRED,
        questions: [{
            type: 'A',
            name: 'google.com'
        }]
    })

    return buf;
}

이 예에서 생성된 코드는 함수로 래핑되지 않고 이를 명시적으로 호출할 필요는 없습니다. “dist” 디렉터리가 결과입니다:

$ cat dist/wp_out.js code.js > njs_dns_bundle.js

이제 파일 끝부분에서 코드를 호출하겠습니다.

var b = set_buffer(global.dns);
console.log(b);

그리고 노드를 사용하여 실행합니다.

$ node ./njs_dns_bundle_final.js
Buffer [Uint8Array] [
    0,   1,   1, 0,  0,   1,   0,   0,
    0,   0,   0, 0,  6, 103, 111, 111,
  103, 108, 101, 3, 99, 111, 109,   0,
    0,   1,   0, 1
]

예상대로 작동하는지 확인한 다음 njs로 실행합니다.

$ njs ./njs_dns_bundle_final.js
Uint8Array [0,1,1,0,0,1,0,0,0,0,0,0,6,103,111,111,103,108,101,3,99,111,109,0,0,1,0,1]

응답은 다음과 같이 구문 분석할 수 있습니다.

function parse_response(buf)
{
    var bytes = buf.split('').map(v=>v.charCodeAt(0));

    var b = global.Buffer.from(bytes);

    var packet = dnsPacket.decode(b);

    var resolved_name = packet.answers[0].name;

    // expected name is 'google.com', according to our request above
}