[ TIL 221212 ] Express

ponyo·2022년 12월 12일
0

Today I Learned

목록 보기
23/30

Express

Install

Node.js를 위한 빠르고 개방적인 간결환 웹 프레임워크

yarn add express

Express와 함께 사용할 수 있는 서드파티 패키지들

express-useragent

접근한 클라이언트의 정보(운영체제정보, 브라우저 정보, IP주소 등)를 취득할 수 있는 기능을 제공

yarn add express-useragent

serve-static

특정 폴더를 통째로 웹에 노출시키는 기능 (폴더 구조가 그대로 URL 노출됨)
img, css, js, 정적 html 파일 등을 호스팅하고자 할 대 사용함

yarn add serve-static

serve-favicon

favicon 파일을 브라우저에 전달한다.

yarn add serve-favicon

Express 기반 백엔드 구현 기본 코드 구조

/*---------------------------------------------------------- 
 | 1) 모듈참조
 -----------------------------------------------------------*/ 
/*----------------------------------------------------------
 | 2) Express 객체    생성
 -----------------------------------------------------------*/ 
/*----------------------------------------------------------
 | 3) 클라이언트의    접속시    초기화
 -----------------------------------------------------------*/ 
/*----------------------------------------------------------
 | 4) Express 객체의    추가    설정
 -----------------------------------------------------------*/ 
/*----------------------------------------------------------
 | 5) 각 URL별    백엔드    기능    정의
 -----------------------------------------------------------*/ 
/*----------------------------------------------------------
 | 6) 설정한    내용을    기반으로    서버    구동    시작
 -----------------------------------------------------------*/
/*---------------------------------------------------------- 
 //* 1) 모듈참조
 -----------------------------------------------------------*/

//&  ** 직접 구현한 모듈 */
const logger = require('../helper/LogHelper');
const { myip, urlFormat } = require('../helper/UtilHelper');

//& ** 내장 모듈 */
const url = require('url');
const fs = require('fs');
const { join, resolve } = require('path');

//& ** 설치가 필요한 모듈 */
const dotenv = require('dotenv');
const express = require('express'); // Express 본체
const useragent = require('express-useragent'); // 클라이언트의 정보를 조회할 수 있는 기능
const serveStatic = require('serve-static'); // 특정 폴더의 파일을 URL로 노출 시킴
const serveFavicon = require('serve-favicon'); // favicon 처리
const bodyParser = require('body-parser'); // POST 파라미터 처리
const methodOverride = require('method-override'); // PUT, DELETE 파라미터 처리
const cookieParser = require('cookie-parser'); // Cookie 처리
const expressSession = require('express-session') // Session 처리


/*----------------------------------------------------------
 //* 2) Express 객체 생성
 -----------------------------------------------------------*/
// 여기서 생성한 app 객체의 use() 함수를 사용해서
// 각종 외부 기능, 설정 내용, URL을 계속해서 확장하는 형태로 구현이 console.error('진행된다',진행된다)
const app = express();

// 설정 파일 내용 가져오기 
const configFileName = process.env.NODE_ENV !== 'production' ? '.env.server.development' : '.env.server.production';
const configPath = join(resolve(), configFileName);

// 파일이 존재하지 않을 경우 강제로 에러 발생함.
if (!fs.existsSync(configPath)) {
  try {
    throw new Error();
  } catch (e) {
    console.error('================================');
    console.error('|   Configuration Init Error   |');
    console.error('================================');
    console.error('환경설정 파일을 찾을 수 없습니다. 환경설정 파일의 경로를 확인하세요.');
    console.error(`환경설정 파일 경로: ${configPath}`);
    console.error('프로그램을 종료합니다.');
    process.exit(1);
  }
}

// 설정파일을 로드한다.
dotenv.config({ path: configPath });


/*----------------------------------------------------------
 //* 3) 클라이언트의 접속시 초기화
 -----------------------------------------------------------*/
//& ** app 객체에 UserAgent 모듈을 탑재 */
// --> Express 객체(app)에 추가되는 확장 기능들을 Express에서는 미들웨어라고 부른다.
// --> UserAgent 모듈은 초기화 콜백함수에 전달되는 req, res객체를 확장하기 때문에 다른 모듈들보다 먼저 설정되어야 한다.

app.use(useragent.express());

// 클라이언트의 접속을 감지
app.use((req, res, next) => {
  logger.debug('클라이언트가 접속했습니다.');

  // 클라이언트가 접속한 시간
  const beginTime = Date.now();

  // 클라이언트가 요청한 페이지 URL
  // 콜백함수에 전달되는 req 파라미터는 클라이언트가 요청한 URL의 각 부분을 변수로 담고 있다.
  const current_url = urlFormat({
    protocol: req.protocol, // ex) http://
    host: req.get('host'), // ex) 172.16.141.1
    port: req.port, // ex) 3000
    pathname: req.originalUrl, // ex) /page1.html
  });

  logger.debug(`[${req.method}] ${decodeURIComponent(current_url)}`);

  // 클라이언트의 IP 주소 (출처: 스택오버플로우)
  const ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress || req.socket.remoteAddress || req.connection.socket.remoteAddress;

  // 클라이언트의 디바이스 정보 기록 (UserAgent 사용)
  logger.debug(`[clinet] ${ip} / ${req.useragent.os} / ${req.useragent.browser} (${req.useragent.version}) / ${req.useragent.platform}`);

  // 클라이언트의 접속이 종료된 경우의 이벤트 --> 모든 응답의 전송이 완료된 경우
  res.on('finish', () => {
    // 접속 종료시간
    const endTime = Date.now();

    // 이번 접속에서 클라이언트가 머문 시간 = 백엔드가 실행하는게 걸린 시간
    const time = endTime - beginTime;

    logger.debug(`클라이언트의 접속이 종료되었습니다. ::: [runtime] ${time}ms`);
    logger.debug('--------------------------------');
  })

  next();
});

/*----------------------------------------------------------
 //* 4) Express 객체의 추가 설정
 -----------------------------------------------------------*/
/** POST 파라미터 수신 모듈 설정. 추가되는 미들웨어들 중 가장 먼저 설정해야 함 */
// body-parser를 이용해 application/x-www-form-urlencoded 파싱
// extended: true --> 지속적 사용.
// extended: false --> 한번만 사용.

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.text()); // TEXT 형식의 파라미터 수신 가능.
app.use(bodyParser.json()); // JSON 형식의 파라미터 수신 가능.

/** HTTP PUT, DELETE 전송방식 확장 */
// 브라우저 개발사들이 PUT, DELETE 방식으로 전송하는 HTTP Header 이름
app.use(methodOverride('X-HTTP-Method')); // Microsoft
app.use(methodOverride('X-HTTP-Method-Override')); // Google/GData
app.use(methodOverride('X-HTTP-Override')); // IBM

/** 쿠키를 처리할 수 있는 객체 연결 */
// cookie-parser는 데이터를 저장, 조회 할 때 암호화 처리를 동반한다.
// 이 때 암호화에 사용되는 key 문자열을 개발자가 정해야 한다.
app.use(cookieParser(process.env.COOKIE_ENCRYPT_KEY));

/** 세션 설정 */
app.use(expressSession({
  // 암호화 키
  secret: process.env.SESSION_ENCRYPT_KEY,
  // 세션이 초기화 되지 않더라도 새로 저장할지 여부 (일반적으로 false)
  resave: false, // ^ false가 초기값
  // 세션이 저장되기 전에 기존의 초기화 상태로 만들지 여부
  saveUninitialized: false // ^ false가 초기값
}))

/** HTML, CSS, IMG, JS 등의 정적 파일을 URL에 노출시킬 폴더 연결 */
// "http://아이피(혹은 도메인):포트번호" 이후의 경로가 router에 등록되지 않은 경로라면
// static 모듈에 연결된 폴더 안에서 해당 경로를 탐색한다.
app.use('/', serveStatic(process.env.PUBLIC_PATH));

/** favicon 설정 */
app.use(serveFavicon(process.env.FAVICON_PATH));

/** 라우터 (URL 분배기) 객체 설정 --> 맨 마지막에 설정 */
const router = express.Router();
// 라우터를 express에 등록
app.use('/', router);

/*----------------------------------------------------------
 //* 5) 각 URL별 백엔드 기능 정의
 -----------------------------------------------------------*/
/** step-2에서 추가되는 부분 */
//router.route(path).get|post|put|delete((req, res, next) => {})
router.get('/page1', (req, res, next) => {
  // 브라우저에게 전달할 응답 내용
  let html = '<h1>Page1</h1>';
  html += '<h2>Express로 구현한 Node.js 백엔드 페이지</h2>';

  /** 응답보내기(1) - Node 순정 방법 */
  // res.writeHead(200);
  // res.write(html);
  // res.end();

  /** 응답보내기(2) - Express의 간결화된 방법 */
  // res.status(200);
  // res.send(html);

  // 메서드 체인 가능
  res.status(200).send(html);
});

router.get('/page2', (req, res, next) => {
  // 브라우저에게 전달할 응답 내용
  // let html = '<h1>Page2</h1>';
  // html += '<h2>Node.js Backend Page</h2>';
  let html = { a: 100, b: 200 };
  res.status(200).send(html);
});

router.get('/page3', (req, res, next) => {
  // 페이지 강제 이동
  res.redirect('https://www.naver.com');
})

/** step-3에서 추가되는 내용 */
// 아래의 html 파일들을 통해 테스트 해야함
// --> public/get_params_by_link.html
// --> public/get_params_by_form.html
// --> public/get_params_by_js.html
// --> public/get_params_by_ajax.html
router.get('/send_get', (req, res, next) => {
  // ex) ?answer=400&age=10&height=175&weight=80
  // GET 파라미터들은 req.query 객체의 하위 데이터로 저장된다.
  logger.debug('[프론트엔드로부터 전달받은 GET 파라미터]');
  for (let key in req.query) {
    const str = '\t >> ' + key + '=' + req.query[key];
    logger.debug(str);
  }

  // send_get?answer=0000 형태로 접근한 경우 answer파라미터 값 추출
  // const answer = req.query['answer'];
  const answer = req.query.answer;
  let html = null;

  if (parseInt(answer) == 300) {
    html = "<h1 style='color:#0066ff'>정답입니다.</h1>";
  } else {
    html = "<h1 style='color:#ff6600'>오답입니다.</h1>";
  }

  res.status(200).send(html);
});

// PATH 파라미터들은 브라우저를 통해 직접 접속하여 테스트
// ex) http://172.16.46.1: 3001/send_url/이젠아카데미/노드js
router.get('/send_url/:username/:age', (req, res, next) => {
  // PATH 파라미터는 req.params 객체의 하위 데이터로 저장된다.
  logger.debug('[프론트엔드로부터 전달받은 URL 파라미터]');
  for (let key in req.params) {
    const str = '\t >> ' + key + '=' + req.params[key];
    logger.debug(str);
  }

  const html = "<h1><span style='color:#0066ff'>" + req.params.username + "</span>님은 <span style='color:#ff6600'>" + req.params.age + '</sapn>세 입니다.</h1>';

  res.status(200).send(html);
})

// ^ queryString 는 req.query 객체에 들어있고/ path는 req.params에 들어있고 / post, put, delete 는 req.body에 들어있다. 

/** step-4에서 추가되는 내용 */
//& POST 파라미터를 처리하기 위한 라우터 등록 */
// Insomnia를 통한 테스트
router.post('/send_post', (req, res, next) => {
  // URL 파라미터들은 req.body 객체의 하위 데이터로 저장된다.
  logger.debug('[프론트엔드로부터 전달받은 POST 파라미터]');
  for (let key in req.body) {
    const str = '\t >> ' + key + '=' + req.body[key];
    logger.debug(str);
  }

  const html = "<h1><span style='color:#0066ff'>" + req.body.username + "</span>님의 이메일 주소는 <span style='color:#ff6600'>" + req.body.email + '</span> 입니다.</h1>';

  res.status(200).send(html);
})

//& PUT 파라미터를 처리하기 위한 라우터 등록 */
// Insomnia를 통한 테스트
router.put('/send_put', (req, res, next) => {
  // URL 파라미터들은 req.body 객체의 하위 데이터로 저장된다.
  logger.debug('[프론트엔드로부터 전달받은 POST 파라미터]');
  for (let key in req.body) {
    const str = '\t >> ' + key + '=' + req.body[key];
    logger.debug(str);
  }

  const html = "<h1><span style='color:#0066ff'>" + req.body.username + "</span>님의 이메일 주소는 <span style='color:#ff6600'>" + req.body.grade + '</span>학년 입니다.</h1>';

  res.status(200).send(html);
})

//& Delete 파라미터를 처리하기 위한 라우터 등록 */
// Insomnia를 통한 테스트
router.delete('/send_delete', (req, res, next) => {
  // URL 파라미터들은 req.body 객체의 하위 데이터로 저장된다.
  logger.debug('[프론트엔드로부터 전달받은 POST 파라미터]');
  for (let key in req.body) {
    const str = '\t >> ' + key + '=' + req.body[key];
    logger.debug(str);
  }

  const html = "<h1><span style='color:#0066ff'>" + req.body.username + "</span>님의 이메일 주소는 <span style='color:#ff6600'>" + req.body.point + '</span>점입니다.</h1>';

  res.status(200).send(html);
})

//& 상품에 대한 Restful API 정의하기
// Insomnia를 통한 테스트
// 위의 형태처럼 개별적인 함수로 구현 가능하지만 대부분 하나의 URI에 메서드 체인을 사용해서 4가지 전송 방식을 한번에 구현
router
  .get('/product/:productNumber', (req, res, next) => {
    // URL Params 형식으로 조회할 상품의 일련번호를 전달받아야 한다.
    const html = "<h1><span style='color:#0066ff'>" + req.params.productNumber + "</span>번 상품 <span style='color:#ff6600'>조회</span>하기</h1>";
    res.status(200).send(html);
  })
  .post('/product', (req, res, next) => {
    // <form> 상에 저장할 상품 정보를 입력 후 전송한다. (주로 관리자 기능)
    // 저장시에는 일련번호는 전송하지 않으며 저장 후 자동으로 발급되는 일련번호를 프론트에게 돌려줘야 한다.
    let html = "<h1><span style='color:#0066ff'>" + req.body.productNumber +
      "</span> 상품 <span style='color:#ff6600'>등록</span>하기</h1>";

    html += `<p>상품명: ${req.body.productName}</p>`
    html += `<p>재고수량: ${req.body.qty}</p>`;

    res.status(200).send(html);
  })
  .put('/product/:productNumber', (req, res, next) => {
    // <form> 상에 수정 상품 정보를 입력 후 전송한다. (주로 관리자 기능)
    // 몇번 상품을 수정할지 식별하기 위해 상품 일련번호가 함께 전송된다.
    const html = "<h1><span style='color:#0066ff'>" + req.params.productNumber +
      "</span>상품 <span style='color:#ff6600'>수정</span>하기</h1>";
    res.status(200).send(html);
  })
  .delete('/product/:productNumber', (req, res, next) => {
    // 삭제할 상품의 일련번호 전송
    const html = "<h1><span style='color:#0066ff'>" + req.params.productNumber +
      "</span>상품 <span style='color:#ff6600'>삭제</span>하기</h1>";
    res.status(200).send(html);
  });

//& Cookie
/** step-5에서 추가되는 내용 */
// 아래의 html파일을 통해 테스트 해야 함
// --> public/cookie.html
router
  .post("/cookie", (req, res, next) => {
    // POST로 전달된 파라미터 받기
    const msg = req.body.msg;

    // 일반 쿠키 저장하기 -> 유효시간을 30초로 설정
    res.cookie('my_msg', msg, {
      maxAge: 30 * 1000,
      path: "/",
      // domain: ".naver.com" 추후 도메인이 있으면 이렇게 작성해야함
    });

    // 암호화된 쿠키 저장하기 -> 유효시간을 30초로 설정
    res.cookie("my_msg_signed", msg, {
      maxAge: 30 * 1000,
      path: "/",
      signed: true,
    });

    res.status(200).send("ok");
  })
  .get("/cookie", (req, res, next) => {
    // 일반 쿠키들은 req.cookies 객체의 하위 데이터로 저장된다. (일반 데이터)
    for (let key in req.cookies) {
      const str = '[cookies] ' + key + '=' + req.cookies[key];
      logger.debug(str);
    }

    // 암호화된 쿠키값들은 req.signedCookies 객체의 하위 데이터로 저장된다.
    for (let key in req.signedCookies) {
      const str = '[signedCookies] ' + key + '=' + req.signedCookies[key];
      logger.debug(str);
    }

    // 원하는 쿠키값을 가져온다.
    const my_msg = req.cookies.my_msg;
    const my_msg_signed = req.signedCookies.my_msg_signed;

    const result_data = {
      my_msg: my_msg,
      my_msg_signed: my_msg_signed,
    };

    res.status(200).send(result_data);
  })
  .delete("/cookie", (req, res, next) => {
    // 저장시 domain, path를 설정했다면 삭제 시에도 동일한 값을 지정해야 함
    res.clearCookie('my_msg', { path: '/' });
    res.clearCookie('my_msg_signed', { path: '/' });
    res.status(200).send('clear');
  });

//& Session
/** step-6에서 추가되는 내용 */
// Insomnia로 테스트
router
  .post('/session', (req, res, next) => {
    // POST로 전송된 변수값을 추출
    const username = req.body.username;
    const nickname = req.body.nickname;

    // 세선 져장
    req.session.username = username;
    req.session.nickname = nickname;

    // 결과 응답
    const json = { rt: "ok" };
    res.status(200).send(json);
  })
  .get('/session', (req, res, next) => {
    // 저장되어 있는 모든 session값 탐색
    for (let key in req.session) {
      const str = '[session] ' + key + '=' + req.session[key];
      logger.debug(str);
    }

    // 세션 데이터를 JSON으로 구성 후 클라이언트에게 응답으로 전송
    const my_data = {
      username: req.session.username,
      nickname: req.session.nickname,
    };

    res.status(200).send(my_data);
  })
  .delete('/session', async (req, res, next) => {
    let result = 'ok';
    let code = 200;

    try {
      await req.session.destroy();
    } catch (e) {
      logger.error(e.message);
      result = e.message;
      code = 500;
    }

    const json = { rt: result };
    res.status(code).send(json);
  });

//& Login
// public/login.html

router
  .post('/session/login', (req, res, next) => {
    const id = req.body.userid;
    const pw = req.body.userpw;

    logger.debug('id=' + id);
    logger.debug('pw=' + pw);

    let login_ok = false;
    if (id == 'node' && pw == '1234') {
      logger.debug('로그인 성공');
      login_ok = true;
    }

    let result_code = null;
    let result_msg = null;

    if (login_ok) {
      req.session.userid = id;
      req.session.userpw = pw;
      result_code = 200;
      result_msg = 'ok';
    } else {
      result_code = 403;
      result_msg = 'fail';
    }

    const json = { rt: result_msg };
    res.status(result_code).send(json);
  })
  .delete('/session/login', async (req, res, next) => {
    let result = 'ok';
    let code = 200;

    try {
      await req.session.destroy();
    } catch (e) {
      logger.error(e.message);
      result = e.message;
      code = 500;
    }

    const json = { rt: result };
    res.status(code).send(json);
  })
  .get('/session/login', (req, res, next) => {
    const id = req.session.userid;
    const pw = req.session.userpw;

    let result_code = null;
    let result_msg = null;

    if (id !== undefined && pw !== undefined) {
      logger.debug('현재 로그인 중이 맞습니다.');
      result_code = 200;
      result_msg = 'ok';
    } else {
      logger.debug('현재 로그인 중이 아닙니다.');
      result_code = 400;
      result_msg = 'fail';
    }

    const json = { rt: result_msg };
    res.status(result_code).send(json);
  })

/*----------------------------------------------------------
 //* 6) 설정한 내용을 기반으로 서버 구동 시작
 -----------------------------------------------------------*/

const ip = myip();

app.listen(process.env.PORT, () => {
  logger.debug('-------------------------------');
  logger.debug('|     Start Express Server     |');
  logger.debug('-------------------------------');

  ip.forEach((v, i) => {
    logger.debug(`server address => http://${v}:${process.env.PORT}`);
  });

  logger.debug('-------------------------------');
});

/** 프로그램(서버) 종료 이벤트 */
process.on('exit', function () {
  logger.debug('백엔드가 종료되었습니다.');
});

/** Ctrl+C를 눌러서 프로그램을 강제 종료 시킬 때의 이벤트 */
process.on('SIGINT', () => {
  // 정상적으로 프로그램을 종료하도록 한다.
  // --> 위에서 정의한 close 이벤트가 호출된다.
  process.exit();
})
profile
😁

0개의 댓글