본문 바로가기
BackEnd

[Node] Exrpess란, Express를 사용하는 이유

by SoriKim 2023. 11. 3.
반응형

1. Express란? 

Express는 웹 및 모바일 애플리케이션을 위한 일련의 강력한 기능을 제공하는 간결하고 유연한 Node.js 웹 애플리케이션 프레임 워크로 Node.js 기반 HTTP API 서버 프로그램을 보다 쉽고 빠르게 개발하고, 유지/보수 할 수 있게 해줍니다. 

 

Express.js, Nest.js, Koa.js 등과 같이 Node.js와 호환 될 수 있는 Server-side Framework들이 무수히 많기 때문에 Node.js 기반으로 서버를 개발한다는 것은 자유도가 높고 무한한 확장 가능성을 이야기 합니다. 

Node 개발자는 npm에 등록 되어있는 node package(라이브러리 or 프레임워크)에 대한 늘 주기적인 관심을 기울어야 합니다. 국내 뿐 아닌 해외 현업 개발시장에 어떤 것들이 활발히 사용되고 왜 인기를 끄는지 이해할 필요가 있으며 이런 동향은 React conference, GraphQL conference, JavaScript conference 등 컨퍼런스 발표영상을 보면 크게 도움이 됩니다. 

 

2.  HTTP API Server에 Express 프레임워크가 없다면?

 

Express가 없다면 웹 서버 프로그램을 어떻게 개발해야 할까요?

하나의 디렉토리를 생성하여 설명하겠습니다. 

먼저 test-backend 라는 명칭의 디렉토리를 생성해 해당 디렉토리 내부에 터미널에서 npm init -y 명령어를 입력해 프로젝트를 시작합니다.

mkdir test-backend 
cd test-backend
npm init -y

 

test-backend 디렉토리 내부에 아래와 같은 파일을 생성합니다. 

// withoutServerExpress.js
const http = require('http') 

const server = http.createServer((req, res) => { 
  console.log('request received')

  res.setHeader('Content-Type', 'application/json')
  res.end(JSON.stringify({ message: "Welcome to http server without express!" }))
});

server.listen(3000, "127.0.0.1", () => {
  console.log('server is running on PORT 3000!')
})

코드 설명 

1. Node.js 내장 http 모듈을 가져와 아래에서 사용할 수 있도록 변수에 담습니다. 

2. http.createServer라는 메소드는 인자로 또 다른 함수를 받습니다 (callback 함수). 인자로 받은 함수의 첫 번째 인자는 http request의 정보가 담겨있는 객체, 두 번째 인자는 http response 객체입니다. 서버에 요청이 들어오면 이 인자로 받은 함수가 실행됩니다.

3. 요청에 대한 응답의 header를 application/json 형태로 세팅합니다.

4. res.end 함수를 통해 요청에 대한 응답을 마무리하며 함수의 인자로 넘겨주는 값이 클라이언트가 받는 응답이 됩니다.

5. server는 앞서 생성한 서버를 의미하며 이 서버 객체의 listen함수는 인자로 포트번호와 콜백함수를 받습니다. 포트번호로 서버를 연다는 의미이며, 서버가 실행될 때의 로직을 콜백함수 안에서 처리할 수 있습니다. 보통 서버가 켜져있다는 로그 메시지를 남깁니다. 

6. Port 번호에 해당하는 3000은 Express 프레임워크에 공식적으로 지정한 default Port번호 입니다. 하지만 늘 고정된 값이 아니며 3000 이외의 번호로도 언제든 서버를 열 수 있으니 다양하게 test 해보셔도 됩니다. 

 

위의 코드를 실행하기 위해 파일이 있는 디렉토리 안에서 터미널을 열어 node withoutServerExpress.js의 명령어를 입력하면 server id running on PORT 300! 이라고 뜬다면 첫 노드 서버를 개설한 것입니다. 

 

서버가 정상적으로 열렸다면 요청과 응답을 받아 볼 차례입니다. 웹 브라우저를 하나 띄운 후(위의 코드가 실행 중인 상태에서 브라우저를 띄워야 합니다.) 주소창에 localhost:3000 이라고 입력해보세요. 그 후 해당 브라우저에 { message: "Welcome to http server without express!" }라고 뜬다면 성공입니다. 

 

브라우저 외 다양한 클라이언트 툴을 사용해 http server에 요청을 보낼 수 있으며 보통 개발 환경에서는 httpie 혹은 postman을 활용합니다. 

 

httpie를 사용해 위에서 실행한 http server에 요청을 보내보겠습니다. 

# httpie 라는 터미널 전용 http client 프로그램이 설치되어있어야 합니다. 
# 각각의 환경에 맞게 아래 명령어를 통해서 httpie 를 설치하세요.

# mac 환경
$ brew install httpie
# ubuntu 환경 
$ apt-get install httpie
# or
$ sudo apt-get install httpie

# 성공적으로 설치가 끝난후 아래 명령어를 터미널 창에서 입력 해 보세요.
http localhost:3000


위의 코드와 같이 입력하고 나서 아래와 같이 응답이 뜬다면 서버가 성공적으로 작동하고 있는 것입니다. 

[📍 httpie를 실행하기 위해서도 위의 브라우저에 서버를 띄웠던 것과 동일하게 node withoutServerExpress.js 명령어를 입력해 서버가 실행되고 있는 상태여야 합니다. ]

 

현재까지는 단순히 Res 요청을 받고 응답을 받았지만 우리의 어플리케이션 등은 점점 더 규모가 커질 겁니다. 유저를 회원가입도, 로그인도, 프론트엔드 측에서 요구하는 다양한 정보도 응답으로 보내주고 처리해야 합니다. 이렇게 해당 자원에 대해 다른 함수(로직)을 실행하도록 하는 것을 Routing(라우팅)이라고 합니다. Express와 같은 웹 프레임워크를 적용하지 않은 앱의 코드가 얼만큼이나 복잡해질 수 있는지 아래 예시를 통해 확인해보겠습니다. 

 

withoutServerExpress.js 파일에 회원임을 나타내는 users와 게시물을 의미하는 posts라는 변수에 서버 통신 시 사용할 데이터를 담습니다. 

// withoutServerExpress.js

const users = [
  {
    id: 1,
    name: "Rebekah Johnson",
    email: "Glover12345@gmail.com",
    password: "123qwe",
  },
  {
    id: 2,
    name: "Fabian Predovic",
    email: "Connell29@gmail.com",
    password: "password",
  },
];

const posts = [
  {
    id: 1,
    title: "간단한 HTTP API 개발 시작!",
    content: "Node.js에 내장되어 있는 http 모듈을 사용해서 HTTP server를 구현.",
    userId: 1,
  },
  {
    id: 2,
    title: "HTTP의 특성",
    content: "Request/Response와 Stateless!!",
    userId: 1,
  },
];

이 후, request 객체에서 url과 method에 따라 각각의 경우 조건문으로 분기하여 라우팅을 진행합니다. 

앱에서 의도한 다양한 로직(회원가입, 로그인 등)을 처리해주어야 하며 라우팅은 아래의 내용과 같이 http method별로 GET, POST, PUT, DELETE등의 내용을 적절한 로직으로 안내해줌으로써 이루어 집니다. 이 때 express를 사용하지 않고 순수 node.js 기반해 코드를 작성하면 다양한 로직들이 if-elif의 연쇄 중첩 적용으로 이루어지게 되며 해당 구조 자체가 불필요하게 복잡해진다는 것을 느끼게 됩니다. 

 

// withoutServerExpress.js

const http = require("http");
const server = http.createServer((request, response) => {
  const { url, method } = request;

  if (method === "GET") {
    if (url === "/ping") {
      response.writeHead(200, { "Content-Type": "application/json" });
      response.end(JSON.stringify({ message: "pong" }));
    } else if (url === "/users") {
      response.writeHead(200, { "Content-Type": "application/json" });
      response.end(JSON.stringify({ users: users }));
    } else if (url.startsWith("/users")) {
      const userId = parseInt(url.split("/")[2]);
      const user = users.find((user) => user.id === userId);

      response.writeHead(200, { "Content-Type": "application/json" });
      response.end(JSON.stringify({ user: user }));
    } else if (url === "/posts") {
      response.writeHead(200, { "Content-Type": "application/json" });
      response.end(JSON.stringify({ posts: posts }));
    }
  } else if (method === "POST") {
    if (url === "/users") {
      let body = "";

      request.on("data", (data) => {
        body += data;
      });
      request.on("end", () => {
        const user = JSON.parse(body);

        users.push({
          id: user.id,
          name: user.name,
          email: user.email,
          password: user.password,
        });

        response.end("ok");
      });
    } else if (url === "/posts") {
      let body = "";

      request.on("data", (data) => {
        body += data;
      });
      request.on("end", () => {
        const post = JSON.parse(body);

        posts.push({
          id: post.id,
          name: post.title,
          content: post.content,
        });

        response.end("ok");
      });
    }
  } else if (method === "PATCH") {
    if (url.startsWith("/posts")) {
      let body = "";

      request.on("data", (data) => {
        body += data;
      });

      request.on("end", () => {
        const inputPost = JSON.parse(body);
        const postId = parseInt(url.split("/")[2]);
        const post = posts.find((post) => post.id === postId)

        post.title = inputPost.title;
        post.content = inputPost.content;

        response.writeHead(200, { "Content-Type": "application/json" });
        response.end(
          JSON.stringify({
            id: post.id,
            title: post.title,
            content: post.content,
          })
        );
      });
    }
  } else if (method === "DELETE") {
    if (url.startsWith("/posts")) {
      const postId = parseInt(url.split("/")[2]);
      const post = posts.find((post) => post.id === postId);
      delete post;

      response.writeHead(204, { "Content-Type": "application/json" });
      response.end(
        JSON.stringify({
          message: "NO_CONTENT",
        })
      );
    }
  }
});

server.listen(3000, "127.0.0.1", () {
  console.log("Listening to requests on port 3000");
});

만일 위와 같이 간단한 로직을 실행하는 앱이 아닌, 큰 규모의 서비스를 제공해야 한다면 수행해야 하는 분기처리와 소스코드의 양이 배 이상으로 늘어날 것입니다. 또한 서버를 실행하는 함수 안에 수많은 조건문과 로직을 모듈화 하는 데 있어서 불필요한 큰 수고를 들이게 됩니다. 이런 불편함을 해소하기 위해 탄생한 것이 Express와 같은 프레임워크 입니다. 

 

3. Express 적용

Express는 Node 개발자들이 다수 채택하는 프레임워크로 앞서 언급한 라우팅과 로직의 모듈화를 위해 사용됩니다. 맨 처음에 설명한 것과 같이 Express가 개발자로 하여금 더욱 더 읽기 쉽고 유연하며 지속가능한 백엔드 앱을 개발할 수 있게끔 돕는 도구라는 것을 의미합니다. 

 

위에서 작성한 코드에 엔드포인트를 Express를 적용해 구현한다면 아래와 같은 코드로 작성될 수 있습니다. 

// withoutServerExpress.js

const modularizedFunctions = require('./modularizedFunctions.js')
const express = require('express')
const app = express()

app.use(express.json())

//get 요청 처리 라우팅
app.get('/ping', (req, res) => {res.json({ message: '/pong'})})
app.get('/users', modularizedFunctions.getUsers)
app.get('/users/:userId', modularizedFunctions.getUserByUserId)
app.get('/posts', modularizedFunctions.getPosts)

//post 요청 처리 라우팅
app.post('/users', modularizedFunctions.createUser)
app.post('/posts', modularizedFunctions.createPost)

//patch 요청 처리 라우팅
app.patch('/posts/:postId', modularizedFunctions.updatePost)

//delete 요청 처리 라우팅
app.delete('/posts/:postId', modularizedFunctions.deletePost)

app.listen(3000, "127.0.0.1", function() {
  console.log('listening on port 3000')
})

 

// modularizedFunctions.js

// 먼저 서버통신시 가공할 데이터를 정의합니다.
const users = [
    {
      id: 1,
      name: "Rebekah Johnson",
      email: "Glover12345@gmail.com",
      password: "123qwe",
    },
    {
      id: 2,
      name: "Fabian Predovic",
      email: "Connell29@gmail.com",
      password: "password",
    },
  ];
  
const posts = [
{
    id: 1,
    title: "간단한 HTTP API 개발 시작!",
    content: "Node.js에 내장되어 있는 http 모듈을 사용해서 HTTP server를 구현.",
    userId: 1,
},
{
    id: 2,
    title: "HTTP의 특성",
    content: "Request/Response와 Stateless!!",
    userId: 1,
},
];
  

// 앞서 express 없이 작성한 sendPosts 함수와 비교했을 때,
// express 덕분에 JSON.stringify 함수를 별도로 사용할 필요없이
// response 객체의 json 메소드를 활용합니다.

const getUsers = (req, res) => {
    res.json({ users })
  }

const getUserByUserId = (req, res) => {
    const userId = req.params.userId
    const user = users.find((user) => user.id == userId)
    res.json({ user })
}  
  
const getPosts = (req, res) => {
    res.json({ posts })
  }

const createUser = (req, res) => {
    const user = req.body
    const newUser = users.push({
        id: user.id,
        name: user.name,
        email: user.email,
        password: user.password,
      });  
    res.json({ message: 'created!', 'user_id' : newUser })
}

const createPost = (req, res) => {
    const post = req.body
    const newPost = posts.push({
        id: post.id,
        title: post.title,
        content: post.content,
      });
    res.json({ message: 'created!', 'post_id' : newPost })
}

const updatePost = (req, res) => {
    const inputPost = req.body
    const postId = req.params.postId
    const post = posts.find((post) => post.id == postId)

    post.title = inputPost.title;
    post.content = inputPost.content;

    res.json({ message: 'updated!', 'updatedPost' : post })
}

const deletePost = (req, res) => {
    const postId = req.params.postId
    const indexOfPostId = posts.findIndex((post) => post.id == postId)

    delete posts[indexOfPostId]

    res.json({ message: 'deleted!'})
}

// serverWithExpress.js 에서 사용하기 위해 모듈로 내보냅니다.
module.exports = {
    getUsers,
    getUserByUserId,
    getPosts,
    createUser,
    createPost,
    updatePost,
    deletePost
};

 

Express를 적용하기 전/후의 가장 큰 차이점은 라우팅 분리, 로직의 모듈화라고 볼 수 있습니다. 

node.js의 http모듈을 이용해 만든 코드의 경우 if-elif의 중첩 적용으로 인해 코드가 불필요하게 복잡해지며 가독성도 많이 떨어지게 됩니다. 하지만 Express를 이용하여 구현한 코드의 경우 기능별로 별도 파일로 관리할 수 있다는 사실을 확인할 수 있었으며 추후 더욱 복잡해질 디자인/아키텍쳐 패턴을 적용하는데 기본 원리로 적용될 것입니다. 

 

반응형

댓글