Node.js의 동기 요청


99

3 http API를 순차적으로 호출해야하는 경우 다음 코드에 대한 더 나은 대안은 무엇일까요?

http.get({ host: 'www.example.com', path: '/api_1.php' }, function(res) { 
  res.on('data', function(d) { 

    http.get({ host: 'www.example.com', path: '/api_2.php' }, function(res) { 
      res.on('data', function(d) { 

        http.get({ host: 'www.example.com', path: '/api_3.php' }, function(res) { 
          res.on('data', function(d) { 


          });
        });
        }
      });
    });
    }
  });
});
}

청소하는 것 외에는 그보다 더 잘할 수 있다고 생각하지 않습니다.
— hvgotcodes

2
순서대로 정렬해야하는 이유는 무엇입니까?
— Raynos

11
@Raynos 당신은 당신이 api_2에 보낼 것을 알기도 전에 api_1에서 일부 데이터를해야 할 수도 있습니다
— andyortlieb

9
Futures는 상당히 사용되지 않는다는 점을 언급 할 가치가 있습니다. Bluebird 또는 Q와 같은 최신 라이브러리를 사용하는 것이
— 좋습니다

1
제목과 질문이 서로 모순됩니다. 질문에서 동기 요청을 설명하는 것이 아니라 일반적으로 각각 비동기 적으로 발생하는 일련의 요청을 설명합니다. 큰 차이점-동기식 호출 차단 및 일련의 비동기 작업이 차단되지 않습니다 (UI 차단, 서버의 다른 요청 처리 차단). sync-request이 질문의 제목에 대한 좋은 답변이지만 질문의 코드가 의미하는 바에 대한 답변이 아닌 라이브러리에 대한 답변이 아래에 있습니다 . 약속에 대한 아래 답변이 더 나은 답변입니다. 무슨 뜻입니까?
— Jake

답변:


69

사용 deferreds 좋아 Futures.

var sequence = Futures.sequence();

sequence
  .then(function(next) {
     http.get({}, next);
  })
  .then(function(next, res) {
     res.on("data", next);
  })
  .then(function(next, d) {
     http.get({}, next);
  })
  .then(function(next, res) {
    ...
  })

범위를 전달해야하는 경우 다음과 같이하십시오.

  .then(function(next, d) {
    http.get({}, function(res) {
      next(res, d);
    });
  })
  .then(function(next, res, d) { })
    ...
  })

nodejs에 대한 대기 및 지연을 제공하는 IcedCoffeScript를 사용해보십시오.
— Thanigainathan 2014

이것이 비 차단입니까? 내 말은 다음 기능을 차단하고 있지만 다른 비동기 기능의 실행을 차단하지는 않습니다.
— Oktav 2014 년

1
예, 지연된 메서드는 비 차단 / 비동기입니다.
— dvlsg 2014

4
ES6 약속 API 효과적으로 심지어 "선물"의 저자에 따르면,이를 교체해야합니다
— 알렉산더 밀스

Futures는 매우 오래되어 더 이상 사용되지 않습니다. 대신 q를 참조하십시오.
— Jim Aho

53

나는 Raynos의 솔루션도 좋아하지만 다른 흐름 제어 라이브러리를 선호합니다.

https://github.com/caolan/async

각 후속 함수에서 결과가 필요한지 여부에 따라 직렬, 병렬 또는 폭포수를 사용합니다.

직렬로 실행해야하는 경우 시리즈 이지만 이후의 각 함수 호출에서 결과가 반드시 필요하지는 않습니다.

병렬 로 실행할 수있는 경우 병렬 로 실행할 수있는 경우 각 병렬 함수에서 각각의 결과가 필요하지 않으며 모두 완료되면 콜백이 필요합니다.

각 함수에서 결과를 모핑하고 다음으로 전달하려는 경우 폭포

endpoints = 
 [{ host: 'www.example.com', path: '/api_1.php' },
  { host: 'www.example.com', path: '/api_2.php' },
  { host: 'www.example.com', path: '/api_3.php' }];

async.mapSeries(endpoints, http.get, function(results){
    // Array of results
});

9
var http = require ( 'http');
— Elle Mundy 2013

7
하. example.com은 실제로 이런 종류의 용도로 설계된 도메인입니다. 와.
— meawoppl 2014 년

async.series 코드는 적어도 async v0.2.10부터 작동하지 않습니다. series ()는 최대 2 개의 인수 만 취하고 첫 번째 인수의 요소를 함수로 실행하므로 async는 객체를 함수로 실행하려고하면 오류가 발생합니다.
— 뚜껑

1
forEachAsync ( github.com/FuturesJS/forEachAsync )를 사용하여이 코드에서 의도 한 것과 유사한 작업을 수행 할 수 있습니다 .
— 뚜껑

이것은 내가 원하는 것을 정확하게 수행합니다. 감사합니다!
— aProperFox

33

내 공통 노드 라이브러리를 사용하여이 작업을 수행 할 수 있습니다 .

function get(url) {
  return new (require('httpclient').HttpClient)({
    method: 'GET',
      url: url
    }).finish().body.read().decodeToString();
}

var a = get('www.example.com/api_1.php'), 
    b = get('www.example.com/api_2.php'),
    c = get('www.example.com/api_3.php');

3
쓰레기, 나는 :( 작동 것 그것은 나던 생각 upvotedrequire(...).HttpClient is not a constructor
— moeiscool

30

동기화 요청

지금까지 내가 찾아서 사용한 가장 쉬운 방법은 sync-request 이며 노드와 브라우저를 모두 지원합니다!

var request = require('sync-request');
var res = request('GET', 'http://google.com');
console.log(res.body.toString('utf-8'));

그것이 lib 폴 백이 있지만 미친 구성이나 복잡한 lib 설치가 아닙니다. 그냥 작동합니다. 여기에서 다른 예제를 시도해 보았고 할 일이 많거나 설치가 작동하지 않을 때 당황했습니다!

노트:

sync-request가 사용 하는 예제 는를 사용할 때 잘 작동하지 않으며 res.getBody()get body가 수행하는 모든 작업은 인코딩을 수락하고 응답 데이터를 변환하는 것입니다. 그냥 할 res.body.toString(encoding)대신.


동기화 요청이 매우 느리다는 것을 알았습니다 . 제 경우에는 10 배 더 빠른 github.com/dhruvbird/http-sync 다른 하나를 사용 하게되었습니다.
— Filip Spiridonov 2015 년

나는 그것을 위해 어떤 느린 실행도하지 않았습니다. 이것은 자식 프로세스를 생성합니다. 시스템이 얼마나 많은 CPU를 사용하고 어떤 버전의 노드를 사용하고 있습니까? 전환이 필요한지 여부를 확인하고 싶습니다.
— jemiloii

필립의 말에 동의합니다. 이것은 느립니다.
— Rambo7

내가 플립을 요청했지만 응답이 없습니다. 시스템이 얼마나 많은 CPU를 사용하고 어떤 버전의 노드를 사용하고 있습니까?
— jemiloii

이것은 상당한 양의 CPU를 사용하며 프로덕션 용도로는 권장되지 않습니다.
— moeiscool

20

API 목록과 함께 재귀 함수를 사용합니다.

var APIs = [ '/api_1.php', '/api_2.php', '/api_3.php' ];
var host = 'www.example.com';

function callAPIs ( host, APIs ) {
  var API = APIs.shift();
  http.get({ host: host, path: API }, function(res) { 
    var body = '';
    res.on('data', function (d) {
      body += d; 
    });
    res.on('end', function () {
      if( APIs.length ) {
        callAPIs ( host, APIs );
      }
    });
  });
}

callAPIs( host, APIs );

편집 : 버전 요청

var request = require('request');
var APIs = [ '/api_1.php', '/api_2.php', '/api_3.php' ];
var host = 'www.example.com';
var APIs = APIs.map(function (api) {
  return 'http://' + host + api;
});

function callAPIs ( host, APIs ) {
  var API = APIs.shift();
  request(API, function(err, res, body) { 
    if( APIs.length ) {
      callAPIs ( host, APIs );
    }
  });
}

callAPIs( host, APIs );

편집 : 요청 / 비동기 버전

var request = require('request');
var async = require('async');
var APIs = [ '/api_1.php', '/api_2.php', '/api_3.php' ];
var host = 'www.example.com';
var APIs = APIs.map(function (api) {
  return 'http://' + host + api;
});

async.eachSeries(function (API, cb) {
  request(API, function (err, res, body) {
    cb(err);
  });
}, function (err) {
  //called when all done, or error occurs
});

이것이 제가 요청하는 가변 목록 (600 개 항목 및 증가)을 가지고 있기 때문에 제가 사용한 방법입니다. 즉, 코드에 문제가 있습니다. API 출력이 청크 크기보다 크면 '데이터'이벤트가 요청 당 여러 번 생성됩니다. 다음과 같이 데이터를 "버퍼링"하고 싶습니다. var body = ''; res.on ( 'data', function (data) {body + = data;}). on ( 'end', function () {callback (body); if (APIs.length) callAPIs (host, APIs);} );
— Ankit Aggarwal

업데이트되었습니다. 재귀를 통해 문제를 더 간단하고 유연하게 만드는 방법을 보여주고 싶었습니다. 개인적으로 저는 여러 콜백을 쉽게 건너 뛸 수 있기 때문에 이런 종류의 요청 모듈을 항상 사용합니다.
— generalhenry

@generalhenry, 요청 모듈을 사용하려면 어떻게해야합니까? 요청을 사용하여 위의 코드를 제공 할 수 있습니까?
— Scotty

요청 버전과 요청 / 비동기 버전을 추가했습니다.
— generalhenry

5

이 문제에 대한 해결책은 끝이없는 것 같습니다. 여기에 하나 더 있습니다. :)

// do it once.
sync(fs, 'readFile')

// now use it anywhere in both sync or async ways.
var data = fs.readFile(__filename, 'utf8')

http://alexeypetrushin.github.com/synchronize


링크 한 라이브러리가 OP 문제에 대한 해결책을 제공하지만 귀하의 예에서 fs.readFile은 항상 동기화됩니다.
— 에릭

1
아니요, 콜백을 명시 적으로 제공하고 원하는 경우 비동기 버전으로 사용할 수 있습니다.
— Alex Craft

1
예제는 파일 시스템 통신이 아닌 http 요청에 대한 것입니다.
— Seth

5

또 다른 가능성은 완료된 작업을 추적하는 콜백을 설정하는 것입니다.

function onApiResults(requestId, response, results) {
    requestsCompleted |= requestId;

    switch(requestId) {
        case REQUEST_API1:
            ...
            [Call API2]
            break;
        case REQUEST_API2:
            ...
            [Call API3]
            break;
        case REQUEST_API3:
            ...
            break;
    }

    if(requestId == requestsNeeded)
        response.end();
}

그런 다음 각각에 ID를 할당하고 연결을 닫기 전에 완료해야하는 작업에 대한 요구 사항을 설정할 수 있습니다.

const var REQUEST_API1 = 0x01;
const var REQUEST_API2 = 0x02;
const var REQUEST_API3 = 0x03;
const var requestsNeeded = REQUEST_API1 | REQUEST_API2 | REQUEST_API3;

좋아요, 예쁘지 않습니다. 순차 호출을하는 또 다른 방법입니다. NodeJS가 가장 기본적인 동기 호출을 제공하지 않는 것은 유감입니다. 그러나 나는 비동기성에 대한 유혹이 무엇인지 이해합니다.


4

순차적 사용.

sudo npm install sequenty

또는

https://github.com/AndyShin/sequenty

아주 간단합니다.

var sequenty = require('sequenty'); 

function f1(cb) // cb: callback by sequenty
{
  console.log("I'm f1");
  cb(); // please call this after finshed
}

function f2(cb)
{
  console.log("I'm f2");
  cb();
}

sequenty.run([f1, f2]);

또한 다음과 같은 루프를 사용할 수 있습니다.

var f = [];
var queries = [ "select .. blah blah", "update blah blah", ...];

for (var i = 0; i < queries.length; i++)
{
  f[i] = function(cb, funcIndex) // sequenty gives you cb and funcIndex
  {
    db.query(queries[funcIndex], function(err, info)
    {
       cb(); // must be called
    });
  }
}

sequenty.run(f); // fire!

3

요청 라이브러리를 사용하면 문제 를 최소화 할 수 있습니다.

var request = require('request')

request({ uri: 'http://api.com/1' }, function(err, response, body){
    // use body
    request({ uri: 'http://api.com/2' }, function(err, response, body){
        // use body
        request({ uri: 'http://api.com/3' }, function(err, response, body){
            // use body
        })
    })
})

그러나 최대한의 굉장함을 얻으려면 Step과 같은 제어 흐름 라이브러리를 시도해야합니다. 허용되는 경우 요청을 병렬화 할 수도 있습니다.

var request = require('request')
var Step    = require('step')

// request returns body as 3rd argument
// we have to move it so it works with Step :(
request.getBody = function(o, cb){
    request(o, function(err, resp, body){
        cb(err, body)
    })
}

Step(
    function getData(){
        request.getBody({ uri: 'http://api.com/?method=1' }, this.parallel())
        request.getBody({ uri: 'http://api.com/?method=2' }, this.parallel())
        request.getBody({ uri: 'http://api.com/?method=3' }, this.parallel())
    },
    function doStuff(err, r1, r2, r3){
        console.log(r1,r2,r3)
    }
)

3

2018 년부터 ES6 모듈과 약속을 사용하여 다음과 같은 함수를 작성할 수 있습니다.

import { get } from 'http';

export const fetch = (url) => new Promise((resolve, reject) => {
  get(url, (res) => {
    let data = '';
    res.on('end', () => resolve(data));
    res.on('data', (buf) => data += buf.toString());
  })
    .on('error', e => reject(e));
});

그리고 다른 모듈에서

let data;
data = await fetch('http://www.example.com/api_1.php');
// do something with data...
data = await fetch('http://www.example.com/api_2.php');
// do something with data
data = await fetch('http://www.example.com/api_3.php');
// do something with data

코드는 비동기 컨텍스트 ( async키워드 사용 ) 에서 실행되어야합니다.


2

제어 흐름 라이브러리가 많이 있습니다. 저는 conseq를 좋아 합니다 (... 제가 작성했기 때문입니다.) 또한 on('data')여러 번 실행할 수 있으므로 restler 와 같은 REST 래퍼 라이브러리를 사용합니다 .

Seq()
  .seq(function () {
    rest.get('http://www.example.com/api_1.php').on('complete', this.next);
  })
  .seq(function (d1) {
    this.d1 = d1;
    rest.get('http://www.example.com/api_2.php').on('complete', this.next);
  })
  .seq(function (d2) {
    this.d2 = d2;
    rest.get('http://www.example.com/api_3.php').on('complete', this.next);
  })
  .seq(function (d3) {
    // use this.d1, this.d2, d3
  })

2

이것은 Raynos에 의해 잘 대답되었습니다. 그러나 답변이 게시 된 이후로 시퀀스 라이브러리가 변경되었습니다.

시퀀스가 작동하려면 https://github.com/FuturesJS/sequence/tree/9daf0000289954b85c0925119821752fbfb3521e 링크를 따르십시오 .

이것은 당신이 그것을 작동시킬 수있는 방법입니다 npm install sequence:

var seq = require('sequence').Sequence;
var sequence = seq.create();

seq.then(function call 1).then(function call 2);

1

다음은 인덱스 대신 배열의 인수를 사용하는 @ andy-shin의 내 버전입니다.

function run(funcs, args) {
    var i = 0;
    var recursive = function() {
        funcs[i](function() {
            i++;
            if (i < funcs.length)
                recursive();
        }, args[i]);
    };
    recursive();
}

1

... 4 년 후 ...

다음은 프레임 워크 Danf를 사용한 원래 솔루션입니다 (이러한 종류의 코드는 필요하지 않고 일부 구성 만 필요).

// config/common/config/sequences.js

'use strict';

module.exports = {
    executeMySyncQueries: {
        operations: [
            {
                order: 0,
                service: 'danf:http.router',
                method: 'follow',
                arguments: [
                    'www.example.com/api_1.php',
                    'GET'
                ],
                scope: 'response1'
            },
            {
                order: 1,
                service: 'danf:http.router',
                method: 'follow',
                arguments: [
                    'www.example.com/api_2.php',
                    'GET'
                ],
                scope: 'response2'
            },
            {
                order: 2,
                service: 'danf:http.router',
                method: 'follow',
                arguments: [
                    'www.example.com/api_3.php',
                    'GET'
                ],
                scope: 'response3'
            }
        ]
    }
};

order병렬로 실행하려는 작업에 동일한 값을 사용하십시오 .

더 짧게 만들고 싶다면 수집 프로세스를 사용할 수 있습니다.

// config/common/config/sequences.js

'use strict';

module.exports = {
    executeMySyncQueries: {
        operations: [
            {
                service: 'danf:http.router',
                method: 'follow',
                // Process the operation on each item
                // of the following collection.
                collection: {
                    // Define the input collection.
                    input: [
                        'www.example.com/api_1.php',
                        'www.example.com/api_2.php',
                        'www.example.com/api_3.php'
                    ],
                    // Define the async method used.
                    // You can specify any collection method
                    // of the async lib.
                    // '--' is a shorcut for 'forEachOfSeries'
                    // which is an execution in series.
                    method: '--'
                },
                arguments: [
                    // Resolve reference '@@.@@' in the context
                    // of the input item.
                    '@@.@@',
                    'GET'
                ],
                // Set the responses in the property 'responses'
                // of the stream.
                scope: 'responses'
            }
        ]
    }
};

상기 살펴보세요 개요 더 많은 정보를위한 프레임 워크를.


1

http.request (분석 보고서를 작성하기 위해 탄력적 검색에 대한 ~ 10,000 집계 쿼리) 속도를 제한해야했기 때문에 여기에 왔습니다. 다음은 내 컴퓨터를 질식 시켰습니다.

for (item in set) {
    http.request(... + item + ...);
}

내 URL은 매우 간단하므로 이것은 원래 질문에 사소하게 적용되지 않을 수 있지만 잠재적으로 적용 가능하고 여기에 나와 비슷한 문제로 여기에 도착하고 사소한 JavaScript 무 라이브러리 솔루션을 원하는 독자에게 쓸 가치가 있다고 생각합니다.

내 직업은 주문 의존적이지 않았고 이것을 막는 첫 번째 접근 방식은 그것을 청크하기 위해 쉘 스크립트로 래핑하는 것입니다 (JavaScript를 처음 사용하기 때문입니다). 그것은 기능적이지만 만족스럽지 않았습니다. 결국 내 JavaScript 해결 방법은 다음과 같습니다.

var stack=[];
stack.push('BOTTOM');

function get_top() {
  var top = stack.pop();
  if (top != 'BOTTOM')
    collect(top);
}

function collect(item) {
    http.request( ... + item + ...
    result.on('end', function() {
      ...
      get_top();
    });
    );
}

for (item in set) {
   stack.push(item);
}

get_top();

collect 와 get_top 간의 상호 재귀처럼 보입니다 . 시스템이 비동기식이고 수집 함수 가 on. ( 'end' 에서 이벤트에 대해 숨겨진 콜백으로 완료 되기 때문에 효과가 있는지 잘 모르겠습니다 .

원래 질문에 적용하기에 충분히 일반적이라고 생각합니다. 내 시나리오와 같이 시퀀스 / 세트가 알려진 경우 모든 URL / 키를 한 번에 스택에 푸시 할 수 있습니다. 이동하면서 계산되면 on ( 'end' 함수는 get_top () 직전에 스택의 다음 URL을 푸시 할 수 있습니다. . 결과가 중첩되지 않고 API를 호출 할 때 리팩토링하기가 더 쉬울 수 있습니다. 변화.

나는 이것이 위의 @generalhenry의 간단한 재귀 버전과 효과적으로 동일하다는 것을 알고 있습니다 (그래서 나는 그것을 찬성했습니다!)


0

슈퍼 요청

이것은 요청을 기반으로하고 promise를 사용하는 또 다른 동기 모듈입니다. 사용하기 매우 간단하고 모카 테스트와 잘 작동합니다.

npm install super-request

request("http://domain.com")
    .post("/login")
    .form({username: "username", password: "password"})
    .expect(200)
    .expect({loggedIn: true})
    .end() //this request is done 
    //now start a new one in the same session 
    .get("/some/protected/route")
    .expect(200, {hello: "world"})
    .end(function(err){
        if(err){
            throw err;
        }
    });

0

이 코드는 프라 미스 배열을 동기 및 순차적으로 실행하는 데 사용할 수 있으며 그 후에 .then()호출 에서 최종 코드를 실행할 수 있습니다 .

const allTasks = [() => promise1, () => promise2, () => promise3];

function executePromisesSync(tasks) {
  return tasks.reduce((task, nextTask) => task.then(nextTask), Promise.resolve());
}

executePromisesSync(allTasks).then(
  result => console.log(result),
  error => console.error(error)
);

0

await, Promises를 사용하지 않고 (우리 자신을 제외하고) 어떤 (외부) 라이브러리를 포함하지 않고도 당신 (그리고 나)이 원하는 것을 실제로 얻었습니다.

방법은 다음과 같습니다.

node.js와 함께 사용할 C ++ 모듈을 만들고 해당 C ++ 모듈 함수가 HTTP 요청을 만들고 데이터를 문자열로 반환하며 다음을 수행하여 직접 사용할 수 있습니다.

var myData = newModule.get(url);

시작할 준비 가 되셨 습니까?

1 단계 : 컴퓨터의 다른 곳에 새 폴더를 만듭니다.이 폴더를 사용하여 module.node 파일 (C ++에서 컴파일 됨)을 빌드하고 나중에 이동할 수 있습니다.

새 폴더에서 (정리를 위해 mynewFolder / src에 넣었습니다) :

npm init

그때

npm install node-gyp -g

이제 2 개의 새 파일을 만듭니다 : 1, something.cpp라고 부르고이 코드를 그 안에 넣습니다 (또는 원하는 경우 수정).

#pragma comment(lib, "urlmon.lib")
#include <sstream>
#include <WTypes.h>  
#include <node.h>
#include <urlmon.h> 
#include <iostream>
using namespace std;
using namespace v8;

Local<Value> S(const char* inp, Isolate* is) {
    return String::NewFromUtf8(
        is,
        inp,
        NewStringType::kNormal
    ).ToLocalChecked();
}

Local<Value> N(double inp, Isolate* is) {
    return Number::New(
        is,
        inp
    );
}

const char* stdStr(Local<Value> str, Isolate* is) {
    String::Utf8Value val(is, str);
    return *val;
}

double num(Local<Value> inp) {
    return inp.As<Number>()->Value();
}

Local<Value> str(Local<Value> inp) {
    return inp.As<String>();
}

Local<Value> get(const char* url, Isolate* is) {
    IStream* stream;
    HRESULT res = URLOpenBlockingStream(0, url, &stream, 0, 0);

    char buffer[100];
    unsigned long bytesReadSoFar;
    stringstream ss;
    stream->Read(buffer, 100, &bytesReadSoFar);
    while(bytesReadSoFar > 0U) {
        ss.write(buffer, (long long) bytesReadSoFar);
        stream->Read(buffer, 100, &bytesReadSoFar);
    }
    stream->Release();
    const string tmp = ss.str();
    const char* cstr = tmp.c_str();
    return S(cstr, is);
}

void Hello(const FunctionCallbackInfo<Value>& arguments) {
    cout << "Yo there!!" << endl;

    Isolate* is = arguments.GetIsolate();
    Local<Context> ctx = is->GetCurrentContext();

    const char* url = stdStr(arguments[0], is);
    Local<Value> pg = get(url,is);

    Local<Object> obj = Object::New(is);
    obj->Set(ctx,
        S("result",is),
        pg
    );
    arguments.GetReturnValue().Set(
       obj
    );

}

void Init(Local<Object> exports) {
    NODE_SET_METHOD(exports, "get", Hello);
}

NODE_MODULE(cobypp, Init);

이제라는 동일한 디렉토리에 새 파일을 만들고 something.gyp다음과 같은 내용을 넣습니다.

{
   "targets": [
       {
           "target_name": "cobypp",
           "sources": [ "src/cobypp.cpp" ]
       }
   ]
}

이제 package.json 파일에 다음을 추가하십시오. "gypfile": true,

이제 : 콘솔에서 node-gyp rebuild

전체 명령을 통과하고 오류없이 끝에 "ok"라고 말하면 (거의) 괜찮은 것입니다. 그렇지 않은 경우에는 주석을 남기십시오.

그러나 그것이 작동한다면 build / Release / cobypp.node (또는 당신을 위해 호출 된 것)로 이동하여 주 node.js 폴더에 복사 한 다음 node.js에 복사하십시오.

var myCPP = require("./cobypp")
var myData = myCPP.get("http://google.com").result;
console.log(myData);

..

response.end(myData);//or whatever
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.