개인프로젝트를 진행하다보면 100% 프론트엔드만으로 구현할 수 없을 때가 종종있다. 그래서 간단한 서버를 만들어 구현을 하곤 했는데 이때 express를 선택했다.

왜 express를 선택했을까?

이유는 간단했다. 내가 가장 익숙한 언어인 Javascript로 간단히 서버를 구축할 수 있기 때문이다.

이런 단순한 이유로 그동안 간단히 서버를 구축해야할때 express를 선택했다.

그러던 중 이번에도 간단히 서버를 구축해야하는 경우가 생겨 express로 서버를 구축할려고 했다.

하지만, 이왕 express를 자주 사용하니 제대로 공부해보기로 결정했다.


Node란?

Express에 대해 이야기하기 전에 우선 Node에 대해 알아보자

특징

다른 웹 서버와 다르게 싱글 스레드를 이용한다.

싱글 스레드를 사용한다는게 언뜻 보면 퇴보한 것처럼 보일 수 있으나 이는 무척 영리한 결정이었다.

단일 스레드는 웹, 앱을 만드는 작업을 단순화하며 앱에서 멀티 스레드 성능이 필요하다면 노드 인스턴스를 늘리기만 해도 멀티 스레드의 성능을 누릴 수 있다.

이는 눈속임처럼 들린다고 생각할 수 있지만 결국 서버 병렬화를 통해 멀티 스레드를 모방한다는 것이다.

또한, 클라우드 컴퓨팅이 대세가 되어 서버를 범용 자원으로 간주하는 사고방식이 늘어나는 현 상황에도 더 잘 들어맞다.

컴파일 과정이 포함되지 않으므로 유지보수와 배포 과정 단순

앱을 작성하는 관점에서 본다면 노드 앱은 .net이나 자바 앱 보다 PHP나 루비 앱에 더 가깝다.

노드가 사용하는 Javascript 엔진(V8)이 Javascript를 C나 C++과 비슷하게 네이티브 컴퓨터 코드로 컴파일하긴 하지만 이 과정은 사용자에게 노출되지 않는다.

그렇기에 사용자가 보기에는 순수한 인터프리터 언어와 다를 것이 없다.

따로 컴파일하는 과정에 포함되지 않으므로 유지보수와 배포 과정은 단순해진다.

즉, Javascript 파일을 업데이트하기만 하면 나머지는 자동으로 적용이 된다.

인터프리터: 프로그래밍 언어의 소스 코드를 바로 실행하는 컴퓨터 프로그램 또는 환경

완적히 플랫폼 독립적

Node는 윈도우나 맥OS, 리눅스 같은 주요 운영체제에 쉽게 설치할 수 있고 협업하기도 쉽다.

운영하는 사람들의 PC의 운영체제가 각기 다르다면 이를 맞추기 위한 작업이 필수로 존재해야할 것이다.

하지만 Node의 경우 플랫폼 독립적인 특징을 갖고 있어 이런 불필요한 작업이 전혀 필요가 없고 어떤 운영체제에서든 서버를 설치하고 동작하게 만들 수 있다.

노드 시작하기

노드, npm 설치는 더 자세히 설명해주는 글이 많기에 이는 생략하겠다.

참고: Node.js 설치 - 위키 독스


Hello World!

모든 프로그래밍 예제를 보여줄때 Hello world를 보여주는 관습이 있듯이 node로 hello world를 만들어 보자

const http = require("http");
const port = process.env.PORT || 3000;

const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("Hello world!");
});

server.listen(port, () =>
  console.log(
    `포트번호: ${port}`
  )
);

위 코드를 작성하고 콘솔창에 node {파일명}.js를 입력하면 화면에 콘솔창에 포트번호: ${port} 문구가 생긴다.

그리고 http://localhost:3000 으로 이동하면 hello world가 화면에 띄워진걸 확인할 수 있다.

content type을 text/plain이 아닌 text/html로 바꾸고 hello world가 아닌 HTML을 전송이 해볼 수도 있다.


이벤트 주도 프로그래밍

노드의 배경이 되는 철학은 이벤트 주도 프로그래밍 이다.

이벤트 주도 프로그래밍은 어떤 이벤트가 발생할지, 그 이벤트에 대해 어떻게 반응해야 할지 프로그래머가 이해해야 한다는 뜻이다.

이벤트 주도 프로그래밍을 설명할 때는 대개 사용자 인터페이스를 통해 설명한다.

이벤트가 서버에서 일어난다는 점은 낯선 개념이지만, 기본 발상은 동일하다.

위의 예제에서 http.createServer 메서드는 HTTP 요청이 일어날 때마다 이 콜백함수를 호출한다.

이것도 이벤트 주도 프로그래밍의 일종이다.

또한, 사용자가 애플리케이션의 한 영역에서 다른 영역으로 이동하는 것도 이벤트이다. 이런 내비게이션 이벤트에 애플리케이션이 어떻게 반응할지 저하는것을 라우팅이라고 부른다.


라우팅

라우팅은 클라이언트가 요청한 콘텐츠를 전송하는 매커니즘을 가리킨다.

클라이언트는 콘텐츠를 경로와 쿼리스트링으로 요청한다.

위의 예제에서 페이지를 더 만들어 보자.

const http = require("http");
const port = process.env.PORT || 3000;

const server = http.createServer((req, res) => {
  // 쿼리 스트링, 옵션인 마지막 슬래시를 없애고 소문자로 바꿔서 url을 정규화한다.
  const path = req.url.replace(/\/?(?:\?.*)?$/, "").toString();

  switch (path) {
    case "":
      res.writeHead(200, { "Content-Type": "text/plain" });
      res.end("Home");
      break;
    case "/about":
      res.writeHead(200, { "Content-Type": "text/plain" });
      res.end("About");
      break;
    default:
      res.writeHead(200, { "Content-Type": "text/plain" });
      res.end("Not Found");
      break;
  }
});

server.listen(port, () => console.log(`포트번호: ${port}`));

정규식 설명
  • \/ : /를 선택하기 위해선 \이 필수
  • \/?: /가 0번 또는 1번 나타날 수 있음을 나타냄
  • (?:): 그룹화 캡쳐X (캡쳐는 일종의 복사본을 생성하는 개념 즉, 복사본이 생성되지 않음)
  • (?:\?.*): ?를 선택하기 위해 \이 필수, ?뒤의 모든 문자열이 있거나 없거나
  • (?:..)?: 이런 그룹이 있거나 없거나(최대 한개)
  • $: 앞의 문자열로(그룹으로) 끝남

즉, /about?age=20 => ?age=20이 선택됨


정적 자원 전송

이제 실제 HTML과 이미지를 전송해보자

우선 정적 파일들을 보관할 public 폴더를 만들자.

해당 폴더에 필요한 html 파일들을 만들고 img 이름의 서브 폴더도 만들자.

그럼 다음과 같은 폴더 구성이 될 것이다.

파일 구성

그럼 이제 아까 파일에서 더 html 파일과 이미지를 전송하는 코드를 추가해보자

const http = require("http");
const fs = require("fs");
const port = process.env.PORT || 3000;

const serveStaticFile = (res, path, contentType, responseCode = 200) => {
  fs.readFile(__dirname + path, (err, data) => {
    if (err) {
      res.writeHead(500, { "Content-Type": "text/plain" });
      return res.end("500 - Internal Error");
    }
    res.writeHead(responseCode, { "Content-Type": contentType });
    res.end(data);
  });
};

const server = http.createServer((req, res) => {
  const path = req.url.replace(/\/?(?:\?.*)?$/, "").toString();
  switch (path) {
    case "":
      serveStaticFile(res, "/public/home.html", "text/html");
      break;
    case "/about":
      serveStaticFile(res, "/public/about.html", "text/html");
      break;
    case "/img/logo.png":
      serveStaticFile(res, "/public/img/logo.png", "image/png");
      break;
    default:
      serveStaticFile(res, "/public/404.html", "text/html");
      break;
  }
});

server.listen(port, () => console.log(`포트번호: ${port}`));

fs.readFile은 파일을 비동기적으로 읽는 메서드이다.

동기적으로 읽는 fs.readFileSync도 있다.

__dirname은 현재 실행 중인 스크립트가 존재하는 디렉터리이다.

/ 접속

/

/about 접속

/about

/404 접속

/404

/img/logo.png 접속

/img/logo.png


참고 자료

댓글남기기