http 모듈로 서버 만들기
#요청과 응답 이해하기
자 이제부터 서버를 만들어보자
서버에서는 요청의 내용을 읽고 처리한 뒤 클라이언트에 응답을 보낸다.
그러니까 일단 요청을 받는 부분과 응답을 보내는 부분이 있어야 한다.
const http = require('http');
http.createServer((req, res) => {
// 여기에 어떻게 응답할 지 적어줍니다.
});
http 서버가 있어야 웹 브라우저의 요청을 처리할 수 있으므로 http 모듈읠 사용했다.
http모듈에는 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번 포트에서 서버 대기 중입니다!');
});
// 출력결과
8080번 포트에서 서버 대기 중입니다!
이렇게하면 8080포트에서 응답오기를 기다림.
res객체를 보자.
writeHead는 응답에 대한 정보를 기록하는 메서드
첫 번째 인수로 성공적인 요청임을 의미하는 200을
두 번째 인수로 응답에 대한 정보를 보내는데 콘텐츠의 형식이 HTML임을 가르키고 있음. 또한 한글 표시를 위해 charset을 uft-8로 지정.
이 정보가 기록되는 부분을 header라고 한다.
write메서드에는
첫 번째 인수는 클라이언트로 보낼 데이터다.
지금은 HTML문자열을 보냈지만 버퍼를 보낼 수도 있다.
또한 여러 번 호출해서 데이터를 여러 개 보내도 된다.
데이터가 기록되는 부분을 body라고 한다.
end메서드는 응답을 종료하는 메서드이다.
만약 인수가 있다면 그 데이터도 클라이언트로 보내고 응답을 종료
위 예제는 res.write에서 write로 바디문을 end로 저 문구를 보내면서 응답이 종료된 것이다.
브라우저는 응답 내용을 받아서 렌더링합니다.
listen 메서드에 콜백 함수를 넣는 대신,ㄷ ㅏ음과 같이 서버에 listening 이벤트 리스너를 붙여도 됩니다. 추가로 error 리스너도 붙여봤습니다.
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);
});
서버는 참고로 변경사항을 즉각 반영하지 않음. 종료했다가 다시 실행해야 한다.
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('8081번 포트에서 서버 대기 중입니다!');
});
// 만약 포트 번호가 같다면 서버가 죽는다.
서버를 여러개 만들 수도 있다.
서버 포트가 같으면 서버가 죽는다.
실무에서 서버를 여러개 띄우는 일은 드물다.
write와 end에 일일히 HTML을 적는거는 비효율 적이므로 HTML 파일을 만들어 두는것이 바람직하다
그 HTML 파일은 fs모듈로 읽어서 전송할 수 있다.
// server2.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Node.js 웹 서버</title>
</head>
<body>
<h1>Node.js 웹 서버</h1>
<p>만들 준비되셨나요?</p>
</body>
</html>
// server2.js
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' });
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번 포트에서 서버 대기 중입니다!');
});
요청 들어오면 먼저 fs 모듈로 HTML 파일을 읽습니다.
data에 저장된 버퍼를 그대로 클라이언트에 보내면 된다.
이전에는 문자열을 보냈지만, 저렇게 버퍼를 보낼 수도 있다.
예기치 못한 에러가 발생한 경우에는 에러 메시지를 응답
에러 메시지는 일반 문자열이므로 text/plain 사용
HTTP 상태코드 간단정리
- 2XX: 성공알리는 코드 200(성공), 201(작성됨)
- 3XX: 리다이렉션(다른 페이지 이동)을 알리는 코드 어떤 주소를 입력했는데 다른페이지로 넘어갈 때
301(영구이동) 302(임시이동)
304(수정되지 않음): 요청의 응답으로 캐시를 사용
- 4XX: 요청오류 요청 자체에 오류
400(잘못된 요청) 401(권한 없음) 403(금지됨) 404(찾을 수 없음)
- 5XX: 서버 오류를 나타냄 요청은 제대로 왔지만 서버에 오류가 발생
res.writeHead로 클라이언트에 직접 보내는 경우는 거의 없고, 예기치 못한 에러가 발생하면서 서버가 알아서 5XX대 코드를 보낸다.
500(내부 서버 오류) 502(불량 게이트웨이) 503(서비스를 사용할 수 없음)
요청 과정에서 오류 발생했다고 응답을 보내지 않으면 안된다. 요청이 성공했든 실패했든 응답을 클라이언트로 보내서 요청이 마무리 되었음을 알린다.
응답을 보내지 않는다면 클라이언트는 서버로부터 응답이 오길 하염없이 기다리다가 시간초과를 내야한다.
#REST와 라우팅 사용하기
REST란?
REpresentational State Transfer의 줄임말로 서버의 자원 정의하고 자원에 대한 주소를 지정하는 방법을 가리킨다.
일종의 약속이라고 봐도 무방.
자원이라고 해서 꼭 파일일 필요는 없고 서버가 행할 수 있는 것들을 통틀어서 의미한다고 보면 된다.
- get: 서버 자원 가져올 때, 바디 넣지 않고 퀴리스트링 사용
- post: 서버에 자원을 새로 등록하고자 할때, 요청의 본문에 새로 등록할 데이터를 넣어 보낸다.
- put: 서버 자원의 일부만 수정하고자 할때. 요청의 본문에 치환할 데이터를 넣어 보낸다.
- patch: 서버 자원의 일부만 수정하고자 할때. 요청의 본문에 일부 수정할 데이터를 넣어 보낸다.
- delete: 서버의 자원을 삭제하고자 할때, 요청의 본문에 데이터를 넣지 않는다.
- options: 요청을 하기 전에 통신 옵션을 설명하기 위해 사용
주소 하나가 요청 메소드 여러개를 가질 수 있음.
이제는 REST를 사용한 주소 체계로 RESTful한 웹 서버를 만들어보자.
REST를 따르는 서버를 'RESTful'하다고 표현한다.
// restFront.css
a { color: blue; text-decoration: none; }
// restFront.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<title>RESTful SERVER</title>
<link rel="stylesheet" href="./restFront.css" />
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
<div>
<form id="form">
<input type="text" id="username">
<button type="submit">등록</button>
</form>
</div>
<div id="list"></div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="./restFront.js"></script>
</body>
</html>
async function getUser() { // 로딩 시 사용자 가져오는 함수
try {
const res = await axios.get('/users');
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 });
getUser();
} catch (err) {
console.error(err);
}
});
const remove = document.createElement('button');
remove.textContent = '삭제';
remove.addEventListener('click', async () => { // 삭제 버튼 클릭
try {
await axios.delete('/user/' + key);
getUser();
} catch (err) {
console.error(err);
}
});
userDiv.appendChild(span);
userDiv.appendChild(edit);
userDiv.appendChild(remove);
list.appendChild(userDiv);
console.log(res.data);
});
} catch (err) {
console.error(err);
}
}
// restFront.js
window.onload = getUser; // 화면 로딩 시 getUser 호출
// 폼 제출(submit) 시 실행
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 });
getUser();
} catch (err) {
console.error(err);
}
e.target.username.value = '';
});
// about.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>RESTful SERVER</title>
<link rel="stylesheet" href="./restFront.css" />
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
<div>
<h2>소개 페이지입니다.</h2>
<p>사용자 이름을 등록하세요!</p>
</div>
</body>
</html>
// restServer.js
const http = require('http');
const fs = require('fs').promises;
const users = {}; // 데이터 저장용
http.createServer(async (req, res) => {
try {
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': 'application/json; charset=utf-8' });
return res.end(JSON.stringify(users));
}
// /도 /about도 /users도 아니면
try {
const data = await fs.readFile(`.${req.url}`);
return res.end(data);
} catch (err) {
// 주소에 해당하는 라우트를 못 찾았다는 404 Not Found error 발생
}
} else if (req.method === 'POST') {
if (req.url === '/user') {
let body = '';
// 요청의 body를 stream 형식으로 받음
req.on('data', (data) => {
body += data;
});
// 요청의 body를 다 받은 후 실행됨
return req.on('end', () => {
console.log('POST 본문(Body):', body);
const { name } = JSON.parse(body);
const id = Date.now();
users[id] = name;
res.writeHead(201, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('ok');
});
}
} 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;
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
return res.end('ok');
});
}
} else if (req.method === 'DELETE') {
if (req.url.startsWith('/user/')) {
const key = req.url.split('/')[2];
delete users[key];
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
return res.end('ok');
}
}
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번 포트에서 서버 대기 중입니다');
});
restServer.js가 핵심
res.end 앞에 return을 붙이는 이유는 end를 호출하면 함수가 종료된다고 착각하곤 한다. 노드도 일반적인 자바스크립트 문법을 따르므로 return을 붙이지 않는 한 함수가 종료되지 않는다. 따라서 다음에 코드가 이러지는 경우에는 return로 종료한다.
만약 end가 여러 번 실행된다면 Error가 발생한다.
해당 예제에서는 데이터가 메모리에 저장되어서 서버 종료하면 소실된다는거, 7,8 장에서 데이터베이스 다룰거임.
# 쿠키와 세션 이해하기
클라이언트에게 보내는 요청에는 한가지 큰 단점이 있는데, 바로 누가 보내는지 모른다는 것이다.
물론 요청을 보내는 IP주소나 브라우저의 정보를 받아올 수는 있다. 하지만 여러 컴퓨터가 공통으로 IP주소를 갖거나 한 컴퓨터를 여러사람이 사용할 수 있다.
로그인을 구현하려면 쿠케와 세션을 알고 있어야 한다.
누구인지 기억하기 위해 서버는 요청에 대한 응답을 할 때 쿠키라는 것을 같이 보냄.
쿠키는 유효 기간이 있으며 name=lgvv처럼 단순한 '키-값'이다.
서버로부터 쿠키가 오면 웹 브라우저는 쿠키를 저장해뒀다가 다음에 요청할 때마다 쿠키를 동봉해서 보냄
서버는 요청에 있는 쿠키를 읽어서 누구인지 판단함.
브라우저는 쿠키를 알아서 자동으로 동봉해서 보내줌.
서버에서 브라우저로 보낼때마 코드 작성하면 된다.
쿠키는 요청의 헤더에 담겨 함꼐 전송된다. 헤어(Set-Cookie) 여부에 따라 쿠키를 저장한다.
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번 포트에서 서버 대기 중입니다!');
});
쿠키는 문자열 형식으로 존재
name=lgvv;year=2000 형태로 존재. 쿠키 간에는 세미콜론을 넣어 각각을 구분
createServer 메서드의 콜백에서는 req 객체에 담겨 있는 쿠키를 가져온다. 쿠키는 req.headers.cookie에 들어있음. req.headers 메서드를 사용. 조금 전에 쿠키는 요청과 응답의 헤더를 통해 오간다.
응답의 헤더에 쿠키를 기록해야 한다. res.writeHead를 작성 Set-Cookie는 브라우저한테 다음과 같은 값의 쿠키를 저장하라는 의미.
파비큔이랑 웹 사이트에 탭에 보이는 이미지를 말함.
다음에는 그 쿠키가 나인지 식별해주는 방법을 알아보자.
// cookie2.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>쿠키&세션 이해하기</title>
</head>
<body>
<form action="/login">
<input id="name" name="name" placeholder="이름을 입력하세요" />
<button id="login">로그인</button>
</form>
</body>
</html>
// cookie2.js
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;
}, {});
http.createServer(async (req, res) => {
const cookies = parseCookies(req.headers.cookie); // { mycookie: 'test' }
// 주소가 /login으로 시작하는 경우
if (req.url.startsWith('/login')) {
const { query } = url.parse(req.url);
const { name } = qs.parse(query);
const expires = new Date();
// 쿠키 유효 시간을 현재시간 + 5분으로 설정
expires.setMinutes(expires.getMinutes() + 5);
res.writeHead(302, {
Location: '/',
'Set-Cookie': `name=${encodeURIComponent(name)}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`,
});
res.end();
// name이라는 쿠키가 있는 경우
} 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(8084, () => {
console.log('8084번 포트에서 서버 대기 중입니다!');
});
이 경우에는 쿠키가 앱단에서 노출되어서 위험함
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=/`,
});
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번 포트에서 서버 대기 중입니다!');
});
좀 잘라진게 쿠키에 이름 담아서 보내는 대신 uniqueInt라는 숫자값을 보냄. 사용자의 이름과 만료 시간은 uniqueInt 속성명 아래에 있는 session이라는 객체에 대신 저장함.
# https와 http2
https 모듈은 웹 서버에 SSL 암호화를 추가.
GET, POST 할 때 데이터를 암호화해서 중간에 채가도 확인할 수 없게 만들어버림.
로그인이나 결제에서 필수로 필요한 이유.
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번 포트에서 서버 대기 중입니다!');
});
인증 기관에서 SSL을 구입해야 하는데, Let's Encrpyt 같은 기관에서 무료로 발급해주기도 함.
const https = require('https');
const fs = require('fs');
https.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번 포트에서 서버 대기 중입니다!');
});
http2 사용하면 속도도 더 개선된다.
http1.1에서도 파이프라인 기술을 적용하므로 저정도로 차이가 나지는 않지만 http/2가 더 효율적인 것은 분명
아래는 http/2를 적용한거임.
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번 포트에서 서버 대기 중입니다!');
});
코드는 똑같은데 저기만 다르다!
# Cluster
cluster 모듈은 기본적으로 싱글 프로세스로 동작하는 노드가 CPU 코어를 모두 사용할 수 있게 해주는 모듈.
요청이 들어오면 병렬로 실행된 서버의 개수만큼 요청이 분산되게 할 수 있음.
서버에 무리가 그만큼 덜 생김.
메모리를 공유하지 못하는 단점도 존재 따라서 세션을 메모리에 저장하는 경우 문제가 될 수 있음. 이는 레디스 등의 서버를 도입해 해결할 수 있음.
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`마스터 프로세스 아이디: ${process.pid}`);
// CPU 개수만큼 워커를 생산
for (let i = 0; i < numCPUs; i += 1) {
cluster.fork();
}
// 워커가 종료되었을 때
cluster.on('exit', (worker, code, signal) => {
console.log(`${worker.process.pid}번 워커가 종료되었습니다.`);
console.log('code', code, 'signal', signal);
cluster.fork();
});
} else {
// 워커들이 포트에서 대기
http.createServer((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(() => { // 워커 존재를 확인하기 위해 1초마다 강제 종료
process.exit(1);
}, 1000);
}).listen(8086);
console.log(`${process.pid}번 워커 실행`);
}
워커 스레드랑 비슷함. 하지만 프로세스임
요청이 들어오면 만ㄷ르어진 워커 프로세스에 요청을 분배함.
요청하는 주소 많아지면 if문 많아져서 관리 복잡해짐
Express 모듈을 이제 알아보자 #5에서
'Node.js' 카테고리의 다른 글
[Node.js] #6 익스프레스 웹 서버 만들기 (0) | 2023.07.23 |
---|---|
[Node.js] #5 패키지 매니저 (0) | 2023.07.23 |
[Node.js] #3 노드 기능 알아보기 (0) | 2023.07.22 |
[Node.js] #2 알아둬야 할 자바스크립트 (0) | 2023.07.17 |
[Node.js] #1 핵심개념 이해하기 (0) | 2023.07.16 |