초간단 웹서버 만들기

Jeris·2023년 5월 5일
0

코드잇 부트캠프 0기

목록 보기
84/107

웹 서버 만들기

http core module

http core module은 HTTP 서버와 클라이언트를 사용할 수 있게 합니다.

Node.js의 HTTP 인터페이스는 사용하기 어려웠던 프로토콜의 많은 기능을 지원하도록 설계되었습니다. HTTP 인터페이스는 전체 요청이나 응답에서 제이터를 전부 버퍼링하지 않도록 주의하여 사용자가 데이터를 스트리밍할 수 있도록 합니다.

HTTP 메시지 헤더는 다음과 같은 객체로 표현됩니다:

{ 'content-length': '123',
  'content-type': 'text/plain',
  'connection': 'keep-alive',
  'host': 'example.com',
  'accept': '*/*' }

키는 소문자로 표시되고 값은 수정되지 않습니다.

지원 가능한 HTTP 애플리케이션의 넓은 스펙트럼을 위해 Node.JS HTTP API는 매우 low-level입니다. 기본적으로 스트림 핸들링과 메시지 구문 분석만 처리합니다. 메시지를 헤더와 바디로 parsing하지만 실제 헤더와 바디를 parsing하진 않습니다. 즉, 메시지를 헤더와 바디를 나눈 후의 처리는 클라이언트가 처리해야 한다는 것입니다.

예시

import http from "http";

const server = http.createServer((request, response) =>
  response.end("<h1>Hello world!</h1>")
);
server.listen(3000);

http.createServer([options][, requestListener])

  • http.Server 서버 객체를 생성하고 반환합니다.
  • requestListener로 요청 처리를 위한 콜백을 받습니다. 이 콜백은 클라이언트로부터 요청이 들어왔을 때 실행됩니다. 콜백은 requestresponse 두 개의 아규먼트를 받습니다.
  • request 객체는 클라이언트로부터 들어온 요청 정보를 담고 있으며, response 객체는 클라이언트에게 보낼 응답 정보를 담고 있습니다.

server.listen([port[, host[, backlog]]][, callback])

  • 생성한 HTTP 서버를 지정한 port와 IP 주소에서 수신 대기 상태로 만드는 메서드입니다.
  • 첫 번째 아규먼트는 서버가 대기할 port이며, 두 번째 아규먼트는 서버가 바인딩할 IP 주소입니다.
  • 콜백을 받을 수도 있습니다. 콜백은 서버가 성공적으로 바인딩 되었을 때 호출됩니다. 이를 통해, 서버가 바인딩되기 전에 다른 작업을 수행할 수 있도록 할 수 있습니다.

socket.end([data[, encoding]][, callback])

  • HTTP 응답(Response)을 종료하는 메서드입니다. 이 메서드를 사용하여 응답을 종료하면, 해당 응답이 완료됨을 클라이언트에게 알리게 됩니다.
  • response body로 데이터를 함께 보낼 수도 있습니다. 이렇게 전달된 데이터는 문자열 또는 버퍼(Buffer) 형태여야 합니다. 전달된 데이터는 클라이언트가 요청한 리소스에 대한 응답 내용으로 사용됩니다.

네트워크 지식

통신 규약(프로토콜, protocol)

프로토콜은 컴퓨터 내부 또는 컴퓨터 간에 데이터를 교환하는 방법을 정의하는 규칙 시스템입니다. 장치 간 통신을 위해서는 장치가 교환되는 데이터 형식에 동의해야 합니다. 형식을 정의하는 규칙 집합을 프로토콜이라고 합니다. 프로토콜에는 ftp, telnet, ssh, pop3, smtp, http, https 등이 있습니다.

특수한 IP 주소, 127.0.0.1

로컬호스트(localhost)를 가리키는 IP 주소입니다. 컴퓨터 자기 자신을 가리키며, 로컬에서 서버 프로그램을 테스트하기 위한 용도로 사용됩니다.

Port

네트워크 상에서 프로그램 간에 데이터를 주고받을 때, 어떤 프로그램과 통신하는지를 식별하기 위한 번호입니다. 0부터 65535까지 사용할 수 있으며, 일반적으로 1024 이하의 포트 번호는 시스템에서 예약되어 있습니다. 예를 들어, HTTP 프로토콜의 경우 80번 포트를 사용하며, HTTPS 프로토콜의 경우 443번 포트를 사용합니다.


도메인 네임과 IP 주소

도메인 네임(Domain Name)

도메인 네임은 DNS(Domain Name System)라고 하는 시스템을 바탕으로 생성되는 문자열 이름으로, 인터넷 세계에 존재하는 수많은 IP 주소를 추상적으로 나타내기 위해 사용되는 개념입니다.

DNS는 계층적인 구조로 되어 있습니다. 최상위에는 루트 도메인(root domain) 서버가 있습니다. 루트 도메인은 인터넷 전체를 나타내는 단위로, 도메인 네임을 입력할 때도 표시하지 않는 경우가 많습니다. 만약 도메인 네임에서 루트 도메인까지 나타내려면 도메인 네임의 오른쪽 끝에 점을 하나 더 찍어주면 됩니다.

루트도메인 바로 하위에는 최상위 도메인(.com, .org, .net 등)을 관리하는 TLD(Top-Level Domain) 서버가 있습니다. TLD는 사이트가 속한 국가 또는 사이트가 제공하는 서비스의 카테고리를 나타냅니다.

그리고 각각의 Top-level Domain 서버 하위에는 SLD(Second-Level Domain) 서버가 있습니다. codeit, naver, daum, google 등 서비스를 대표하는 이름이 바로 Second-Level Domain입니다.

그리고 여기서 한 단계씩 더 내려가서 www.codeit.kr의 www(Third-Level 도메인), test.www.codeit.kr의 test(Fourth-Level 도메인)까지 쭉 도메인이 생겨날 수 있는데요. 하지만 실제 서비스에서는 보통 Third-Level 도메인 정도까지만 사용하는 경우가 많습니다.

예시로, https://www.google.com은 https://172.217.175.100 이라는 IP 주소에 접근하는 도메인입니다.

DNR(Domain Name Resolution)

브라우저에서 도메인 네임만으로도 특정 컴퓨터와 통신할 수 있는 것은 본격적인 통신을 시작하기 전에 도메인 네임을 IP 주소로 변환해주는 절차가 존재하기 때문입니다. 이 과정을 DNR이라고 합니다.

참조

DNR의 순서(로컬 DNS 캐시에 도메인의 IP 주소가 없을 때)

주소창에 www.naver.com을 입력하면 일어나는 일

1단계 : 로컬 컴퓨터는 기본적으로 설정된 네임 서버(Name Server)에 codeit.kr 의 IP 주소를 알려달라는 요청을 보냅니다. 네임 서버라는 건 도메인 네임을 IP 주소로 변환하는 과정에 참여하는 서버들입니다. 내 컴퓨터에서 맨 처음 어떤 네임 서버에 요청할 것인지는 미리 설정되어 있고, 기존의 설정에서 다른 네임 서버로 바꾸는 것도 가능합니다. 내 컴퓨터가 사용하는 네임 서버에 관한 설정은 OS마다 다릅니다.

2, 3단계 : 로컬 컴퓨터의 요청을 받은 네임 서버는 이제 루트 네임 서버(Root Name Server)에 '.kr'로 끝나는 도메인 네임들을 관리하는 네임 서버의 주소를 알려달라는 요청을 보냅니다. 루트 네임 서버는 '.kr' 네임 서버의 IP 주소를 알려줍니다.

4, 5단계 : 네임 서버는 '.kr' 네임 서버에게 'codeit.kr'의 IP 주소를 알려줄 수 있는, 'codeit.kr' 네임 서버의 IP 주소를 알려달라고 요청합니다. '.kr' 네임 서버는 'codeit.kr' 네임 서버의 IP 주소를 알려줍니다.

6, 7단계 : 네임 서버는 'codeit.kr' 네임 서버에게 'codeit.kr'의 IP 주소를 알려달라고 요청합니다. 네임 서버는 'codeit.kr'의 실제 IP 주소를 응답으로 얻게 됩니다.

8단계 : 네임 서버는 로컬 컴퓨터에게 codeit.kr의 IP 주소를 알려주고, 이 IP 주소를 갖고 코드잇 서버와 본격적인 통신을 시작합니다.


URL 해부해보기

const url = new URL(
  "http://example.com/business/mart/item?category=14&id=2965"
);

console.log(url.protocol);  // http:
console.log(url.host);  // example.com
console.log(url.pathname);  // /business/mart/item
console.log(url.search);  // ?category=14&id=2965

위 코드는 Node.js의 URL 클래스를 사용하여 파싱된 URL 정보 중 일부를 출력하는 예시입니다.

해당 코드에서는 http://example.com/business/mart/item?category=14&id=2965라는 문자열을 URL 생성자로 전달하여 url 객체를 생성합니다. 이 URL에는 프로토콜(http), 호스트(example.com), 경로(business/mart/item), 쿼리(category=14&id=2965)가 포함되어 있습니다.

그리고 나서 url.protocol, url.host, url.pathname, url.search 등의 속성을 출력합니다. 이를 통해, URL에서 각각 프로토콜, 호스트, 경로, 쿼리 정보에 접근할 수 있습니다.


포트를 지정 안 했는데도 접속되는 이유

일반적인 URL에서는 포트 번호가 없는데도 문제없이 원하는 페이지에 잘 접속이 되는 건 특정 프로토콜로 통신을 하는 서버 프로그램은 특정 포트 번호를 사용하도록 정해져 있기 때문입니다.

예를 들어, http 프로토콜로 통신하는 서버 프로그램은 포트 번호 80번을, https 프로토콜로 통신하는 서버 프로그램은 포트 번호 443번을 쓰도록 정해져 있습니다.

위 이미지에서 각 프로토콜마다 포트 번호가 할당되어 있다는 걸 알 수 있습니다. 특정 프로토콜 기반으로 통신을 하는 서버 프로그램은 지정된 포트 번호를 갖고 실행되어야 합니다. 그리고 많은 서비스들이 이 규칙을 지켜서 서버 프로그램을 실행하고 있습니다.

사실 http://example.com/business/mart/item?category=14&id=2965http://example.com:80/business/mart/item?category=14&id=2965라고 써야 정확한 URL입니다. 하지만 포트 번호를 생략해도 맨 앞의 http, https 같은 프로토콜 정보를 보고 브라우저는 자동으로 그에 맞는 포트 번호를 설정하고 서버에 접속을 시도합니다.

http 프로토콜로 통신하는 서버 프로그램을 80번이 아닌 포트에서 실행하고, 브라우저에서 접속을 해도 문제가 생기지는 않습니다. http 프로토콜 기반으로 통신하는 서버 프로그램은 포트 번호 80번을 사용하라는 것은 일종의 약속일 뿐 다른 포트 번호를 사용해도 기술적으로는 아무런 문제가 없기 때문입니다.

다만, 서버 프로그램은 다른 사람들이 외부에서 접속할 때 보통 포트 번호 없는 URL로 접속하는 경우가 많기 때문에, 실제 서비스를 배포할 때에는 정해진 포트 번호를 사용해야 합니다.


라우팅해보기

라우팅(Routing)

라우팅은 네트워크 또는 여러 네트워크 간 또는 여러 네트워크 간의 트래픽 경로를 선택하는 프로세스입니다. 웹 개발에서의 라우팅은 클라이언트 요청에 따라 서버에서 적절한 처리를 수행하는 것을 의미합니다. 보통 웹 애플리케이션에서는 클라이언트의 HTTP 요청 메서드와 URL 정보를 기반으로 서버에서 적절한 처리를 수행합니다.

Node.js로 라우팅해보기

import http from "http";

const users = ["Tom", "Andy", "Jessica", "Paul"];

const server = http.createServer((request, response) => {
  if (request.url === "/") {
    response.end("<h1>Welcome!</h1>");
  } else if (request.url === "/users") {
    response.end("<h1>" + users + "</h1>");
  } else {
    response.end("<h1>Page Not Available");
  }
});

server.listen(3000);

request.url은 hostname 까지의 부분을 생략하고 path 이후 부분부터의 url을 리턴합니다. 그리고 브라우저에서 root("/") URL을 요청할 때 슬래시 기호를 생략하면, 대부분의 브라우저는 자동으로 슬래시 기호를 붙여서 요청합니다.


http 모듈로 라우팅할 때의 불편함

import http from "http";

const users = ["Tom", "Andy", "Jessica", "Paul"];

const server = http.createServer((request, response) => {
  if (request.url === "/") {
    response.end("<h1>Welcome!</h1>");
  } else if (request.url === "/users") {
    response.end(`<h1>${users}/h1>`);
  } else if (request.url.split("/")[1] === "users") {
    const userIdx = request.url.split("/")[2];
    const userName = users[userIdx - 1];
    response.end(`<h1>${userName}</h1>`);
  } else {
    response.end("<h1>Page Not Available");
  }
});

server.listen(3000);

이렇게 http 모듈로 라우팅을 구현할 경우

  • 라우팅 로직이 길어질 경우 코드가 복잡해집니다.
  • 각 요청에 대해 직접 URL을 파싱하여 처리해야 합니다.
  • 라우팅 처리에 대한 중복 코드가 많아지고 유연성이 떨어집니다.

위와 같은 단점들이 있습니다. 이러한 문제들을 해결하기 위해 보통은 Express와 같은 외부 라이브러리를 사용하여 라우팅을 구현합니다.


express 모듈로 하는 더 세련된 라우팅

Express.js

Express.js는 Node.js의 서드 파티 모듈으로, 웹 어플리케이션 및 RESTful API를 쉽게 개발할 수 있도록 도와줍니다. 미들웨어 구조를 채택하여 다양한 기능들을 제공하며, 라우팅과 HTTP 요청 처리를 단순화하여 개발자가 보다 쉽게 웹 어플리케이션을 만들 수 있도록 합니다. 또한, Express.js는 다양한 템플릿 엔진과 함께 사용 가능하며, Node.js 생태계 내에서 가장 인기 있는 프레임워크 중 하나입니다.

더 자세한 내용은 다음 Express.js 강의에서 다룰 예정입니다.

예시

import express from "express";

const app = express();

const users = ["Tom", "Andy", "Jessica", "Paul"];

app.get("/", (request, response) => {
  response.end("<h1>Welcome!</h1>");
});

app.get("/users", (request, response) => {
  response.end(`<h1>${users}</h1>`);
});

app.get("/users/:id", (request, response) => {
  const userName = users[request.params.id - 1];
  response.end(`<h1>${userName}</h1>`);
});

app.get("*", (request, response) => {
  response.end("<h1>Page Not Available</h1>");
});

app.listen(3000);

Express를 사용하면 if ... else 문 없이도 라우팅을 할 수 있습니다. 또, :id같은 동적인 URL 파라미터를 request.params.id으로 접근할 수 있게 해주고 와일드 카드 문자열(*)도 사용할 수 있게 해줍니다.


Reference

profile
job's done

0개의 댓글