if (process.env.NODE_ENV === 'production') {
app.use(morgan('combined'));
} else {
app.use(morgan('dev'));
}
- process.env.NODE_ENV는 배포 환경인지 개발 환경인지를 판단할 수 있는 환경 변수이다
- 배포 환경일 때는 morgan을 combined 모드로 사용하고, 개발 환경일 때는 dev 모드로 사용한다.
.env 파일은 정적 파일이기 때문에 NODE_ENV를 동적으로 바꾸는 방법이 필요하다. (뒤에 cross-env 설명 참고)
const sessionOption = {
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
};
if (process.env.NODE_ENV === 'production') {
sessionOption.proxy = true;
// sessionOption.cookie.secure = true;
}
app.use(session(sessionOption));
- proxy와 cookie.secure를 true로 바꾼다.
- 무조건 이렇게 적용해야 하는 것은 아니며 https 를 적용할 때만 true로 바꾸면 된다.
시퀄라이즈에서의 문제점
1. 비밀번호가 하드 코딩되어 있다.
2. JSON 파일이므로 변수를 사용할 수 없다. -> 시퀄라이즈는 JSON 대신 JS 파일을 설정 파일로 쓸 수 있게 지원한다.
config 폴더에서 config.json을 지우고 대신 config.js를 생성한다.
require('dotenv').config(); // dotenv 모듈 사용
module.exports = {
development: { // process.env가 development일 때 해당 설정내용 적용
username: 'root',
password: process.env.SEQUELIZE_PASSWORD,
database: 'nodebird',
host: '127.0.0.1',
dialect: 'mysql',
},
test: {
username: "root",
password: process.env.SEQUELIZE_PASSWORD,
database: "nodebird_test",
host: "127.0.0.1",
dialect: "mysql"
},
production: { // process.env가 production일 때 해당 내용 적용
username: 'root',
password: process.env.SEQUELIZE_PASSWORD,
database: 'nodebird',
host: '127.0.0.1',
dialect: 'mysql',
logging: false, // 배포환경일 때 쿼리 명령어를 숨김
},
};
- JS 파일이므로 dotenv 모듈을 사용할 수 있다.
- password 외에도 process.env로 변경해도 된다. -> username 속성이나 host 속성은 각각 아이디와 DB 서버 주소 역할을 하므로 숨기는 게 좋다.
- process.env가 development일 떼, production일 때 각각의 설정 내용이 적용된다.
- 쿼리를 수행할 때마다 콘솔에 SQL문이 노출된다. 배포 환경에서는 어떤 쿼리가 수행되는지 숨기는 것이 좋다. production일 경우에는 logging에 false를 줘서 쿼리 명령어를 숨긴다.
.env 파일에 데이터베이스 비밀번호를 기록해 둔다.
SEQUELIZE_PASSWORD=데이터베이스 비밀번호
cross-env 패키지를 사용하면 동적으로 process.env(환경변수)를 변경할 수 있다.
기존 package.json의 scripts 부분을 아래 코드로 변경한다.
"scripts": {
"start": "NODE_ENV=production PORT=80 node server",
"dev": "nodemon server",
"test": "jest"
},
- 서버 실행을 위한 npm 스크립트를 두 개로 나눴다.
그러나 리눅스 환경에서는 process.env를 이 방식으로 설정할 수 없다. -> cross-env가 사용된다.
corss-env 설치
npm i cross-env
package.json 수정
{
...
"scripts": {
"start": "cross-env NODE_ENV=production PORT=80 node server", // 앞에 cross-env를 붙임
"dev": "nodemon server",
"test": "jest"
},
- 앞에 cross-env를 붙임으로써 윈도우에서도 실행된다.
sanitize-html과 csurf 패키지는 각각 XSS(Cross Site Scripting), CSRF(Cross Site Request Forgery) 공격을 막기 위한 패키지이다.
npm i sanitize-html
npm i csurf
XSS는 악의적인 사용자가 사이트에 스크립트를 삽입하는 공격이다.
- 악성 사용자가 게시글이나 댓글 등을 업로드할 때 자바스크립트가 포함된 태그를 올리면, 나중에 다른 사용자가 그 게시글이나 댓글을 볼 때 해당 스크립트가 실행되어 예기치 못한 동작을 하게 된다.
const sanitizeHtml = require('sanitize-html');
const html = "location.href = '<a href=https://gilbut.co.kr'>https://gilbut.co.kr'</a>"; console.log(sanitizeHtml(html)); // ''
- 사용자가 업로드한 HTML을 sanitize-html 함수로 감싸면 허용하지 않는 태그나 스크립트는 제거된다.
- 두 번째 인수로 허용할 부분에 대한 옵션을 넣을 수 있다.
CSRF는 사용자가 의도치 않게 공격자가 의도한 행동을 하게 만드는 공격이다.
- 내가 한 행동이 내가 한 것이 맞다는 사실을 인증해야 한다.
- 이때 CSRF 토큰이 사용되고, csurf 패키지는 이 토큰을 쉽게 발급하거나 검증할 수 있도록 돕는다.
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => { // form을 렌더링하는 라우터
res.render('csrf', { csrfToken: req.csrfToken() });
});
app.post('/form', csrfProtection, (req, res) => { // form에서 보낸 데이터를 처리하는 라우터
res.send('ok');
});
- 익스프레스의 미들웨어 형식으로 동작한다.
- 렌더링할 때 CSRF 토큰을 같이 제공한다.
- 토큰은 req.csrfToken()으로 가져올 수 있다.
- 프런트엔드에 렌더링된 CSRF 토큰을 나중에 form을 제출할 때 데이터와 함께 제출하면 된다.
pm2는 원활한 서버 운영을 위한 패키지이다.
장점 )
서버가 에러로 인해 꺼졌을 때 서버를 다시 켜준다.
또한 멀티 프로세싱을 지원해 노드 프로세스 개수를 한 개 이상으로 늘릴 수 있다.
- pm2를 사용해서 프로세스를 여러 개 만들면 다른 코어들까지 사용할 수 있다.
- 클라이언트로부터 요청이 올 때 알아서 요청을 여러 노드 프로세스에 고르게 분배하여 하나의 프로세스가 받는 부하가 적어지므로 서비스를 더 원활하게 운영할 수 있다.
단점 )
멀티 스레딩이 아니므로 서버의 메모리 같은 자원을 공유하지 못한다.
- 세션을 공유할 수 있게 해주기 위해 주로 멤캐시드(Memcached)나 레디스(Redis) 같은 서비스를 사용한다.
- 최대한 프로세스 간에 공유하는 것(세션 등)이 없도록 설계해야 하며 공유해야 하는 데이터가 있다면 데이터베이스를 사용해야 한다.
npm i pm2
pm2는 nodemon처럼 콘솔에 입력하는 명령어이다.
"scripts": {
"start": "cross-env NODE_ENV=production PORT=80 pm2 start server.js",
"dev": "nodemon server",
"test": "jest"
},
start 스크립트에 node server 대신 pm2 start server.js를 입력한다.
- pm2로 스크립트를 실행하는 명령어이다.
npm start로 pm2를 실행하면 노드 프로세스가 실행된 후 콘솔에 다른 명령어를 입력할 수 있다.
- pm2가 노드 프로세스를 백그라운드로 돌리므로 가능해진다.
백그라운드에서 돌고 있는 노드 프로세스를 확인할 방법이 필요한데, npx pm2 list 명령어를 사용한다.
- 현재 프로세스 정보가 표시된다.
pm2 프로세스를 종료하고 싶다면 콘솔에 npx pm2 kill을 입력하면 된다.
서버를 재시작하고 싶다면 npx pm2 reload all 명령어를 사용한다.
"scripts": {
"start": "cross-env NODE_ENV=production PORT=80 pm2 start server.js -i 0",
"dev": "nodemon server",
"test": "jest"
},
- 0은 현재 CPU 코어 개수만큼 프로세스를 생성한다는 뜻이다.
- -1은 프로세스를 CPU 코어 개수보다 한 개 덜 생성하겠다는 뜻이다.
npx pm2 monit
현재 프로세스를 모니터링할 수 있다.
실제 서버를 운영할 때 console.log와 console.error를 대체하기 위한 모듈이다.
npm i winston
logger.js 작성
const { createLogger, format, transports } = require('winston');
const logger = createLogger({
level: 'info',
format: format.json(),
transports: [
new transports.File({ filename: 'combined.log' }), // combined.log 파일에 info 이상 단계의 모든 로그를 기록
new transports.File({ filename: 'error.log', level: 'error' }), // error.log 파일에 error 단계의 로그만 기록
],
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new transports.Console({ format: format.simple() }));
}
module.exports = logger;
- winston 패키지의 createLogger 메서드로 logger를 만든다.
- 인수로 logger에 대한 설정을 넣어줄 수 있다 -> 설정으로는 level, format, transports 등이 있다.
• level은 로그의 심각도를 의미
• format은 로그의 형식
• transports는 로그 저장 방식을 의미
logger 객체를 만들어 다른 파일에서 사용하면 된다.
const logger = require('./logger');
logger.info('hello');
logger.error(error.message);
- 로그를 콘솔에만 출력하는 것이 아니라, 파일로도 저장할 수 있어 실제 서비스를 운영할 때 유용하다.
서버의 각종 취약점을 보완해주는 패키지들이며, 익스프레스 미들웨어로서 사용할 수 있다.
npm i helmet hpp
개발 환경에서는 사용할 필요가 없으므로 배포 환경일 때만 적용하면 된다.
const helmet = require('helmet');
const hpp = require('hpp');
if (process.env.NODE_ENV === 'production') {
app.use(morgan('combined'));
app.use(
helmet({
contentSecurityPolicy: false, // 필요 없는 옵션은 해제
crossOriginEmbedderPolicy: false,
crossOriginResourcePolicy: false,
}),
);
app.use(hpp());
} else {
app.use(morgan('dev'));
}
멀티 프로세스 간 세션 공유를 위해 레디스와 익스프레스를 연결해주는 패키지이다.
세션 아이디와 실제 사용자 정보를 데이터베이스에 저장할 때 사용하는 데이터베이스가 레디스이다.
npm i redis connect-redis
레디스를 호스팅해주는 redislabs를 쓰는 것이 편리하다.
REDIS_HOST=redis-16721.c14.us-east-1-2.ec2.cloud.redislabs.com
REDIS_PORT=16721
REDIS_PASSWORD=41lL904Z153mw6YkZZd1TDCQkcoZrJXG
.env 파일에 Public endpoint와 password를 넣는다.
const hpp = require('hpp');
const redis = require('redis');
const RedisStore = require('connect-redis')(session); // session을 인수로 넣어서 호출
dotenv.config(); // .env 파일에 적힌 process.env 객체의 값들은 dotenv.config() 이후에 생성됨
const redisClient = redis.createClient({ // redisClient 객체를 생성
url: `redis://${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`,
password: process.env.REDIS_PASSWORD,
legacyMode: true,
});
redisClient.connect().catch(console.error);
const pageRouter = require('./routes/page');
...
const sessionOption = {
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
store: new RedisStore({ client: redisClient }), // client 속성에 redisClient 객체를 연결
};
connect-redis 패키지로부터 RedisStore 객체를 require한다.
- connect-redis는 express-session에 의존성이 있기 때문에 session을 인수로 넣어서 호출해야 한다.
- redis 패키지의 createClient 메서드로 redisClient 객체를 생성한 후 url과 password 속성에 접속 정보를 입력한다.
express-session 미들웨어에는 store 옵션을 추가하여 RedisStore에 저장한다.
RedisStore의 옵션으로 client 속성에 redisClient 객체를 연결하면 된다.
노드 버전을 업데이트하기 위한 패키지이다.
설치된 노드 버전을 확인하는 명령어는 nvm list이다.
nvm list
새로운 버전을 설치하고 싶다면 nvm install [버전]을 입력한다.
nvm install 18.7.0
설치된 버전을 사용하려면 nvm use [버전명]을 입력한다.
nvm use 18.7.0
업그레이드 후 npm 충돌 시
노드 버전을 업그레이드한 후 기존 npm 패키지들이 동작하지 않는 경우 npm rebuild 명령어로 해결하면 된다.
.gitignore 파일 생성
node_modules
uploads
*.log
coverage
// 실제 서버에서는 .env도 깃에 추가하면 안됨
- 인스턴스를 생성한다.
- SSH를 사용하여 연결한다.
- 데이터베이스를 설치한다.
$ sudo apt-get update
$ sudo apt-get install -y gnupg
$ sudo wget https://dev.mysql.com/get/mysql-apt-config_0.8.23-1_all.deb
$ sudo dpkg -i mysql-apt-config_0.8.23-1>_all.deb
$ sudo apt update
$ sudo apt-get install -y mysql-server
- git clone 명령어를 사용해 깃허브에 올렸던 소스 코드를 내려받는다.
- 아파치 서버를 종료하는 명령어를 입력한다.
$ cd /opt/bitnami
$ sudo ./ctlscript.sh stop apache
- npm 패키지를 설치하고 서버를 실행한다.
$ cd ~/node-deploy
$ npm ci
$ npx sequelize db:create --env production
$ sudo npm i -g pm2
$ sudo NODE_ENV=production PORT=80 pm2 start server.js -i 0
서버가 실행되지 않는다면 sudo pm2 logs --err 명령어를 입력해 어떤 에러가 발생했는지 확인할 수 있다.
- sudo pm2 reload all로 서버를 재시작하면 된다.
- 프로젝트를 생성한다.
- 인스턴스를 생성한다.
- 외부 IP 옆 SSH를 눌 해당 인스턴스의 콘솔로 접근한다.
- 루트계정으로 접속한다.
sudo su
- 우분투에 노드와 MySQL를 설치한다.
$ cd node-deploy
$ npm ci
$ npx sequelize db:create --env production
$ npm i -g pm2
$ NODE_ENV=production PORT=80 pm2 start server.js -i 0
- npm 패키지들을 설치하고 서버를 실행한다.
문제
1. ( process.env.NODE_ENV )는 배포 환경인지 개발 환경인지를 판단할 수 있는 환경 변수이다
2. ( cross-env ) 패키지를 사용하면 정적 파일인 .env를 동적으로 변경할 수 있다.
3. 시퀄라이즈는 JSON 대신 ( JS ) 파일을 설정 파일로 쓸 수 있게 지원한다.
4. 쿼리를 수행할때마다 콘솔에 SQL문이 노출되지 않기 위해 production일 경우 ( logging )에 ( false )를 줘서 쿼리 명령어를 숨긴다.
5. ( CSRF )는 사용자가 의도치 않게 공격자가 의도한 행동을 하게 만드는 공격이다.
6. ( connect-redis ) 패키지는 멀티 프로세스 간 세션 공유를 위해 레디스와 익스프레스를 연결해주는 패키지이다.
7. ( pm2 )는 멀티 프로세싱을 지원하지만 서버의 메모리 같은 자원을 공유하지 못한다.
코드문제
1. 주석 부분의 코드를 채우시오.
const { createLogger, format, transports } = require('winston');
const logger = createLogger({
level: 'info',
format: format.json(),
transports: [
// combined.log 파일에 info 이상 단계의 모든 로그를 기록
// error.log 파일에 error 단계의 로그만 기록
],
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new transports.Console({ format: format.simple() }));
}
module.exports = logger;
답)
transports: [
new transports.File({ filename: 'combined.log' }),
new transports.File({ filename: 'error.log', level: 'error' }),
]
2. 3개의 주석에 알맞은 코드를 작성하시오.
if (// 배포환경일 경우) {
// morgan을 combinded 모드로 사용
} else {
// morgan을 dev 모드로 사용
}
답)
if (process.env.NODE_ENV === 'production') {
app.use(morgan('combined'));
} else {
app.use(morgan('dev'));
}
출처: 조현영 , 『Node.js 교과서』 개정판 3판, 길벗, 15장
[Node.js 1] 12장 웹 소켓으로 실시간 데이터 전송하기 (0) | 2024.01.12 |
---|---|
[노드 1팀] 11장. 노드 서비스 테스트하기 (1) | 2024.01.05 |
[Node.js 1] 10장 웹 API 서버 만들기 (0) | 2023.12.29 |
[Node.js 1] 9장 익스프레스로 SNS 서비스 만들기 (1) | 2023.12.22 |
[노드 1팀] 8장. 몽고디비 (1) | 2023.12.01 |