Node.js 개발자가 저지르는 가장 흔한 실수 10가지
게시 됨: 2022-03-11Node.js가 세상에 공개된 순간부터 Node.js는 칭찬과 비판을 모두 받았습니다. 논쟁은 여전히 계속되고 있으며 곧 끝나지 않을 수도 있습니다. 이러한 논쟁에서 우리가 종종 간과하는 것은 모든 프로그래밍 언어와 플랫폼이 우리가 플랫폼을 사용하는 방식에 따라 생성되는 특정 문제를 기반으로 비판된다는 것입니다. Node.js가 안전한 코드를 작성하는 것이 얼마나 어려운지, 동시성이 높은 코드를 작성하는 것이 얼마나 쉬운지에 관계없이 플랫폼은 꽤 오랫동안 사용되어 왔으며 수많은 강력하고 정교한 웹 서비스를 구축하는 데 사용되었습니다. 이러한 웹 서비스는 확장성이 뛰어나며 인터넷에서 오랜 시간 동안 안정성을 입증했습니다.
그러나 다른 플랫폼과 마찬가지로 Node.js는 개발자 문제 및 문제에 취약합니다. 이러한 실수 중 일부는 성능을 저하시키는 반면, 다른 실수는 Node.js를 달성하려는 모든 것에 사용할 수 없는 것처럼 보이게 만듭니다. 이 기사에서는 Node.js를 처음 접하는 개발자가 자주 범하는 10가지 일반적인 실수와 Node.js 전문가가 되기 위해 피할 수 있는 방법을 살펴보겠습니다.
실수 #1: 이벤트 루프 차단
Node.js의 JavaScript(브라우저와 마찬가지로)는 단일 스레드 환경을 제공합니다. 즉, 애플리케이션의 두 부분이 병렬로 실행되지 않습니다. 대신 I/O 바인딩된 작업을 비동기적으로 처리하여 동시성을 달성합니다. 예를 들어 Node.js가 데이터베이스 엔진에 문서를 가져오도록 요청하면 Node.js가 애플리케이션의 다른 부분에 집중할 수 있습니다.
// Trying to fetch an user object from the database. Node.js is free to run other parts of the code from the moment this function is invoked.. db.User.get(userId, function(err, user) { // .. until the moment the user object has been retrieved here })
그러나 수천 개의 클라이언트가 연결된 Node.js 인스턴스의 CPU 바인딩 코드는 이벤트 루프를 차단하여 모든 클라이언트를 기다리게 하는 데 필요한 전부입니다. CPU 바운드 코드에는 큰 배열을 정렬하려는 시도, 매우 긴 루프 실행 등이 포함됩니다. 예를 들어:
function sortUsersByAge(users) { users.sort(function(a, b) { return a.age < b.age ? -1 : 1 }) }
이 "sortUsersByAge" 함수를 호출하는 것은 작은 "users" 배열에서 실행되는 경우 괜찮을 수 있지만 큰 배열의 경우 전체 성능에 끔찍한 영향을 미칩니다. 이것이 절대적으로 수행되어야 하고 이벤트 루프를 기다리는 다른 것이 없을 것이라고 확신하는 경우(예를 들어 이것이 Node.js로 빌드하는 명령줄 도구의 일부이고 전체가 동기식으로 실행되더라도 문제가 되지 않음), 문제가 되지 않을 수 있습니다. 그러나 한 번에 수천 명의 사용자에게 서비스를 제공하려는 Node.js 서버 인스턴스에서 이러한 패턴은 치명적일 수 있습니다.
이 사용자 배열이 데이터베이스에서 검색되는 경우 이상적인 솔루션은 이미 정렬된 데이터베이스에서 직접 가져오는 것입니다. 이벤트 루프가 금융 거래 데이터의 긴 이력의 합계를 계산하기 위해 작성된 루프에 의해 차단된 경우 이벤트 루프를 호그하는 것을 피하기 위해 일부 외부 작업자/대기열 설정으로 지연될 수 있습니다.
보시다시피, 이러한 종류의 Node.js 문제에 대한 확실한 해결책은 없으며 각 경우를 개별적으로 해결해야 합니다. 기본적인 아이디어는 전면을 향한 Node.js 인스턴스(클라이언트가 동시에 연결하는 인스턴스) 내에서 CPU 집약적인 작업을 수행하지 않는 것입니다.
실수 2: 콜백을 두 번 이상 호출
JavaScript는 예전부터 콜백에 의존해 왔습니다. 웹 브라우저에서 이벤트는 콜백처럼 작동하는 (종종 익명의) 함수에 대한 참조를 전달하여 처리됩니다. Node.js에서 콜백은 약속이 도입될 때까지 코드의 비동기 요소가 서로 통신하는 유일한 방법이었습니다. 콜백은 여전히 사용 중이며 패키지 개발자는 여전히 콜백을 중심으로 API를 설계합니다. 콜백 사용과 관련된 한 가지 일반적인 Node.js 문제는 콜백을 두 번 이상 호출하는 것입니다. 일반적으로 비동기식으로 수행하기 위해 패키지에서 제공하는 함수는 비동기식 작업이 완료될 때 호출되는 마지막 인수로 함수를 예상하도록 설계되었습니다.
module.exports.verifyPassword = function(user, password, done) { if(typeof password !== 'string') { done(new Error('password should be a string')) return } computeHash(password, user.passwordHashOpts, function(err, hash) { if(err) { done(err) return } done(null, hash === user.passwordHash) }) }
마지막까지 "done"이 호출될 때마다 return 문이 어떻게 나오는지 확인하십시오. 이는 콜백을 호출해도 현재 함수의 실행이 자동으로 종료되지 않기 때문입니다. 첫 번째 "반환"이 주석 처리된 경우 이 함수에 문자열이 아닌 암호를 전달하면 여전히 "computeHash"가 호출됩니다. "computeHash"가 이러한 시나리오를 처리하는 방법에 따라 "완료"가 여러 번 호출될 수 있습니다. 다른 곳에서 이 함수를 사용하는 사람은 전달한 콜백이 여러 번 호출될 때 완전히 방심할 수 있습니다.
이 Node.js 오류를 피하기 위해서는 주의만 하면 됩니다. 일부 Node.js 개발자는 모든 콜백 호출 전에 return 키워드를 추가하는 습관을 채택합니다.
if(err) { return done(err) }
많은 비동기 함수에서 반환 값은 거의 의미가 없으므로 이 접근 방식을 사용하면 이러한 문제를 쉽게 피할 수 있습니다.
실수 #3: 콜백을 깊게 중첩
종종 "콜백 지옥"이라고 하는 심층 중첩 콜백은 그 자체로 Node.js 문제가 아닙니다. 그러나 이렇게 하면 코드를 빠르게 제어할 수 없게 만드는 문제가 발생할 수 있습니다.
function handleLogin(..., done) { db.User.get(..., function(..., user) { if(!user) { return done(null, 'failed to log in') } utils.verifyPassword(..., function(..., okay) { if(okay) { return done(null, 'failed to log in') } session.login(..., function() { done(null, 'logged in') }) }) }) }
작업이 복잡할수록 더 나빠질 수 있습니다. 이러한 방식으로 콜백을 중첩하면 오류가 발생하기 쉽고 읽기 어렵고 코드를 유지 관리하기가 어렵습니다. 한 가지 해결 방법은 이러한 작업을 작은 기능으로 선언한 다음 연결하는 것입니다. 이에 대한 가장 깨끗한 솔루션 중 하나는 Async.js와 같은 비동기 JavaScript 패턴을 처리하는 유틸리티 Node.js 패키지를 사용하는 것입니다.
function handleLogin(done) { async.waterfall([ function(done) { db.User.get(..., done) }, function(user, done) { if(!user) { return done(null, 'failed to log in') } utils.verifyPassword(..., function(..., okay) { done(null, user, okay) }) }, function(user, okay, done) { if(okay) { return done(null, 'failed to log in') } session.login(..., function() { done(null, 'logged in') }) } ], function() { // ... }) }
"async.waterfall"과 유사하게 Async.js가 다양한 비동기 패턴을 처리하기 위해 제공하는 다른 많은 기능이 있습니다. 간결함을 위해 여기서는 더 간단한 예를 사용했지만 현실은 종종 더 나쁩니다.
실수 #4: 콜백이 동기적으로 실행되기를 기대하기
콜백을 사용한 비동기 프로그래밍은 JavaScript와 Node.js만의 고유한 기능은 아니지만 인기를 얻게 된 원인입니다. 다른 프로그래밍 언어에서는 명령문 사이를 이동하는 특정 명령이 없는 한 두 명령문이 차례로 실행되는 예측 가능한 실행 순서에 익숙합니다. 그렇더라도 이들은 종종 조건문, 루프문 및 함수 호출로 제한됩니다.
그러나 JavaScript에서 콜백을 사용하면 대기 중인 작업이 완료될 때까지 특정 기능이 제대로 실행되지 않을 수 있습니다. 현재 함수의 실행은 중단 없이 끝까지 실행됩니다.
function testTimeout() { console.log(“Begin”) setTimeout(function() { console.log(“Done!”) }, duration * 1000) console.log(“Waiting..”) }
"testTimeout" 함수를 호출하면 먼저 "Begin"을 인쇄한 다음 "Waiting.."을 인쇄한 다음 "Done!" 메시지를 인쇄합니다. 약 1초 후.
콜백이 실행된 후 발생해야 하는 모든 것은 콜백 내에서 호출되어야 합니다.
실수 #5: "module.exports" 대신 "exports"에 할당
Node.js는 각 파일을 작은 격리된 모듈로 취급합니다. 패키지에 "a.js"와 "b.js"라는 두 개의 파일이 있는 경우 "b.js"가 "a.js"의 기능에 액세스하려면 "a.js"가 속성을 추가하여 파일을 내보내야 합니다. 내보내기 개체:
// a.js exports.verifyPassword = function(user, password, done) { ... }
이 작업이 완료되면 "a.js"가 필요한 모든 사람에게 "verifyPassword" 속성 기능이 있는 객체가 제공됩니다.

// b.js require('a.js') // { verifyPassword: function(user, password, done) { ... } }
그러나 이 함수를 일부 객체의 속성이 아닌 직접 내보내려면 어떻게 해야 합니까? 이를 위해 내보내기를 덮어쓸 수 있지만 전역 변수로 취급해서는 안 됩니다.
// a.js module.exports = function(user, password, done) { ... }
모듈 객체의 속성으로 "내보내기"를 처리하는 방법에 주목하십시오. 여기에서 "module.exports"와 "exports"를 구분하는 것은 매우 중요하며, 이는 종종 새로운 Node.js 개발자들 사이에서 좌절의 원인이 됩니다.
실수 #6: 내부 콜백에서 오류 던지기
JavaScript에는 예외라는 개념이 있습니다. Java 및 C++와 같은 예외 처리 지원이 있는 거의 모든 기존 언어의 구문을 모방하여 JavaScript는 try-catch 블록에서 예외를 "던지고" 잡을 수 있습니다.
function slugifyUsername(username) { if(typeof username === 'string') { throw new TypeError('expected a string username, got '+(typeof username)) } // ... } try { var usernameSlug = slugifyUsername(username) } catch(e) { console.log('Oh no!') }
그러나 try-catch는 비동기 상황에서 예상한 대로 작동하지 않습니다. 예를 들어, 하나의 큰 try-catch 블록으로 많은 비동기 활동이 있는 큰 코드 덩어리를 보호하려는 경우 반드시 작동하지는 않습니다.
try { db.User.get(userId, function(err, user) { if(err) { throw err } // ... usernameSlug = slugifyUsername(user.username) // ... }) } catch(e) { console.log('Oh no!') }
"db.User.get"에 대한 콜백이 비동기식으로 실행된 경우 try-catch 블록이 포함된 범위는 콜백 내부에서 발생한 오류를 계속 잡을 수 있도록 컨텍스트에서 오랫동안 벗어났을 것입니다.
이것은 Node.js에서 오류가 다른 방식으로 처리되는 방식이며 모든 콜백 함수 인수에서 (err, ...) 패턴을 따르는 것이 필수적입니다. 모든 콜백의 첫 번째 인수는 오류가 발생하면 오류가 될 것으로 예상됩니다. .
실수 #7: 숫자를 정수 데이터 유형으로 가정
JavaScript의 숫자는 부동 소수점입니다. 정수 데이터 유형이 없습니다. float의 한계를 강조하기에 충분히 큰 숫자가 자주 발생하지 않기 때문에 이것이 문제가 될 것이라고 예상하지 못할 것입니다. 바로 이때 이와 관련된 실수가 발생합니다. 부동 소수점 숫자는 특정 값까지만 정수 표현을 보유할 수 있으므로 계산에서 해당 값을 초과하면 즉시 엉망이 되기 시작합니다. 이상하게 보일 수 있지만 다음은 Node.js에서 true로 평가됩니다.
Math.pow(2, 53)+1 === Math.pow(2, 53)
불행히도 JavaScript에서 숫자의 단점은 여기서 끝나지 않습니다. Numbers는 부동 소수점이지만 정수 데이터 유형에서 작동하는 연산자는 여기에서도 작동합니다.
5 % 2 === 1 // true 5 >> 1 === 2 // true
그러나 산술 연산자와 달리 비트 연산자와 시프트 연산자는 이러한 큰 "정수" 숫자의 후행 32비트에서만 작동합니다. 예를 들어, "Math.pow(2, 53)"를 1만큼 이동하려고 하면 항상 0으로 평가됩니다. 동일한 큰 숫자로 1의 비트 또는 1을 수행하려고 하면 1로 평가됩니다.
Math.pow(2, 53) / 2 === Math.pow(2, 52) // true Math.pow(2, 53) >> 1 === 0 // true Math.pow(2, 53) | 1 === 1 // true
큰 숫자를 처리할 필요가 거의 없을 수도 있지만 처리할 경우 node-bigint와 같은 큰 정밀도 숫자에 대한 중요한 수학 연산을 구현하는 큰 정수 라이브러리가 많이 있습니다.
실수 #8: 스트리밍 API의 장점 무시
다른 웹 서버에서 콘텐츠를 가져와 요청에 대한 응답을 제공하는 프록시와 같은 작은 웹 서버를 구축한다고 가정해 보겠습니다. 예를 들어 Gravatar 이미지를 제공하는 작은 웹 서버를 구축합니다.
var http = require('http') var crypto = require('crypto') http.createServer() .on('request', function(req, res) { var email = req.url.substr(req.url.lastIndexOf('/')+1) if(!email) { res.writeHead(404) return res.end() } var buf = new Buffer(1024*1024) http.get('http://www.gravatar.com/avatar/'+crypto.createHash('md5').update(email).digest('hex'), function(resp) { var size = 0 resp.on('data', function(chunk) { chunk.copy(buf, size) size += chunk.length }) .on('end', function() { res.write(buf.slice(0, size)) res.end() }) }) }) .listen(8080)
Node.js 문제의 이 특정 예에서 우리는 Gravatar에서 이미지를 가져와 버퍼로 읽은 다음 요청에 응답합니다. Gravatar 이미지가 너무 크지 않다는 점을 감안할 때 이것은 그렇게 나쁜 일이 아닙니다. 그러나 우리가 프록시하는 콘텐츠의 크기가 수천 메가바이트였다고 상상해 보십시오. 훨씬 더 나은 접근 방식은 다음과 같습니다.
http.createServer() .on('request', function(req, res) { var email = req.url.substr(req.url.lastIndexOf('/')+1) if(!email) { res.writeHead(404) return res.end() } http.get('http://www.gravatar.com/avatar/'+crypto.createHash('md5').update(email).digest('hex'), function(resp) { resp.pipe(res) }) }) .listen(8080)
여기에서 이미지를 가져오고 단순히 클라이언트에 응답을 파이프합니다. 어떤 경우에도 제공하기 전에 전체 콘텐츠를 버퍼로 읽을 필요가 없습니다.
실수 #9: 디버깅 목적으로 Console.log 사용하기
Node.js에서 "console.log"를 사용하면 거의 모든 것을 콘솔에 인쇄할 수 있습니다. 객체를 전달하면 JavaScript 객체 리터럴로 인쇄됩니다. 임의의 수의 인수를 허용하고 모두 공백으로 구분하여 깔끔하게 인쇄합니다. 개발자가 자신의 코드를 디버그하기 위해 이것을 사용하고 싶은 유혹을 느끼는 데에는 여러 가지 이유가 있습니다. 그러나 실제 코드에서는 "console.log"를 사용하지 않는 것이 좋습니다. 디버그하기 위해 코드 전체에 "console.log"를 작성하고 더 이상 필요하지 않을 때 주석 처리하는 것을 피해야 합니다. 대신 디버그와 같이 이를 위해 구축된 놀라운 라이브러리 중 하나를 사용하십시오.
이와 같은 패키지는 애플리케이션을 시작할 때 특정 디버그 라인을 활성화 및 비활성화하는 편리한 방법을 제공합니다. 예를 들어, 디버그를 사용하면 DEBUG 환경 변수를 설정하지 않음으로써 디버그 라인이 터미널에 인쇄되는 것을 방지할 수 있습니다. 그것을 사용하는 것은 간단합니다:
// app.js var debug = require('debug')('app') debug('Hello, %s!', 'world')
디버그 라인을 활성화하려면 DEBUG 환경 변수를 "app" 또는 "*"로 설정하여 이 코드를 실행하기만 하면 됩니다.
DEBUG=app node app.js
실수 #10: 감독자 프로그램을 사용하지 않음
Node.js 코드가 프로덕션 환경에서 실행 중이든 로컬 개발 환경에서 실행 중이든 상관없이 프로그램을 오케스트레이션할 수 있는 감독자 프로그램 모니터는 매우 유용합니다. 최신 애플리케이션을 설계하고 구현하는 개발자가 자주 권장하는 한 가지 방법은 코드가 빨리 실패해야 한다고 권장합니다. 예기치 않은 오류가 발생하면 처리하려고 하지 말고 프로그램이 충돌하도록 하고 감독자가 몇 초 안에 다시 시작하도록 하십시오. 수퍼바이저 프로그램의 이점은 충돌한 프로그램을 다시 시작하는 것에만 국한되지 않습니다. 이러한 도구를 사용하면 충돌 시 프로그램을 다시 시작할 수 있을 뿐만 아니라 일부 파일이 변경되면 다시 시작할 수 있습니다. 이것은 Node.js 프로그램 개발을 훨씬 더 즐거운 경험으로 만듭니다.
Node.js에 사용할 수 있는 수퍼바이저 프로그램이 많이 있습니다. 예를 들어:
오후2
영원히
노드몬
감독자
이러한 모든 도구에는 장단점이 있습니다. 그들 중 일부는 동일한 시스템에서 여러 응용 프로그램을 처리하는 데 적합하고 다른 일부는 로그 관리에 더 적합합니다. 그러나 그러한 프로그램을 시작하려면 이 모든 것이 공정한 선택입니다.
결론
알 수 있듯이 이러한 Node.js 문제 중 일부는 프로그램에 치명적인 영향을 미칠 수 있습니다. 일부는 Node.js에서 가장 간단한 것을 구현하려고 하는 동안 좌절의 원인이 될 수 있습니다. Node.js를 사용하면 초보자가 매우 쉽게 시작할 수 있지만 여전히 엉망이 되기 쉬운 영역이 있습니다. 다른 프로그래밍 언어의 개발자는 이러한 문제 중 일부와 관련이 있을 수 있지만 이러한 실수는 새로운 Node.js 개발자에게 매우 일반적입니다. 다행히도 그들은 피하기 쉽습니다. 이 짧은 가이드가 초보자가 Node.js에서 더 나은 코드를 작성하고 우리 모두를 위해 안정적이고 효율적인 소프트웨어를 개발하는 데 도움이 되기를 바랍니다.