Node.js와 Express를 활용한 서버 구축
Table of Content
1. MVC 디자인 패턴
History
•
1979년 제록스 팔로알토 연구소에서 트라이브 린스케이지라는 분이 MVC를 처음 만들었다.
•
복잡하고 어려운 데이터들을 다양한 관점에서 다루기 위한 솔루션으로 MVC가 등장했음
◦
참고로 저 연구소는 객체 지향 프로그래밍 OOP를 창시한 앨런 케이도 있다.
◦
현재 사용되고 있는 컴퓨터 및 네트워크 전반에 대한 가장 중요한 원천 기술을 개발한 곳으로 유명(GUI OS, 이더넷(LAN연결방식), 볼 마우스, 광 마우스)
MVC(Model-View-Controller) 소프트웨어 디자인 패턴?
•
사용자 인터페이스, 데이터 및 논리 제어를 구현하는데 널리 사용되는 소프트웨어 디자인 패턴
•
소프트웨어의 비즈니스 로직과 화면을 구분하는데 중점을 두고 있음
•
이러한 "관심사 분리" 는 더 나은 업무의 분리와 향상된 관리를 제공
•
MVC의 주요 장점
1.
컴포넌트의 명확한 역할 분리로 인해 서로간의 결합도를 낮추기 위함
2.
코드의 재사용성 및 확장성을 높이기 위함
3.
서비스를 유지보수하고 테스트하는데 용이해짐
4.
개발자 간의 커뮤니케이션 효율성을 높일 수 있음
2. MVC의 요소
Model
•
소프트웨어나 애플리케이션에서 정보 및 데이터 부분을 의미
•
Controller에게 받은 데이터를 조작(가공)하는 역할을 수행
View
•
Controller에게 받은 Model의 데이터를 사용자에게 시각적으로 보여주기 위한 역할을 수행 사용자에게 보여지는 화면
Controller
•
요청을 파악하고 URL에 적절한 Method를 호출하여 Service에서 비즈니스 로직을 처리
•
이 후 결과를 Model에 저장하여 View에게 전달하는 역할을 수행
•
결국 Controller는 Model과 View의 역할을 분리하는 중요한 요소
3. Front-Controller 패턴
•
프론트 컨트롤러 패턴은 MVC 패턴이 발전하면서 나타난 개념
•
1990년대 후반에서 2000년대 초반에 웹 애플리케이션의 복잡성이 증가함에 따라 패턴화 됨
•
모든 요청을 단일 진입점(Controller)에서 처리하여 공통적인 요청 처리 로직을 중앙에서 관리하고, 요청을 적절한 핸들러로 분기하는 구조
◦
과거에는 각 요청마다 요청을 받는 객체가 별도로 존재해야 했다.
▪
ex) Java진영의 Servlet 초기 구조 1997년 → 2004년 Spring Web MVC DispatcherServlet(프론트컨트롤러 방식)
▪
이처럼 다른 언어의 웹 개발 라이브러리들은 개념을 확립해나가면서 변천사를 거치고 있었다.
4. Node.js와 Express 입장에서 MVC
보다 최적화된 웹 개발 환경부터 시작
•
Java, PHP 등 오랜 역사를 가진 생태계에서는 그 변천 과정이 있지만
•
Node.js는 2009년, Express.js가 2010년에 개발 된 것을 생각하면 이미 확립된 디자인 패턴과 아키텍처를 기반으로 새로운 환경에 최적화된 형태로 발전
•
따라서 우리는 기본 사양처럼 해당 개념들이 적용된 상태를 사용하고 있어 효율적인 개발이 가능한 배경이 있음을 이해
◦
요청을 라우터가 받아내고 → 각 컨트롤러로 분기 → 데이터, 비지니스로직을 통해 → 뷰에 그려서 반환
◦
컨트롤러가 요청과 응답의 흐름에서 클라이언트와 서버의 관문 역할을 하게 된다.
5. MVC 패턴으로 리팩토링
5.1 리팩토링 이전 코드
현재 우리의 코드 상태이다. app.js는 서버실행부터 라우터, 데이터 가공 등 모든 책임을 가지고 있다.
const express = require('express')
const ejs = require('ejs')
const bodyParser = require('body-parser')
const mysql = require('mysql2')
require('dotenv').config();
const app = express()
const port = 3000
app.set('view engine', 'ejs')
app.set('views', './views')
// static file serving
app.use(express.static(__dirname+'/public'))
// parsing application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false}))
// parsing JSON
app.use(bodyParser.json())
// MySQL Connection Pool :
// MySQL 커넥션을 사용 할 때는, 주로 커넥션 풀을 이용하여 관리하는 것이 권장된다.
const connectionPool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PW,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
connectionLimit: 10, // 최대 연결 수 설정(필요시)
insecureAuth: true,
});
// MySQL connection check
connectionPool.getConnection((err, connection) => {
if (err) {
console.error('MySQL 연결 중 에러 발생: ', err);
} else {
console.log('MySQL에 연결되었습니다.');
connection.release();
}
});
app.get('/', (req, res) => {
res.render('index');
})
app.get('/blog', (req, res) => {
res.render('blog');
})
app.get('/users', (req, res) => {
res.render('users');
})
app.get('/contact', (req, res) => {
res.render('contact');
})
app.post('/api/contact', (req, res) => {
const name = req.body.name;
const phone = req.body.phone;
const email = req.body.email;
const memo = req.body.memo;
const SQL_Query = `INSERT INTO contact(name, phone, email, memo, create_at, modify_at)
VALUES ('${name}', '${phone}', '${email}', '${memo}', NOW(), NOW())`
connectionPool.query(SQL_Query, (err, result) => {
if (err) {
console.error('데이터 삽입 중 에러 발생: ', err);
res.status(500).send('내부 서버 오류')
} else {
console.log('데이터가 삽입 되었습니다.');
res.send("<script> alert('문의사항이 등록되었습니다.'); location.href='/' </script>")
}
})
})
app.get('/contactList', (req, res) => {
const selectQuery = `SELECT * FROM contact ORDER BY id DESC`;
connectionPool.query(selectQuery, (err, result) => {
if (err) {
console.error('데이터 조회 중 에러 발생: ', err);
res.status(500).send('내부 서버 오류')
} else {
console.log('데이터가 조회 되었습니다.');
console.log(result);
res.render('contactList', {lists:result});
}
});
});
app.delete('/api/contactDelete/:id', (req, res) => {
const id = req.params.id;
const deleteQuery = `DELETE FROM contact WHERE id='${id}'`
connectionPool.query(deleteQuery, (err, result) => {
if (err) {
console.error('데이터 삭제 중 에러 발생: ', err);
res.status(500).send('내부 서버 오류')
} else {
console.log('데이터가 삭제 되었습니다.');
console.log(result);
res.send("<script> alert('문의사항이 삭제되었습니다.'); location.href='/contactList' </script>")
}
})
})
app.put('/api/contactUpdate/:id', (req, res) => {
const id = req.params.id;
const status = "done";
const updateQuery = `UPDATE contact SET status = '${status}' WHERE id = '${id}'`
connectionPool.query(updateQuery, (err, result) => {
if (err) {
console.error('데이터 수정 중 에러 발생: ', err);
res.status(500).send('내부 서버 오류')
} else {
console.log('데이터가 수정 되었습니다.');
console.log(result);
res.send("<script> alert('문의사항의 상태가 변경되었습니다.'); location.href='/contactList' </script>")
}
})
})
app.listen(port, () => {
console.log(`Node Legacy App listening on port ${port}`)
})
JavaScript
복사
5.2 MVC 분리
JavaScript 진영에서는 NestJS 이전까지 프로젝트 구조화의 정확한 패턴을 강요하지 않는 유연성이 배경이기 때문에 딱히 정해진 패턴의 구조는 없다. 하지만 현재 app.js는 프로젝트의 메인 함수로써 너무 많은 책임과 역할을 가지고 있다.
따라서, MVC 패턴에 따라 폴더를 구조화하여 관리하고자 한다. 메인 서버 실행 파일 app.js로부터 각 관련 부분을 폴더 및 파일로 모듈화 시켜서 사용하게 된다.
View 분리
•
이미 우리는 뷰 템플릿 엔진을 사용하면서 views 폴더에 클라이언트에 반환될 화면으로 그려질 템플릿들을 정의해두었다.
•
또한 데이터가 전달되더라도 그려 질 수 있는 EJS의 데이터 바인딩 문법 등을 사용하면서 View의 역할에 맞게끔 분리된 상태이다.
Model 분리
•
모델은 비지니스로직과 데이터베이스와 관련된 부분이 이동해야 한다.
•
root/models/ 폴더를 생성한다.
◦
contactModel.js파일 생성 후 아래와 같이 app.js로부터 데이터베이스와 상호작용하는 관련 코드를 이동
◦
ps. 코드 이동 시 쿼리문의 표현 방식을 변경
▪
인젝션 공격 방지 및 기본 최적화, 가독성 향상을 위한 Parameterized Query 사용
▪
파라미터화로 ${..} → ? 로 쿼리문장이 변경된다. 이는 mysql2가 제공하는 기능
▪
인젝션 해킹 공격은 악의적인 쿼리문으로 개인 정보를 탈취하는 방법 중 하나
▪
${..}와 같은 부분에 쿼리의 주석 표현인 --등을 삽입하여 이후 컬럼인 패스워드 등을 생략하는 방법
const connectionPool = require('./db');
// 데이터 조회
const getContacts = (callback) => {
const selectQuery = `SELECT * FROM contact ORDER BY id DESC`;
connectionPool.query(selectQuery, (err, results) => {
callback(err, results);
});
};
// 데이터 삽입
const addContact = (contact, callback) => {
const insertQuery = `INSERT INTO contact(name, phone, email, memo, create_at, modify_at) VALUES (?, ?, ?, ?, NOW(), NOW())`;
connectionPool.query(insertQuery, [contact.name, contact.phone, contact.email, contact.memo], (err, result) => {
callback(err, result);
});
};
// 데이터 삭제
const deleteContact = (id, callback) => {
const deleteQuery = `DELETE FROM contact WHERE id = ?`;
connectionPool.query(deleteQuery, [id], (err, result) => {
callback(err, result);
});
};
// 데이터 업데이트
const updateContactStatus = (id, callback) => {
const updateQuery = `UPDATE contact SET status = 'done' WHERE id = ?`;
connectionPool.query(updateQuery, [id], (err, result) => {
callback(err, result);
});
};
module.exports = {
getContacts,
addContact,
deleteContact,
updateContactStatus,
};
JavaScript
복사
◦
동일 폴더에 db.js 생성 후 아래 데이터베이스 연결과 관련된 내용을 이동
const mysql = require('mysql2');
require('dotenv').config();
// MySQL connection Pool :
const connectionPool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PW,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
connectionLimit: 10,
insecureAuth: true,
});
// MySQL connection check
connectionPool.getConnection((err, connection) => {
if (err) {
console.error('MySQL에 연결 중 에러 발생:', err);
} else {
console.log('MySQL에 연결되었습니다.');
connection.release();
}
});
module.exports = connectionPool;
JavaScript
복사
Controller 분리
•
컨트롤러는 Routes와 Controller로 분리하고자 한다. 현재 프로젝트 규모로는 구조가 과도하긴 하지만 개념상 구조와 일치시켜보고자 진행했다.
•
root/routes 폴더 생성
◦
router.js 파일 생성 후 라우터의 역할에 맞게 요청을 각 컨트롤러로 전달 각 엔드포인트 요청 URL을 표기 (REST에 따라 자원명으로 통일함)
const express = require('express');
const router = express.Router();
const apiController = require('../controllers/apiController');
const viewController = require('../controllers/viewController');
/* route to view controllers */
// 메인 뷰페이지 반환
router.get('/', viewController.getIndexViewPage);
// 기타 페이지 반환 추가..
/* route to api controllers */
// Contact 기능
// 데이터 삽입
router.post('/api/contact/', apiController.addContact);
// 데이터 조회
router.get('/api/contact/', apiController.getContacts);
// 데이터 삭제
router.delete('/api/contact/:id', apiController.deleteContact);
// 데이터 업데이트
router.put('/api/contact/:id', apiController.updateContact);
module.exports = router;
JavaScript
복사
•
root/controllers 폴더 생성
◦
apiController.js 파일 생성 후 데이터 요청, 응답 전용 컨트롤러의 역할에 맞게 각 요청 호출을 받고 비지니스로직+데이터계층으로 전달 후 응답 반환
const contactModel = require('../models/contactModel');
// 데이터 삽입
const addContact = (req, res) => {
const contact = {
name: req.body.name,
phone: req.body.phone,
email: req.body.email,
memo: req.body.memo
};
contactModel.addContact(contact, (err, result) => {
if (err) {
console.error('데이터 삽입 중 에러 발생:', err);
return res.status(500).json({ message: '내부 서버 오류' });
}
res.status(201).json({ message: '문의사항이 등록되었습니다.', contactId: result.insertId });
});
};
// 데이터 조회
const getContacts = (req, res) => {
contactModel.getContacts((err, result) => {
if (err) {
console.error('데이터 조회 중 에러 발생:', err);
return res.status(500).send('내부 서버 오류');
}
res.json(result);
});
};
// 데이터 삭제
const deleteContact = (req, res) => {
const id = req.params.id;
contactModel.deleteContact(id, (err, result) => {
if (err) {
console.error('데이터 삭제 중 에러 발생:', err);
return res.status(500).send('내부 서버 오류');
}
res.status(200).json({ message: '문의사항이 삭제되었습니다.' });
});
};
// 데이터 업데이트
const updateContact = (req, res) => {
const id = req.params.id;
contactModel.updateContactStatus(id, (err, result) => {
if (err) {
console.error('데이터 업데이트 중 에러 발생:', err);
return res.status(500).send('내부 서버 오류');
}
res.status(200).json({ message: '문의사항이 업데이트되었습니다.' });
});
};
module.exports = {
addContact,
getContacts,
deleteContact,
updateContact
};
JavaScript
복사
◦
viewController.js 파일 생성 후 특정 뷰 페이지 반환
const getIndexViewPage = (req, res) => {
res.render('index');
};
module.exports = {
getIndexViewPage,
};
JavaScript
복사
6. 기타
JSON이란?
json.org
•
JSON (JavaScript Object Notation)은 경량의 DATA-교환 형식이다. 이 형식은 사람이 읽고 쓰기에 용이하며, 기계가 분석하고 생성함에도 용이하다. JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999의 일부에 토대를 두고 있다.
•
JSON은 완벽하게 언어로 부터 독립적이지만 C-family 언어 - C, C++, C#, Java, JavaScript, Perl, Python 그외 다수 - 의 프로그래머들에게 친숙한 관습을 사용하는 텍스트 형식이다. 이러한 속성들이 JSON을 이상적인 DATA-교환 언어로 만들고 있다.
•
JSON은 주로 웹 애플리케이션에서 서버와 클라이언트 간에 데이터를 교환하는 데 사용하는 형식
CRUD란?
대부분의 컴퓨터 소프트웨어가 가지는 기본적인 데이터 처리 기능인 Create(생성), Read(읽기), Update(갱신), Delete(삭제)를 말한다.
CRUD 작업은 웹 개발 에서 가장 기본적인 개념 중 하나. 간단한 웹 애플리케이션을 구축하든 복잡한 엔터프라이즈 시스템을 구축하든 CRUD 작업을 이해하는 것은 데이터 작업에 필수적.
소프트웨어 개발에서 CRUD 작업의 이점은 다양하며 애플리케이션의 효율성, 유지 관리성 및 사용자 경험에 크게 기여하며 주요 이점은 다음과 같다.
•
표준화 :
◦
CRUD 작업은 데이터 스토리지 시스템과 상호 작용하기 위한 공통 프레임워크를 설정하여 개발자가 다양한 애플리케이션 및 플랫폼을 쉽게 이해하고 작업할 수 있도록 함
•
단순화된 개발 프로세스 :
◦
CRUD 모델을 고수함으로써 개발자는 데이터 조작을 위해 구현할 명확한 작업 세트가 있으므로 애플리케이션 작성 프로세스를 간소화할 수 있음. 이는 일관되고 효율적인 개발 관행을 유지
•
향상된 유지 관리 :
◦
CRUD 기반 응용 프로그램은 일반적으로 데이터 관리에 대한 표준화된 접근 방식을 따르기 때문에 유지 관리가 쉬움
◦
이를 통해 개발자는 필요에 따라 애플리케이션의 문제를 해결하고 수정하기 쉬워짐
•
향상된 사용자 경험 :
◦
CRUD 작업을 구현하면 사용자가 애플리케이션 내에서 데이터 생성, 읽기, 업데이트 및 삭제와 같은 필수 작업을 원활하게 수행할 수 있음, 결과 직관적이고 만족스러운 사용자 경험이 제공
•
모듈성과 유연성 :
◦
CRUD 기반 애플리케이션에서 관심사를 분리하면 모듈성과 유연성이 향상됨
◦
이는 개발자가 전체 시스템에 영향을 주지 않고 특정 응용 프로그램 부분을 쉽게 수정하거나 확장할 수 있음을 의미
•
확장성 :
◦
CRUD 모델에 따라 응용 프로그램은 증가하는 데이터 또는 사용자 요구등 확장에 대해 직관적으로 성능 향상 및 서비스 복구에 빠르게 대처
•
다양한 기술과의 호환성 :
◦
CRUD 작업은 관계형 데이터베이스, NoSQL 데이터베이스, RESTful API 및 GraphQL 등 서로 다른 기술이라도 CRUD 기반 패턴을 인지하여 활용, 통합하기가 더 쉬워짐
REST 설계에 따라 엔드 포인트 수정
•
기존 contactCreate, contactUpdate 와 같이 엔드포인트를 각각 명시하던 것에서 contact로 통일하고 HTTP Method에 따라서 분리 작동되도록 변경, Fetch 요청부분에서도 확인 필요.
•
SPA(Single Page Application)은 보통 CSR(Client Side Rendering) 방식으로 표현되는 경우가 많지만, Node.js와 기본 Express.js로 간단한 실습을 했으므로 이를 활용하여 SSR(Server Side Rendering)을 활용한 SPA를 구현하게 되었다. 이는 백엔드 개발자도 기본적인 프론트엔드 페이지 구성과 연관성을 연습하기 위함이다.
◦
여기서 개인의 커스터마이즈에 따라 View Controller에 여러 엔드포인트의 페이지를 반환하는 MPA로 확장한다면 일반적인 홈페이지의 개념을 구현하게 된다.
구조화 결과
•
프로젝트의 규모와 복잡성을 고려하여 적절한 구조화를 선택하는 것이 중요하다. 작은 프로젝트에서는 간단한 구조가 효율적일 수 있지만, 규모가 커질수록 명확한 구조가 필요해진다.
•
Node.js에서는 지금과 같은 형태의 폴더 구조를 가지지 않는 것이 일반적이다. 이는 NestJS의 선행 학습을 위해 디자인 패턴 학습에 맞추어 진행해본 케이스이다.
•
하지만 작은 규모의 프로젝트인 점과 비지니스 로직이 복잡하지 않은 위와 같은 규모의 프로젝트에서는 과도한 구조화로 오히려 복잡도가 높아진 모습도 볼 수 있다.
•
하지만 이 방식 또한 유연성을 강조하는 Node.js의 가치관 내에서는 유지보수성을 위한 한가지 선택일 수 있다. 따라서 무분별한 분리보다는 각 기능의 역할과 책임에 따라 관련 코드를 분리해보는 연습으로 마무리하고자 한다.
•
이후 NestJS의 CLI의 자동화 프로젝트 구조 생성, 각종 데코레이터(@애너테이션) 등으로 매우 함축적으로 위와 같은 구조화 과정이 진행된다. 따라서 어떠한 과정을 거치게 되었는지 웹 프로젝트 구조 변화를 보여주고자 진행되었다.
•
이후 각 계층별 코드량이 증가하는 실전 프로젝트에서 위와 유사한 구조를 직접 확인하게 된다.
결론
•
리팩토링을 통한 관심사의 분리
◦
Model : 데이터와 비즈니스 로직 관리
◦
View : 사용자 인터페이스 및 데이터 표현
◦
Controller : 사용자 요청 처리 및 Model과 View 연결
◦
Main : 애플리케이션 시작점 및 초기화
Related Posts
Search