-
Node.js 스터디 3주차BackEnd/Node.js 2022. 2. 5. 00:33
Node.js 교과서 4장 Http모듈로 서버 만들기를 공부하고 정리한 글
4.1 요청과 응답 이해하기
서버는 클라이언트의 요청을 받아 내용을 읽고 처리한 뒤, 응답을 보내준다.
따라서 요청을 받는 부분과 응답을 보내는 부분이 필요하다. 어떤 요청에서 어떤 작업을 수행할지도 등록해두어야 한다.
const http = require('http'); http.createServer((req, res) => { // 보낼 응답 });
http 모듈을 사용해서 웹 브라우저 요청을 처리한다. 요청이 들어올 때마다 콜백함수가 실행된다.
const http = require('http'); http.createServer((req, res) => { //서버 객체 생성 res.writeHead(200, {'Content-Type' : 'text/html; charset=utf-8'}); //응답 헤더 res.write('<h1>Hello Node!</h1>'); //응답 본문 res.end('<p>Hello Server!</p>'); //응답 데이터 전송 종료 }) .listen(8080, () => { //서버 연결, 첫 번째 인자는 포트 번호, 두 번째 인자는 실행할 콜백 함수 console.log('8080번 포트에서 서버 대기 중입니다!'); });
실행 결과는 아래처럼 나온다.
웹페이지(프론트)측에서 응답인 Hello Node! Hello Server을 확인할 수 있고, vscode상에는 listen에 작성한 8080번 포트에서 서버 대기중입니다 콘솔 출력을 확인할 수 있다.
listening 리스너를 붙이는 방식도 가능하다.
const http = require('http'); const server = http.createServer((req, res) => { res.writeHead(200, {'Content-Type' : 'text/html; charset=utf-8'}); res.write('<h1>Hello Node!</h1>'); res.end('<p>Hello Server!</p>'); }); server.listen(8080); server.on('listening', () => { console.log('8080번 포트에서 서버 대기 중입니다. '); }); server.on('error', (error) => { console.error(error); })
잘 쓰이진 않지만, createServer을 여러 번 호출하면 서버를 여러개 동시 실행할 수 있다. (단 포트 번호는 달라야 함)
const http = require('http'); http.createServer((req, res) => { res.writeHead(200, {'Content-Type' : 'text/html; charset=utf-8'}); res.write('<h1>Hello Node!</h1>'); res.end('<p>Hello Server!</p>'); }) .listen(8080, () => { console.log('8080번 포트에서 서버 대기 중입니다!'); }); http.createServer((req, res) => { res.writeHead(200, {'Content-Type' : 'text/html; charset=utf-8'}); res.write('<h1>Hello Node!</h1>'); res.end('<p>Hello Server!</p>'); }) .listen(8081, () => { console.log('8080번 포트에서 서버 대기 중입니다!'); });
write와 end에 일일이 HTML적는 방식말고 미리 만들어둔 파일을 저번 단원에서 공부한 fs모듈을 사용해 읽어서 전송할 수 있다.
<!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>Node.js 웹 서버</title> </head> <body> <h1>Node.js 웹 서버</h1> <p>만들 준비되셨나요?</p> </body> </html>
const http = require('http'); const fs = require('fs').promises; http.createServer(async (req, res) => { try { const data = await fs.readFile('./server2.html'); res.writeHead(200, { 'Content-Type' : 'text/html; charset=utf-8 '}); //html 파일 res.end(data); } catch (err) { console.error(err); res.writeHead(500, { 'Content-Type' : 'text/plain; charset=utf-8 '}); //에러메시지는 일반 텍스트 res.end(err.message); } }) .listen(8081, () => { console.log('8081번 포트에서 서버 대기중입니다. '); })
data 변수에 fs모듈의 readFile메소드로 server2.html파일을 버퍼로 읽고, 그대로 클라이언트에 전달해준다.
4.2 REST와 라우팅 사용하기
- REST란 무엇인가?
✔ REpresentational State Transfer의 줄임말로, 서버의 자원을 정의하고 이 자원에 대한 주소를 지정하는 약속을 의미
✔ 자원을 이름(표현)으로 구분하여 자원의 상태(정보)를 주고 받는 것이다.
✔ HTTP 프로토콜을 활용하기에 웹의 장점 활용이 가능하다.
- 자원 / 행위 / 표현 3가지로 구성이 된다
자원 : URI (인터넷 상의 자원을 식별하기 위한 문자열의 구성)
행위 : HTTP Method (EX : GET, POST, PUT, DELETE등..)
표현 : 클라이언트 - 서버가 주고받는 데이터 형식(xml, json등)
- REST의 특징?
✔ 서버-클라이언트 구조 (자원을 요청하는 쪽이 Client, 자원을 제공하는 쪽이 Server)
✔ HTTP 프로토콜이 Stateless함 -> REST역시 무상태성
-> statelss는 클라이언트의 상태 정보(세션, 쿠키정보 등을)를 별도로 기록하지 않는다. 들어오는 요청만 단순히 처리함.
✔ 캐싱 기능 사용 가능 - 캐싱 처리 가능 여부를 명시해야 함
✔ 인터페이스 일관성
✔ 계층 구조 (REST서버는 다중 계층으로 구성될 수 있으며 클라이언트는 직접 통신하는지 중간서버(프록시)와 통신하는지 알 수 없습니다)
참고한 블로그 : https://dev-coco.tistory.com/97
REST를 따르는 서버를 RESTful 하다고 표현한다. REST에 기반한 서버 주소 구조를 미리 설계하고 시작하면 체계적으로 프로그래밍 할 수 있다.
* 소문자, 명사를 사용하여 주소를 명시하며, /(슬래쉬) 기호로 계층을 구분한다. /로 끝내면 혼용의 여지가 있으니 /로 끝내지 말 것
메소드 주소 역할 GET / restFront.html파일 제공 GET /about about.html파일 제공 GET /users 사용자 목록 제공 GET 기타 정적 파일 POST /user 사용자 등록 PUT /user/:id 해당 id 사용자 수정 DELETE /user/:id 해당 id 사용자 제거 프론트 html 코드도 있지만 생략하고 .. 서버 코드는 아래와 같다
async function getUser() { //사용자 정보를 가져오는 함수 try { const res = await axios.get('/users'); //users 주소로 GET 요청 왔을 때 처리. const users = res.data; const list = document.getElementById('list'); list.innerHTML = ''; //사용자마다 반복적으로 화면 표시하고 이벤트를 연결함 Object.keys(users).map(function (key) { const userDiv = document.createElement('div'); const span = document.createElement('span'); span.textContent = users[key]; //수정 버튼 클릭시 이벤트 const edit = document.createElement('button'); edit.textContent = '수정'; edit.addEventListener('click', async() => { const name = prompt('바꿀 이름을 입력하세요'); if(!name) { return alert('이름을 반드시 입력하셔야 합니다. '); } // 이름이 없는 경우 try { await axios.put('/user/' + key, {name}); // /user/:id 로 PUT(해당 ID 사용자 수정) getUser(); //다시 유저 정보 불러옴 } catch(err) { console.error(err); } }); //삭제 버튼 클릭시 이벤트 const remove = document.createElement('button'); remove.textContent = '삭제'; remove.addEventListener('click', async () => { try { await axios.delete('/user/' + key); // /user/:id 로 DELETE(해당 ID 사용자 삭제) getUser(); } catch(error) { console.error(error); } }); userDiv.appendChild(span); userDiv.appendChild(edit); userDiv.appendChild(remove); list.appendChild(userDiv); console.log(res.data); }); } catch (err) { console.error(err); } } window.onload = getUser; // 화면 로드시 getUser호출 //폼 제출시 document.getElementById('form').addEventListener('submit', async (e) => { e.preventDefault(); const name = e.target.username.value; if(!name) { return alert('이름을 입력하세요'); } try { await axios.post('/user', {name}); // /user로 POST(해당 사용자 새로 등록) getUser(); } catch(err) { console.error(err); } e.target.username.value = ''; });
아래 코드에서 메소드와 URL에 따라 파일을 제공한다.
const http = require('http'); const fs = require('fs').promises; const users = {}; http.createServer(async (req, res) => { try { console.log(req.method, req.url); if(req.method === 'GET') { if(req.url === '/') { const data = await fs.readFile('./restFront.html'); res.writeHead(200, { 'Content-Type' : 'text/html; charset = utf-8'}); return res.end(data); } else if (req.url === '/about') { const data = await fs.readFile('./about.html'); res.writeHead(200, {'Content-Type' : 'text/html; charset=utf-8' }); return res.end(data); } else if (req.url === '/users') { res.writeHead(200,{ 'Content-Type' : 'text/plain; charset = utf-8'}); return res.end(JSON.stringify(users)); } try { const data = await fs.readFile(`.${req.url}`); return res.end(data); } catch (err) { } } else if (req.method === 'POST') { if(req.url === '/user') { let body = ''; req.on('data', (data) => { body += data; }); return req.on('end', () => { console.log('POST 본문(body):', body); const { name } = JSON.parse(body); const id = Date.now(); users[id] = name; res.writeHead(201); res.end('등록 성공'); }); } } else if (req.method === 'PUT') { if(req.url.startsWith('/user/')) { const key = req.url.split('/')[2]; let body = ''; req.on('data', (data) => { body += data; }); return req.on('end', () => { console.log('PUT 본문 (Body):', body); users[key] = JSON.parse(body).name; return res.end(JSON.stringify(users)); }); } } else if (req.method === 'DELETE') { if(req.url.startsWith('/user/')) { const key = req.url.split('/')[2]; delete users[key]; return res.end(JSON.stringify(users)); } } res.writeHead(404); return res.end('NOT FOUND'); } catch (err) { console.error(err); res.writeHead(500, {'Content-Type' : 'text-plain; charset=utf-8'}); res.end(err.message); } }) .listen(8082, () => { console.log('8082번 포트에서 서버 대기 중입니다.'); });
- Express로 라우터 분리가 안되어 있어서 if문의 중첩이 연결되어 있다. 뒤에서 배울 Express모듈과 라우팅을 사용하면 좀 더 깔끔해진다.
- 아래는 실행화면이다.
+) 데이터를 영구적으로 저장하려면 DB사용을 해야한다.
4.3 쿠키와 세션 이해하기
HTTP는 connectionless하고 stateless하다. 서버가 적절한 응답을 하고 나면 연결이 끊어지고, 클라이언트의 정보를 저장해두지 않는다. 그렇다면 로그인 후 인증 상태를 어떻게 유지할 수 있는가?
- 쿠키 ?
✔ 요청을 보내는 클라이언트에 대한 정보.
✔ 서버의 경우 처음 요청이 들어왔을 때 요청자를 추정할만한 정보를 쿠키로 만들어 보내면 클라이언트는 쿠키 정보를 저장해놨다 다음 요청부터 쿠키를 함께 서버에 전송함으로써 사용자를 구분할 수 있다.
const http = require('http'); http.createServer((req, res) => { console.log(req.url, req.headers.cookie); res.writeHead(200, { 'Set-Cookie' : 'mycookie=test' }); res.end('Hello Cookie'); }) .listen(8083, () => { console.log('8083번 포트에서 서버 대기 중입니다!'); });
실행하면 아래와 같은 결과를 확인할 수 있다.
로그인 상태를 유지해보는 코드를 만들어보자!
const http = require('http'); const fs = require('fs').promises; const url = require('url'); const qs = require('querystring'); const parseCookies = (cookie='') => cookie //쿠키는 문자열 형태 parseCookies로 객체 형태로 변경 .split(';') .map(v => v.split('=')) .reduce((acc, [k,v]) => { acc[k.trim()] = decodeURIComponent(v); return acc; } , {}); http.createServer(async (req, res) => { const cookies = parseCookies(req.headers.cookie); if(req.url.startsWith('/login')) { const {query} = url.parse(req.url); const {name} = qs.parse(query); const expires = new Date(); expires.setMinutes(expires.getMinutes() + 5); res.writeHead(302, { Location : '/', 'Set-Cookie' : `name=${encodeURIComponent(name)}; Expires = ${expires.toGMTString()}; HttpOnly: Path=/`, }); res.end(); } else if (cookies.name) { res.writeHead(200, { 'Content-Type' : 'text/plain; charset=utf-8'}); res.end(`${cookies.name}님 안녕하세요`) } else { try { const data = await fs.readFile('./cookie2.html'); res.writeHead(200, {'Content-Type' : 'text/html; charset=utf-8'}); res.end(data); } catch (err) { res.writeHead(500, {'Content-Type' : 'text/plain; charset=utf-8'}); res.end(err.message); } } }) .listen(8080, () => { console.log('8080번 포트에서 서버 대기중입니다!'); });
+) url.parse와 qs.parse가 deprecated되었는데... host부분없이 pathname만 오는 주소라서 whatwg방식이 처리할 수 없는듯 하다.
실행결과는 아래와 같다.
- 쿠키명=쿠키값 : 기본적인 쿠키의 값
- Expires = 날짜 : 만료 기한, 쿠키가 이시간이 지나면 자동 제거.
- max-age = 초 : 해당 초가 지나면 쿠키가 제거
- 세션?
✔ 세션또한 쿠키처럼 클라이언트 정보를 저장할 수 있으나, 쿠키는 브라우저의 쿠키 저장소에 저장되는 반면 세션은 서버에 저장된다.
✔ 서버에 사용자 정보를 저장하고 세션아이디로 클라이언트와 소통
const http = require('http'); const fs = require('fs').promises; const url = require('url'); const qs = require('querystring'); const parseCookies = (cookie = '') => cookie .split(';') .map(v=> v.split('=')) .reduce((acc, [k, v]) => { acc[k.trim()] = decodeURIComponent(v); return acc; }, {}); const session = {}; http.createServer(async (req, res) => { const cookies = parseCookies(req.headers.cookie); if(req.url.startsWith('/login')) { const {query} = url.parse(req.url); const {name} = qs.parse(query); const expires = new Date(); expires.setMinutes(expires.getMinutes() + 5); const uniqueInt = Date.now(); session[uniqueInt] = { name, expires, }; res.writeHead(302, { Location : '/', 'Set-Cookie' : `session=${uniqueInt}; Expires = ${expires.toGMTString()}; HttpOnly: Path=/`, }); //uniqueInt : 쿠키에 이름 대신 숫자 값 보냄 res.end(); } else if (cookies.session && session[cookies.session].expires > new Date()){ res.writeHead(200, {'Content-Type' : 'text/plain; charset=utf-8'}); res.end(`${session[cookies.session].name}님 안녕하세요`); } else { try { const data = await fs.readFile('./cookie2.html'); res.writeHead(200, {'Content-Type' : 'text/html; charset=utf-8'}); res.end(data); } catch (err) { res.writeHead(500, {'Content-Type' : 'text/plain; charset=utf-8'}); res.end(err.message); } } }) .listen(8085, () => { console.log('8085번 포트에서 서버 대기중입니다!'); });
보통 보안상의 문제 등으로 직접 구현하기보단 npm에 있는 모듈 등을 사용하는 경우가 많다!
추가로 참고한 블로그 : https://hyuntaeknote.tistory.com/3
4.4 https와 http2
- https : 기존의 http 프로토콜을 암호화시킨 버전이다. SSL/TLS등을 통해 전송되는 정보들을 암호화시켜 보안 측면에서 뛰어나다.
서버에 암호화를 적용하려면 https모듈을 사용하며, 인증서 발급이 필요하다. 인증서가 있는 경우에는 아래와 같이 작성하면 된다. 기존 작성 코드 중, createServer의 첫 번째 인수에 인증서 관련 옵션을 넣는다고 생각하면 된다.
const http = require('https'); http.createServer({ cert : fs.readFileSync('도메인 인증서 경로'), key : fs.readFileSync('도메인 비밀키 경로'), ca : [ fs.readFileSync('상위 인증서 경로'), fs.readFileSync('상위 인증서 경로'), ], }, (req, res) => { res.writeHead(200, {'Content-Type' : 'text/html; charset=utf-8'}); res.write('<h1>Hello Node!</h1>'); res.end('<p>Hello Server!</p>'); }) .listen(443, () => { console.log('443번 포트에서 서버 대기 중입니다!'); }); //실제 서버에서는 443포트
- http2 : 최신 HTTP 프로토콜
http/1.1 : 하나의 연결 당 하나의 요청을 처리한다. 3-way handshacking이 반복적으로 발생하며 RTT(round-trip time, 전송하고 응답이 돌아올 때까지 걸리는 시간)가 증가하게 된다.
-> http2는 이를 개선해서, 속도 상향을 위해 하나의 커넥션으로 동시에 여러개 주고받을 수 있으며, 클라이언트의 요청을 최소화하기 위해 요청하지 않은 정보도 보내줄 수 있다. (server-push)
const http2 = require('http2'); const fs = require('fs'); http2.createSecureServer({ cert : fs.readFileSync('도메인 인증서 경로'), key : fs.readFileSync('도메인 비밀키 경로'), ca : [ fs.readFileSync('상위 인증서 경로'), fs.readFileSync('상위 인증서 경로'), ], }, (req, res) => { res.writeHead(200, {'Content-Type' : 'text/html; charset=utf-8'}); res.write('<h1>Hello Node!</h1>'); res.end('<p>Hello Server!</p>'); }) .listen(443, () => { console.log('443번 포트에서 서버 대기 중입니다!'); });
사용법은 https모듈과 거의 유사하다.
4.5 cluster
- cluster 모듈이란?
싱글 프로세스로 동작하는 노드가 CPU코어를 모두 사용할 수 있게끔 만들어 주는 모듈
const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length; //CPU 코어 개수 if(cluster.isMaster) { console.log(`마스터 프로세스 아이디 : ${process.pid}`); for (let i = 0; i<numCPUs; i+= 1) { cluster.fork(); //CPU코어 개수만큼 fork } cluster.on('exit', (worker, code, signal) => { console.log(`${worker.process.pid}번 워커가 종료되었습니다.`); console.log('code', code, 'signal', signal); cluster.fork(); //워커 하나 종료 시 새로운 워커 생성, 서버 종료를 막는다. }); } else { http.createServer(async (req, res) => { res.writeHead(200, { 'Content-Type' : 'text/html; charset = utf-8'}); res.write('<h1>Hello Node!</h1>'); res.end('<p>Hello Cluster!</p>'); setTimeout(() => { process.exit(1); }, 1000); }).listen(8086); console.log(`${process.pid}번 워커 실행`); }
본인의 cpu코어 개수만큼 fork했기 때문에 실행결과를 확인하면 출력된 워커 갯수가 코어 개수와 동일하다. 나는 i7 그램을 사용하는데, 8개의 워커가 출력되어 있음을 확인할 수 있다.
- 위 코드처럼 새로운 워커 생성으로 막기보단 오류 자체의 원인을 해결해야함.
- 클러스터링으로 예기치 못한 에러로 서버의 종료를 막을 순 있음
- 실무에서는 주로 pm2모듈을 사용해서 cluster 기능을 사용한다.
-> 추가로 pm2는 이렇게 생겼다. pm2 status 쳤을 때 나오는 화면인데, 에러가 발생하면 status error로 바뀌지만 서버 자체가 멈추진 않는다.
'BackEnd > Node.js' 카테고리의 다른 글
Node.js 스터디 5주차 (0) 2022.02.15 Node.js 스터디 4주차 (0) 2022.02.07 Node.js 스터디 2주차 (0) 2022.01.25 Node.js 스터디 1주차 (0) 2022.01.18 Node.js에서 로그인 기능 구현하기(+github 위키) (0) 2021.08.28