노드 8을 사용할 때입니까?
게시 됨: 2022-03-11노드 8이 나왔습니다! 사실, Node 8은 이제 견고한 실제 사용을 볼 수 있을 만큼 충분히 오래되었습니다. 빠르고 새로운 V8 엔진과 함께 async/await, HTTP/2 및 async hooks를 포함한 새로운 기능이 제공됩니다. 그러나 당신의 프로젝트를 위한 준비가 되었습니까? 알아 보자!
편집자 주: Node 10(코드명 Dubnium )도 나왔다는 것을 알고 계실 것입니다. 우리는 두 가지 이유로 노드 8( Carbon )에 집중하기로 선택했습니다. (1) 노드 10이 이제 장기 지원(LTS) 단계에 진입하고, (2) 노드 8이 노드 10보다 더 중요한 반복을 표시했습니다. .
노드 8 LTS의 성능
이 놀라운 릴리스의 성능 개선 사항과 새로운 기능을 살펴보는 것으로 시작하겠습니다. 한 가지 주요 개선 영역은 Node의 JavaScript 엔진입니다.
어쨌든 JavaScript 엔진 이란 정확히 무엇입니까?
JavaScript 엔진은 코드를 실행하고 최적화합니다. 표준 인터프리터 또는 JavaScript를 바이트코드로 컴파일하는 JIT(Just-In-Time) 컴파일러일 수 있습니다. Node.js에서 사용하는 JS 엔진은 인터프리터가 아니라 모두 JIT 컴파일러입니다.
V8 엔진
Node.js는 처음부터 Google의 Chrome V8 JavaScript 엔진 또는 간단히 V8 을 사용했습니다. 일부 노드 릴리스는 최신 버전의 V8과 동기화하는 데 사용됩니다. 그러나 여기에서 V8 버전을 비교할 때 V8과 노드 8을 혼동하지 않도록 주의하십시오.
소프트웨어 컨텍스트에서 우리는 종종 "v8"을 "버전 8"에 대한 속어 또는 공식 축약형으로 사용하므로 일부에서는 "Node V8" 또는 "Node.js V8"을 "NodeJS 8"과 혼동할 수 있습니다. ,” 하지만 명확하게 하기 위해 이 기사 전체에서 이를 피했습니다. V8은 항상 Node.js 버전이 아니라 엔진을 의미합니다.
V8 릴리스 5
노드 6은 V8 릴리스 5를 JavaScript 엔진으로 사용합니다. (Node 8의 처음 몇 가지 포인트 릴리스도 V8 릴리스 5를 사용하지만 Node 6보다 최신 V8 포인트 릴리스를 사용합니다.)
컴파일러
V8 릴리스 5 및 이전 버전에는 두 개의 컴파일러가 있습니다.
- Full-codegen 은 간단하고 빠른 JIT 컴파일러이지만 느린 기계어 코드를 생성합니다.
- Crankshaft 는 최적화된 기계어 코드를 생성하는 복잡한 JIT 컴파일러입니다.
스레드
깊숙이, V8은 하나 이상의 스레드 유형을 사용합니다.
- 메인 스레드는 코드를 가져와 컴파일한 다음 실행합니다.
- 보조 스레드는 주 스레드가 코드를 최적화하는 동안 코드를 실행합니다.
- 프로파일러 스레드는 성능이 저하된 메서드에 대해 런타임에 알립니다. 그런 다음 크랭크샤프트는 이러한 방법을 최적화합니다.
- 다른 스레드는 가비지 수집을 관리합니다.
컴파일 과정
먼저 Full-codegen 컴파일러는 JavaScript 코드를 실행합니다. 코드가 실행되는 동안 프로파일러 스레드는 엔진이 최적화할 방법을 결정하기 위해 데이터를 수집합니다. 다른 스레드에서 Crankshaft는 이러한 방법을 최적화합니다.
문제
위에서 언급한 접근 방식에는 두 가지 주요 문제가 있습니다. 첫째, 건축학적으로 복잡하다. 둘째, 컴파일된 기계어 코드는 훨씬 더 많은 메모리를 소비합니다. 소비되는 메모리의 양은 코드가 실행되는 횟수와 무관합니다. 한 번만 실행되는 코드도 상당한 양의 메모리를 차지합니다.
V8 릴리스 6
V8 릴리스 6 엔진을 사용하는 첫 번째 노드 버전은 노드 8.3입니다.
릴리스 6에서 V8 팀은 이러한 문제를 완화하기 위해 Ignition 및 TurboFan을 구축했습니다. Ignition 및 TurboFan은 각각 Full-codegen 및 CrankShaft를 대체합니다.
새로운 아키텍처는 더 간단하고 메모리를 덜 소모합니다.
Ignition은 JavaScript 코드를 기계 코드 대신 바이트 코드로 컴파일하여 많은 메모리를 절약합니다. 이후 최적화 컴파일러인 TurboFan은 이 바이트코드로부터 최적화된 기계어 코드를 생성합니다.
특정 성능 향상
이전 Node 버전과 비교하여 Node 8.3+의 성능이 변경된 영역을 살펴보겠습니다.
객체 생성
객체 생성은 노드 6보다 노드 8.3 이상에서 약 5배 빠릅니다.
기능 크기
V8 엔진은 몇 가지 요소를 기반으로 기능을 최적화할지 여부를 결정합니다. 한 가지 요인은 기능 크기입니다. 작은 함수는 최적화되지만 긴 함수는 최적화되지 않습니다.
함수 크기는 어떻게 계산됩니까?
구형 V8 엔진의 크랭크축은 "문자 수"를 사용하여 기능 크기를 결정합니다. 함수의 공백과 주석은 최적화 가능성을 줄입니다. 나는 이것이 당신을 놀라게 할 것이라는 것을 알고 있지만 그 당시에는 댓글이 속도를 약 10% 감소시킬 수 있었습니다.
Node 8.3+에서 공백 및 주석과 같은 관련 없는 문자는 기능 성능에 해를 끼치지 않습니다. 왜 안 돼?
새로운 TurboFan은 기능 크기를 결정하기 위해 문자를 계산하지 않기 때문입니다. 대신 AST(추상 구문 트리) 노드를 계산하므로 실제 함수 명령어 만 고려합니다. Node 8.3+를 사용하면 주석과 공백을 원하는 만큼 추가할 수 있습니다.
Array 화 인수
JavaScript의 일반 함수는 암시적 Array 유사 argument 객체를 전달합니다.
Array 와 유사하다는 것은 무엇을 의미합니까?
arguments 객체는 배열 처럼 작동합니다. length 속성이 있지만 forEach 및 map 과 같은 Array 의 기본 제공 메서드가 없습니다.
arguments 객체가 작동하는 방식은 다음과 같습니다.
function foo() { console.log(arguments[0]); // Expected output: a console.log(arguments[1]); // Expected output: b console.log(arguments[2]); // Expected output: c } foo("a", "b", "c"); 그렇다면 어떻게 arguments 객체를 배열로 변환할 수 있을까요? 간결한 Array.prototype.slice.call(arguments) 사용.
function test() { const r = Array.prototype.slice.call(arguments); console.log(r.map(num => num * 2)); } test(1, 2, 3); // Expected output: [2, 4, 6] Array.prototype.slice.call(arguments) 은 모든 노드 버전에서 성능을 저하시킵니다. 따라서 for 루프를 통해 키를 복사하면 더 잘 수행됩니다.
function test() { const r = []; for (index in arguments) { r.push(arguments[index]); } console.log(r.map(num => num * 2)); } test(1, 2, 3); // Expected output [2, 4, 6] for 루프는 약간 번거롭죠? 확산 연산자를 사용할 수 있지만 노드 8.2 이하에서는 느립니다.
function test() { const r = [...arguments]; console.log(r.map(num => num * 2)); } test(1, 2, 3); // Expected output [2, 4, 6]Node 8.3+에서는 상황이 변경되었습니다. 이제 스프레드가 for 루프보다 훨씬 빠르게 실행됩니다.
부분 적용(카레) 및 바인딩
Currying은 여러 인수를 취하는 함수를 각각의 새 함수가 하나의 인수만 취하는 일련의 함수로 분해하는 것입니다.
간단한 add 기능이 있다고 가정해 보겠습니다. 이 함수의 커리 버전은 하나의 인수 num1 을 사용합니다. 다른 인수 num2 를 취하고 num1 과 num2 의 합계를 반환하는 함수를 반환합니다.
function add(num1, num2) { return num1 + num2; } add(4, 6); // returns 10 function curriedAdd(num1) { return function(num2) { return num1 + num2; }; } const add5 = curriedAdd(5); add5(3); // returns 8 bind 메서드는 간결한 구문으로 커리 함수를 반환합니다.
function add(num1, num2) { return num1 + num2; } const add5 = add.bind(null, 5); add5(3); // returns 8 따라서 bind 는 믿을 수 없지만 이전 Node 버전에서는 느립니다. Node 8.3+에서는 bind 가 훨씬 빠르며 성능 저하에 대한 걱정 없이 사용할 수 있습니다.
실험
노드 6과 노드 8의 성능을 높은 수준에서 비교하기 위해 여러 실험이 수행되었습니다. 이는 Node 8.0에서 수행되었으므로 V8 릴리스 6 업그레이드 덕분에 Node 8.3+에 특정한 위에서 언급한 개선 사항은 포함되지 않습니다.
노드 8의 서버 렌더링 시간은 노드 6보다 25% 적었습니다. 대규모 프로젝트의 경우 서버 인스턴스의 수를 100에서 75로 줄일 수 있었습니다. 이는 놀라운 일입니다. Node 8에서 500개의 테스트 모음을 테스트하는 것이 10% 더 빨랐습니다. Webpack 빌드가 7% 빨라졌습니다. 일반적으로 결과는 노드 8에서 눈에 띄는 성능 향상을 보여주었습니다.
노드 8 기능
속도는 Node 8의 유일한 개선 사항이 아니었습니다. 또한 몇 가지 편리한 새 기능, 아마도 가장 중요한 async/await 를 가져왔습니다.
노드 8의 비동기/대기
콜백과 약속은 일반적으로 JavaScript에서 비동기 코드를 처리하는 데 사용됩니다. 콜백은 유지 관리할 수 없는 코드를 생성하는 것으로 유명합니다. 그들은 JavaScript 커뮤니티에서 혼란(특히 콜백 지옥 으로 알려짐)을 일으켰습니다. Promise는 오랫동안 콜백 지옥에서 우리를 구해 주었지만 여전히 동기 코드의 깔끔함이 부족했습니다. Async/await는 동기 코드처럼 보이는 비동기 코드를 작성할 수 있는 최신 접근 방식입니다.
그리고 async/await는 이전 Node 버전에서 사용할 수 있었지만 Babel을 통한 추가 사전 처리와 같은 외부 라이브러리와 도구가 필요했습니다. 이제 기본적으로 즉시 사용할 수 있습니다.
나는 async/await가 기존의 약속보다 우수한 몇 가지 경우에 대해 이야기할 것입니다.
조건부
데이터를 가져오고 페이로드를 기반으로 새 API 호출이 필요한지 여부를 결정할 것이라고 상상해 보십시오. "conventional promise" 접근 방식을 통해 이것이 어떻게 수행되는지 보려면 아래 코드를 살펴보십시오.
const request = () => { return getData().then(data => { if (!data.car) { return fetchForCar(data.id).then(carData => { console.log(carData); return carData; }); } else { console.log(data); return data; } }); };보시다시피 위의 코드는 하나의 추가 조건에서 이미 지저분해 보입니다. 비동기/대기에는 더 적은 중첩이 포함됩니다.
const request = async () => { const data = await getData(); if (!data.car) { const carData = await fetchForCar(data); console.log(carData); return carData; } else { console.log(data); return data; } };오류 처리
Async/await는 try/catch에서 동기 및 비동기 오류를 모두 처리할 수 있는 액세스 권한을 부여합니다. 비동기 API 호출에서 오는 JSON을 구문 분석하려고 한다고 가정해 보겠습니다. 단일 try/catch는 구문 분석 오류와 API 오류를 모두 처리할 수 있습니다.
const request = async () => { try { console.log(await getData()); } catch (err) { console.log(err); } };중간 값
Promise에 다른 Promise에서 해결되어야 하는 인수가 필요한 경우 어떻게 합니까? 이는 비동기식 호출이 연속적으로 수행되어야 함을 의미합니다.
일반적인 약속을 사용하면 다음과 같은 코드로 끝날 수 있습니다.
const request = () => { return fetchUserData() .then(userData => { return fetchCompanyData(userData); }) .then(companyData => { return fetchRetiringPlan(userData, companyData); }) .then(retiringPlan => { const retiringPlan = retiringPlan; }); };Async/await는 연결된 비동기 호출이 필요한 경우에 빛을 발합니다.
const request = async () => { const userData = await fetchUserData(); const companyData = await fetchCompanyData(userData); const retiringPlan = await fetchRetiringPlan(userData, companyData); };병렬로 비동기
둘 이상의 비동기 함수를 병렬로 호출하려면 어떻게 해야 합니까? 아래 코드에서는 fetchHouseData 가 해결될 때까지 기다린 다음 fetchCarData 를 호출합니다. 이들 각각은 서로 독립적이지만 순차적으로 처리됩니다. 두 API가 모두 해결될 때까지 2초 동안 기다립니다. 이것은 좋지 않다.

function fetchHouseData() { return new Promise(resolve => setTimeout(() => resolve("Mansion"), 1000)); } function fetchCarData() { return new Promise(resolve => setTimeout(() => resolve("Ferrari"), 1000)); } async function action() { const house = await fetchHouseData(); // Wait one second const car = await fetchCarData(); // ...then wait another second. console.log(house, car, " in series"); } action();더 나은 접근 방식은 비동기식 호출을 병렬로 처리하는 것입니다. async/await에서 이것이 어떻게 달성되는지 알아보려면 아래 코드를 확인하십시오.
async function parallel() { houseDataPromise = fetchHouseData(); carDataPromise = fetchCarData(); const house = await houseDataPromise; // Wait one second for both const car = await carDataPromise; console.log(house, car, " in parallel"); } parallel();이러한 호출을 병렬로 처리하면 두 호출 모두에 대해 1초만 기다리게 됩니다.
새로운 핵심 라이브러리 기능
노드 8은 또한 몇 가지 새로운 핵심 기능을 제공합니다.
파일 복사
노드 8 이전에는 파일을 복사하기 위해 두 개의 스트림을 만들고 하나에서 다른 스트림으로 데이터를 파이프했습니다. 아래 코드는 읽기 스트림이 데이터를 쓰기 스트림으로 파이프하는 방법을 보여줍니다. 보시다시피 코드는 파일 복사와 같은 간단한 작업에 대해 복잡합니다.
const fs = require('fs'); const rd = fs.createReadStream('sourceFile.txt'); rd.on('error', err => { console.log(err); }); const wr = fs.createWriteStream('target.txt'); wr.on('error', err => { console.log(err); }); wr.on('close', function(ex) { console.log('File Copied'); }); rd.pipe(wr); 노드 8 fs.copyFile 및 fs.copyFileSync 는 훨씬 적은 번거로움으로 파일을 복사하는 새로운 접근 방식입니다.
const fs = require("fs"); fs.copyFile("firstFile.txt", "secondFile.txt", err => { if (err) { console.log(err); } else { console.log("File copied"); } });약속과 콜백파이
util.promisify 는 일반 함수를 비동기 함수로 변환합니다. 입력된 함수는 일반적인 Node.js 콜백 스타일을 따라야 합니다. 콜백을 마지막 인수로 취해야 합니다(예: (error, payload) => { ... } .
const { promisify } = require('util'); const fs = require('fs'); const readFilePromisified = promisify(fs.readFile); const file_path = process.argv[2]; readFilePromisified(file_path) .then((text) => console.log(text)) .catch((err) => console.log(err)); 보시다시피 util.promisify 는 fs.readFile 을 비동기 함수로 변환했습니다.
반면 Node.js는 util.callbackify 와 함께 제공됩니다. util.callbackify 는 util.promisify 의 반대입니다. 비동기 함수를 Node.js 콜백 스타일 함수로 변환합니다.
읽기 및 쓰기에 대한 destroy 기능
노드 8의 destroy 기능은 읽기 또는 쓰기 가능한 스트림을 파괴/닫기/중단하는 문서화된 방법입니다.
const fs = require('fs'); const file = fs.createWriteStream('./big.txt'); file.on('error', errors => { console.log(errors); }); file.write(`New text.\n`); file.destroy(['First Error', 'Second Error']); 위의 코드는 새 텍스트라는 텍스트가 있는 big.txt (아직 없는 경우)라는 새 파일을 생성 New text. .
노드 8의 Readable.destroy 및 Writeable.destroy 함수는 close 이벤트와 선택적 error 이벤트 destroy 내보냅니다.
스프레드 연산자
확산 연산자(일명 ... )는 노드 6에서 작동했지만 배열 및 기타 반복 가능 항목에서만 작동했습니다.
const arr1 = [1,2,3,4,5,6] const arr2 = [...arr1, 9] console.log(arr2) // expected output: [1,2,3,4,5,6,9]노드 8에서 객체는 스프레드 연산자를 사용할 수도 있습니다.
const userCarData = { type: 'ferrari', color: 'red' }; const userSettingsData = { lastLoggedIn: '12/03/2019', featuresPlan: 'premium' }; const userData = { ...userCarData, name: 'Youssef', ...userSettingsData }; console.log(userData); /* Expected output: { type: 'ferrari', color: 'red', name: 'Youssef', lastLoggedIn: '12/03/2019', featuresPlan: 'premium' } */노드 8 LTS의 실험적 기능
실험 기능은 안정적이지 않고 더 이상 사용되지 않으며 시간이 지나면 업데이트될 수 있습니다. 안정화될 때까지 프로덕션 에서 이러한 기능을 사용하지 마십시오.
비동기 후크
비동기 후크는 API를 통해 Node 내부에서 생성된 비동기 리소스의 수명을 추적합니다.
비동기 후크를 계속 사용하기 전에 이벤트 루프를 이해해야 합니다. 이 비디오가 도움이 될 수 있습니다. 비동기 후크는 비동기 함수를 디버깅하는 데 유용합니다. 그들은 몇 가지 응용 프로그램이 있습니다. 그 중 하나는 비동기 함수에 대한 오류 스택 추적입니다.
아래 코드를 살펴보십시오. console.log 는 비동기 함수입니다. 따라서 비동기 후크 내에서 사용할 수 없습니다. fs.writeSync 가 대신 사용됩니다.
const asyncHooks = require('async_hooks'); const fs = require('fs'); const init = (asyncId, type, triggerId) => fs.writeSync(1, `${type} \n`); const asyncHook = asyncHooks.createHook({ init }); asyncHook.enable();비동기 후크에 대해 자세히 알아보려면 이 비디오를 시청하세요. 특히 Node.js 가이드의 관점에서 이 기사는 예시적인 애플리케이션을 통해 비동기 후크를 이해하는 데 도움이 됩니다.
노드 8의 ES6 모듈
Node 8은 이제 ES6 모듈을 지원하므로 다음 구문을 사용할 수 있습니다.
import { UtilityService } from './utility_service';Node 8에서 ES6 모듈을 사용하려면 다음을 수행해야 합니다.
- 명령줄에
--experimental-modules플래그 추가 -
.js에서.mjs로 파일 확장명 이름 바꾸기
HTTP/2
HTTP/2는 자주 업데이트되지 않는 HTTP 프로토콜에 대한 최신 업데이트이며 Node 8.4+는 기본적으로 실험 모드에서 이를 지원합니다. 이전 버전인 HTTP/1.1보다 빠르고 안전하며 효율적입니다. 그리고 구글은 당신이 그것을 사용할 것을 권장합니다. 그러나 그것은 또 무엇을합니까?
다중화
HTTP/1.1에서 서버는 한 번에 연결당 하나의 응답만 보낼 수 있습니다. HTTP/2에서 서버는 병렬로 둘 이상의 응답을 보낼 수 있습니다.
서버 푸시
서버는 단일 클라이언트 요청에 대해 여러 응답을 푸시할 수 있습니다. 이것이 유익한 이유는 무엇입니까? 웹 애플리케이션을 예로 들어보겠습니다. 전통적으로,
- 클라이언트가 HTML 문서를 요청합니다.
- 클라이언트는 HTML 문서에서 필요한 리소스를 찾습니다.
- 클라이언트는 필요한 각 리소스에 대해 HTTP 요청을 보냅니다. 예를 들어 클라이언트는 문서에 언급된 각 JS 및 CSS 리소스에 대해 HTTP 요청을 보냅니다.
서버 푸시 기능은 서버가 이러한 모든 리소스에 대해 이미 알고 있다는 사실을 활용합니다. 서버는 해당 리소스를 클라이언트에 푸시합니다. 따라서 웹 애플리케이션 예제의 경우 클라이언트가 초기 문서를 요청한 후 서버가 모든 리소스를 푸시합니다. 이것은 대기 시간을 줄입니다.
우선순위
클라이언트는 우선 순위 체계를 설정하여 필요한 각 응답이 얼마나 중요한지 결정할 수 있습니다. 그런 다음 서버는 이 체계를 사용하여 메모리, CPU, 대역폭 및 기타 리소스 할당의 우선 순위를 지정할 수 있습니다.
오래된 나쁜 습관 버리기
HTTP/1.1은 멀티플렉싱을 허용하지 않았기 때문에 느린 속도와 파일 로딩을 은폐하기 위해 몇 가지 최적화 및 해결 방법이 사용되었습니다. 불행히도 이러한 기술은 RAM 소비를 증가시키고 렌더링을 지연시킵니다.
- 도메인 샤딩: 연결이 분산되어 병렬로 처리되도록 여러 하위 도메인을 사용했습니다.
- CSS와 JavaScript 파일을 결합하여 요청 수를 줄입니다.
- 스프라이트 맵: HTTP 요청을 줄이기 위해 이미지 파일을 결합합니다.
- 인라인: 연결 수를 줄이기 위해 CSS 및 JavaScript가 HTML에 직접 배치됩니다.
이제 HTTP/2를 사용하면 이러한 기술을 잊고 코드에 집중할 수 있습니다.
그러나 HTTP/2를 어떻게 사용합니까?
대부분의 브라우저는 보안 SSL 연결을 통해서만 HTTP/2를 지원합니다. 이 문서는 자체 서명된 인증서를 구성하는 데 도움이 될 수 있습니다. 생성된 .crt 파일과 .key 파일을 ssl 이라는 디렉토리에 추가합니다. 그런 다음 server.js 파일에 아래 코드를 추가합니다.
이 기능을 활성화하려면 명령줄에서 --expose-http2 플래그를 사용해야 합니다. 즉, 우리 예제의 실행 명령은 node server.js --expose-http2 입니다.
const http2 = require('http2'); const path = require('path'); const fs = require('fs'); const PORT = 3000; const secureServerOptions = { cert: fs.readFileSync(path.join(__dirname, './ssl/server.crt')), key: fs.readFileSync(path.join(__dirname, './ssl/server.key')) }; const server = http2.createSecureServer(secureServerOptions, (req, res) => { res.statusCode = 200; res.end('Hello from Toptal'); }); server.listen( PORT, err => err ? console.error(err) : console.log(`Server listening to port ${PORT}`) );물론 노드 8, 노드 9, 노드 10 등은 여전히 이전 HTTP 1.1을 지원합니다. 표준 HTTP 트랜잭션에 대한 공식 Node.js 문서는 오랫동안 부실하지 않을 것입니다. 그러나 HTTP/2를 사용하고 싶다면 이 Node.js 가이드를 통해 더 깊이 들어갈 수 있습니다.
그래서 결국 Node.js 8을 사용해야 합니까?
Node 8은 성능이 개선되고 async/await, HTTP/2 등과 같은 새로운 기능이 추가되었습니다. 종단 간 실험에 따르면 노드 8은 노드 6보다 약 25% 더 빠릅니다. 이는 상당한 비용 절감으로 이어집니다. 따라서 미개발 프로젝트의 경우 절대적으로! 하지만 기존 프로젝트의 경우 Node를 업데이트해야 합니까?
기존 코드를 많이 변경해야 하는지 여부에 따라 다릅니다. 이 문서는 노드 6에서 오는 경우 모든 노드 8 주요 변경 사항을 나열합니다. 최신 노드 8 버전을 사용하여 프로젝트의 모든 npm 패키지를 다시 설치하여 일반적인 문제를 방지하는 것을 잊지 마십시오. 또한 프로덕션 서버에서와 같이 개발 머신에서 항상 동일한 Node.js 버전을 사용하십시오. 행운을 빕니다!
- 도대체 왜 Node.js를 사용할까요? 사례별 자습서
- Node.js 애플리케이션의 메모리 누수 디버깅
- Node.js에서 보안 REST API 만들기
- Cabin Fever 코딩: Node.js 백엔드 튜토리얼
