node.js 코딩 패턴
Routes - Controllers - Services
node.js 는 코드의 특성별로 소스 파일을 나눈다고 한다.
처음에 잘 구조를 잡아두어야 유지보수할 때 큰 이득을 볼 수 있다....!
먼저 개념 정리...
모듈
모듈이란 관련된 코드들을 하나의 코드 단위로 캡슐화하는 것을 말한다.
다음과 같은 greeting.js 라는 파일이 있다고 하자. 이 파일은 두개의 함수를 포함하고 있다.
모듈 추출하기(exporting)
greeting.js 의 코드가 다른 파일에서 사용될 때 그 효용성이 증가할 것이다. 이러한 일을 하기 위해서는 다음과 같은 3가지의 단계를 거쳐야 한다.
1. greeting.js 파일의 코드 첫 부분에 다음과 같은 코드가 존재해야 한다.
// greeting.js
var exports = module.exports = {};
2. 다른 파일에서 exports 객체를 사용하기를 원한다면 greeting.js 파일에서 다음과 같이 작성해야 한다.
// greetings.js
// var exports = module.exports = {};
exports.sayHelloInEnglish = function() {
return "HELLO";
};
exports.sayHelloInSpanish = function() {
return "Hola";
};
위의 코드에서 exports 를 module.exports 로 대체할 수 있으며 같은 결과를 얻을 수 있다.
이 부분이 잘 이해가 가지 않는다면 exports 와 module.exports 가 같은 객체를 참조한다고 기억하기 바란다.
3. module.exports 의 현재 값은 다음과 같습니다.
module.exports = {
sayHelloInEnglish: function() {
return "HELLO";
},
sayHelloInSpanish: function() {
return "Hola";
}
};
모듈 사용하기(importing)
main.js 라는 새로운 파일에서 greeting.js 의 메소드를 사용할 수 있도록 import 하는 과정은 다음과 같다.
1. 먼저 require 이라는 키워드는 Node.js 에서 module(모듈) 을 import(추가) 하기 위해 사용한다.. require 는 다음과 같이 정의되어 있다.
var require = function(path) {
// ...
return module.exports;
};
2. main.js 에서 greetings.js 를 require 합니다.
// main.js
var greetings = require("./greetings.js");
위의 코드는 아래와 동일한 코드이다.
// main.js
var greetings = {
sayHelloInEnglish: function() {
return "HELLO";
},
sayHelloInSpanish: function() {
return "Hola";
}
};
3. main.js 에서 greeting.js 의 값과 메소드에 접근할 수 있다.
// main.js
var greetings = require("./greetings.js");
// "Hello"
greetings.sayHelloInEnglish();
// "Hola"
greetings.sayHelloInSpanish();
중요 포인트
require 키워드는 object 를 반환한다.
그리고 module.exports 와 exports 는 call by reference (같은 주소값) 로 동일한 객체를 바라보고 있고, 리턴되는 값은 항상 module.exports 입니다.
모듈은 기본적으로 객체이고, 이 객체를 module.exports, exports 모두 바라보고 있는데, 최종적으로 return 되는 것은 무조건 module.exports 라는 것이다.
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res) {
res.render('index', { title: 'Express' });
});
module.exports = router;
위의 소스는 다음과 같이 해석할 수 있다.
express.Router( ) 가 리턴한 "객체"에 일부 프로퍼티를 수정한 뒤, 이 객체 자체를 모듈로 리터난 것이다.
출처
https://jongmin92.github.io/2016/08/25/Node/module-exports_exports/
다른 예시
두가지의 파일이 있다.
// num.js 파일
const odd = '홀수입니다';
const even = '짝수입니다';
module.exports = { odd, even };
// fun.js 파일
const {odd, even} = require('./num');
function numTest(num) {
if(num % 2 === 0) return even;
else {
return odd;
}
}
module.exports = { odd, even, numTest }; // 이렇게 odd, even 을 한번 더 내보낼 수 있음
기본적으로 try catch 문으로 예외를 처리
에러가 발생할 만한 곳은 try catch 로 감싼다.
// error1.js
setInterval(() => {
console.log('시작');
try {
throw new Error('서버를 고장내주마!'); // 에러를 throw 한 것을 catch 에서 받는다
} catch (err){ // 에러가 아닌 것처럼 처리해준다.
console.err(err);
}
}, 1000);
서버와 클라이언트
클라이언트가 서버로 요청을 보낸다.
서버는 요청을 처리하고
처리 후 클라이언트로 응답을 보낸다.
->
HTTP 프로토콜로 서로 요청과 응답을 주고 받는다.
클라이언트 : 서버한테 "메인페이지 주세요~~"
노드로 http 서버 만들기
createServer 로 요청 이벤트에 대기
req 객체는 요청에 관한 정보가, res 객체는 응답에 관한 정보가 담겨 있다.
const http = require('http');
http.createServer((req, res) => { // 요청이 오면 실행
// 여기에 어떻게 응답할지 적습니다.
});
/*
프로미스
콜백 헬이라고 불리는 지저분한 자바스크립트 코드의 해결책
프로미스 : 내용이 실행되었지만 결과를 아직 반환하지 않은 객체
then을 붙이면 결과를 반환함
실행이 완료되지 않았으면 완료된 후에 then 내부 함수가 실행됨
Resolve(성공리턴값) -> then으로 연결
Reject(실패리턴값)-> catch 로 연결
Finally 부분은 무조건 실행됨
*/
const condition = true; // true 면 resolve, false 면 reject
const promise = new Promise((resolve, reject) => {
if(condition){
resolve('성공');
} else {
reject('실패');
}
});
// 여기엔 다른 코드가 들어갈 수 있음
promise
.then((message) => {
console.log(message); // 성공(resolve)한 경우 실행
.catch((error)=> {
console.error(error);
})
const promise = setTimeoutPromise(3000) // 잠시 저장하고
console.log('딴짓');
console.log('딴짓');
// 딴짓하다가
promise.then(()=>{
// 지금할래
});
promise 를 어떨때 쓰냐..
"저 파일을 읽어와, 네이버에 요청을 한번 보냈다가 와"
근데 그 요청이 항상 성공하는 것이 아님... 실패할 확률이 있다.....
비동기에는 항상 실패할 가능성을 염두해야 한다.
성공이면 resolve 호출
네이버가 날 거절하면 reject 호출
resolve 를 호출하면 then 으로 가고,
reject 를 호출하면 catch 로 간다.
async / await
async function 의 도입
변수 = await 프로미스; 인 경우 프로미스가 resolve 된 값이 변수에 저장
변수 await 값; 인 경우 그 값이 변수에 저장
async function findAndSaveUser(Users){
let user = await User.findOne({});
user.name = 'zero';
user = await user.save();
user = await User.findOne({ gender: 'm' });
// 생략
}
Promise 는 프로미스가 생성된 시점에는 알려지지 않았을 수도 있는 값을 위한 대리자로, 비동기 연산이
종료된 이후에 결과 값과 실패 사유를 처리하기 위한 처리기를 연결할 수 있습니다.
프로미스를 사용하면 비동기 메서드에서 마치 동기 메서드처럼 값을 반환할 수 있습니다.
다만 최종 결과를 반환하는 것이 아니고, 미래의 어떤 시점에 결과를 제공하겠다는 '약속(프로미스)'을
반환합니다.
생성자 = Promise( )
새로운 Promise 객체를 생성합니다. 주로 프로미스를 지원하지 않는 함수를 감쌀 때 사용합니다.
정적 메서드 = Promise.reject(reason)
주어진 사유로 거부하는 Promise 객체를 반환합니다.
정적 메서드2 = Promise.resolve( )
주어진 값으로 이행하는 Promise 객체를 반환합니다.
이때 지정한 값이 then 가능한 (then 메서드를 가지는) 값인 경우, Promise.resolve( ) 가 반환하는
프로미스는 then 메서드를 "따라가서" 자신의 최종 상태를 결정합니다. 그 외의 경우, 반환된 프로미스는
주어진 값으로 이행합니다.
let myFirstPromise = new Promise((resolve, reject) => {
// 우리가 수행한 비동기 작업이 성공한 경우 resolve(...)를 호출하고, 실패한 경우 reject(...)를 호출합니다.
// 이 예제에서는 setTimeout()을 사용해 비동기 코드를 흉내냅니다.
// 실제로는 여기서 XHR이나 HTML5 API를 사용할 것입니다.
setTimeout( function() {
resolve("성공!") // 와! 문제 없음!
}, 250)
})
myFirstPromise.then((successMessage) => {
// successMessage는 위에서 resolve(...) 호출에 제공한 값입니다.
// 문자열이어야 하는 법은 없지만, 위에서 문자열을 줬으니 아마 문자열일 것입니다.
console.log("와! " + successMessage)
});
async & await 실용 예제
async & await 문법이 가장 빛을 발하는 순간은 여러 개의 비동기 처리 코드를 다룰 때이다.
아래와 같이 각각 사용자와 할 일 목록을 받아오는 HTTP 통신 코드가 있다고 하자.
function fetchUser() {
var url = 'https://jsonplaceholder.typicode.com/users/1'
return fetch(url).then(function(response) {
return response.json();
});
}
function fetchTodo() {
var url = 'https://jsonplaceholder.typicode.com/todos/1';
return fetch(url).then(function(response) {
return response.json();
});
}
위 함수들을 실행하면 각각 사용자 정보와 할 일 정보가 담긴 프로미스 객체가 반환된다.
자 이제 이 두 함수를 이용하여 할 일 제목을 출력해보겠다. 살펴볼 예제 코드의 로직은 아래와 같다.
1. fetchUser( ) 를 이용하여 사용자 정보 호출
2. 받아온 사용자 아이디가 1이면 할 일 정보 호출
3. 받아온 할 일 정보의 제목을 콘솔에 출력
async function logTodoTitle() {
var user = await fetchUser();
if (user.id === 1) {
var todo = await fetchTodo();
console.log(todo.title); // delectus aut autem
}
}
logTodoTitle( ) 를 실행하면 콘솔에 deletus aut autem 가 출력될 것이다.
위 비동기 처리 코드를 만약 콜백이나 프로미스로 했다면 훨씬 더 코드가 길어졌을 것이고 가독성도 좋지 않았을 거다.
이처럼 async await 문법을 이용하면 기존의 비동기 처리 코드 방식으로 사고하지 않아도 되는 장점이 생긴다.
async & await 예외 처리
async & await 에서 예외를 처리하는 방법은 바로 try & catch 이다.
프로미스에서 에러 처리를 위해 .catch( ) 를 사용했던 것처럼 async 에서는 catch { } 를 사용하면 된다.
async function logTodoTitle() {
try {
var user = await fetchUser();
if (user.id === 1) {
var todo = await fetchTodo();
console.log(todo.title); // delectus aut autem
}
} catch (error) {
console.log(error);
}
}
위의 코드를 실행하다가 발생한 네트워크 통신 오류뿐만 아니라 간단한 타입 오류 등의 일반적인 오류까지도 catch로 잡아낼 수 있다. 발견된 에러는 error 객체에 담기기 때문에 에러의 유형에 맞게 에러 코드를 처리해주시면 된다.
참고 출처
https://joshua1988.github.io/web-development/javascript/js-async-await/
Node.js 디렉터리 구조
├───models
│ ├───user.model.js
├───routes
│ ├───user.route.js
├───services
│ ├───user.service.js
├───controllers
│ ├───user.controller.js
모듈식 코드 구조의 경우 논리는 이러한 디렉터리와 파일로 나누어 져야 합니다.
모델 - 모델의 스키마 정의
경로 - API 경로가 컨트롤러에 매핑
컨트롤러 - 컨트롤러는 요청 매개 변수 확인, 쿼리, 올바른 코드로 응답 보내기의 모든 로직을 처리합니다.
서비스 - 서비스에는 데이터베이스 쿼리와 반환 객체 또는 오류 발생이 포함됩니다.
모델 - 경로 - 컨트롤러 - 서비스 코드 구조
user.model.js
var mongoose = require('mongoose')
const UserSchema = new mongoose.Schema({
name: String
})
const User = mongoose.model('User', UserSchema)
module.exports = User;
user.routes.js
var express = require('express');
var router = express.Router();
var UserController = require('../controllers/user.controller')
router.get('/', UserController.getUsers)
module.exports = router;
user.controllers.js
var UserService = require('../services/user.service')
exports.getUsers = async function (req, res, next) {
// Validate request parameters, queries using express-validator
var page = req.params.page ? req.params.page : 1;
var limit = req.params.limit ? req.params.limit : 10;
try {
var users = await UserService.getUsers({}, page, limit)
return res.status(200).json({ status: 200, data: users, message: "Succesfully Users Retrieved" });
} catch (e) {
return res.status(400).json({ status: 400, message: e.message });
}
}
user.services.js
var User = require('../models/user.model')
exports.getUsers = async function (query, page, limit) {
try {
var users = await User.find(query)
return users;
} catch (e) {
// Log Errors
throw Error('Error while Paginating Users')
}
}
서비스에서 쿼리를 불러오고
컨트롤러에서 서비스 쿼리를 컨트롤..
게시판 CRUD
board-route
var express = require('express')
var router = express.Router()
const BoardController = require('../controllers/board-controller')
router.get('/:boardId', BoardController.getBoard)
router.get('/', BoardController.getBoards)
router.post('/', BoardController.insertBoard)
router.patch('/:boardId', BoardController.updateBoard)
router.delete('/:boardId', BoardController.deleteBoard)
router.get('/:boardId/comment', BoardController.getComments)
router.post('/:boardId/comment', BoardController.insertComment)
router.patch('/:boardId/comment/:commentId', BoardController.updateComment)
router.delete('/:boardId/comment/:commentId', BoardController.deleteComment)
module.exports = router
board-controller
const BoardService = require('../services/board-service')
exports.getBoard = async (req, res, next) => {
let { boardId } = req.params
try {
let rows = await BoardService.getBoard(boardId)
return res.json(rows[0])
} catch (err) {
return res.status(500).json(err)
}
}
// 생략...
exports.deleteComment = async (req, res, next) => {
let { boardId, commentId } = req.params
try {
let del = await BoardService.deleteComment(boardId, commentId)
return res.json(del)
} catch (err) {
return res.status(500).json(err)
}
}
board-service
const pool = require('../database/pool')
const BoardQuery = require('../queries/board-query')
exports.getBoard = async (boardId) => {
try {
let data = await pool.query(BoardQuery.getBoard, [boardId])
return data[0]
} catch (err) {
console.log(err)
throw Error(err)
}
}
// 생략...
exports.deleteComment = async (boardId, commentId) => {
let conn = await pool.getConnection()
try {
await conn.beginTransaction()
let del = await conn.query(BoardQuery.deleteComment, [commentId])
if (del[0].affectedRows == 1) {
let upd = await conn.query(BoardQuery.minusCommentCnt, [boardId])
}
await conn.commit()
return del[0]
} catch (err) {
conn.rollback()
console.log(err)
throw Error(err)
} finally {
conn.release()
}
}
process.env
시스템 환경변수들이 들어있는 객체
비밀키(데이터베이스 비밀번호, 서드파티 앱 키 등)를 보관하는 용로도로 쓰임
환경변수는 process.env 로 접근 가능
const secretId = process.env.SECRET_ID; // 이렇게 비밀키를 가리는 용도로 쓰임. 직접 쓰면 위험
const secretCode = process.env.SECRET_CODE;
일부 환경 변수는 노드 실행 시 영향을 미침
예시) NODE_OPTIONS(노드 실행 옵션), UV_THREADPOOL_SIZE(스레드풀 개수)
max-old-space-size: 노드가 사용할 수 있는 메모리를 지정하는 옵션
ex)
NODE_OPTIONS = --max-old-space-size=8192 ...
'🌿 Node' 카테고리의 다른 글
express 공부 (0) | 2022.02.20 |
---|---|
espress 기초 다지기 - 미들웨어편 (0) | 2022.02.20 |
express 파일 구조와 몽고 디비 (0) | 2022.02.16 |
노드 이해하기2 (1) | 2022.02.13 |
GET, POST, DELETE, PUT (0) | 2022.02.13 |