Node.js로 웹 개발하기 - 02. Express.js

2024. 12. 28. 16:51Programming Language/Node.js

반응형

01. Express를 사용하는 이유

Express.jsNode.js를 위한 웹 애플리케이션 프레임워크로, 웹 서버를 쉽고 빠르게 구축할 수 있도록 도와주는 라이브러리이다.

Express는 특히 RESTful API나 웹 애플리케이션의 백엔드 부분을 구축할 때 많이 사용된다.

Express는 쉽게 배우고 사용할 수 있으며 인기 있는 프레임워크이기 때문에 활발한 커뮤니티풍부한 문서가 존재한다.

그렇기에 문제 해결에 도움이 되는 자료나 오픈 소스 패키지들이 많다.

또한 다른 프레임 워크가 있더라도 여러 프레임 워크가 Express를 기반으로 만들어지기 때문에 다른 프레임워크를 학습하기에도 도움이 된다.

02. Express.js의 기본 구조 코드 생성

Express.js를 사용하기 위해서 먼저 프로젝트를 하나 생성해서 package.json을 npm init을 사용해서 생성해주고

 

그리고 express를 사용하기위해서 npm을 통해서 express를 설치해주자.

 

그리고 이제 node.js의 진입점이될 server.js파일을 하나 생성해주자.

 

이제 이 server.js에서 express앱의 전체적인 구조를 생성해주게 되는데 

express의 기본적인 구조는 

  • Express 모듈 불러오기
  • Express 애플리케이션 생성
  • 기본적인 라우팅 설정
  • 서버 포트 설정 및 서버 실행

으로 구성된다.

 

각각의 코드에 대해서 설명해보자면

  • Express 모듈 불러오기

const express = require('express'); 코드로 Express 모듈을 불러온다.

const express = require('express');

 

이걸 통해서 Express 애플리케이션을 생성할 수 있게 된다.

 

  • Express 애플리케이션 생성

const app = express();로 애플리케이션 객체를 생성하고 이 객체를 통해서 다양한 설정과 라우팅을 처리한다.

const app = express();

 

  • 라우팅 설정

app.get('/', (req, res) => {...});는 HTTP GET 요청을 처리하는 기본 라우팅을 설정한다.

req는 요청 객체, res는 응답 객체이다.

이는 이전에 우리가 createServer에서 사용했던 라우팅 설정과 비슷한 기능을 한다.

app.get('/', (req, res) => {
  res.send('Hello, World!');
});

 

  • 서버 실행

app.listen(PORT, () => {...});는 서버가 특정 포트에서 요청을 받을 수 있도록 설정한다.

const PORT = 3000;
app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

 

 

Express를 사용하면 http를 사용해서 서버를 생성했을때와 다르게 응답 상태코드나, 어떤 Content-Type같은걸 따로 지정하지 않아도 내부적으로 알아서 처리해서 설정해준다.

 

이렇게 작성한 후에 node server.js를 터미널에 입력해서 서버를 실행시켜준 다음에 브라우저에서 localhost:6600으로 접속해보면 

이렇게 출력되는 것을 볼 수 있다.

 

express()

express 메서드는 애플리케이션 객체를 반환한다.

이 객체는 요청을 처리하고 응답을 보낼 수 있는 여러가지 메서드를 포함하고 있다.

쉽게 생각해서 express()는 웹 애플리케이션만드는 밀키트 생성 함수라고 생각하면 된다.

 

res.send()

Express.js에서 응답(response)을 클라이언트에게 보내는 데 사용되는 메서드이다.

이 메서드는 HTTP 응답 본문을 생성하고, 그 데이터를 클라이언트에게 전달하는 역할을 한다.

이 send 메서드는 전달되는 형태를 인식해서 자동으로 Content-Type을 적절하게 설정해서 보내준다.

응답 코드도 설정하지 않으면 자동으로 200 OK를 전송한다.

그니까 내부에 그냥 본문만 작성해서 전달하면 그 값을 사용자에게 타입에 맞게 보여주는 똑똑한 함수라고 보면 된다.

 

* 응답 상태 코드를 임의로 넣고 싶다면 res.status(404).send('Not Found')와 같이 사용하면 응답 코드가 지정되어 사용자한테 보여진다.

상태코드를 404로 설정하면 값이 정상적으로 들어오고 통신도 정상적으로 했어도 404 코드를 반환한다.

 

app.get()

app.get()은 Express.js에서 HTTP GET 요청을 처리하는 데 사용되는 메서드이다.

클라이언트(브라우저나 다른 서비스)가 특정 URL 경로로 GET 요청을 보내면, 서버는 app.get()을 사용하여 해당 요청에 대한 응답을 처리한다.

 

app.get() 매서드의 원형은 

app.get(path, callback)

이며 각 매개변수는 

 

  • path : 요청을 받을 URL 경로
  • callback : 요청을 처리하는 미들웨어 함수로 이 함수는 요청과 응답 객체를 매개변수로 받아서 요청에 대해 어떤 작업을 하고 어떤 응답을 줄지에 대해서 정의한다.

쉽게 말하면 app.get 메서드는 클라이언트에서 보낸 get메서드 중 path에 해당하는 경우는 callback의 로직을 통해서 응답을 반환하게 해주는 메서드이다.

 

동일하게 메서드가 post로 변경되면 app.post() 함수를 사용하면 되고 이 또한 사용 방법은 동일하다 그저 메서드의 종류만 바뀌었을 뿐..

 

app.listen()

app.listen() 함수는 http의 listen함수와 사용방법이 동일하다.

간단하게 함수 원형과 매개변수의 종류에 대해서만 보자면 

app.listen(port, [hostname], [backlog], [callback])
  • port (필수): 서버가 요청을 수신할 포트 번호로 서버와 클라이언트 간의 통신 채널을 정의하는 것이다.
  • hostname (선택): 서버가 리슨할 호스트 이름 또는 IP 주소이다. 기본값은 localhost으로 이 값을 설정하면 해당 IP 주소에서만 요청을 받을 수 있다.
  • backlog (선택): 서버가 처리할 수 있는 최대 연결 대기 수이다. 이 값은 연결 요청이 많을 경우 대기열에서 대기할 수 있는 최대 요청 수를 설정한다. 일반적으로 값은 큰값으로 설정하진 않고 설정하지 않으면 기본값이 사용됩니다.
  • callback (선택): 서버가 성공적으로 시작되었을 때 호출되는 콜백 함수이다. 서버가 실행되면 이 함수가 호출되어, 서버가 준비되었음을 알 수 있다.

03. req.params

req.params는 Express.js에서 URL 경로에 포함된 동적 파라미터를 가져오는 객체이다.

이 객체는 경로 매개변수(route parameters)로 정의된 값들을 포함한다.

주로 RESTful API에서 특정 리소스를 식별하거나 특정 동작을 처리할 때 사용된다.

 

# 경로 매개변수 - URL 경로에 포함된 변수 값을 동적으로 받아올 수 있는데 이렇게 받아온 변수를 경로 매개변수라고 부른다.

ex)  /user/:id , /name/:name

 

이 req.params를 사용하는 방법을 살펴보면 

예를 들어서 전역 변수에 nation 배열 객체로 국가코드와 국명이 들어 있다고 가정해보자.

그리고 이걸 /nation 으로 접근하면 

전체 국가 배열 객체를 응답하도록 설정해주면 

이렇게 전체가 표출된다.

우리는 국가코드를 사용해서 특정 국가만 표출하고 싶다.

이때 우리는 경로 매개변수와 req.params를 사용한다.

먼저 get메서드에 path에 해당하는 부분에 /nation이라고 되어 있는 부분을 /nation/:code라고 변경해주자.

이러면 이제 우리가 /nation/1 혹은 /nation/82로 요청을 보내면 code 부분에 해당 값을 담아 변수처럼 사용이 가능해진다.

이걸 req객체의 params객체에 담겨 있기에 req.params.code를 출력해보면 

이렇게 url로 보낸 값이 변수처럼 사용되는 것을 볼수  있다.

이걸 사용해서 code로 전달해주는 값에 맞는 객체만 찾아서 전달하도록 분기를 태워줄 수 있다.

추가로 경로 매개변수를 선택적으로 사용할 것이고, 없더라도 동일 로직을 사용하길 바란다면 :경로매개변수명 뒤에 ?를 붙여주면 경로에 추가로 경로 매개변수에 들어갈 값을 넣어주지 않더라도 에러를 발생시키지 않는다.

 

이렇게 req.params를 사용하면 다양한 로직을 구현할 수 있으니 다양하게 사용해보도록 하자(경로 매개변수 값이 없다면 에러로 던져준다던가 )

04. res.json() vs res.send()

res.json()과 res.send()는 둘 다 Express.js에서 HTTP 응답을 보내는 메서드로 전체적으로 비슷하지만 목적과 동작에 차이가 있다.

 

res.json()

JSON 데이터를 클라이언트에 응답으로 보낼 때 사용한다.

JSON 형식으로 데이터를 자동으로 직렬화(serialize)하여 적절한 Content-Type 헤더(application/json)를 설정하고 응답을 보낸다.

JSON.stringify()를 내부적으로 호출하여 객체를 JSON 문자열로 변환하기에 따로 JSON.stringify를 사용할 필요가 없어진다.

Content-Type 헤더를 자동으로 application/json으로 설정하고 객체, 배열, 또는 다른 JSON 데이터 구조를 안전하게 처리한다.

 

res.send()

모든 유형의 응답을 클라이언트에 보낼 때 사용한다.

문자열, HTML, JSON, 버퍼(Buffer), 또는 기타 데이터 형식을 보낼 수 있는 범용적인 메서드이다.

send메서드는 Content-Type 헤더를 데이터 유형에 따라 자동으로 설정한다.

JSON 데이터를 보낼 수도 있지만, 이 경우 res.json()만큼 명시적이지는 않다.

직렬화를 수동으로 수행하지 않아도 객체나 배열을 보낼 수 있다.

 

더보기

JavaScript 객체를 네트워크를 통해 전송하거나 파일에 저장하려면 이를 JSON 문자열로 변환해야 하는데, 이 변환 과정을 JSON 직렬화라고함

 

여기서 말하는 직렬화는 이런 변환과정을 의미하는듯 보임

 

res.json()의 소스 

express.js의 소스를 github에서 확인해보면 (https://github.com/expressjs/express/blob/master/lib/response.js)

이렇게 res.json을 정의하는 부분을 확인할 수 있는데 해당 코드를 한줄 한줄 설명해보자면

var app = this.app;

this는 현재 응답 객체(res)로 app을 사용해서 응답 객체가 속한 Express 애플리케이션 객체를 가져온다.

이는 설정 값(json escape, json replacer, json spaces)을 조회하기 위함이다.

var escape = app.get('json escape');

Express 앱 설정에서 json escape 옵션을 가져온다.

json escape 옵션을 활성화하면 JSON 데이터 내의 <, >, & 같은 HTML에서 문제가 될 수 있는 문자들이 안전하게 이스케이프된다( <script>가 \u003Cscript\u003E로 변환하는 과정).

var replacer = app.get('json replacer');

Express 앱 설정에서 json replacer 옵션을 가져온다.

json replacer는 JSON 데이터를 직렬화할 때 특정 키를 필터링하거나 값을 변환하기 위해 사용되고 이 값은 JSON.stringify의 replacer 매개변수로 전달된다.

더보기

JSON.stringify의 형태는 

JSON.stringify(value, replacer, space)

와 같이 되어 있는데 각각의 매개변수는 

 

  • value: JSON으로 변환할 객체
  • replacer: 데이터를 변환하거나 필터링하는 함수(또는 배열)
  • space: JSON 문자열을 들여쓰기 할 때 사용하는 공백 수

를 의미한다.

 

그리고 여기서 replacer를 설정하지 않았던 상태로 사용하면 모든 값을 JSON 문자열로 전환했었는데 여기에 replacer를 설정하면 직렬화되는 값의 형태가 변경된다.

 

replacer는 함수 혹은 배열의 형태를 가질 수 있는데

 

함수의 형태인 경우는 

const obj = { id: 1, name: 'Alice', password: 'secret' };

const jsonString = JSON.stringify(obj, (key, value) => {
  if (key === 'password') return undefined; // 'password' 속성을 제거
  return value; // 나머지 속성은 그대로 유지
});

console.log(jsonString);
// 결과: '{"id":1,"name":"Alice"}'

 와 같이 사용할 수 있다.

이때 replacer의 매개변수로는 JSON.stringify로 JSON 문자열로 전환하려던 값의 key값과 value 값을 전달받는다.

그리고 이걸 사용해서 전달 받은 값을 조작할 수 있게 되고 조작된 return value 값이 실제로 직렬화, JSON 문자열이 되는 값에 사용되는 데이터가 된다.

 

이때 만약 반환값이 undefined인 경우에는 해당 값은 직렬화에서 제외된다(key, value를 가져와서 loop돌리듯 하나하나 체크하는 과정으로 보임).

이는 속성 값을 변환하거나 추가 로직을 처리할 때 유용하다.

 

배열의 형태인 경우

배열에 포함된 속성 이름만 JSON에 포함된다.

const obj = { id: 1, name: 'Alice', age: 25, password: 'secret' };

// 'id'와 'name' 속성만 포함
const jsonString = JSON.stringify(obj, ['id', 'name']);

console.log(jsonString);
// 결과: '{"id":1,"name":"Alice"}'

이렇게 JSON에 포함하고 싶은 속성이 정해져 있다면 배열에 속성값만 넣어주면 해당 속성만을 직렬화에 가져다 사용한다.

이는 단순한 필터링 작업에 유용하게 사용될 수 있다.

 

물론 replacer가 없는 경우는 모든 값이 직렬화에 사용된다.

var spaces = app.get('json spaces');

Express 애플리케이션의 json spaces 설정값을 가져온다.

json spaces는 JSON 데이터를 직렬화할 때 들여쓰기(인덴트) 크기를 설정한다.

기본값은 0(들여쓰기 없음)이며, 값을 2로 설정하면 읽기 쉬운 포맷으로 출력된다.

더보기

위에서 봤던 JSON.stringify의 매개변수중  

JSON.stringify(value, replacer, space)

세번째 매개변수인 space에 해당하는 내용으로 이는 그냥 표출될때 들여쓰기를 어떤식으로 할지에 대해 설정하는 부분으로 

 

  • space가 0이면 들여쓰기가 전혀 없는, 모든 속성이 한 줄로 이어진 JSON 문자열이 생성된다.
const obj = { id: 1, name: 'Alice', age: 25 };

const jsonString = JSON.stringify(obj, null, 0);
console.log(jsonString);
// 결과: {"id":1,"name":"Alice","age":25}
  • space가 1이면 각 중첩된 객체나 배열이 한 칸 공백으로 들여쓰기 적용된다.
const jsonString = JSON.stringify(obj, null, 1);
console.log(jsonString);
// 결과:
// {
//  "id": 1,
//  "name": "Alice",
//  "age": 25
// }
  • space가 2이면 두 칸의 공백으로 들여쓰기가 적용된다.
const jsonString = JSON.stringify(obj, null, 2);
console.log(jsonString);
// 결과:
// {
//   "id": 1,
//   "name": "Alice",
//   "age": 25
// }
  • 숫자 값이 4라면 네 칸의 공백으로 들여쓰기가 적용된다.
const jsonString = JSON.stringify(obj, null, 4);
console.log(jsonString);
// 결과:
// {
//     "id": 1,
//     "name": "Alice",
//     "age": 25
// }

 

space 값은 내가 원하는대로(3 , 5, 6.. 등등) 설정할 수 있으나 보통은 0, 2, 4를 많이 사용한다. 

var body = stringify(obj, replacer, spaces, escape)

stringify는 내부적으로 JSON.stringify와 비슷한 함수이다.

각각의 전달 되는 값을 통해 위에서 말한 과정을 수행하고 JSON 문자열로 변환된 데이터를 반환한다.

더보기

stringify 또한 req.json을 정의한 파일 내부에서 확인이 가능하다.

function stringify (value, replacer, spaces, escape) {
  // v8 checks arguments.length for optimizing simple call
  // https://bugs.chromium.org/p/v8/issues/detail?id=4730
  var json = replacer || spaces
    ? JSON.stringify(value, replacer, spaces)
    : JSON.stringify(value);

  if (escape && typeof json === 'string') {
    json = json.replace(/[<>&]/g, function (c) {
      switch (c.charCodeAt(0)) {
        case 0x3c:
          return '\\u003c'
        case 0x3e:
          return '\\u003e'
        case 0x26:
          return '\\u0026'
        /* istanbul ignore next: unreachable default */
        default:
          return c
      }
    })
  }

  return json
}

 

escape를 제외한 값은 그대로 JSON.stringify로 전달되고 escape는 내부 로직에 사용된다.

 

if (!this.get('Content-Type')) {
    this.set('Content-Type', 'application/json');
}

 

this.get('Content-Type')는 응답 객체(res)의 Content-Type 헤더를 확인하고 설정되지 않은 경우 application/json로 설정해준다.

 

return this.send(body);

최종적으로 그렇게 직렬화한 데이터를 send()에게 전달한다.

 

res.send()의 소스

동일 소스 내부에서 send도 확인이 가능한데 

res.send = function send(body) {
  var chunk = body;
  var encoding;
  var req = this.req;
  var type;

  // settings
  var app = this.app;

  switch (typeof chunk) {
    // string defaulting to html
    case 'string':
      if (!this.get('Content-Type')) {
        this.type('html');
      }
      break;
    case 'boolean':
    case 'number':
    case 'object':
      if (chunk === null) {
        chunk = '';
      } else if (Buffer.isBuffer(chunk)) {
        if (!this.get('Content-Type')) {
          this.type('bin');
        }
      } else {
        return this.json(chunk);
      }
      break;
  }

  // write strings in utf-8
  if (typeof chunk === 'string') {
    encoding = 'utf8';
    type = this.get('Content-Type');

    // reflect this in content-type
    if (typeof type === 'string') {
      this.set('Content-Type', setCharset(type, 'utf-8'));
    }
  }

  // determine if ETag should be generated
  var etagFn = app.get('etag fn')
  var generateETag = !this.get('ETag') && typeof etagFn === 'function'

  // populate Content-Length
  var len
  if (chunk !== undefined) {
    if (Buffer.isBuffer(chunk)) {
      // get length of Buffer
      len = chunk.length
    } else if (!generateETag && chunk.length < 1000) {
      // just calculate length when no ETag + small chunk
      len = Buffer.byteLength(chunk, encoding)
    } else {
      // convert chunk to Buffer and calculate
      chunk = Buffer.from(chunk, encoding)
      encoding = undefined;
      len = chunk.length
    }

    this.set('Content-Length', len);
  }

  // populate ETag
  var etag;
  if (generateETag && len !== undefined) {
    if ((etag = etagFn(chunk, encoding))) {
      this.set('ETag', etag);
    }
  }

  // freshness
  if (req.fresh) this.status(304);

  // strip irrelevant headers
  if (204 === this.statusCode || 304 === this.statusCode) {
    this.removeHeader('Content-Type');
    this.removeHeader('Content-Length');
    this.removeHeader('Transfer-Encoding');
    chunk = '';
  }

  // alter headers for 205
  if (this.statusCode === 205) {
    this.set('Content-Length', '0')
    this.removeHeader('Transfer-Encoding')
    chunk = ''
  }

  if (req.method === 'HEAD') {
    // skip body for HEAD
    this.end();
  } else {
    // respond
    this.end(chunk, encoding);
  }

  return this;
};

 

send의 경우는 json과 비교하기 위해서 확인해야할 부분은 

  switch (typeof chunk) {
    // string defaulting to html
    case 'string':
      if (!this.get('Content-Type')) {
        this.type('html');
      }
      break;
    case 'boolean':
    case 'number':
    case 'object':
      if (chunk === null) {
        chunk = '';
      } else if (Buffer.isBuffer(chunk)) {
        if (!this.get('Content-Type')) {
          this.type('bin');
        }
      } else {
        return this.json(chunk);
      }
      break;
  }

이 부분으로 데이터의 형태가 Object(객체)인 케이스 중, null이 아니거나 buffer의 형태가 아니라면 무조건 json으로 보내버린다는 것이다.

 

그 의미는 결국 

// .send(Object)

         object 전달      문자열전달
send(Object) => json(Object) => send(string)

의 과정을 거친다는 것이다.

결국 send는 object를 넣으면 json을 통해서 string 값으로 바꾼 다음에 send(string)으로 처리하게 되는 것이다

json을 바로 쓰면 send가 타입을 체크하는 과정을 그냥 건너 뛰고 사용하게 되는 것 과 같은 것이다.

 

그래서 그냥 json형태면 req.json()을 사용하는게 조금 더 낫다는 것이다.

05. res.send() vs res.end()

res.send()는 HTTP 응답 본문클라이언트에게 보내는 메서드로 자동으로 Content-Type을 설정하고, 다양한 데이터 타입을 처리할 수 있다.

반면 res.end()는 응답을 종료하는 메서드로, 응답 본문을 보내고 전송을 끝내는 역할을 한다.

res.send()와 달리, Content-Type을 자동으로 설정하지 않기에 Content-Type을 설정하려면, 수동으로 설정해야한다.

res.end()는 응답을 종료하는 메서드이므로, 응답을 끝내기 전에 반드시 모든 데이터를 보내야한다. 

해당 메서드가 실행된 다음엔 추가적으로 다른 응답을 보낼 수 없다.

그렇기에 404 에러에 대한 응답을 보낼 때 유용하다.

 

결론적으로, 응답 본문을 보내고 종료하려면 res.send()를 사용하고, 본문 없이 응답을 종료하려면 res.end()를 사용하는 것이 좋다.

 

* res.end()의 경우도 본문을 작성해서 보낼 수 있으나 ,Content-Type과 eTag라는 속성값이 자동으로 생성되지 않기에 사용자가 직접 설정해줘야 한다는 불편함이 있다.

 

*eTag란 엔터티 태그라고 부르며 HTTP 응답 헤더 중 하나로, 리소스(파일, 데이터 등)의 버전을 나타내는 식별자이다.

서버는 리소스의 버전이나 상태를 나타내는 고유한 값을 ETag로 생성하여 클라이언트에게 전달하고 클라이언트는 ETag를 이용해 서버의 리소스가 변경되었는지 확인할 수 있다.

이는 클라이언트는 이전에 받은 리소스의 ETag 값을 저장해 두었다가, 서버에 다시 요청할 때 If-None-Match 헤더에 ETag를 포함하여 전송하는데 서버는 클라이언트가 보낸 ETag와 현재 리소스의 ETag를 비교하여, 리소스가 변경되지 않았다면 304 Not Modified 상태 코드를 응답하는데 이 안에는 리소스가 포함되지 않기에 이전 데이터를 그대로 사용하게 된다.

이를 통해 불필요한 데이터 전송을 방지하고, 네트워크 성능을 최적화할 수 있는 기술이다.

06. 포스트맨 설치하기

Postman은 API를 테스트하고 개발하는 데 사용되는 프로그램이다.

개발자와 테스터가 API를 설계, 테스트, 디버깅, 문서화하고 모니터링하는 데 유용하다.

나는 보통 접근하지 못하는 API에 대해서 테스트 및 디버깅을 위해서 많이 사용하는 것 같다.

 

https://www.postman.com/

 

Postman: The World's Leading API Platform | Sign Up for Free

Accelerate API development with Postman's all-in-one platform. Streamline collaboration and simplify the API lifecycle for faster, better results. Learn more.

www.postman.com

 

포스트맨 사이트에 섭속해서 

운영체제에 맞게 클릭하면

이렇게 설치할 수 있는 페이지로 이동한다.

설치 버튼을 누르면 

바로 설치가 시작된다.

실행하면 

이렇게 설치 화면이 나오고 

금방 설치된다.

나는 기존에도 구글 ID를 사용해서 사용했기에 바로 그냥 Continue with Google로 접속해보겠다.

ID가 없다면 새로 만들거나 본인 처럼 Google 계정을 연동하는 것을 추천한다.

로그인을 하면 설치가 완료되고 

이렇게 생성된 것을 볼 수 있다.

 

켜보면 

이런 화면이 보이는데 왼쪽 위에 + 버튼을 눌러서 Collection을 생성하면 

이렇게 새로운 컬렉션이 생성된다.

이 컬렉션은 API를 종류별로 모아두기 위한 디렉터리 쯤으로 생각해주면 될것같다.

여기 Add a request를 선택하면 

이렇게 request를 보낼수 있는 창이 하나 생성되는데 

이렇게 전송을 위한 메서드를 선택할 수 도 있고 

post 요청에 담기 위한 Body를 담을 수도 있고 오른쪽 위에 Cookies를 눌러보면

이렇게 세션과 같이 쿠키에 담기는걸 설정해서 환경을 구성할 수 도 있다.

07. nodemon 설치

Nodemon은 Node.js 애플리케이션 개발 중에 유용한 개발 도구로, 애플리케이션 코드를 변경할 때마다 수동으로 서버를 다시 시작하지 않고도 자동으로 서버를 재시작해주는 역할을 한다.

 

nodemon은 global로 설치도 가능하고 local로 설치도 가능하다.

global로 설치하면 어느 프로젝트던 사용이 가능하다.

그러나 프로젝트 별로 Nodemon이 호환되는 버전이 있기 때문에 충돌이 발생하는 경우가 생길 수 있다.

 

우선 global로 설치하는 방법은 

npm install -g nodemon

로 프로젝트의 루트에서 

기존에 node 로 실행하던 방법을

nodemon app.js

와 같이 사용할 수 있다.

 

local로 설치하는 방법은 

npm install --save-dev nodemon

//혹은 

npm install -D nodemon

와 같은 방식으로 설치할 수 있다.

로컬에 설치한 nodeman을 실행하는 방법은 

npx nodemon app.js

로 실행한다.

기존에 글로벌로는 nodemon app.js로 실행했었는데 로컬로 설치하면 npx로 찾아줘야하는 이유는 로컬로 설치한 nodemon은 프로젝트의 node.modules 디렉토리에만 존재하기 때문에 글로벌 명령어로는 접근할 수 없다.

이를 npx가 해결해주는데 npx는 로컬에 설치된 실행파일을 자동으로 찾아 실행한다.

또한 글로벌로 설치한 경우도 찾지 못하면 npx가 글로벌로 설치된 nodemon을 찾아 실행해준다.

 

package.json에 script에 

"scripts": {
  "start": "npx nodemon app.js"
}

이렇게 설정해두면 

npm start

으로 실행할 수 있다.

이제는 nodemon을 사용해서 서버를 켜주도록 하자

08. middleware에 대해서

Node.js의 미들웨어(Middleware)요청(request)응답(response) 사이에서 특정 작업을 처리하는 중간단계 함수를 말한다.

주로 Express.js 같은 웹 프레임워크에서 사용된다.

 

Middleware는 클라이언트로부터 들어온 요청을 처리하기 전에 실행되며, 필요한 경우 다음 작업으로 요청을 넘긴다.

이 과정은 체인처럼 연결되어 있으며, 이를 미들웨어 체인이라 부른다.

 

Middleware함수는 기본적으로 3개의 인수를 받는다.

 

  • req: 클라이언트 요청 객체 (Request Object)
  • res: 서버 응답 객체 (Response Object)
  • next: 다음 미들웨어를 호출하는 함수

미들웨어의 형태는 보통로

function middleware(req, res, next) {
  // 작업 수행
  next(); // 다음 미들웨어로 넘어가기
}

 

구성된다.

 

여기서 next()는 다음 미들웨어를 호출하는 것으로 next()를 호출하지 않으면 요청을 다음 미들웨어나 라우터로 넘어가지 않고 멈춘다. 그로 인해 서버가 멈추거나 요청이 처리되지 않는다.

 

이를 대략적인 흐름으로 보자면 

request
   ↓
middleware1 // 내부의 next()를 만나면 다음 middleware2로 넘겨준다
   ↓
middleware2 // 내부의 next()를 만나면 다음 middleware3로 넘겨준다
   ↓
middleware3 // 내부의 next()를 만나면 다음 middleware로 넘겨준다
   ↓	
response

* middleware는 임의의 이름을 지정한 것이다.

 

결론적으로 Express 애플리케이션은 본질적으로 미들웨어 기능 호출이라고 보면 된다.

 

Express.js에서는 Middleware를 크게 4가지로 분류할 수 있다.

  • 애플리케이션 레벨(Middleware) 

애플리케이션 전역에서 동작하는 Middleware

const express = require('express');
const app = express();

// 애플리케이션 레벨 미들웨어
app.use((req, res, next) => {
  console.log('Request URL:', req.url);
  next(); // 다음 미들웨어로 이동
});

app.get('/', (req, res) => {
  res.send('Hello World');
});

app.listen(3000);

 

  • 라우터 레벨 Middleware

특정 라우터에만 적용되는 Middleware

const express = require('express');
const router = express.Router();

router.use((req, res, next) => {
  console.log('라우터 레벨 미들웨어 실행');
  next();
});

router.get('/user', (req, res) => {
  res.send('User Page');
});

app.use('/api', router);

 

  • 빌트인 Middleware

 

 

Express.js에 내장된 Middleware

const express = require('express');
const app = express();

// JSON 요청 본문을 파싱하는 미들웨어
app.use(express.json());

app.post('/data', (req, res) => {
  console.log(req.body); // 요청 본문 데이터
  res.send('Data received');
});

 

  • 서드파티 Middleware

외부 라이브러리를 사용해 특정 기능을 추가

const express = require('express');
const morgan = require('morgan'); // 서드파티 미들웨어
const app = express();

app.use(morgan('tiny')); // 요청 로깅
app.get('/', (req, res) => {
  res.send('Hello World');
});

app.listen(3000);

 

09. middleware 생성하기

미들웨어는 레벨에 따라서 생성하는 방법이 다른데 먼저 미들웨어를 구성하는 함수가 두개 있는데 app.use()와 app.get()함수이다.

 

app.use()

app.use()는 Express.js에서 미들웨어를 등록하는 데 사용되는 메서드이다.

더보기

app.use()의 함수 원형은 

app.use([path], callback);

 와 같이 구성되어 있고 

 

  • path (선택 사항): 이 미들웨어가 적용될 경로를 지정한다. 경로를 지정하지 않으면, 해당 미들웨어는 모든 요청에 대해 적용된다.
  • callback: 미들웨어 함수 또는 라우트 핸들러로, 요청을 처리하는 데 사용된다. 이 함수는 (req, res, next)를 인자로 받는다.

콜백에 사용되는 미들웨어 함수는 

function (req, res, next) {
  // 미들웨어 로직
  next(); // 다음 미들웨어로 넘기기
}

와 같이 작성할 수 있다.

 

app.use()를 구성해보자면

app.use('/user', (req, res, next) => {
  console.log('User 경로에 요청이 왔습니다.');
  next();
});

 와 같이 구성하는데 이는 /user의 url로 요청이 들어온다면 해당 미들웨어를 실행시키는데 그 콜백함수의 내용은 console에 "User 경로에 요청이 왔습니다."를 출력한 후에 next() 호출해서 다음 미들웨어로 이동한다.

이때 이 미들웨어는 다른 경로로 요청이 왔을때는 호출되지 않는다.

 

이때 app.use의 경우는 app.get과 비슷한데 app.use는 요청 메서드에 관계없이 모든 요청 메서드를 처리해준다.

app.get()

Express.js에서 GET 요청을 처리하기 위한 라우팅 메서드이다

이 메서드는 특정 URL 경로HTTP GET 요청에 대해 실행될 콜백 함수를 지정하는 데 사용된다.

더보기

app.get()의 함수 원형은 

app.get(path, callback);

로 매개변수는 

 

  • path: 요청 경로를 정의한다. 이 경로는 URL의 일부로, 클라이언트가 요청할 주소이다.
  • callback: 클라이언트의 요청을 처리할 함수이다. 이 함수는 요청 객체(req), 응답 객체(res), 그리고 next()를 인자로 받는다.

app.get메서드의 콜백함수는 여러개를 설정할 수 있는데 여러개 설정하는 경우는 미들웨어를 사용하는 것과 비슷하게 실행할 수 있다.

app.use('/user/:id', (req, res, next) => {
  console.log(`User ID is: ${req.params.id}`);
  next();  // 다음 미들웨어 또는 라우트 핸들러로 이동
}, (req, res) => {
  res.send('User page');
});

이렇게 설정하면 두번째 매개변수로 설정된 첫번째 콜백 함수를 실행하고 난 후에 next를 만나면 세번째 매개변수로 설정된 두번째 콜백함수를 실행한다.

 

그런데 만약 콜백함수를 하나만 설정한다면 next()함수를 사용할수없고, 다른 말로 next()를 사용하지 않는다면 콜백함수를 하나만 설정할 수 밖에 없다

 

app.get()는 다른 함수 혹은 미들웨어로 요청을 전달해줄 수 없게 되어 있고 그냥 요청을 받고 요청을 처리한 후 응답을 보내는 역할만 한다.

1. 애플리케이션 레벨 미들웨어 생성

애플리케이션 레벨 미들웨어는 app.use() 또는 HTTP 메서드(app.get, app.post 등)와 함께 사용된다

const express = require('express');
const app = express();

// 애플리케이션 레벨 미들웨어 생성
app.use((req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next(); // 다음 미들웨어로 이동
});

app.get('/', (req, res) => {
  res.send('Hello, World!');
});

app.listen(3000, () => console.log('Server is running on port 3000'));

이렇게 코드를 작성하면

app.use((req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next(); // 다음 미들웨어로 이동
});

이 부분에서 어떤 요청이 들어오던 상관없이 가장 먼저 해당 로직을 수행하게 된다.

그리고 next()함수를 만나면 다음 미들웨어인 get 메서드로 보내준다.

 

2. 라우터 레벨 미들웨어 생성

라우터 레벨 미들웨어는 특정 라우트에만 적용되도록 설계할 수 있다.

const express = require('express');
const app = express();

// 특정 경로에 대해 동작하는 미들웨어
app.use('/user', (req, res, next) => {
  console.log('User route middleware 실행');
  next();  // 요청을 계속 처리하도록 넘김
});

// /user 경로에 대한 라우트 핸들러
app.get('/user', (req, res) => {
  res.send('User page');
});

app.listen(3000, () => console.log('Server is running on port 3000'));

이렇게  use메서드를 사용해서 첫번째 전달인자로 /user라는 url을 넣어주면 해당 url에서만 해당 미들웨어가 작동한다.

3. 서드파티 미들웨어 활용

서드파티 미들웨어는 이미 만들어진 기능(예: 로깅, 인증, CORS 설정 등)을 제공하며, 쉽게 사용할 수 있다.

const express = require('express');
const morgan = require('morgan');
const app = express();

// morgan 미들웨어 사용
app.use(morgan('tiny'));

app.get('/', (req, res) => {
  res.send('Hello, World!');
});

app.listen(3000, () => console.log('Server is running on port 3000'));

여기서 morgan의 경우는 next()를 자동으로 호출하기에 직접 선언해줄 필요가 없다.

 

다양한 레벨로 미들웨어를 설정할 수 있는데, 이 정도만 알아보고 넘어가자.

추후에 필요하다면 더 찾아서 공부해보자.

 

강의에서는 그냥 req.use를 사용하는 방법에 대해서 만 알려줬음..!

10. POST 요청하기 & express.json()

이번에는 post요청을 통해서 body에 값을 전달하면서 사용해보자.

이전에 c:'c'했던 것 처럼 진행할건데, 나라번호랑 나라이름을 선택적으로 조회했던 프로젝트를 통해서 나라번호, 나라명을 전달하면 추가되는 요청을 만들어보자.

 

기존에 코드를 보면 

이렇게 구성되어 있는데 추가로 post 요청을 라우트 핸들러를 하나 추가해보면 

addnation이라는 url을 post로 받을 경우에 가지고 오는 body의 정보를 가져오는데 그때 사용하는게 req객체의 body객체 안에 있는 code와 nation을 가져오는데 

이는 body에 json형태로 전달하는 값의 key값을 쓰면 된다.

body에 들어오는 값이 

{
    'code':83,
    'nation':'japan'
}

의 형태가 될텐데 이때의 key 값을 req.body의 뒤에 속성값으로 사용하면 된다.

그리고 이 값을 기존에 nations에 push 해서 추가해주고 

이 값을 res.json을 통해서 응답하자.

이 post 요청은 postman을 통해서 보내보자.

이렇게 구성하고 body를 

이렇게 설정해서 요청을 sending해보면 

이렇게 에러가 발생하는 것을 볼 수 있다.

 

이는 req.body가 undefined인 상태로 처리되기 때문이다.

이건 보통 Express에서는 기본적으로 요청 본문(req.body)을 처리하지 않기 때문에 이걸 처리하기 위한 미들웨어가 추가되어야 한다.

 

req.body를 사용하려면, body-parser 또는 express.json() 미들웨어를 사용하여 요청 본문을 파싱해야한다.

Express 4.16.0 이후부터는 express.json()과 express.urlencoded() 미들웨어가 기본적으로 내장되어 제공되기에 이걸 상용해야한다.

 

express.json()

// JSON 형식의 본문을 파싱하는 미들웨어 추가
app.use(express.json());

이 미들웨어는 모든 요청에 대해서 express가 요청본문을 JSON으로 파싱해 req.body에 넣어주고 다음 코드로 넘겨준다.

이렇게 미들웨어를 추가해주면 

이렇게 추가되는 것을 볼 수 있고 브라우저를 새로고침 해보면 

이렇게 값이 추가된것을 볼 수 있다.

 

요청 body가 없을떄는 404에러 처리를 해줄 수 도 있다.

요청을 보내보면 

이렇게 에러를 잘 뱉는걸 볼 수 있다.

근데 

소스를 이런식으로 생성하면 

정상적으로 에러를 만드는것 같지만

이럴땐 

중앙에 .json이후로도 아래 라인이 실행되면서 newNation이 없어 에러를 발생시킨다.

그렇기 때문에 404를 이렇게 구성하고 반환할때에는 return을 해줘서 끊어줘야만 한다.

이러면 정상적으로 에러만 벹고 응답이 오는 것을 볼 수 있다.

 

11. Model, View, Controller

Model, View, Controller를 사용하는 디자인 패턴은 MVC 디자인 패턴이 있다.

이는 애플리케이션을 세 가지 주요 컴포넌트로 분리하여 구조화하는 방식이다.

 

1. Model

데이터와 관련된 로직을 처리하며, 애플리케이션의 핵심 비즈니스 로직데이터 저장소(예: 데이터베이스)와 상호작용한다.

 

  • 데이터를 가져오고, 저장하며, 수정하는 작업을 담당한다.
  • 상태와 데이터를 캡슐화하여 뷰(View)와 컨트롤러(Controller)에서 독립적이다.
  • 데이터가 변경되면 알림을 보내 뷰(View)를 업데이트할 수 있다.

 

2. View 

사용자에게 데이터를 시각적으로 표시하는 부분으로 사용자 인터페이스(UI)를 구성하며 Model에서 데이터를 가져와 렌더링 한다.

 

  • HTML, CSS, JavaScript 등으로 작성된 프런트엔드 코드가 포함된다.
  • 데이터를 보여주지만 직접 데이터 처리는 하지 않는다.
  • 사용자 입력(예: 버튼 클릭, 폼 제출)을 Controller로 전달한다.

3. Controller

사용자로부터 입력을 받고, 이를 처리하여 Model과 View를 연결한다.

애플리케이션의 동작 흐름을 제어하는 중간 관리자 역할을 한다.

 

  • 사용자 요청(HTTP 요청)을 받고, 해당 요청을 처리하기 위해 모델(Model)에 작업을 요청합니다.
  • 모델(Model)에서 데이터를 받아와서 뷰(View)에 전달합니다.
  • 입력 데이터를 검증하거나, 로직을 처리하고 필요한 응답을 생성합니다.

 

 

보통 MVC 모델의 요청과 응답의 과정은 

View를 통한 사용자의 요청
       ↓
Controller - 요청을 받아 로직을 처리한 후 데이터를 Model에 요청
       ↓
Model - 데이터베이스에서 요청된 데이터를 가져오거나 새로운 데이터를 저장한다.
       ↓
Controller - 모델에서 전달한 데이터를 받아 View에게 전달한다
       ↓
View - 컨트롤러로 부터 받은 데이터를 렌더링 해서 사용자에게 보여준다.

 

 

12. 앱 소스 코드 MVC 패턴으로 변경하기

먼저 기존의 소스코드에 models, views, controllers 폴더를 생성해주고 

먼저 컨트롤러부터 만들어보자면 nation관련 로직이 들어갈 nations.controller.js를 생성해주자.

그리고 server.js에 있는 nation관련된 로직을 모두 nations.controller.js에 이동시키면 되는데, 

이렇게 두개이다.

이렇게 옮겨주자.

그리고 이렇게 만든 controller를 모듈로써 외부로 내보내기를 해주자.

그리고 server.js에 돌아와서 가장 위쪽에 require로 이 모듈을 import 해주자.

사용할때에는 기존에 로직 중에서 

이렇게 콜백 함수부분에 생성한 함수를 넣어주는데 

매개변수를 따로 넣어주지 않고 바로 그냥 사용한다.

더보기

nationsController.postAddNation만 전달

  • 여기서 nationsController.postAddNation은 참조(Reference)이다.
  • 이 함수는 요청이 들어올 때 Express가 필요한 시점에 호출한다.
  • 요청이 들어오기 전에는 실행되지 않고, 요청 시 req와 res 객체를 자동으로 전달한다.
app.post("/addnation", nationsController.postAddNation);

 

nationsController.postAddNation(req, res)를 호출

  • 이 방식은 함수를 즉시 실행한다.
  • 서버가 실행될 때, 요청이 들어오기도 전에 실행되어 버린다.
  • 그 결과, Express 라우트 설정 자체가 제대로 동작하지 않거나, 실행 시점에 에러가 발생할 수 있다.
// 잘못된 사용
app.post("/addnation", nationsController.postAddNation(req, res));

 

 

Express가 요청 시점에 핸들러 함수 호출을 관리하므로, 함수 참조만 전달해야 한다.

즉시 실행하면 요청 없이 함수가 실행되거나, req와 res가 없어서 에러가 발생한다.

 

 

다음은 

이 부분인데 이부분은 데이터로써 모델으로 볼 수 있다.

그렇기에 model에 nation.model.js라고 파일을 하나 생성하고 

이렇게 구성해주자.

그리고 server.js로 돌아와서 import 해준 후에 

이 부분을 지워주면 된다.

 

이렇게 MVC 패턴으로 변경해줄 수 있다.

 

13. Router 모듈화

Express.js에서 라우터 모듈화란, 애플리케이션의 라우트를 작은 단위로 나누어 독립적인 모듈로 관리하는 것을 의미한다.

이때 express.Router()라는 함수가 핵심적인 역할을 한다.

 

express.Router()

express.Router()는 Express.js에서 제공하는 미니 라우터 객체이다.

 

더보기

express.Router()는 미니 라우터 객체?

"미니" 라우터라고 부르는 이유는, express.Router()가 Express 애플리케이션 전체가 아닌, 특정 라우트 그룹을 처리하기 위한 경량화된 객체이기 때문이다.

Express 애플리케이션(express())은 전체 앱을 다루는 큰 컨테이너이고, express.Router()는 특정 URL 경로나 기능(예: 사용자, 상품 등)에 집중하여 처리할 수 있도록 만들어진 모듈화된 라우터이다.

 

 

이 객체를 사용하면, 메인 애플리케이션(app)과 별도로 라우트를 정의하고 모듈화할 수 있다.

Express 앱처럼 작동하며, 라우트를 정의하고 미들웨어를 추가할 수 있다.

최종적으로 app.use()를 사용하여 애플리케이션에 통합된다.

 

이 모듈화를 하는 방법은 우선 기존에 우리가 server.js를 생성했던 내용을 보자.

이 소스 중에서 라우팅에 해당하는 부분을 보면 이 라우트 모듈화를 쓰기에는 안맞는 부분이 있어서 조금 수정해보자면 

이 post 메서드 부분에서도 라우팅을 nations를 기점으로 시작하도록 url을 수정해주자.

여기에 추가적으로 get 메서드일때 기본적으로 /nations로 요청을 할 경우에는 전체리스트를 보이도록 하는 라우터를 하나 더 생성하고 

nations.controller.js파일에서 

함수를 생성해주고 기존에 선택적으로 code를 받던 부분을 필수값으로 수정해주고 

해당 함수에서 파라미터를 안가져올 경우에는 404 에러를 반환하도록 수정해주자.

당연히 새로 만든 함수는 export를 해줘야 한다

 

이제 본격적으로 라우팅을 모듈화 해보면 가장 먼저 라우팅을 해줄 파일을 하나 생성해야하는데 routes라는 폴더를 root 경로에 하나 생성해주고 내부에 nations.routes.js 파일을 생성해주자.

이렇게 생성한 route파일에서 가장 먼저 express 모듈을 추가해줘야 한다.

그 다음에 express.route 함수를 선언해주자.

그리고 이렇게 생성한 라우터 객체를 통해서 들어오는 경로를 확인해서 라우트를 해줄 것이다.

이제 server.js에 있는 

nations에 관련된 라우터들을 모두 라우터 모듈에 넣어주면 된다.

그리고 app을 nationRouter로 수정해주고 난 다음에 

export로 nationRouter를 내보내주면 된다.

그리고 server.js에서 nationsRouter를 import 해주고 

그리고 이렇게 부른 라우터는 use 메서드를 통해서 미들웨어를 하나 생성한 후에 nations라는 url을 넣어준 후에 콜백함수 부분에 넣어주면 된다.

이렇게 선언한 다음엔 

nations 부분을 모두 제거해줘도 된다.

그리고 nationsController는 server.js에서 import 했기에 그 부분도 nation.routes.js 파일로 가져와주자.

이렇게 만든 nationRouter의 경우는 nations에 대한 요청을 모두 라우팅 해주는 라우터들을 모아두는 파일이 된다.

서버를 시작한 후에 

브라우저에서 실행해보면

정상적으로 실행되는 것을 볼 수 있다.

14. RESTful API

RESTful API는 REST(Representational State Transfer) 아키텍처 스타일을 기반으로 설계된 웹 API이다.

REST는 클라이언트와 서버 간의 통신을 일관되고 간단하게 만드는 규칙이나 원칙의 집합이다.

RESTful API는 이런 REST의 원칙을 준수하며, 주로 HTTP 프로토콜을 사용한다.

 

RESTful API의 핵심은 리소스(Resource)이다.

리소스는 애플리케이션에서 데이터를 표현하며, 특정 URI(Uniform Resource Identifier)로 식별된다.

 

리소스란건 사용자, 게시물, 상품 등 요청에 대한 내용으로 URL로 표현된다.

/users -> 사용자 리소스
/products -> 상품 리소스

 

그리고 이 리소스에 대한 작업은 HTTP 메서드로 표현된다.

 

  • GET: 리소스를 조회 (읽기)
  • POST: 리소스를 생성 (쓰기)
  • PUT: 리소스를 전체 수정 (갱신)
  • PATCH: 리소스를 부분 수정 (갱신)
  • DELETE: 리소스를 삭제 (제거)

 

리소스와 메서드를 포함해서 작업된 내용은

동작 HTTP 메서드 URL 설명
모든 사용자 조회 GET /users 모든 사용자 목록 반환
특정 사용자 조회 GET /users/{id} ID로 특정 사용자 조회
사용자 생성 POST /users 새로운 사용자 생성
사용자 정보 수정 PUT /users/{id} 특정 사용자의 정보 전체 수정
사용자 정보 일부 수정 PATCH /users/{id} 특정 사용자의 정보 일부 수정
사용자 삭제 DELETE /users/{id} 특정 사용자 삭제

 

 

15. Express에서 파일 전송하기(res.sendFile)

이번에는 파일전송에 대해서 알아보자.

파일 전송하는 것은 res 객체를 사용해서 sendFile메소드를 이용하면 된다.

 

가장 먼저 정적 파일을 보관할 폴더를 하나 생성해주자.

public/images로 폴더를 하나 생성하고 이 내부에 아무 이미지 파일 하나만 넣어주자.

먼저 sendFile에 대해서 먼저 확인해보자.

 

sendFile

res.sendFile은 Express에서 HTTP 응답으로 파일을 전송하는 데 사용되는 메서드이다.

파일을 브라우저에 다운로드하거나 열 수 있도록 서버에서 클라이언트로 직접 전송할 수 있다.

 

res.sendFile() 메서드는 파일의 절대 경로(absolute path)를 사용해서 특정 파일을 클라이언트로 전송한다.

더보기

이 절대경로는 path라는 내장 모듈을 사용해서 가져오게 된다.

const path = require('path');

 

그 후에 이 가져온 모듈을 사용해서 절대경로를 생성해줄 건데 

이 path라는 모듈은 파일 및 디렉토리 경로를 결합하거나(join), 절대 경로로 변환하거나(resolve), 경로의 디렉토리 이름 또는 확장자를 추출하는 기능을 제공한다.

 

우리는 여기서 join 기능을 사용할 것이다. 

join 기능은 디렉터리의 경로를 결합하는 기능으로 아래와 같이 사용하는데 이 join이란 메서드는 여러개의 경로 조각을 결합해서 하나의 경로를 생성한다.

이 메서드는 자동으로 OS에 맞게 디렉터리의 구분자(window의 경우는 /)를 알아서 추가해준다.

const filePath = path.join(__dirname, 'files', 'example.txt')

여기서 __dirname은 은 현재 실행 중인 Node.js 파일의 디렉토리 경로를 나타낸다.

그래서 지금 실행하는 파일인 server.js가 존재하는 디렉터리의 경로를 반환한다.

/Users/example/project /server.js일 경우 server.js에서 실행하면 __dirname의 값음 /Users/example/project가 된다.

(이는 프로젝트의 폴더 구조가 변경되어도 코드가 제대로 작동하도록 만들어준다.)

여기서 files라는 부분은 example.txt가 존재하는 디렉터리의 위치를 의미한다.

저렇게 구성되어 나오는 경로는 /실행되는 위치까지의 path/files/example.txt 가 되고 이는 지금 실행한 파일의 디렉터리를 기준으로 files라는 디렉터리 내부에 있는 example.txt의 경로를 반환해라 라는 의미가 된다.

res.sendFile()는 세개의 인수를 받을 수 있는데 첫번째 인수는 파일의 절대경로를 넣어줘야 하고 두번째 인수는 옵션 객체로, 파일 전송에 대한 추가 설정을 추가할 수 있고 세번째 인수는 파일 전송 중 오류를 처리하기 위한 콜백함수를 옵셔널하게 지정할 수 있다.

const filePath = path.join(__dirname, 'files', 'example.txt'); // 전송할 파일의 절대 경로

//에러 콜백을 추가한 경우
res.sendFile(filePath, (err) => { 
    if (err) {
        console.error('파일 전송 중 에러 발생:', err);
        res.status(500).send('파일을 전송할 수 없습니다.');
    }
});

//옵션 설정을 추가한 경우
res.sendFile(filePath, {
    headers: {
        'Content-Disposition': 'attachment; filename="custom-name.jpg"' // 다운로드 파일 이름 지정
    }
});

//옵션 + 에러 콜백
res.sendFile(filePath, {
    headers: {
        'Content-Disposition': 'attachment; filename="custom-name.jpg"' // 다운로드 파일 이름 지정
    }
}, (err) => { 
    if (err) {
        console.error('파일 전송 중 에러 발생:', err);
        res.status(500).send('파일을 전송할 수 없습니다.');
    }
});

 

 

이제 실제 사용을 해보자면 기존에 우리가 기능을 만들어 뒀던 nations.controller.js파일에서 추가적으로 파일을 가져다 주는 함수를 하나 생성해주자.

우리는 저장한 국기가 한국 국기밖에 없기에 그냥 요청을 받으면 한국 국기만을 보여주는 함수를 생성해주도록 하자.

제일 먼저 절대 경로를 받아오기 위한 path 모듈을 가져오고

함수 내부에서 server.js가 존재하는 root 디렉터리에서 public/images 디렉터리 내부에 있는 Korea.jpg 파일의 절대경로를 가져오기 위해서 join 함수를 사용해주자.

주의해야 할점은 controller는 controller폴더에 존재하기에 한번 상위로 나가줘야한다.

그래서 경로에 ..이 dirname다음에 추가되어야한다.

그리고 이렇게 생성된 절대경로를 sendfile에게 전달해주자.

이걸 한번에 실행해도 무방하다.

이렇게 설정한 다음에 모듈로 내보내주고

추가로 nations.routes.js에 post 메서드로 /flag로 경로를 하나 주가해주면 

/nations/flag의 경로로 접근했을때 

이렇게 파일을 가져오는 것을 볼 수 있다.

헤더를 눌러보면

컨텐츠 타입이 알아서 jpeg로 되어 있는데 이는 express가 알아서 컨트롤 해줌을 알 수 있다.

16. express.static()

이제 node.js를 사용해서 서버단이 아닌 화면단을 생성해보도록하자.

화면단을 위해서는 이 소단원의 제목인 express모듈의 static() 메서드가 필요하다.

 

express.static()은 Express.js에서 정적 파일(HTML, CSS, JavaScript, 이미지 등)을 제공하기 위해 사용하는 미들웨어 함수이다.

이 함수는 특정 디렉토리정적 파일을 제공하는 루트 디렉토리로 설정하여, 해당 디렉토리 내의 파일을 클라이언트가 직접 요청할 수 있도록 한다.

 

기본적인 사용방법은 아래와 같으며 

const express = require('express');
const app = express();
const path = require('path');

// 정적 파일 경로 설정
app.use(express.static(path.join(__dirname, 'public')));

// 서버 시작
app.listen(3000, () => {
  console.log('Server is running on http://localhost:3000');
});

 

부분적으로 확인해보자면

app.use(express.static(path.join(__dirname, 'public')));

이렇게 코드를 작성해서 public 디렉토리를 정적 파일을 제공하는 루트 디렉터리로 설정해주면 public 디렉터리에 index.html파일이 존재한다면 

http://localhost:3000/index.html

로 요청을 보내면 접근이 가능하다.

 

이때 URL 경로에 가상의 기본적인 경로를 추가해줄 수 도 있다.

app.use('/static', express.static(path.join(__dirname, 'public')));

를 설정해주면 

http://localhost:3000/static/index.html

로 접근하면 정적파일에 접근이 가능하다.

이 경우는 실제 디렉터리 구조에서는 존재하지 않는 static이라는 경로를 추가해서 public이라는 경로를 연결하도록 만들어 준다.

 

 

express.static함수의 경우는 두번째 매개변수로 옵션 객체를 추가로 받을 수 있는데 주된 옵션 내용은 

 

  • fallthrough: 요청 경로에 해당하는 파일이 없을 때 다음 미들웨어로 넘길지 결정 (기본값: true)
  • index: 디렉토리를 요청했을 때 제공할 기본 파일 이름을 설정 (기본값: 'index.html')
  • dotfiles: 숨김 파일(파일명이 .로 시작하는 파일)을 제공할지 설정
    • allow: 숨김 파일 제공
    • deny: 숨김 파일 차단 (403 에러)
    • ignore: 무시 (404 에러 반환)

이고 이를 사용하는 방법은 아래와 같다.

app.use(
  express.static(path.join(__dirname, 'public'), {
    dotfiles: 'deny',
    index: 'home.html',
  })
);

이는 / 루트 경로로 접근했을때 public으로 접근을 하게 하고 이때 숨긴 파일에 접근하려고 할때 403에러를 반환한다.

그리고 기본 루트 경로로 접근했을때는 home.html을 가져오도록 하게 하겠다는 설정을 넣어준 것이다.

 

실 사용을 하기 위해서 먼저 public폴더에 index.html을 생성해주자.

그리고 간단하게 내용을 작성해주자.

이렇게 생성해주고 나서 server.js로 이동해서 

이렇게 express.static을 사용해서 public으로 접근하도록 코드를 작성해주고 나서 서버를 시작해주고 나서 서버로 접근하면 

이렇게 index.html로 접근이 가능하다.

 

이때 가상의 경로를 추가하면 

이건 상대경로로 접근하는 것이고, 지금 접근하던 경로의 하위 경로에서 

이렇게 폴더를 추가해서 server.js에 접근하면 

이렇게 접근을 못한다.

이를 방지하기 위해서 절대경로로 경로를 설정해줘야 하는데 

 path객체의 join을 통해서 _dirname이랑 public을 연결해서 해당 경로를 연결해주자.

이렇게 설정하고 

서버를 시작한 후에 접근하면 

 

17. Template Engine 사용하기

Node.js의 템플릿 엔진(template engine)은 서버 측에서 데이터를 HTML과 결합하여 동적으로 웹 페이지를 생성하는 데 사용되는 도구이다.

이 엔진은 HTML 템플릿에 데이터를 삽입하거나 동적으로 HTML 콘텐츠를 렌더링할 수 있도록 돕는다.

 

이 템플릿 엔진의 주요 역할은 아래와 같다.

 

  • 서버에서 데이터를 HTML로 변환하여 클라이언트로 보냄.
  • HTML 코드에 동적으로 데이터를 삽입.
  • 코드와 뷰(HTML)를 분리하여 유지보수를 쉽게 함.
  • 루프, 조건문, 변수 삽입 등 프로그래밍 논리를 HTML에 추가.

이 템플릿 엔진에는 여러가지 종류가 있으나 주로 사용되는 템플릿 엔진은 EJS (Embedded JavaScript Templates), Pug (구 Jade), Handlebars, Mustache등이 있다.

템플릿 엔진의 사용방식은 비슷 비슷 하기에 하나만 배워도도 다른 템플릿 엔진을 배우는데 어려움이 없을 것으로 예상된다.

 

우리는 HBS(Handlebars)를 사용해보도록 하자.

먼저 HBS 모듈을 설치해주자.

npm install hbs

 

이렇게 추가한 모듈은 express를 통해서 사용하게 된다.

server.js에서 

// View 엔진으로 Handlebars(hbs) 설정
app.set('view engine', 'hbs');

 

이 코드를 설정해주면 view engine을 hbs로 사용할거라고 설정해주고

// 뷰 폴더 경로 설정
app.set('views', path.join(__dirname, 'views'));

이렇게 넣어줘서 hbs에서 view엔진이 사용할 템플릿 파일을 찾을 폴더의 경로를 알려줘야 한다.

MVC 구조에서 V에 해당하는 views폴더에 index.hbs라는 파일을 하나 생성해주자.

 

그리고 나서 기존에 있는 public/index.html의 내용을 복사해서 넣어주고 

이제 h1태그 내부에 있는 내용을 지워주고 중괄호 두개를 사용해서 괄호를 만들어주자.

그리고 그 내부에 miniTitle이라고 넣어줘 보자.

이건 약간 변수 같은 개념으로써 이제 이 값은 서버로 부터 받아와서 보여줄 것이다.

 

server.js를 가서 get 메서드를 하나를 만들어주는데 

여기서 추가적으로 render 라는 res의 함수를 하나 더 사용해서 전송할 hbs파일을 지정해준다.

이때 render의 첫번째 매개변수는 hbs파일명을 작성하고

두번째 매개변수는 해당 파일을 렌더링 시키면서 전달할 값을 key-value의 형태로 제공해줘야 한다.

이렇게 만들어진 key는 hbs파일에서 {{}}(중괄호 두개)의 안에 들어가는 변수의 값으로 삽입될 것이다.

이제 이렇게 넣은 다음에 서버를 실행해보면 

이렇게 실행되는 것을 볼 수 있다.

 

18. Template Layout 생성하기

Node.js의 Template Layer는 서버 측에서 동적으로 HTML을 생성하는 데 사용되는 템플릿 엔진을 의미한다.

템플릿 엔진은 HTML과 서버 데이터를 결합하여 동적인 웹 페이지를 생성하는 역할을 한다.

이러한 템플릿 엔진은 웹 애플리케이션의 뷰(View) 레이어에 해당한다.

 

쉽게 생각하면 전체적인 틀이 되는 부분이 template layout이고 그 내부에서 내용물만 갈아끼는 형태로 구성되도록 하는 방식을 이야기한다.

 

먼저 기존에 우리가 만들었던 hbs파일을 복사해서 layout.hbs파일을 하나 생성해주자.

그리고 우리는 이안에서 body태그 내부에 있는것 빼고 모두 지워주자.

그리고 이게 layout.hbs로 된 페이지가 열린것인지 확인하기 위해서 태그를 하나 추가해주고

body 태그 내부에 이번엔 중괄호 두개가 아니라 중괄호 3개를 쓰고 body라고 변수 넣듯이 넣어주자.

그 다음에

index.hbs에서는 body 내부에 있는 내용만 남기고 지워주자.

이러고 서버를 재시작해서 확인해보면

이렇게 레이아웃 폴더로 열린것을 확인할 수 있다.

의문점이 드는건 layout.hbs라는 것을 열라고 하지도 않았는데 자동으로 layout.hbs를 열고 또 거기에 {{{body}}}안에 자동으로 index.hbs를 넣어준다는 것이다.

이는 express-handlebars에서 defaultLayout을 설정하지 않으면, 기본적으로 views/layouts/layout.hbs 파일을 자동으로 레이아웃 파일로 사용하기 때문이다.

그리고 서버에서 요청을 받아 값을 어떤 view파일을 가져다 보여줄것인지를 get요청의 render에 설정해줬다면 그걸 body안에 넣어주는 것이다.

이를 확인하기 위해 추가적으로 nations.hbs라는 파일을 하나 더 생성해주고 이 안에는 p태그 하나만 사용해서 내용을 추가로 작성해주자.

그리고 서버에서 헤당 값을 받아 보여줄 메서드를 하나 추가해주자.

nations.controller에 함수를 하나 추가로 생성해주고 

그 안에는 send가 아니라 render를 통해서 nations.hbs를 반환시켜주는데 그때 값 who를 넣어보내주자.

이제 이 함수를 export해주고

nations.routes에 추가로 라우터 하나를 생성해주고

서버를 재시작 한 후에 /nations/please/getoff로 이동해보면

이렇게 template layout을 사용해서 nations.hbs를 출력한것을 확인할 수 있다.

 

layout.hbs의 이름이 isnotlayout.hbs로 변경한다면

서버에서 이전처럼 url로 접근한다 하더라도 

layout을 통해 접근하는게 아님을 알 수 있다.

 

그렇기에 layout.hbs는 명칭이 layout.hbs여야하고 이게 default 값으로 설정되어 설정값없이 layout 템플릿을 사용할 수 있음을 알 수 있다.

반응형