Node.js 웹 프레임워크는 실제로 어떻게 작동할까요? (Express.js & Next.js 내부)
Grace Collins
Solutions Engineer · Leapcell

Node.js HTTP 모듈을 사용하여 웹 프레임워크를 작성하는 방법
Node.js로 웹 애플리케이션을 개발할 때 http
모듈은 기본적인 핵심 구성 요소입니다. 이 모듈을 사용하면 몇 줄의 코드만으로 HTTP 서버를 시작할 수 있습니다. 다음으로 http
모듈을 사용하여 간단한 웹 프레임워크를 작성하고 HTTP 요청이 도착하여 응답하는 전체 프로세스를 이해하는 방법을 자세히 살펴보겠습니다.
HTTP 서버 시작
다음은 HTTP 서버를 시작하기 위한 간단한 Node.js 코드 예제입니다.
'use strict'; const { createServer } = require('http'); createServer(function (req, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello World\n'); }) .listen(3000, function () { console.log('Listening on port 3000') });
위 코드를 실행할 때 터미널에서 curl localhost:3000
명령을 사용하면 서버에서 반환된 Hello World
메시지를 볼 수 있습니다. 이는 Node.js가 소스 코드에서 많은 세부 사항을 캡슐화했으며 주요 코드는 lib/_http_*.js
와 같은 파일에 저장되어 있기 때문입니다. 다음으로 HTTP 요청이 도착하여 응답하는 소스 코드 구현을 자세히 살펴보겠습니다.
HTTP 요청 처리
서버 인스턴스 생성
Node.js에서 HTTP 요청을 수신하려면 먼저 http.Server
클래스의 인스턴스를 생성하고 해당 request
이벤트를 수신해야 합니다. HTTP 프로토콜은 애플리케이션 계층에 있고 기본 전송 계층은 일반적으로 TCP 프로토콜을 사용하므로 net.Server
클래스는 http.Server
클래스의 부모 클래스입니다. 특정 HTTP 관련 부분은 net.Server
클래스의 인스턴스의 connection
이벤트를 수신하여 캡슐화됩니다.
// lib/_http_server.js // ... function Server(requestListener) { if (!(this instanceof Server)) return new Server(requestListener); net.Server.call(this, { allowHalfOpen: true }); if (requestListener) { this.addListener('request', requestListener); } // ... this.addListener('connection', connectionListener); // ... } util.inherits(Server, net.Server);
요청 데이터 구문 분석
이때 TCP를 통해 전송된 데이터를 구문 분석하려면 HTTP 파서가 필요합니다.
// lib/_http_server.js const parsers = common.parsers; // ... function connectionListener(socket) { // ... var parser = parsers.alloc(); parser.reinitialize(HTTPParser.REQUEST); parser.socket = socket; socket.parser = parser; parser.incoming = null; // ... }
파서 parser
는 "풀"에서 얻어진다는 점에 유의해야 하며, 이 "풀"은 free list
데이터 구조를 사용합니다. 그 목적은 생성자에 대한 빈번한 호출로 인한 성능 저하를 방지하기 위해 파서를 최대한 재사용하는 것이며, 개수에 대한 상한(http 모듈에서는 1000)도 있습니다.
// lib/freelist.js 'use strict'; exports.FreeList = function(name, max, constructor) { this.name = name; this.constructor = constructor; this.max = max; this.list = []; }; exports.FreeList.prototype.alloc = function() { return this.list.length ? this.list.pop() : this.constructor.apply(this, arguments); }; exports.FreeList.prototype.free = function(obj) { if (this.list.length < this.max) { this.list.push(obj); return true; } return false; };
데이터가 TCP를 통해 지속적으로 전송되므로 파서는 Node.js의 핵심 아이디어에 따라 이벤트 기반으로 작동합니다. http-parser
라이브러리가 사용됩니다.
// lib/_http_common.js // ... const binding = process.binding('http_parser'); const HTTPParser = binding.HTTPParser; const FreeList = require('internal/freelist').FreeList; // ... var parsers = new FreeList('parsers', 1000, function() { var parser = new HTTPParser(HTTPParser.REQUEST); // ... parser[kOnHeaders] = parserOnHeaders; parser[kOnHeadersComplete] = parserOnHeadersComplete; parser[kOnBody] = parserOnBody; parser[kOnMessageComplete] = parserOnMessageComplete; parser[kOnExecute] = null; return parser; }); exports.parsers = parsers; // lib/_http_server.js // ... function connectionListener(socket) { parser.onIncoming = parserOnIncoming; }
수신부터 전체 구문 분석까지 완료된 HTTP 요청은 파서에서 다음 이벤트 리스너를 순서대로 통과합니다.
parserOnHeaders
: 들어오는 요청 헤더 데이터를 지속적으로 구문 분석합니다.parserOnHeadersComplete
: 요청 헤더가 구문 분석된 후header
객체를 구성하고 요청 본문에 대한http.IncomingMessage
인스턴스를 만듭니다.parserOnBody
: 들어오는 요청 본문 데이터를 지속적으로 구문 분석합니다.parserOnExecute
: 요청 본문이 구문 분석된 후 구문 분석에 오류가 있는지 확인합니다. 오류가 있으면clientError
이벤트를 직접 트리거합니다. 요청이CONNECT
메서드를 사용하거나Upgrade
헤더가 있는 경우connect
또는upgrade
이벤트를 직접 트리거합니다.parserOnIncoming
: 구문 분석된 특정 요청을 처리합니다.
request
이벤트 트리거
다음은 최종 request
이벤트 트리거를 완료하는 parserOnIncoming
리스너의 핵심 코드입니다.
// lib/_http_server.js // ... function connectionListener(socket) { var outgoing = []; var incoming = []; // ... function parserOnIncoming(req, shouldKeepAlive) { incoming.push(req); // ... var res = new ServerResponse(req); if (socket._httpMessage) { // If true, it means the socket is being occupied by a previous ServerResponse instance in the queue outgoing.push(res); } else { res.assignSocket(socket); } res.on('finish', resOnFinish); function resOnFinish() { incoming.shift(); // ... var m = outgoing.shift(); if (m) { m.assignSocket(socket); } } // ... self.emit('request', req, res); } }
동일한 socket
에서 보낸 요청의 경우 소스 코드는 각각 IncomingMessage
인스턴스 및 해당 ServerResponse
인스턴스를 캐시하는 데 사용되는 두 개의 큐를 유지 관리하는 것을 알 수 있습니다. 이전 ServerResponse
인스턴스가 먼저 소켓
을 점유하고 해당 finish
이벤트를 수신합니다. 이벤트가 트리거되면 해당 큐에서 ServerResponse
인스턴스와 해당 IncomingMessage
인스턴스가 해제됩니다.
HTTP 요청에 응답
응답 단계에서는 작업이 비교적 간단합니다. 들어오는 ServerResponse
는 이미 socket
을 얻었습니다. http.ServerResponse
는 내부 클래스인 http.OutgoingMessage
에서 상속됩니다. ServerResponse#writeHead
를 호출하면 Node.js는 헤더 문자열을 함께 연결하여 ServerResponse
인스턴스의 _header
속성에 캐시합니다.
// lib/_http_outgoing.js // ... OutgoingMessage.prototype._storeHeader = function(firstLine, headers) { // ... if (headers) { var keys = Object.keys(headers); var isArray = Array.isArray(headers); var field, value; for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i]; if (isArray) { field = headers[key][0]; value = headers[key][1]; } else { field = key; value = headers[key]; } if (Array.isArray(value)) { for (var j = 0; j < value.length; j++) { storeHeader(this, state, field, value[j]); } } else { storeHeader(this, state, field, value); } } } // ... this._header = state.messageHeader + CRLF; }
바로 직후 ServerResponse#end
를 호출하면 헤더 문자열 뒤에 데이터를 연결하고 해당 꼬리를 추가한 다음 TCP 연결에 씁니다. 특정 쓰기 작업은 내부 메서드 ServerResponse#_writeRaw
에 있습니다.
// lib/_http_outgoing.js // ... OutgoingMessage.prototype.end = function(data, encoding, callback) { // ... if (this.connection && data) this.connection.cork(); var ret; if (data) { this.write(data, encoding); } if (this._hasBody && this.chunkedEncoding) { ret = this._send('0\r\n' + this._trailer + '\r\n', 'binary', finish); } else { ret = this._send('', 'binary', finish); } if (this.connection && data) this.connection.uncork(); // ... return ret; } OutgoingMessage.prototype._writeRaw = function(data, encoding, callback) { if (typeof encoding === 'function') { callback = encoding; encoding = null; } var connection = this.connection; // ... return connection.write(data, encoding, callback); };
결론
이 시점에서 TCP를 통해 클라이언트에 요청이 다시 전송되었습니다. 이 기사에서는 주요 처리 흐름만 살펴봅니다. 실제로 Node.js 소스 코드는 시간 초과 처리, 소켓
이 점유된 경우의 캐싱 메커니즘, 특수 헤더 처리, 업스트림 문제에 대한 대응, 보다 효율적인 쓰기 헤더 쿼리 등과 같은 더 많은 상황도 고려합니다. 이러한 세부 사항은 모두 심층적인 연구와 학습의 가치가 있습니다. http
모듈 소스 코드를 분석하면 강력한 웹 프레임워크를 구축하기 위해 이를 사용하는 방법을 더 잘 이해할 수 있습니다.
Leapcell: 최고의 서버리스 웹 호스팅
마지막으로 Go 서비스를 배포하는 데 가장 적합한 플랫폼인 **Leapcell**을 추천하고 싶습니다.
🚀 좋아하는 언어로 빌드
JavaScript, Python, Go 또는 Rust로 간편하게 개발하십시오.
🌍 무제한 프로젝트 무료 배포
사용한 만큼만 지불하십시오. 요청도 없고 요금도 없습니다.
⚡ 사용량에 따라 지불, 숨겨진 비용 없음
유휴 요금 없이 원활한 확장성만 제공합니다.
🔹 Twitter에서 팔로우하세요: @LeapcellHQ