과목명 : 웹 서버 프로그래밍(Web Server-side programming with Node.js)
수업일자 : 2023년 05월 11일 (목)
1. 쿠키와 세션(Cookie and Session) 이해하기, Header and Body of HTTP Request & Response, HTTP State Code
1-1. 쿠키(Cookie)
(1) 쿠키는 키와 값의 형태(Key, Value)의 문자열로 브라우저에 저장되어 사용자를 인식하거나 데이터를 저장하는 역할을 수행합니다.
1-2. 쿠키의 필요성
(1) 단순 요청 시엔 한 가지 단점이 존재
- 요청의 주체를 확인할 수 없습니다. (간단히 IP 주소와 브라우저의 정보만 알 수 있음)
- 쿠키와 세션의 개념이 필요합니다.
(2) 쿠키(Cookie)는 Key와 Value의 쌍으로 이루어져 있다.
- 클라이언트의 매 요청마다 서버에 쿠키를 동봉해서 전송합니다.
- 서버는 쿠키 데이터를 파악하여 클라이언트를 구분할 수 있습니다.
1-3. 쿠키 서버 생성하기
const http = require('http');
const parseCookies = (cookie = '') =>
cookie
.split(';')
.map(v => v.split('='))
.reduce((acc, [k, v]) => {
acc[k.trim()] = decodeURIComponent(v);
return acc;
}, {});
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번 포트에서 서버 대기 중입니다!');
});
(1) 쿠키를 읽고 쓰는 것을 직접 구현
- 변수 parseCookies : 쿠키 문자열을 객체로 반환합니다.
- res.writeHead() : 요청 헤더에 입력하는 메소드입니다.
- 'Set-Cookie' : 브라우저가 쿠키를 설정할 수 있도록 명령합니다.
(2) req.headers.cookie
- 쿠키의 데이터가 문자열로 담겨져 있습니다.
(3) req.url
- 요청 주소입니다.
<실행 결과>
(1) 요청이 서버로 전송되고 응답이 왔을 때 쿠키 데이터가 설정됩니다.
(2) favicon.ico의 경우 브라우저가 자동으로 보내는 요청으로 두 번째 요청인 favicon.ico가 쿠키 데이터에 삽입됩니다.
(3) Chrome 브라우저의 개발자 도구의 Network 탭에서 HTTP 요청, 응답 Header, Cookie 정보를 확인할 수 있습니다.
1-4. HTTP 요청과 응답의 Header와 Body
(1) HTTP 요청과 응답의 경우 Header와 Body로 구분할 수 있습니다.
(2) Body 영역의 경우 실제 요청과 응답 간 주고받게 되는 실제 데이터입니다.
(3) 쿠키는 부가적인 정보로써 Header 영역에 저장됩니다.
1-5. HTTP State code
(1) 정의
- HTTP 상태 코드란, 클라이언트에서 보낸 HTTP 요청에 대한 서버의 응답 코드를 의미합니다. 해당 코드로 요청이 성공했는지 또는 실패했는지 판단할 수 있습니다.
(2) 2XX
- 요청에 대한 성공을 알리는 응답입니다. 대표적으로 200(성공), 201(작성됨)이 많이 사용됩니다.
(3) 3xx
- Redirection(다른 페이지로 이동)을 알리는 상태 코드입니다. 어떤 주소를 입력했을 때 다른 주소의 페이지로 넘어갈 때 해당 응답 코드가 사용됩니다. 대표적으로 301(영구 이동), 302(임시 이동) 코드가 존재합니다.
(4) 4XX
- 요청에 대한 오류를 알리는 응답입니다. 요청 자체에 오류가 존재할 때 사용되는 코드로써 대표적으로는 401(권한 없음), 403(금지됨), 404(찾을 수 없음, Not Found)가 존재합니다.
(5) 5XX
- 서버 자체에 오류가 존재할 때의 응답 코드입니다. 요청은 정상적으로 전송됐으나 서버에 오류가 발생했을 때 반환하는 응답 코드입니다. 해당 5XX번 오류가 발생하지 않도록 서버 사이드를 설계할 때 항상 주의해야 합니다.
- res.writeHead()로 직접 보내는 경우는 없고 예기치 못한 오류 발생 시 서버가 자체적으로 5XX번대 응답 코드를 반환합니다. 500(서버 내부 오류), 502(잘못된 게이트웨이), 503(서비스 사용 불가)이 자주 사용되는 응답 코드입니다.
1-6. 쿠키로 본인을 식별하는 방법
- cookie2.js
const http = require('http');
const fs = require('fs').promises;
const url = require('url');
const qs = require('querystring');
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번 포트에서 서버 대기 중입니다!');
});
- 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>
(1) 주소가 /login, /인 경우로 나뉘게 됩니다.
(2) /login 주소의 경우 Query-String으로 전달 받은 이름을 쿠키로 저장합니다.
(3) 그 외의 경우는 쿠키의 존재 여부를 판단하고 쿠키가 존재한다면 인사 멘트, 없다면 로그인 페이지로 리다이렉션됩니다.
<실행 결과>
- localhost:8084로 접속 시 확인할 수 있는 초기 화면입니다.
- 이름을 입력하고 로그인을 누릅니다.
- 로그인에 성공한 화면을 확인할 수 있습니다.
1-7. 쿠키의 다양한 옵션들
- Set-Cookie에서 다양한 옵션을 확인할 수 있습니다.
(1) 쿠키 이름 = 쿠키 값
- 기본적인 쿠키의 구조를 갖는 데이터입니다.
(2) Expires = 날짜
- 쿠키의 만료 기한으로 이 기한이 경과되면 쿠키가 삭제됩니다. 기본값은 클라이언트의 종료 시점까지입니다.
(3) Max-age = 초
- Expires와 유사하지만 날짜 대신 초를 입력할 수 있습니다. 해당 초가 경과하면 쿠키가 제거되며 Expires보다 우선시됩니다.
(4) Domain = 도메인 이름
- 쿠키가 전송될 도메인을 지정할 수 있고 기본값은 현재 도메인입니다.
(5) Path = URL
- 쿠키가 전송될 URL을 지정할 수 있고 기본값은 '/'입니다. 이 경우 모든 URL에서 쿠키를 전송할 수 있게 됩니다.
(6) Secure
- HTTPS일 경우에만 쿠키가 전송됩니다.
(7) HttpOnly
- 설정 시 자바스크립트 코드 레벨에서 쿠키에 접근할 수 없게 됩니다. 쿠키 조작을 방지하기 위해 설정하는 것이 좋습니다.
1-7. 세션(Session) 사용하기
(1) 세션이란, 일정 시간 동안 사용자로부터 들어오는 일련의 요청들을 하나의 상태로 보고, 그 상태를 일정하게 유지시키는 것을 말하며 사용자가 웹 서버에 접속해있는 상태를 하나의 단위로 보는 것을 세션이라고 합니다.
- 여기서 일정 시간은 사용자가 웹 브라우저를 통해 서버에 접속한 후 해당 브라우저의 연결을 종료할 때까지를 의미합니다.
(2) 쿠키의 정보는 노출되고 수정될 위험성이 항상 존재
- 주요 정보는 서버에서 직접 관리하도록 하고 클라이언트에게는 세션 키만 제공합니다.
- 서버에 세션 객체를 생성하고 randomInt(Key)를 만들어 속성명으로 사용합니다.
- 속성 값에 정보를 저장하고 randomInt를 클라이언트로 보냅니다.
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번 포트에서 서버 대기 중입니다!');
});
<실행 결과>
- 이전처럼 아이디를 입력하고 로그인을 시도한 후 개발자 도구에서 확인했을 때 세션 정보가 남아있는 것을 확인할 수 있습니다.
- 세션 생성 후 5분 뒤 세션 데이터가 사라진 것을 확인할 수 있습니다.
2. REST(Representational State Transfer) API와 Routing, HTTP Protocol
2-1. REST(Representational State Transfer)의 정의
(1) 우선 REST란 Representational State Transfer의 약자로 자원을 이름으로 구분하여 해당 자원의 상태를 주고받는 것을 의미합니다.
(2) HTTP URI(Uniform Resource Identifier)을 통해 자원(Resource)을 명시하고, HTTP 메소드(POST, GET, PUT, DELETE)를 통해 해당 자원에 대한 CRUD를 적용하는 것으로써 자원(Resource)의 표현(Representation)에 의한 상태 전달이라고 볼 수 있습니다.
(3) 웹 기반의 HTTP 통신을 위한 소프트웨어 개발 아키텍처의 패턴 중 하나입니다.
(4) REST는 기본적으로 웹의 기존 기술과 HTTP를 그대로 활용하기 때문에 웹의 장점을 극대화할 수 있는 아키텍처 패턴이라고 볼 수 있습니다.
2-2. REST API(Representational State Transfer API)
(1) 위에서 설명된 REST를 기반으로 서비스 API를 구현한 것을 의미합니다. RESTful API는 일반적으로 REST 아키텍처를 구현한 웹 서비스를 위해 나타내는 용어로 REST의 규칙이나 체계를 잘 지킨 웹 서비스에 대해 RESTful API라는 이름을 사용하기도 합니다.
(2) 서버에 요청을 보낼 때는 주소를 통해 요청의 내용을 표현할 수 있습니다.
- /index.html의 경우 index.html에 대한 정보를 보내달라는 뜻을 가지고 있습니다.
- 항상 HTML 형식의 문서를 요구하는 것은 아닙니다.
- 서버가 이해하기 수월한 주소를 사용하는 것이 일반적입니다.
(3) REST API는 서버의 자원을 정의하고 자원의 주소를 정의하여 자원을 주고받는 REST를 기반으로 웹 API를 구현한 것으로 /user이면 이름의 의미대로 사용자에 대한 정보를 /post면 게시글에 관련된 자원을 요청하는 것으로 생각할 수 있습니다.
(4) HTTP Request 메소드
- GET : 서버의 자원을 가져오려고 할 때 사용합니다.
- POST : 서버의 자원을 새롭게 등록(Update)하고자 할 때 사용합니다.
- PUT : 서버의 자원을 요청에 들어있는 자원으로 치환하고자 할 때 사용합니다.
- PATCH : 서버의 자원을 일부 수정하려고 할 때 사용합니다.
- DELETE : 서버의 자원을 삭제하고자 할 때 사용합니다.
2-3. HTTP Protocol
HTTP 메소드 | 주소 | 역할 |
GET | / | restFront.html 파일 제공 |
GET | /about | about.html 파일 제공 |
GET | /users | 사용자 목록 제공 |
GET | 기타 | 기타 정적 파일 제공 |
POST | /users | 사용자 등록 |
PUT | /users/사용자 ID | 해당 사용자의 ID 수정 |
DELETE | /users/사용자 ID | 해당 사용자의 ID 삭제 |
(1) 웹 기반의 서비스라면 클라이언트와 서버는 HTTP를 기반으로 통신
- iOS, Android, 웹 브라우저가 모두 같은 주소로 요청을 보낼 수 있습니다.
- 서버와 클라이언트의 분리
(2) RESTful API
- REST 체계를 사용해 웹 API를 구현한 것으로 REST의 체계를 잘 따른 API나 웹 서비스를 말하며, REST API를 사용한 주소 체계를 이용하는 서버입니다. GET /user는 사용자를 조회하는 요청, POST /user는 사용자를 등록하는 일반적인 요청입니다.
2-4. REST 요청 확인
(1) 개발자 도구를 열고 Network 탭에서 요청 내용을 실시간으로 확인할 수 있습니다.
(2) Name은 요청 주소, Method는 요청 메소드, Status는 HTTP 응답 코드를 나타냅니다.
(3) Protocol은 HTTP 프로토콜, Type은 요청에 대한 종류로 xhr 또는 AJAX 요청이 있습니다.
3. HTTPS & HTTP/2
3-1. HTTPS(HyperText Transfer Protocol Secure)
(1) HTTPS란, 웹 체계의 표준이 되는 HTTP의 보안이 강화된 웹 프로토콜로써 웹 서버에 SSL(Secure Sockets Layer) 암호화를 추가한 모듈입니다.
(2) 이를 통해 오고 가는 데이터를 암호화하기 때문에 중간에서 가로채더라도 데이터의 내용을 확인할 수 없습니다.
3-2. HTTPS 서버
(1) HTTP 서버를 HTTPS 서버로 전환
- 암호화를 위한 인증서 발급이 필요합니다.
(2) createServer() 메소드가 인자 두 개를 받습니다.
- 첫 번째 인자는 인증서와 관련된 옵션 객체입니다.
- pem, crt, key 등 인증서를 구입할 때 얻을 수 있는 파일을 넣습니다.
- 두 번째 인자는 서버에 대한 로직입니다.
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번 포트에서 서버 대기 중입니다!');
});
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번 포트에서 서버 대기 중입니다!');
});
3-3. HTTP/2
(1) HTTP/2 프로토콜의 경우 SSL 암호화를 도입한 HTTP 프로토콜의 두 번째 버전입니다.
(2) 요청 및 응답 방식, 웹의 속도적인 측면이 기존 HTTP/1.1보다 개선되었습니다.
3-4. HTTP/2가 적용된 서버
(1) http 모듈 대신 http2 모듈을 사용하였으며, createServer() 메소드에서 createSecureServer() 메소드로 변경되었습니다.
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번 포트에서 서버 대기 중입니다!');
});
4. Cluster
4-1. Cluster
- Cluster 모듈의 경우 싱글 스레드 기반의 Node가 CPU 코어를 모두 사용할 수 있게 해 주는 모듈입니다.
(1) 포트를 공유하는 노드 프로세스를 여러 개 생성하고 관리할 수 있습니다.
(2) 요청이 다량으로 들어왔을 때, 병렬로 실행된 서버의 개수만큼 요청을 분산시킬 수 있습니다.
(3) 서버를 병렬로 처리함으로써 서버의 부하를 줄일 수 있습니다.
(4) Cluster의 단점으로는 컴퓨터의 자원은 공유할 수 없다는 점이 존재합니다.
4-2. 서버 Clustering - Master process와 Worker process
(1) Master process는 CPU 코어 개수만큼 Worker process를 생성합니다.
(2) 요청이 들어오면 Worker process에 고르게 분배합니다.
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}번 워커 실행`);
}
<실행 결과>
- 서버의 코어 개수만큼 Worker process가 생성된 것을 확인할 수 있습니다.
5. Reference
Node.js 교과서(Node.js Textbook) - 저자 : 조현영
https://www.zerocho.com/book/1
- 학부에서 수강했던 전공 수업 내용을 정리하는 포스팅입니다.
- 내용 중에서 오타 또는 잘못된 내용이 있을 시 댓글로 남겨주시면 감사하겠습니다!
댓글