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