1. http 서버
http 모듈을 사용하면 node.js를 http서버로서 동작할 수 있도록 구현할 수 있습니다.
const http = require('http');
const httpServer = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.write('<h1>Hello</h1>');
res.end('<p>Hello Server!</p>');
});
httpServer.listen(80);
httpServer.on('listening', () => {
console.log('대기중');
});
httpServer.on('error', (err) => {
console.error(err);
});
서버는 createServer로 생성할 수 있으며 요청이 오면 콜백 함수를 통해 응답을 실행합니다. 서버는 listen에서 지정한 포트로 응답을 대기하는 상태가 되고 서버가 시작되면 listening이벤트를 호출합니다.
이 상태에서 서버로 요청이 오면 콜백 함수를 통해 응답하게 되는데 응답에서는 res객체를 통해 writeHead()로 헤더를 쓰고 write()로 본문을 작성한 다음 end()로 응답을 종료하는 방식으로 클라이언트에 응답할 수 있습니다. res객체는 응답에 관한 것이고 req는 요청에 관한 객체입니다.
하지만 writeHead()나 write(), end() 모두 응답에 필요한 데이터를 일일이 문자열로 만들어 응답하는 방식인데 상당히 비효율적이라는 것을 알 수 있습니다. 따라서 실제로 위와 같이 응답처리를 하는 경우는 거의 없고 응답에 필요한 데이터를 웹문서로 만들어 문서 자체를 반환하도록 하는 것이 일반적인 방법입니다.
우선 다음과 같은 내용의 html파일을 생성합니다. 파일명은 index.html입니다.
<!DOCTYPE html>
<html>
<head>
<title>테스트입니다.</title>
</head>
<body>
<h1>cliel</h1>
<a href="http://cliel.com/">여기로 이동</a>
</body>
</html>
그리고 fs모듈을 사용해 해당 파일을 읽고 읽은 데이터를 응답하도록 하면 자연스럽게 파일내용을 사용자에게 응답할 수 있게 됩니다.
const http = require('http');
const fs = require('fs').promise;
const httpServer = http.createServer(async (req, res) => {
try {
const data = await fs.readFile('./index.html');
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(data);
}
catch (err) {
console.error(err);
}
});
또한 파일을 만들어 응답하게 되면 응답하고자 하는 내용에 변경이 필요한 경우 파일내용만 바꿔주면 되므로 node.js를 재시작하지 않아도 되는 장점이 있습니다.
2. URL 요청과 응답
클라이언트가 요청한 요청방식과 URL은 다음과 같은 방법으로 구분할 수 있습니다.
const http = require('http');
const fs = require('fs').promises;
const httpServer = http.createServer(async (req, res) => {
try {
if (req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
if (req.url === '/') {
const data = await fs.readFile('./index.html');
return res.end(data);
}
else if (req.url === '/test') {
const data = await fs.readFile('./test.html');
return res.end(data);
}
try {
const data = await fs.readFile(`.${req.url}.html`);
return res.end(data);
}
catch (err) {}
}
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end('페이지를 찾을 수 없습니다.');
}
catch (err) {
console.error(err);
}
});
httpServer.listen(80);
httpServer.on('listening', () => {
console.log('대기중');
});
httpServer.on('error', (err) => {
console.error(err);
});
req는 요청에 대한 객체인데 이 객체의 method로 요청 방식을, url로 요청한 URL을 구분하고 요청한 URL마다 개별적인 페이지를 만들어 반환할 수 있습니다.
URL 요청에 대해 응답하는 또 다른 방법은 URL요청에 대응하는 페이지나 css, js파일 등 모든 리소스 파일을 마련해 두고 요청 URL자체를 파일로 읽도록 처리한 뒤 URL자체를 파일로 대응해 응답하는 것입니다. 이 부분은 위 예제 안에서 아래 구문으로 처리하고 있습니다.
const data = await fs.readFile(`.${req.url}.html`);
return res.end(data);
<h1>404</h1>
404페이지를 일부러 만들어 URL요청에 대한 응답을 처리했지만 사용자의 요청이 서버에서 응답 가능한 경우가 아니라면 이에 대한 대비도 필요할 것입니다. 아래처럼 말이죠.
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end('페이지를 찾을 수 없습니다.');
3. REST API 처리
REST API 형식의 주소를 가진 서버를 RESTFul이라고 합니다. 위에서 설명드린 방식을 이용하면 REST API요청을 받아 처리하는 서버도 간단히 구현이 가능합니다.
대표적으로 사용되는 POST의 경우를 확인해 보기 위해 아래와 같이 간단한 login.html 페이지를 만들어 봅니다.
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<form action="/data" method="post">
아이디 : <input type="test" id="id" name="id" /><br />
비밀번호 : <input type="password" id="pw" name="pw" /><br />
<button type="submit">확인</button>
</form>
</body>
</html>
사용자가 '확인'버튼을 누르면 /data URL로 id값과 pw값을 보낼 것입니다.
if (req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
if (req.url === '/') {
const data = await fs.readFile('./index.html');
return res.end(data);
}
else if (req.url === '/test') {
const data = await fs.readFile('./test.html');
return res.end(data);
}
try {
const data = await fs.readFile(`.${req.url}.html`);
return res.end(data);
}
catch (err) {}
}
else if (req.method === 'POST') {
if (req.url === '/data') {
let body = {};
req.on('data', (data) => {
data.toString().split('&').map(item => {
const dt = item.split('=');
const key = dt[0];
const value = dt[1];
body[key] = value;
});
});
return req.on('end', () => {
res.writeHead(201, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`보내진 데이터 : ${body['id']} - ${body['pw']}`);
});
}
}
사용자가 POST 요청을 하게 되면 data이벤트에서 POST로 전성되어온 값을 key와 value으로 분리합니다. 그리고 end에서 사용자가 보낸 데이터를 다시 반환하도록 하였습니다.
동일한 방법으로 method를 통해 다른 API요청도 그에 맞게 처리되면 됩니다. 만약 사용자의 요청이 DELETE라면 아래와 같이 처리할 수도 있을 것입니다.
else if (req.method === 'DELETE') {
if (req.url.startWith('/data')) {
//삭제 처리
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('삭제');
}
}
4. 쿠키 다루기
우선 서버에서 클라이언트에 쿠키 설정을 하려면 응답 해더를 통해 'Set-Cookie'로 값을 넘겨줘야 합니다. 아래 예제에서는 login페이지에서 /data로 id와 pw값을 전송하면 이를 받아 id값으로 쿠키 설정을 요청하고 있습니다. 참고로 httpOnly는 javascript 등을 통해서 쿠키에 접근할 수 없도록 하는 것이며 Path를 통해 사이트 전역에 쿠키를 적용하고 있습니다.
else if (req.method === 'POST') {
if (req.url === '/data') {
let body = {};
req.on('data', (data) => {
data.toString().split('&').map(item => {
const dt = item.split('=');
const key = dt[0];
const value = dt[1];
body[key] = value;
});
});
return req.on('end', () => {
res.writeHead(302, { Location : '/user', 'Set-Cookie' : `id=${body['id']}; HttpOnly; Path=/` });
return res.end();
});
}
}
이어서 쿠키 설정과 함께 302 응답으로 클라이언트의 주소를 /user로 바꿔주고 있는데 이때 클라이언트에서는 해당 쿠키값을 설정한 뒤 지정한 /user로 Redirection을 수행합니다.
if (req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
if (req.url === '/') {
const data = await fs.readFile('./index.html');
return res.end(data);
}
else if (req.url === '/test') {
const data = await fs.readFile('./test.html');
return res.end(data);
}
else if (req.url === '/user') {
if (cookie.id) {
return res.end(`${cookie.id}님 환영합니다.`);
}
else {
res.writeHead(302, { Location : '/login', 'Content-Type': 'text/html; charset=utf-8' });
return res.end();
}
}
try {
const data = await fs.readFile(`.${req.url}.html`);
return res.end(data);
}
catch (err) {}
}
/user에서는 실제 쿠키값이 있는지를 확인한 후 쿠키값이 존재한다면 '-님 환영합니다.'라는 표시를, 없다면 사용자를 /login페이지로 돌려보냅니다.
클라이언트에 쿠키값이 설정되어 있는 경우 쿠키 데이터를 자동으로 서버에 전송하기 때문에 서버에서는 실제 사용하려는 쿠키값이 존재하는지만 확인하면 됩니다.
참고로 화면에서 쿠키의 만료시간이 세션으로 잡혀 있습니다. 세션은 웹브라우저를 닫으면 쿠키가 자동으로 삭제됨을 뜻하며 특정 시간까지의 만료시간이 필요하다면 Header를 통해 Expires-로 만료시간을 별도로 지정할 수 있습니다. Expires에는 기간을 지정하는데 단순히 초단위 시간의 카운터를 통해 만료시간을 맞추고자 한다면 Max-age=를 사용합니다. 이 설정은 Expires보다 더 우선합니다.
5. 세션
세션이라고 해서 쿠키와 크게 다르지는 않습니다. 쿠키는 직접 브라우저를 통해서 쿠키의 값을 확인할 수 있고 변경 또한 가능하기 때문에 중요한 정보를 서버의 세션에서 처리하는 경우가 많습니다. 다만 세션도 사용자를 구분해야 하기에 쿠키에서는 중복되지 않는 임의의 값만을 설정하고 해당 값으로 서버에서 설정한 쿠키값으로 사용자를 구분해 세션을 처리합니다.
const http = require('http');
const fs = require('fs').promises;
const getCookies = (cookie = '') =>
cookie.split(';').map(x => x.split('=')).reduce((cki, [i, x]) => {
cki[i.trim()] = decodeURIComponent(x);
return cki;
}, {});
let session = {};
const httpServer = http.createServer(async (req, res) => {
const cookie = getCookies(req.headers.cookie);
try {
if (req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
if (req.url === '/') {
const data = await fs.readFile('./index.html');
return res.end(data);
}
else if (req.url === '/test') {
const data = await fs.readFile('./test.html');
return res.end(data);
}
else if (req.url === '/user') {
if (cookie.session) {
return res.end(`${session[cookie.session].id}님 환영합니다.`);
}
else {
res.writeHead(302, { Location : '/login', 'Content-Type': 'text/html; charset=utf-8' });
return res.end();
}
}
try {
const data = await fs.readFile(`.${req.url}.html`);
return res.end(data);
}
catch (err) {}
}
else if (req.method === 'POST') {
if (req.url === '/data') {
let body = {};
const sessionID = Date.now();
req.on('data', (data) => {
data.toString().split('&').map(item => {
const dt = item.split('=');
const key = dt[0];
const value = dt[1];
body[key] = value;
});
session[sessionID] = {
id : body['id'],
pw : body['pw']
};
});
return req.on('end', () => {
res.writeHead(302, { Location : '/user', 'Set-Cookie' : `session=${sessionID}; HttpOnly; Path=/` });
return res.end();
});
}
}
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end('페이지를 찾을 수 없습니다.');
}
catch (err) {
console.error(err);
}
});
httpServer.listen(80);
httpServer.on('listening', () => {
console.log('대기중');
});
httpServer.on('error', (err) => {
console.error(err);
});
기존 쿠키를 사용하던 예제는 세션으로 바꾼 것입니다. sesstion관리를 위해 session객체 변수를 하나 선언하고 Login페이지에서 전송되어온 id와 pw값을 세션 변수에 추가합니다. 그리고 세션 변수를 추가할 때 키로 사용한 sessionID를 쿠키로 전송합니다.
후에 /user페이지로 접근 시 쿠키값을 통해 세션 ID를 확인하고 해당 ID로 session변수를 확인해 사용자에게 표시합니다.
6. https, http2
node.js에서는 기존 http에 암호화를 적용한 https를 위해 https모듈을 제공하고 제공하고 있습니다.
const https = require('https');
const fs = require('fs');
const httpsServer = https.createServer({
cert: fs.readFileSync('도메인 인증서 파일'),
key: fs.readFileSync('도메인 비밀키 파일'),
ca: [
fs.readFileSync('상위 인증서 파일'),
fs.readFileSync('상위 인증서 파일')
]
},
async (req, res) => {
try {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<h1>hello</h1>')
}
catch (err) {
console.error(err);
}
});
http모듈을 단순히 https로 바꿔주고 createServer에서 cert, key, ca가 포함된 인자를 추가적으로 전달하면 됩니다. 나머지는 기존 http와 동일합니다.
http2는 기존 http에서 다중 전송을 구현해 속도를 더 증가시킨 프로토콜인데 내부적으로 https를 포함하고 있어서 위 예제에서 http2로만 모듈명을 바꿔주면 http2로 통신이 가능한 서버가 됩니다.
7. cluster (클러스터)
node.js는 기본적으로 하나의 프로세스에 하나의 스레드로 동작합니다. 이 단순함의 문제점은 내 CPU의 코어가 8개라 하더라도 하나의 코어만 사용한다는 것입니다. 이러한 문제점을 해소하기 위해 node.js에서는 cluster라는 모듈을 제공하고 있습니다.
const cluster = require('cluster');
const http = require('http');
const cores = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`마스터 실행 : ${process.pid}`);
for (let i = 0; i < cores; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`서버종료 - ${worker.process.pid}`);
console.log(code, signal);
});
}
else {
http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type' : 'text/html; charset=utf-8' });
res.end('<h1>hello</h1>');
}).listen(80);
console.log(`${process.pid} 서버 실행`);
}
cluster에서는 isMaster로 마스터 서버와 다른 작업 서버의 실행을 구분할 수 있습니다. for문을 보시면 cpu의 코어수를 가져와 그만큼 cluster의 fork() 함수를 호출하여 작업 서버를 실행하고 있습니다. 코어수가 8개라면 작업 서버를 8개 생성하여 사용자의 요청을 처리할 것입니다.
마스터 서버는 실제 요청을 처리하는 작업 서버가 아니고 사용자가 서버에 접속을 시도하면 라운드 로빈 방식으로 현재 실행 중인 여러 서버들에 사용자를 분배하는 역할을 수행합니다. 실제로 서버가 8개가 실행 중인 셈이니 단일 서버보다는 훨씬 효휼적인 방식으로 사용자의 요청을 처리할 수 있습니다.
마스터에서는 exit로 각 서버의 중지여부를 알 수 있는데 만약 중지된 서버를 즉각 다시 실행시켜야 한다면 해당 이벤트 안에서 cluster.fork()를 주기만 하면 됩니다. 작업 서버에서는 기존 구현된 서버와 동일하게 사용자의 요청을 처리하는 작업을 수행합니다.
다만 각 서버마다 자원 공유는 하지 않으므로 세션 등의 공유는 할 수 없습니다.
'Server > node.js' 카테고리의 다른 글
[node.js] Express (0) | 2021.03.05 |
---|---|
[node.js] 패키지 관리 (0) | 2021.03.04 |
[node.js] 에러 핸들링 (0) | 2021.03.03 |
[node.js] 이벤트 처리 (0) | 2021.03.03 |
[node.js] 설치 (Windows WSL2) (0) | 2021.03.03 |