[Node.js] chap.3
method-override
GET, POST, PUT, DELETE 중에서 나머지 사용할 수 있게 해줌
npm install method-override // REST API =>
- 세팅
// (server.js)
const methodOverride = require('method-override')
app.use(methodOverride('_method'))
<!-- (edit.ejs) -->
<form action="/add?_method=PUT" method="POST">
<input 어쩌구>
</form>
해당 형식으로 작성하면 PUT 요청을 할 수 있다.
(server.js)
app.put('/edit', function(요청, 결과){
db.collection('post').updateOne( {_id : parseInt(요청.body.id) }, {$set : { 제목 : 요청.body.title , 날짜 : 요청.body.date }},
function(){
console.log('수정완료')
응답.redirect('/list')
});
});
** 서버의 요청(GET, POST, PUT, PATCH, DELETE)이 완료되고 난 후에, 응답이 없으면 브라우저가 멈출 수 있기 때문에 꼭 응답처리가 필요하다.
회원인증
1. Session-based Authentication
- 사용자의 세션정보를 저장해서 로그인 기능을 구현
2. JWT (JSON Web Token)
- 세션데이터를 서버에 저장하지않고 토큰을 사용자에게 주는 방식
3. OAuth (Open Authentication)
- 소셜로그인
* Session-based Authentication
npm install passport passport-local express-session
실제 서비스시 express-session 말고 MongoDB에 세션데이터를 저장해주는 라이브러리를 이용하면 좋다.(ex. connect-mongo)
// (server.js)
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const session = require('express-session');
app.use(session({secret : '비밀코드', resave : true, saveUninitialized: false}));
app.use(passport.initialize());
app.use(passport.session());
:라이브러리에 나와있는 사용법 그대로
- 미들웨어
app.use()와 같은 부분을 미들웨어라고 하는데 요청과 응답 사이에 뭔가를 실행시키는 코드라고 생각하면 된다. 예를 들어 요청이 적법한지 검사하는 것이다. (passport.intialize(), passport.session() 등)
- 로그인 검사
app.post('/login', passport.authenticate('local', {failureRedirect : '/fail'}), function(요청, 응답){
응답.redirect('/')
});
post() 함수의 두번째 파라미터로 passport 라이브러리가 제공하는 인증하는 코드이다. (failureRedirect 부분은 로그인 실패시 이동시켜줄 경로이다.)
* passport
passport.use( new LocalStrategy({
usernameField: 'id', // (요기는 사용자가 제출한 아이디가 어디 적혔는지)
passwordField: 'pw', // (요기는 사용자가 제출한 비번이 어디 적혔는지)
session: true, // (요기는 세션을 만들건지)
passReqToCallback: false, // (요기는 아이디/비번말고 다른 정보검사가 필요한지)
});
- passport 라이브러리 설정코드
// (server.js 하단에 복붙)
passport.use(new LocalStrategy({
usernameField: 'id',
passwordField: 'pw',
session: true,
passReqToCallback: false,
}, function (입력한아이디, 입력한비번, done) {
//console.log(입력한아이디, 입력한비번);
db.collection('login').findOne({ id: 입력한아이디 }, function (에러, 결과) {
if (에러) return done(에러)
if (!결과) return done(null, false, { message: '존재하지않는 아이디요' })
if (입력한비번 == 결과.pw) {
return done(null, 결과)
} else {
return done(null, false, { message: '비번틀렸어요' })
}
})
}));
: LocalStrategy( { 설정 }, function(){ 아이디비번 검사하는 코드 } )
- 세션 생성 및 세션 아이디 쿠키로 보내기
passport.serializeUser(function (user, done) {
done(null, user.id)
});
passport.deserializeUser(function (아이디, done) {
done(null, {}) // 추후 수정
});
serializeUser(): 유저의 id 데이터를 바탕으로 세션데이터를 만듦. 그 세션데이터의 아이디를 쿠키로 만들어서 사용자의 브라우저로 보냄.
deserializeUser(): 세션아이디를 바탕으로 이 유저의 정보를 DB에서 찾는역할을 한다.
// (server.js)
app.get('/mypage', loginCheck, function (req, res) {
console.log(req.user);
res.render('mypage.ejs', {}) // 추후 수정
})
function loginCheck(req, res, next) {
if (req.user) {
next()
}
else {
res.send('로그인안하셨는데요?')
}
}
1. get() 함수에 미들웨어 loginCheck()를 넣으면 /mypage 요청과 mypage.ejs 응답 사이에 loginCheck라는 코드를 실행시켜준다.
2. loginCheck()는 req.user가 있으면 next()로 통과시키고, 없으면 에러메세지를 res.send() 해달라는 뜻이다.
passport.deserializeUser(function (아이디, done) {
db.collection('login').findOne({ id: 아이디 }, function (error, result) {
done(null, result)
})
});
{id : 세션아이디에 숨겨져있던 유저의 아이디} 인 게시물을 찾아서 그 데이터의 결과를 done(null, result)를 실행한다. 그러면 결과가 req.user 부분에 들어간다. (로그인한 유저의 DB 데이터를 볼 수 있다.) 로그아웃은 req.logout()으로 실행할 수 있다.
app.get('/mypage', loginCheck, function (req, res) {
console.log(req.user);
res.render('mypage.ejs', { 사용자: req.user })
})
deserializeUser()를 통해서 req.user 부분에 로그인한 사용자의 id를 통한 DB의 정보를 가져올 수 있기 때문에 mypage.ejs에서 로그인한 사용자의 정보를 화면에 출력할 수 있다.
.env (environment variable) 파일
- 환경에 따라 가변적인 변수 데이터를 모아 놓은 파일
npm install dotenv
- 라이브러리 설치
// (server.js)
require('dotenv').config()
- 설치한 라이브러리 등록
* .env 파일을 server.js 파일과 동일한 경로에 생성한다. *
// (.env 파일)
PORT=8080
DB_URL="mongodb+srv://test@test"
- .env 파일 작성법
// (기존 server.js 코드)
var db;
MongoClient.connect('mongodb+srv://test:test@test', function(err, client){
if (err) return console.log(err)
db = client.db('Example1');
app.listen(8080, function() {
console.log('listening on 8080')
})
})
// (env 파일을 적용하는 server.js 코드)
var db;
MongoClient.connect(process.env.DB_URL, function(err, client){
if (err) return console.log(err)
db = client.db('Example1');
app.listen(process.env.PORT, function() {
console.log('listening on 8080')
})
})
.env 파일에 작성된 변수들은 process.env.변수명 의 형식으로 사용한다.
URL query string
url 입력하는 곳에 /example?key=value 형식으로 요청을 보내면 GET 요청으로 데이터를 보낼 수 있다.
<div class="container input-group mb-2">
<input class="form-control" id="search-input">
<button class="input-group-append btn btn-danger" id="search">검색</button>
</div>
<script>
$('#search').click(function(){
var 입력한값 = $('#search-input').val();
window.location.replace('/search?value=' + 입력한값)
});
</script>
window.location: 현재 해당하는 페이지의 URL을 뜻한다.
app.get('/search', (req, res)=>{
console.log(req.query);
db.collection('post').find({제목 : req.query.value}).toArray((error, result)=>{
console.log(result)
})
})
req.query: 요청받은 URL query string 값을 가져올 수 있다.
indexing
검색할 때, 해당 검색어를 포함한 여러 데이터를 가져오는 방식은
1. 정규식 사용
db.collection('post').find({제목 : /글쓰기/})
- 데이터가 대용량이면 성능 안나옴
2. 인덱싱 사용
Binary Search: (숫자가 정렬되어 있다는 전제하에 => indexing) 정해진 숫자의 처음 중간의 값을 선택하여 그 값과 찾는 값의 크고 작음을 비교하면서 탐색한다.
- query string 찾는 다른 방법
// jQuery
var 자료 = { 이름1 : '값1', 이름2 : '값2' }
$param(자료) //이름1=값1&이름2=값2 이거가 남음
$('form').serialize() // input에 name있는 모든 값
- MongoDB indexing
(해당)collection - indexes - create index
문자열: 'text'
숫자: 1 or -1
해당 작업을 하면 collection의 정렬된 사본을 만들어준다.
app.get('/search', (요청, 응답)=>{
console.log(요청.query);
db.collection('post').find( { $text : { $search: 요청.query.value }} ).toArray((에러, 결과)=>{
console.log(결과)
응답.render('search.ejs', {posts : 결과})
})
})
이 방법으로 간단한 검색엔진처럼 검색이 가능하다. (ex. 단어 -단어, "단어 단어" 등)
단점: 띄어쓰기를 기준으로 검색이 안된다 (영어에 특화된 검색)
- 해결방법
1. 검색할 양을 제한한다.
- 날짜별로 검색하거나, skip(), limit() 함수를 이용해서 페이징 처리를 한다.
2. text Search를 사용한다.
- MongoDB Atlas말고 직접 설치한다. 띄어쓰기 단위 indexing이 아닌 글자 단위의 indexing 알고리즘을 사용할 수 있다. (nGram)
3. Search index를 사용한다.
- MongoDB Atlas에서 Search index를 생성할 수 있다. text index랑 비슷하지만 lucene.korean으로 변경하면 한국어에 쓰기 좋게 조사 등을 제거하고 검색을 할 수 있다.
- Search index
app.get('/search', (req, res) => {
var 검색조건 = [
{
$search: {
index: 'titleSearch', // Search index에서 설정한 name
text: {
query: req.query.value,
path: '제목' // 제목날짜 둘다 찾고 싶으면 ['제목', '날짜']
}
}
},
// { $sort: { _id: 1 } }, // 오름차순 1, 내림차순 -1
// { $limit: 10 } // 갯수 설정
// { $project : { 제목: 1, _id: 1, score: { $meta: "searchScore" } } } // meta 부분은 검색되는 빈도 등의 점수를 기준으로 검색
]
db.collection('post').aggregate(검색조건).toArray(function(error, result){ // find() 대체
console.log(result);
res.render('search.ejs', {posts: result});
})
})
router
서버에 요청하는 수 많은 경로들을 분류하여 관리할 수 있다.
root/routes/파일.js
server.js와 나란한 경로에 routes라는 폴더를 생성하고 그 안에 js 파일을 생성한다.
// 라우팅하는 .js 파일
var router = require('express').Router(); // express의 Router기능
router.get('/shop/shirts', function(req, res){
res.send('셔츠 파는 페이지입니다.');
});
router.get('/shop/pants', function(req, res){
res.send('바지 파는 페이지입니다.');
});
module.exports = router; // import/export 대체 가능
app.use('/', require('./routes/shop.js') ); // 미들웨어 형식으로 라우터를 적용
- URL 단축
app.use('/shop', require('./routes/shop.js') );
/////////////////////////////////////////////
var router = require('express').Router();
router.get('/shop/shirts', function(req, res){
res.send('셔츠 파는 페이지입니다.');
});
router.get('/shop/pants', function(req, res){
res.send('바지 파는 페이지입니다.');
});
module.exports = router;
- 라우터에 미들웨어 적용
var router = require('express').Router();
// router.use(loginCheck); // 전역적으로 loginCheck라는 미들웨어 사용 가능
router.use('/shop/shirts', loginCheck); // '/shop/shirts'의 경로에 loginCheck라는 미들웨어 사용 가능
function loginCheck(req, res, next) {
if (req.user) { next() }
else { res.send('로그인 안하셨는데요?') }
}
router.get('/shop/shirts', function(req, res){
res.send('셔츠 파는 페이지입니다.');
});
router.get('/shop/pants', function(req, res){
res.send('바지 파는 페이지입니다.');
});
module.exports = router;
Google Cloud Platform 배포
1. app.yaml 파일 생성
runtime: nodejs
env: flex
/////////////// 밑으로는 안적어도 자동 생성
manual_scaling:
instances: 1
resources:
cpu: 1
memory_gb: 0.5
disk_size_gb: 10
2. server.js 포트 확인
app.listen(8080, function() {
console.log('listening on 8080')
})
- 구글클라우드 기본 포트가 8080. (.env 파일 세팅있다면 확인)
3. MongoDB Atlas Network Access 모든 아이피 허용 확인
- 모든 아이피 (0.0.0.0) 접속허용 (Allow Access from Anywhere 로 체크)
4. Google Cloud Platform 시작
- 회원가입 및 카드등록 및 프로젝트 생성
- 프로젝트 생성 후 App Engine에서 배포 진행
- SDK 설치 진행
5. 배포
** 프로젝트가 설치되어 있는 경로로 이동한다.**
gcloud init
Google Cloud Platform 개설한 구글 아이디 로그인 및 프로젝트 선택
gcloud app deploy
작업이 끝나면 배포할 소스파일, 이름, url 등을 알려주고 배포 완료.
이미지 처리
이미지 업로드 및 이미지 서버를 만들 수 있다.
<form method="POST" action="/upload" enctype="multipart/form-data" > // multipart => 인코딩을 하지 않아 상대적으로 파일 용량 적게 가져옴
<input type="file" name="profile">
<button type="submit">전송</button>
</form>
: ejs
npm install multer // multipart
multipart/form-data 라이브러리 설치
let multer = require('multer');
// var storage = multer.memoryStorage({}) // memory 상에 저장 (휘발성)
var storage = multer.diskStorage({
destination : function(req, file, cb){ // 이미지 저장 경로
cb(null, './public/image')
},
filename : function(req, file, cb){ // 이미지 이름 설정
cb(null, file.originalname )
},
fileFilter: function (req, file, callback) { // 이미지 저장 시 필터처리
var ext = path.extname(file.originalname);
if(ext !== '.png' && ext !== '.jpg' && ext !== '.jpeg') {
return callback(new Error('PNG, JPG만 업로드하세요'))
}
callback(null, true)
},
limits:{
fileSize: 1024 * 1024 // 이미지 사이즈 제한
}
});
var upload = multer({storage : storage});
: (server.js) multer 세팅
// 이미지 저장 페이지
app.get('/upload', function(req, res){
res.render('upload.ejs')
});
// 이미지 저장처리
app.post('/upload', upload.single('profile'), function(req, res){ // input태그의 name속성 'profile'
res.send('업로드완료')
}); // upload 미들웨어처럼 실행
// 저장된 이미지 불러오기
app.get('/image/:imageName', function(req, res){
res.sendFile(__dirname + '/public/image/' + req.params.imageName); // __dirname => 현재root경로
})
: server.js
서버와의 실시간 소통 (SSE)
1) 1초마다 서버에게 메세지를 요청한다.
2) 서버랑 유저간 지속적인 소통채널을 연다.
2)의 경우가 서버의 부하가 덜 하고 get이나 post 등의 요청은 1회 요청이지만 지속적으로 서버에 응답을 하려할 때 사용한다.
// (server.js)
app.get('/message/:parentid', loginCheck, function(req, res){
res.writeHead(200, {
"Connection": "keep-alive",
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
});
res.write('event: test\n');
res.write(`data: ${JSON.stringify(결과)}\n\n`);
});
1. 지속적 소통채널 개설을 하려면 서버는 res.writeHead() 처럼 쓴다.
2. 유저에게 계속 메세지를 보내고 싶을 때마다 res.write() 한다.
3. event: 이벤트명을 잘 작성하고 data: 전달할내용을 쓰면 된다.
// chat.ejs
var 지금누른채팅방id;
var eventSource; //일단변수
$('.list-group-item').click(function(){
지금누른채팅방id = this.dataset.id;
//프론트엔드에서 실시간 소통채널 여는법
eventSource = new EventSource('/message/' + 지금누른채팅방id);
eventSource.addEventListener('test', function (e){
console.log(JSON.parse(e.data));
});
});
1. GET 요청 대신 new EventSource('/message/' + 지금누른채팅방id); 형태의 코드를 실행하면 서버에서 만들어놓은 실시간 채널에 입장 가능하다.
2. eventSource.addEventListener('서버에서작명한이벤트명') 이런 코드를 쓰면 서버가 보낸 데이터를 수신할 수 있다. 서버가 res.write() 할 때마다 내부 코드를 실행해줍니다.
3. e.data 안에는 서버가 보낸 데이터가 들어있다.
DB 변동사항 실시간 업데이트
* MongoDB change stream
- 데이터 베이스의 변동을 감지하여 서버에게 업데이트 사항을 알려준다.
// ejs
var 지금누른채팅방id;
var eventSource;
$('.list-group-item').click(function(){
지금누른채팅방id = this.dataset.id;
$('.chat-content').html('') //일단 메세지들 전부 비우기
eventSource = new EventSource('/message/' + 지금누른채팅방id);
eventSource.addEventListener('test', function (e){
console.log(e.data);
var 가져온거 = JSON.parse(e.data);
가져온거.forEach((a) => {
$('.chat-content').append(`<li><span class="chat-box">${a.content}</span></li>`)
});
});
});
1. 가져온 데이터를 JSON -> Object 형태로 변환
2. Array로 되어있는 object 안에 하나하나 메세지 document 들을 반복문을 통해 하나씩 분리
3. 그거 안에 있던 메세지들을 <li></li> 태그로 만들어서 원하는 곳에 출력
// server
const 찾을문서 = [
{ $match: { 'fullDocument.parent': 요청.params.parentid } } // collection의 변경 감지 부분
];
const changeStream = db.collection('message').watch(찾을문서);
changeStream.on('change', result => {
console.log(result.fullDocument);
var 추가된문서 = [result.fullDocument]; // 화면에 반영할 부분
응답.write('event: test\n');
응답.write(`data: ${JSON.stringify(추가된문서)}\n\n`);
});
change stream을 이용해서 DB 감시 한다.
- 우선 { parent : 요청.params.parentid } 인 게시물들만 감시
- 그런 게시물들에 변동사항이 생기면 [result.fullDocument] 이걸 유저에게 보내줌
- 물론 [], {} 이런 자료들은 JSON으로 바꿔서 보내야한다.
socket.io
SSE(서버에서 일방적으로 실시간 응답) 말고도 서버와 유저간에 Web Socket을 통해 양방향 실시간 통신이 가능하다.
npm install socket.io
라이브러리 설치
// (server.js)
const http = require('http').createServer(app);
const { Server } = require("socket.io");
const io = new Server(http);
- const app = express()보다 하단에 적용
// 기존
app.listen(process.env.PORT, function(){
console.log('listening on 8080')
});
// 변경
http.listen(process.env.PORT, function(){
console.log('listening on 8080')
});
app: express를 이용해서 서버를 띄움
http: 기본 nodejs 라이브러리 + socket.io를 이용해서 서버를 띄움
- 웹소켓 연결
// (server.js)
app.get('/socket', function(요청,응답){
응답.render('socket.ejs')
});
io.on('connection', function(){ // 웹소켓으로 서버에 connection했을 때
console.log('연결되었어요');
socket.on('user-send', function(data){ // user-send라는 이벤트 발생 시 data 실행
console.log(data) // '안녕하세요'
});
});
연결 됐을 때 할 작업
// ejs
<body>
<script src="https://cdn.socket.io/4.4.0/socket.io.min.js" integrity="sha384-1fOn6VtTq3PWwfsOrk45LnYcGosJwzMHv+Xh/Jx5303FVOXzEnw0EpLv30mtjmlj" crossorigin="anonymous"></script>
<div>채팅방</div>
<button id="send">서버에 메세지 보내기</button>
<script>
var socket = io(); // 웹소켓을 이용해 서버와 실시간 소통채널 개설
$('#send').click(function(){
socket.emit('user-send', '안녕하세요') // (이벤트 명, 전달할 데이터)
})
</script>
</body>
- package.json에 있는 버전 맞춰서 cdn 설치
- emit: 'user-send'라는 이벤트 명으로 보낸 '안녕하세요'라는 데이터 수신
- 서버에서 클라이언트로 데이터 보내기
io.emit('작명', '보낼메세지');
모든 유저에게 메세지를 보낸다 (broadcast 한다)
// (server.js)
io.on('connection', function (socket) {
socket.on('user-send', function (data) { // user-send 이벤트가 일어나면
io.emit('broadcast', data) //모든사람에게 데이터 전송
});
});
// (chat.ejs)
<script>
var socket = io();
$('#send').click(function(){
socket.emit('user-send', '안녕하쇼')
});
socket.on('broadcast', function(data) {
$('#content').append('<div>' + data + '</div>')
});
</script>
- 내가 원하는 사람에게만 메세지보내기
io.on('connection', function(socket){
console.log(socket); // header, id 등이 출력 됌
io.to(socket.id).emit("broadcast", '서버응답임'); 원하는 소켓id를 가진 사람에게만 메세지 보냄
});
- 하위 채팅방 만들기
<!-- (socket.ejs) -->
<button id="room1">채팅방1 입장</button>
<button id="room1-send">채팅방1에 메세지 전송</button>
<script>
$('#room1').click(function(){
socket.emit('joinroom', '제발');
});
$('#room1-send').click(function(){
socket.emit('room1-send', '어쩌구저쩌구' )
});
</script>
- joinroom 이라는 이벤트를 발생시킨다.
- room1-send라는 이벤트를 발생시킨다.
// (server.js)
io.on('connection', function(socket){
socket.on('joinroom', function(data){
socket.join("room1"); // room1이라는 방에 유저를 넣을 수 있다.
});
socket.on('room1-send', function(data){
io.to("room1").emit('broadcast', data);
});
});
- joinroom이라는 이벤트가 발생되면 room1이라는 방을 만들고 유저를 넣는다.
- room1-send라는 이벤트가 발생되면 room1에 있는 사람들에게만 broadcast 해준다.
Node+Express & React 연동
1) npm init
2) npm install express
3) server.js 파일 생성
// (server.js)
const express = require('express');
const path = require('path');
const app = express();
const http = require('http').createServer(app);
http.listen(8080, function () {
console.log('listening on 8080')
});
4. (node or nodemon) server.js
- react 연동
1) react 프로젝트 생성(npx create-react-app 프로젝트명)
2) react 프로젝트 경로로 들어가서 작업 후 npm run build 명령어로 build 파일 생성
// (server.js에 추가)
app.use(express.static(path.join(__dirname, 'react-project/build'))); // 특정 폴더안의 파일을 static 파일로 보내기 위함
app.get('/', function (요청, 응답) {
응답.sendFile(path.join(__dirname, '/react-project/build/index.html'));
});
// 라우팅을 하기 위함
app.get('*', function (요청, 응답) {
응답.sendFile(path.join(__dirname, '/react-project/build/index.html'));
});
- node.js와 react간 ajax요청 시 (여러 const 하단에 추가)
npm install cors --save // 에러날 시에
app.use(express.json());
var cors = require('cors');
app.use(cors());
- 서브디렉토리에 리액트앱 발행하는 경우
// (server.js)
app.use( '/', express.static( path.join(__dirname, 'public') ))
app.use( '/react', express.static( path.join(__dirname, 'react-project/build') ))
app.get('/', function(요청,응답){
응답.sendFile( path.join(__dirname, 'public/main.html') )
})
app.get('/react', function(요청,응답){
응답.sendFile( path.join(__dirname, 'react-project/build/index.html') )
})
// (리액트프로젝트 내의 package.json)
{
"homepage": "/react", // 이 부분 설정
"version": "0.1.0",
... 등
}
- 서버와 리액트 동시에 띄워서 개발
리액트의 localhost와 node.js서버의 localhost의 미리보기를 띄어놓고 개발을 진행하기 위함.
// (리액트프로젝트 내의 package.json)
{
...
"proxy": "http://localhost:8080"
...
}
- 리액트의 package.json에서 proxy부분을 추가
https://create-react-app.dev/docs/proxying-api-requests-in-development/
** socket이 들어가면 추가적으로 수정 필요 **
'NodeJs' 카테고리의 다른 글
[Node.js] chap.2 (0) | 2021.12.21 |
---|---|
[Node.js] chap.1 (0) | 2021.12.20 |