클라이언트에서 서버에 요청(request)을 보내고, 서버에서는 요청의 내용을 읽고 처리한 뒤 클라이언트에 응답(response)를 보낸다. ∴ 서버에 요청 받는 부분 & 응답 보내는 부분이 필요하다.
(요청과 응답은 이벤트 방식이라고 생각하면 된다.)
cf ) http://gilbut.co.kr과 같은 사이트는 80번 포트를 사용하며, 생략 가능하다. https는 443번 포트를 사용하고 생략 가능하다.
1) listen 메서드에 콜백 함수 삽입
const http = require('http');
http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'}); // 200 : 성공적인 요청임을 의미, 콘텐츠 형식이 HTML임을 알림. 한글 표시를 위해 utf-8로 설정.
res.write('<h1>Hello Node!</h1>');
res.end('<p>Hello Server!</p>'); // 인수를 클라이언트로 보내고 응답 종료.
})
.listen(8080, () => { // 서버 연결
console.log('8080번 포트에서 서버 대기 중입니다!');
});
2) listen 메서드에 콜백 함수 대신, 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를 여러 번 호출하여, 한 번에 여러 서버를 실행할 수도 있다. `
3) HTML 파일을 fs 모듈로 읽어 전송
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, () => { // 포트 번호 달라야 함. 포트 번호 같으면 EADDRINUSE 에러 발생. 실무에서는 서버 여러 개 띄우지 않음.
console.log('8081번 포트에서 서버 대기 중입니다!');
});
=> 요청이 들어오면 fs 모듈로 HTML 파일을 읽고, 저장된 버퍼를 그대로 클라이언트에 전송한다.
에러 메시지는 일반 문자열이므로 text/plain을 사용한다.
REpresentational State Transfer
서버의 자원을 정의하고, 자원에 대한 주소를 지정하는 방법을 가리킨다.
* 자원 : 서버가 행할 수 있는 것들
주소는 의미를 명확히 전달하기 위해 명사로 구성된다. ex) /user, /post
📌 REST에서는 주소 외에도 HTTP 요청 메서드를 사용한다.
위의 메서드로 표현하기 애매한 로그인 같은 동작 → POST 사용
const http = require('http');
const fs = require('fs').promises;
const path = require('path');
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(path.join(__dirname, '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(path.join(__dirname, '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));
}
try {
const data = await fs.readFile(path.join(__dirname, 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, { 'Content-Type': 'text/plain; charset=utf-8' });
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;
res.writeHead(201, { 'Content-Type': 'text/plain; charset=utf-8' });
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];
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8'});
return res.end(JSON.stringify(users));
}
}
res.writeHead(404);
return res.end('NOT FOUND');
} catch (err) {
console.error(err);
res.writeHead(500);
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end(err);
}
})
.listen(8082, () => {
console.log('8082번 포트에서 서버 대기 중입니다');
});
=> req.method로 HTTP 요청 메서드를 구분한다.
메서드가 GET인 경우, 요청 주소를 구분하고 주소에 맞는 파일을 제공한다.
존재하지 않는 파일 요청했거나 GET 메서드 요청 아닌 경우라면 404 NOT FOUND 에러를 응답으로 전송한다.
📌 POST, PUT 요청 처리
req.on('data'), req.on('end') → 요청 본문에 들어있는 데이터를 꺼내기 위한 작업이다.
req와 res도 내부적으로 readStream, writeStream으로 되어 있으므로 요청/응답 데이터가 스트림 형식으로 전달된다.
▶ 위 서버를 실행하고, 크롬의 개발자 도구 Network 탭을 통해 Method를 확인해보자.
HTTP 프로토콜 환경은 "connectionless, stateless"한 특성을 가지므로 서버는 클라이언트가 누구인지 매번 확인해야 한다.
이 문제를 보완하기 위해 쿠키와 세션을 사용한다.
쿠키는 클라이언트(브라우저)에 저장되는 키와 값이 들어있는 작은 데이터 파일이다.
※ 우리는 서버에서 브라우저로 쿠키를 보낼 때만 코드를 작성해 처리하면 된다.
쿠키는 요청의 헤더에 담겨 전송되고, 브라우저는 응답의 헤더에 따라 쿠키를 저장한다.
다음은 서버에서 직접 쿠키를 만들어 요청자의 브라우저에 넣는 코드이다.
const http = require('http');
http.createServer((req, res) => {
console.log(req.url, req.headers.cookie); // requ 객체에 담겨 있는 쿠키를 가져옴.
res.writeHead(200, { 'Set-Cookie': 'mycookie=test' }); // 응답을 받은 브라우저는 쿠키 저장. 쿠키 간에는 세미콜론을 넣어 각각을 구분
res.end('Hello Cookie');
})
.listen(8083, () => {
console.log('8083번 포트에서 서버 대기 중입니다!');
})
실행하면 화면에 Hello Cookie가 출력되고 콘솔에서는 다음과 같은 메시지가 뜬다.
=> 서버는 응답 헤더에 mycookie=test라는 쿠키를 심으라고 브라우저에 명령(Set-Cookie)
브라우저는 쿠키를 심었고, 두 번째 요청()의 헤더에 쿠키가 들어 있음을 확인할 수 있다.
📌 /favicon.ico
브라우저가 *파비콘이 뭔지 HTML에서 유추할 수 없으면 서버에 파비콘 정보에 대한 요청을 보낸다.
위 코드 HTML에서 파비콘 정보가 없으므로 브라우저가 추가로 /favicon.ico를 요청한 것이다.
* 파비콘 : 웹 사이트 탭에 보이는 이미지
쿠키를 심고, 사용자를 식별하는 방법을 알아보자.
const http = require('http');
const fs = require('fs').promises;
const path = require('path');
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 url = new URL(req.url, 'http://localhost:8084');
const name = url.searchParams.get('name');
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(path.join(__dirname, '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번 포트에서 서버 대기 중입니다!');
});
→ http://localhost:8084에 접속하면, /로 요청 전송.
cookie2.html에서 form을 통해 로그인 요청 보낼 시, /login으로 요청 전송.
세션이란, 웹에 접근하는 사용자의 상태 및 정보를 서버측에 저장하고 이용하는 것이다.
개인정보를 쿠키에 넣어두는 것은 적절하지 못하며, 쿠키가 조작될 위험이 있다.
서버가 사용자 정보를 관리하도록 하자.
const http = require('http');
const fs = require('fs').promises;
const path = require('path');
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 url = new URL(req.url, 'http://localhost:8085');
const name = url.searchParams.get('name');
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(path.join(__dirname, '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 객체에 대신 저장한다.
cookie.session이 있고 만료 기한을 넘기지 않았다면 session 변수에서 사용자 정보를 가져와 사용한다.
=> 서버에 사용자 정보를 저장하고, 클라이언트와는 세션 아이디로만 소통한다.
※ 실제 배포용 서버에서는 세션을 변수에 저장하지 않는다. 레디스나 멤캐시드와 같은 데이터에 저장해둔다.
https 모듈은 웹 서버에 SSL 암호화를 추가한다. GET/POST 요청 시, 오가는 데이터를 암호화하여 내용을 확인할 수 없게 한다.
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, () => { // 실제 서버에서는 80번 포트 대신 443번 포트 사용
console.log('443번 포트에서 서버 대기 중입니다!');
});
위 코드에서 createServer 메서드는 인수를 2개 받는다. 첫 번째 인수는 인증서에 관련된 옵션 객체로, 인증서 구입 시 제공된 파일들을 fs.readFileSync 메서드로 읽어 cert, key, ca 옵션에 알맞게 넣으면 된다.
노드의 http2 모듈은 SSL 암호화 + 최신 HTTP 프로토콜 http/2를 사용할 수 있게 한다. → 웹 속도 개선
위 코드에서 http2를 적용하려면 다음 2가지를 수정하면 된다.
- https 모듈 → http2
- createServer() → createSecure()
cluster 모듈은 싱글 프로세스로 동작하는 노드가 CPU 코어를 모두 사용할 수 있게 해주는 모듈이다.
ex) 코어 하나당 노드 프로세스 하나가 동작하도록 할 수 있다.
=> 성능 개선 / 메모리 공유 불가 (레디스 등의 서버 도입해 해결)
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}번 워커 실행`);
}
📌 클러스터
마스터 프로세스 : CPU 개수만큼 워커 프로세스 생성, 8086번 포트에서 대기, 요청을 워커 프로세스에 분배.
워커 프로세스 : 실질적인 일 수행.
Quiz
1. ( 클라이언트 )에서 ( 서버 )로 요청을 보내고, ( 서버 ) 에서는 요청을 처리한 뒤 ( 클라이언트 )에 응답을 보낸다.
2. HTTP 요청 메서드 중 리소스를 가져올 때 사용하고, 요청 본문에 데이터를 넣지 않는 메서드는 ( GET )이다.
3. 쿠키란 ( 웹 브라우저 )에 저장되는 데이터 파일이고, 요청의 ( 헤더 )에 담겨 전송된다.
4. ( 브라우저 )는 파비콘이 무엇인지 유추할 수 없으면 ( 서버 )에 파비콘 정보에 대한 요청을 보낸다.
5. 쿠키의 옵션은 ( 세미콜론 )으로 구분하고, 대표적으로 ( 한글 ) 과 ( 줄바꿈 )은 쿠키에 들어갈 수 없다.
6. https 모듈은 웹 서버에 ( SSL 암호화 )를 추가한다.
7. cluster에는 ( 마스터 프로세스 )와 ( 워커 프로세스 )로 구분된다.
Programming Quiz
1. 이벤트 리스너를 가진 노드 서버를 만드시오. 단, 서버에 listening 이벤트 리스너와 error 이벤트 리스너를 붙이시오.
2. 다음을 만족하는 HTTP 서버를 만드시오.
- 서버는 8080 포트에서 실행
- / 경로로 요청이 오면 "Welcome to the Home Page!"라는 응답 전송
- /about 경로로 요청이 오면 "About Us"라는 응답 전송
- 그 외의 경로로 요청이 오면 "Page Not Found"라는 응답과 함께 상태 코드 404를 반환
1번 답
예제 코드 server1-1.js 확인
2번 답
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url === '/') { // 홈 경로
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end("Welcome to the Home Page!"); // 홈 응답 메시지
} else if (req.url === '/about') { // About 페이지 경로
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end("About Us"); // About 페이지 응답 메시지
} else { // 그 외 경로
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end("Page Not Found"); // 404 에러 메시지
}
});
server.listen(8080, () => {
console.log('서버가 8080번 포트에서 실행 중입니다.');
});
출처 : 조현영, 『 Node.js 교과서 개정 3판』, 길벗(2022)
Corner Node.js 1
Editor : Oze
[노드 1팀] 6장. 익스프레스 웹 서버 만들기 (0) | 2024.11.22 |
---|---|
[노드 1팀] 5장. 패키지 매니저 (0) | 2024.11.15 |
[노드 1팀] 3장. 노드 기능 알아보기 (2) (0) | 2024.11.08 |
[노드 1팀] 3장. 노드 기능 알아보기 (1) (0) | 2024.10.11 |
[노드 1팀] 2장. 알아둬야 할 JavaScript (0) | 2024.10.11 |