반응형
1. 요청과 응답 이해하기
- 클라이언트와 서버는 요청과 응답을 주고 받는다
- 고로 이벤트 리스너를 가진 노드 서버를 만들어 확인해보자
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번 포트에서 서버 대기 중입니다!');
});
- req(uest): 요청에 관한 정보
- res(ponse): 응답에 관한 정보
- res.writeHead: 응답에 대한 정보를 기록하는 메서드 (헤더에 기록)
- res.write: 데이터를 클라이언트로 보내는 메서드 (본문에 기록)
- res.end: 응답을 종료하는 메서드
- listen: 포트와 콜백 함수를 보내놓고 요청을 대기함
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);
});
- 이벤트 리스너
- **server.on()**은 이벤트 리스너를 설정하기 위해 사용하는 메서드이다 - 특정 이벤트가 발생할 때 해당 이벤트에 등록된 리스너 함수가 호출된다.
- listening과 error 둘 다 이벤트가 발생할때 콜백 함수를 보내 응답한다
- res.write와 res.end를 일일이 적는 것은 비효율적이므로 미리 HTML 파일을 만들자!
경로가 ./~ 일때 왜 오류가 날까..??
지금까지는 모든 요청에 대해 한 가치 요청밖에 할 수 없었다. 다음 절에서 요청별로 다른 응답을 하는 방법을 알아보자~
2. REST와 라우팅 사용하기
- 서버에 요청을 보낼땐 주소를 통해 요청의 내용을 표현한다. 예를 들어 주소가 /index.html 이면 서버의 index.html을 보내달라는 뜻이다.
- REST: 서버의 자원을 정의하고 자원에 대한 주소를 지정하는 방법을 가리킨다.
- GET: 서버 자원을 가져오고자 할 때 사용. 본문에 데이터를 넣지 않는다
- POST: 서버에 자원을 새로 등록할 때 사용. 새로 등록할 데이터를 넣어 보낸다
- PUT: 서버의 자원을 요청에 들어있는 자원으로 치환할 때 사용. 본문에 수정할 데이터를 넣어 보낸다
- PATCH: 서버 자원의 일부만 수정 할 때 사용. 본문에 수정할 데이터를 넣어 보낸다
- DELETE: 서버의 자원을 삭제할 때 사용. 본문에 데이터를 넣지 않는다
- OPTIONS: 요청을 하기전 통신 옵션을 설명하기 위해 사용
- Ex) GET 메서드의 /user 주소로 요청을 보내면 유저의 정보를 가져오는 것임
- 코드를 작성하기 전 주소 구조를 미리 정리해둔 후 시작하면 더욱 체계적이고 수월하다
- 무작정 시작해서 망한 경험이 있어 더욱 더 와닿는다ㅎ
- 우리는 지금 백엔드 공부 중이니 핵심인 restServer.js 를 가지고 왔다
- 아래의 코드가 복잡해보이지만 이해해보려고 노력했다ㅎ
const http = require('http');
const fs = require('fs').promises;
const path = require('path');
const users = {}; // 데이터 저장용
http.createServer(async (req, res) => {
try {
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));
}
// /도 /about도 /users도 아니면
try {
const data = await fs.readFile(path.join(__dirname, 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('등록 성공');
});
}
} 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(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': 'text/plain; 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, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end(err.message);
}
})
.listen(8082, () => {
console.log('8082번 포트에서 서버 대기 중입니다');
});
- res.end 앞에 return을 붙여줘야 함수가 종료된다. end라고 함수가 끝났다고 오해ㄴㄴ
- method로 HTTP 요청 메서드를 구분하고 있고, if 문을 통해 요청한 메소드와 일치하면 실행시킨다. 만약에 존재하지 않는 파일을 요청하면 404 NOT FOUND 에러가 응답으로 전송된다.
- 개발자 도구(f12)의 네트워크 창을 통해 요청이 어떻게 되는지 알아봤다!
3. 쿠키와 세션 이해하기
- 클라이언트에서 보내는 요청에는 한 가지 큰 단점이있다. 누가 요청을 보내는지 모른다
- 쿠키: 웹에 저장되는 작은 데이터 조각. 응답과 요청 시에 쿠키를 함께 전송함 (헤더에 담겨 전송)
- 세션: 서버 측에서 사용자 데이터를 저장하는 방법. 서버가 고유한 세션 생성하고, 쿠키로 저장
- → 쿠키와 세션을 이용해 로그인 구현!
- 즉, 서버는 미리 클라이언트에 요청자를 추청할 만한 정보를 쿠키로 만들어 보내고, 다음부터는 클라이언트로부터 쿠키를 받아 요청자를 파악한다. (쿠키가 사용자가 누구인지 추적하고 있다는 것)
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 path = require('path');
//1
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);
//2
// 주소가 /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();
//3
// 주소가 /이면서 name이라는 쿠키가 있는 경우
} else if (cookies.name) {
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end(`${cookies.name}님 안녕하세요`);
} else { // 주소가 /이면서 name이라는 쿠키가 없는 경우
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번 포트에서 서버 대기 중입니다!');
});
- 쿠키명=쿠키값: 기본적인 쿠키 값. mycookie = test 와 같은 형식으로 설정
- expires=날짜: 만료 기한. 기한이 지나면 쿠키가 제거되고 기본은 클라이언트가 종료될 때 까지
- parseCookies 함수를 사용해 문자열인 쿠키를 js 객체 형식으로 바꾼다
- 로그인을 위해 GET /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번 포트에서 서버 대기 중입니다!');
});
- 위 코드를 이용해 세션 방식을 사용해 세션 아이디로 소통할 수 있다.
4. https와 http2
- http vs https
- http: 일반 텍스트이므로, 권한이 없는 당사자가 인터넷을 통해 쉽게 액세스하고 읽을 수 있음
- https: 모든 데이터를 암호화된 형태로 전송 (SSL 암호화)
- GET, POST 요청할 때 오가는 데이터를 암호화해 가로채도 내용을 확인할 수 없게 함
- http1 vs http2
- 노드의 http2 모듈은 암호화와 더불어 최신 프로토콜을 사용할 수 있게 함
5. cluster
- cluster 모듈은 싱글 프로세스로 동작하는 노드가 cpu코어를 모두 사용할 수 있게 해줌
- 요청이 많이 들어왔을 때 병렬로 실행된 서버의 개수만큼 요청이 분산 → 서버에 무리가 덜 간다
- 클러스터엔 마스터 프로세스와 워커 프로세스가 있다.
- 마스터 프로세스: cpu 개수만큼 워커 프로세스를 만들고 대기, 요청이 들어오면 워커 프로세스에 분배
- 워커 프로세스: 실적적인 일을 하는 프로세스
- 코드를 이용해 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);
});
} 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}(번 워커 실행`);
}
6. 함께 보면 좋은 자료
- http 모듈 소개: https://nodejs.org/dist/latest-v18.x/docs/api/http.html
- 쿠키 설명: https://developer.mozilla.org/ko/docs/Web/HTTP/Cookies
- 세션 설명: https://developer.mozilla.org/ko/docs/Web/HTTP/Session
- https 모듈 소개: https://nodejs.org/dist/latest-v18.x/docs/api/https.html
- http2 모듈 소개: https://nodejs.org/dist/latest-v18.x/docs/api/http2.html
- cluster 모듈 소개: https://nodejs.org/dist/latest-v18.x/docs/api/cluster.html
반응형
'WINK-(Web & App) > Express.js (Node.js) 스터디' 카테고리의 다른 글
[2024-2 Node.js 스터디] 김민재 #3주차 - 익스프레스 웹 서버 만들기 (1) | 2024.11.04 |
---|---|
[2024-2 Node.js 스터디] 류상우 #2주차 (2) | 2024.10.14 |
[2024-2 Node.js 스터디] 류상우 #1주차 (0) | 2024.10.07 |
[2024-2 Node.js 스터디] 김민재 #1주차 (0) | 2024.10.06 |
[2024 여름방학 Node.js 스터디] 백채린 #4주차 5~6장 (0) | 2024.08.15 |