목표
http 모듈로 서버를 직접 만들어 보면서
실제 서버 동작에 필요한 쿠키, 세션처리, 요청 주소별 라우팅 방법에 대해 알아본다.
1. 요청과 응답 이해하기
1.1 클라이언트와 서버의 관계
서버는 클라이언트가 있기에 동작한다.
request (client->server) ➡ response (server->client)
(1) 클라이언트에서 서버로 요청(request)을 보내고,
(2) 서버에서는 클라이언트의 요청 내용을 읽고 처리한 후 클라이언트에 응답(response)을 보낸다.
1.2 이벤트 리스너를 가진 노드 서버 만들기
서버에는 요청을 받는 부분과 응답을 보내는 부분이 있어야 한다.
요청과 응답은 이벤트 방식이라고 생각하면 된다. 클라이언트로부터 요청이 오면 어떤 작업을 수행할지 이벤트 리스너를 미리 등록해야 한다.
코드
// createServer.js
const http = require('http');
http.createServer((req, res) => {
// 요청이 왔을 떄 어떤 작업을 수행할지 작성한다.
});
http 모듈
http 서버가 있어야 웹 브라우저의 요청을 처리할 수 있다.
http 모듈을 사용하면 이 http 서버를 만들 수 있다.
http.createServer
http 모듈에는 createServer 메소드가 있다.
인수로 요청에 대한 콜백함수를 넣을 수 있고, 요청이 들어올 때마다 매번 콜백 함수가 실행된다.
따라서 이 콜백 함수에 응답을 작성하면 된다.
- req: request. 요청에 관한 정보를 담고 있다.
- res: response. 응답에 관한 정보를 담고 있다.
아직은 콜백 함수에 코드를 작성하지 않았으므로 코드를 실행해도 아무 일이 일어나지 않는다.
1.3 서버 실행하기
응답을 보내는 부분과 서버에 연결하는 부분을 추가하여 서버를 실행하자.
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번 포트에서 서버 대기 중입니다!');
});
$ node server1
8080번 포트에서 서버 대기 중입니다!
http.listen
http.createServer 메서드 뒤에 http.listen 메서드를 작성한다.
http.listen(포트번호, 콜백함수);
클라이언트에 공개할 포트 번호와 포트 연결 완료 후 실행될 콜백 함수를 넣는다.
그러면 이제 파일을 실행하면 서버는 8080 포트에서 요청이 오기를 기다린다.
res.writeHead
응답에 대한 정보를 기록하는 메소드. 이 정보가 기록되는 부분을 헤더(Header)라고 한다.
res.writeHead(StatusCode, object)
- StatusCode : HTTP 상태 코드
HTTP 상태 코드
브라우저에서 보낸 요청이 서버에게 잘 전달되었는지 아닌지 알려주는 상태 코드이다.
다음은 대표적인 상태 코드이다.
- 2XX : 성공을 알리는 상태 코드. 대표적으로 200(성공), 201(작성됨)이 많이 사용된다.
- 3XX : 리다이렉션(다른 페이지로 이동)을 알리는 상태 코드. 어떤 주소를 입력했는데 다른 주소의 페이지로 넘어갈 때 이 코드가 사용된다. 대표적으로 301(영구 이동), 302(임시 이동)이 있다. 304(수정되지 않음)는 요청의 응답으로 캐시를 사용했다는 뜻이다.
- 4XX : 요청 자체에 오류가 있음을 알리는 상태 코드. 대표적으로 400(잘못된 요청), 401(권한 없음), 403(금지됨), 404(찾을 수가 없음)가 있다.
- 5XX : 요청은 제대로 왔지만 서버에 오류가 있음을 알리는 상태 코드. 이 오류가 뜨지 않도록 주의하여 프로그래밍해야 한다. 이 오류를 res.writeHead로 클라이언트에 직접 보내는 경우는 거의 없고, 예기치 못한 에러 발생 시 서버가 알아서 5XX대 코드를 보낸다. 500(내부 서버 오류), 502(불량 게이트 웨이), 503(서비스를 사용할 수 없음)이 자주 사용된다.
- object : 'Content-Type', 'Set-Cookie', 'Location', ... 등 많은 객체가 들어갈 수 있다. 예시에서 사용된 Content-Type에 대해서만 자세히 알아보자.
Content-Type | explain |
text/plain | 기본적인 텍스트(일반 문자열) |
text/html | HTML |
text/css | CSS |
text/xml | XML |
image/jpeg | JPG/JPEG |
image/png | PNG |
video/mpeg | MPEG 비디오 파일 |
audio/mp3 | MP3 |
위의 코드를 다시 살펴보자.
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
- 첫 번째 인수 : 성공적인 요청임을 의미하는 HTTP 상태 코드
- 두 번째 인수 : 응답에 대한 정보를 보내는데 콘텐츠의 형식이 HTML임을 알리고, 한글 표시를 위해 charset을 utf-8로 지정함
res.write
클라이언트에게 보낼 데이터를 작성한다. 데이터가 기록되는 부분을 본문(Body)이라고 한다.
res.write('<h1>Hello Node!</h1>');
현재는 HTML 모양의 문자열을 보냈지만 버퍼를 보낼 수도 있다.
또한 여러 번 호출하여 데이터를 여러 개 보낼 수도 있다.
헤더(Header) | 바디(Body) |
응답에 대한 정보를 기록하는 부분 | 클라이언트에게 보낼 데이터가 기록되는 부분 |
writeHead 메소드에서 작성됨 | write, end 메소드에서 작성됨 |
res.end
응답을 종료하는 메소드.
만약 인수가 있다면 그 데이터도 클라이언트로 보내고 응답을 종료한다.
따라서 위의 예제는 res.write에서 <h1>Hello Node!</h1> 문자열을, res.end에서 <p>Hello Server!</p>문자열을 클라이언트로 보내고 응답을 종료한다.
브라우저는 이 두 개의 응답 내용을 받아서 렌더링한다.
localhost
현재 컴퓨터의 내부 주소이다. 외부에서 접근할 수 없고 자신의 컴퓨터에서만 접근할 수 있어서 서버 개발 시 테스트용으로 많이 사용된다.
localhost 대신 127.0.01을 주소로 사용해도 같다. 이런 숫자 주소를 IP(Internet Protocol)이라고 한다.
포트
서버 내에서 프로세스를 구분하는 번호이다. 서버는 프로세스에 포트를 다르게 할당하여 들어오는 요청을 구분한다.
유명한 포트 번호로는 21(FTP), 80(HTTP), 443(HTTPS), 3306(MYSQL)이 있다.
포트 번호는 IP 주소 뒤에 콜론(:)과 함께 붙여 사용한다.
위의 예제에서는 임의의 포트 번호 8080에 노드 서버(프로세스)를 연결했다. 따라서 http://localhost:8080으로 접근해야 한다.
하지만 네이버(naver.com)와 같은 사이트들은 포트 번호를 따로 표시하지 않는다.
왜냐하면 80번 포트(HTTP)나 443번 포트(HTTPS)를 사용하면 주소에서 포트를 생략할 수 있기 때문이다.
naver.com:80이나 naver.com:443으로 요청해도 네이버 홈페이지에 동일하게 접속된다.
포트 충돌
앞으로 서버를 돌릴 때 80번 포트와 같이 유명한 포트 번호는 사용하지 않을 것이다. 왜냐하면 충돌을 방지하기 위해서다.
일반적으로 컴퓨터에서 80번 포트는 이미 다른 서비스가 사용하고 있을 확률이 크다.
보통 포트 하나에 서비스 하나만 사용할 수 있으므로 다른 서비스가 사용하고 있는 포트를 사용하려고 하면 에러가 발생한다.
❗ Error: listen EADDRINUSE :::포트번호
에러가 발생한 경우에는 그 서비스를 종료하거나 노드의 포트를 다른 번호로 바꾸면 된다.
따라서 예제를 실행할 때는 다른 포트 번호를 사용하고, 실제 배포할 때는 80번이나 443번 포트를 사용한다.
listening / error 이벤트 리스너
❗ 이벤트 리스너: .on('이벤트명', 콜백함수)
혹은 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>');
})
// .listen(80, () => { // 서버 연결
//
// });
server.listen(8080);
server.on('listening', () => {
console.log('8080번 포트에서 서버 대기 중입니다!');
});
server.on('error', (error) => {
console.error(error);
});
여러 서버 실행하기
또한 원하는 만큼 createServer를 호출하여 한 번에 여러 서버를 실행할 수도 있다.
하지만 실행하는 서버의 포트 번호는 달라야 한다. 포트 번호가 같으면 EADDRINUSE 에러가 발생한다.
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번 포트에서 서버 대기 중입니다!');
});
1.4 HTML 파일을 응답으로 보내기
일일이 HTML 문자열을 작성하는 것은 비효율적이므로 미리 HTML 파일을 미리 만들어 놓자.
fs 모듈을 사용해서 HTML 파일을 읽고 전송하면 된다.
const http = require('http');
const fs = require('fs').promises;
http.createServer(async (req, res) => {
try {
const data = await fs.readFile('./04_server3.js');
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8'});
res.end(data);
} catch (err) {
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8'});
res.end(err.message);
}
})
.listen(8080, () => {
console.log('8080번 포트에서 서버 대기 중입니다!');
});
프로미스 형식으로 작성하였으므로 fs 모듈에 .promises를 붙여준다.
에러가 발생했을 때, 에러 메시지는 일반 문자열이므로 콘텐츠 형식으로 text/plain을 작성한다.
fs 모듈
프로젝트 내의 파일을 읽는다.
const data = await fs.readFile('./04_server3.js');
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8'});
res.end(data);
fs 모듈로 HTML 파일을 읽고, data 변수에 저장된 버퍼를 그대로 클라이언트로 가져오면 된다.
이전에는 문자열로 보냈지만, 이렇게 버퍼를 보낼 수도 있다.
2. REST와 라우팅 사용하기
클라이언트가 서버에 요청을 보낼 때는 주소를 통해 요청의 내용을 표현한다.
예로 들어서 주소가 /index.html이면 서버의 index.html을 보내달라는 뜻이라던지, /about.html이라면 about.html을 보내달라는 것처럼 말이다. 이처럼 파일을 보내달라고 할 수도 있고 또는 특정 동작을 행하는 것을 요청할 수도 있다.
서버가 다양한 요청을 잘 이해하기 위해선 서버가 이해하기 쉬운 주소를 사용하는 것이 좋다. 여기서 REST가 등장한다.
2.1 REST
Representational State Transfer. 서버의 자원을 정의하고 자원에 대한 주소를 지정하는 방법을 말한다.
쉽게 이해하자면, 서버의 모든 행동들을 주소로 쉽게 나타내기 위해 만든 규칙이라고 생각하면 된다.
REST에는 (1)주소, (2)HTTP 요청 메소드 가 있다.
주소
주소는 의미를 명확하게 전달하기 위해서 명사로 구성된다. 예로 들면 /user이면 사용자 정보에 관련된 자원을 요청하는 것을, /post면 게시글에 관련된 자원을 요청하는 것이라고 추측할 수 있다.
HTTP 요청 메소드
REST에서는 주소 외에도 HTTP 요청 메소드라는 것도 사용한다. 폼 데이터를 전송할 때 지정한 GET, POST 메소드가 바로 여기서 말하는 HTTP 요청 메소드이다.
종류
- GET(읽기): 서버 자원을 가져올 때 사용한다. 요청의 본문에 데이터를 넣지 않고 요청 주소만 넣는다. 만약 데이터를 서버로 보내야 한다면 쿼리스트링을 사용한다.
- POST(생성): 서버에 자원을 새로 등록할 때 사용한다. 요청의 본문에 새로 등록할 데이터를 넣는다.
- PUT(덮어쓰기): 서버의 자원을 요청에 들어 있는 자원으로 치환할 때 사용한다. 요청의 본문에 치환할 데이터를 넣는다.
- PATCH(수정하기): 서버 자원의 일부만 수정할 때 사용한다. 요청의 본문에 일부 수정할 데이터를 넣어 보낸다.
- DELETE(삭제): 서버의 자원을 삭제할 때 사용한다. 요청의 본문에 데이터를 넣지 않는다.
- OPTIONS: 요청하기 전에 통신 옵션을 설명하기 위해 사용한다. 12장에서 자주 사용될 것이다.
주로 GET과 POST가 대표적으로 많이 쓰인다.
하나의 주소에 여러 개의 요청 메소드를 가질 수 있다.
예로 들어서 /user 주소 여러 요청 메소드를 보낸다고 하자. GET 메소드를 보내면 사용자 정보를 가져오는 요청이고, POST 메소드를 보내면 새로운 사용자를 등록하려 한다는 것이다.
만약 로그인처럼 위의 메솝드로 표현하기 애매한 동작이 있다면 그냥 POST를 사용하면 된다.
REST의 장점
- 주소와 메소드만 보고 요청의 내용을 알아볼 수 있다.
- GET 메소드는 브라우저에서 캐싱(기억)할 수도 있으므로 같은 주소로 GET 요청을 할 때 서버에서 가져오는 것이 아니라 캐시에서 가져올 수도 있다. 이렇게 캐싱이 되면 성능이 좋아진다.
- HTTP 통신을 하면 iOS, 안드로이드, 웹 등 어떤 클라이언트든지 모두 같은 주소로 요청을 보낼 수 있다. 즉, 서버와 클라이언트가 분리되어 있다. 이렇게 되면 추후에 서버를 확장할 때 클라이언트에 구애되지 않아서 좋다.
2.2 RESTful한 웹 서버 만들기
이제 REST를 사용한 주소 체계로 웹 서버를 만들어보자.
REST를 따르는 서버를 'RESTful하다'고 표현한다.
Step.1 주소 설계하기
코드를 작성하기 전에 대략적인 주소를 먼저 설계한다. 주소 구조를 정리하고 코딩을 시작하면 더 체계적으로 프로그래밍할 수 있다.
HTTP 요청 메소드 | 주소 | 역할 |
GET | / | restFront.html 파일 제공 |
/about | about.html 파일 제공 | |
/users | 사용자 목록 제공 | |
기타 | 기타 정적 파일 제공 | |
POST | /user | 사용자 등록 |
PUT | /user/사용자id | 해당 id의 사용자 수정 |
DELETE | /user/사용자id | 해당 id의 사용자 제거 |
Step.2 파일 생성하기
restFront.css, restFront.html, restFront.js, about.html, restServer.js 파일을 만들고 다음과 같이 코드를 작성한다.
restFront.css
a {
color: blue;
text-decoration: none;
}
restFront.html (Home 버튼을 눌렀을 때 띄우는 페이지)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RESTful SERVER</title>
<link rel="stylesheet" href="./restFront.css">
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</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="./restFront.js"></script>
</body>
</html>
restFront.js (Home 화면에서 아래에 위치한 리스트를 띄우는 클라이언트 코드)
async function getUser() { // 로딩 시 사용자 정보를 가져오는 함수
try {
const res = await axios.get('/users'); // 요청 주소 /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');
const edit = document.createElement('button');
span.textContext = users[key];
edit.textContent = '수정';
edit.addEventListener('click', async () => { // 수정 버튼 클릭 이벤트
const name = prompt('바꿀 이름을 입력하세요.'); // 사용자의 입력값은 result에 저장된다.
if (!name) {
return alert('이름을 반드시 입력하셔야 합니다.');
}
try {
await axios.put('/user/' + key, { name }); // 입력되면 /user/유저명 주소에 결과값을 넣는 것을 요청한다.
getUser(); // 수정 후 업데이트 된 목록을 다시 가져온다.
} catch (err) {
console.error(err);
}
});
const remove = document.createElement('button');
remove.textConrtent = '삭제';
remove.addEventListener('click', async () => { //삭제 버튼 클릭 이벤트
try {
await axios.delete('/user/' + key); // 해당 username을 /user/유저명 주소에서 삭제하는 것을 요청한다.
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);
}
}
window.onload = getuser; // 화면 로딩 시 getuser 호출함
// 폼 제출(submit) 시 실행 이벤트
document.getElementById('form').addEventListener('submit', async (e) => {
e.preventDefault(); // 이벤트의 기본행동 취소
const name = e.target.username.value;
if (!name) { // 사용자가 폼에 username을 입력하지 않고 제출한 경우
return alert('이름을 입력하세요.');
}
try {
await axios.post('/user', { name }); // 입력한 username을 /user 주소에 데이털 추가를 요청한다.
getUser(); // 추가 후 업데이트 된 목록을 다시 가져온다.
} catch (err) {
console.error(err);
}
e.target.username.value = ''; // 입력하면 username 입력칸을 비운다.
});
about.html (about 버튼을 눌렀을 때 띄우는 페이지)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Restful SERVER</title>
</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((req, res) => {
try {
console.log(req.method, req.url);
if(req.method === 'GET') { // 1. 가져오기
if(req.url === '/') { // Home 화면
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') { // 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': 'text/plain; charset=utf-8'});
return res.end(JSON.stringify(uesrs));
}
// 주소가 /도 /about도 아니면
try {
const data = await fs.readFile(`.${req.url}`);
return res.end(data);
} catch (err) {
// 주소에 해당하는 라우트를 못 찾았다는 404 Not Found error 발생
}
} else if (req.method === 'POST') { // 2. 생성(등록)
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);
res.end('등록 성공');
});
}
} else if (req.method === 'PUT') { // 3. 수정
if(req.url.startsWith('/user/')){
const key = req.url.split('/')[2]; // localhost:8002/user/date
let body = '';
req.on('data', (data) => {
body += data;
});
return req.on('end', () => {
console.log('PUT 본문(Body):', body);
users[key] = JSON.parse(body).name;
return res.end(JSON.stringify(users));
});
}
} else if (req.method === 'DELETE') { // 4. 삭제
if(req.url.startsWith('/user/')){
const key = req.url.split('/')[2]; // localhost:8002/user/date
delete users[key];
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번 포트에서 서버 대기 중입니다.');
})
(+) Object.keys()
해당 객체에 담겨있는 모든 key 요소들이 담긴 배열로 반환한다.
const object1 = {
a: 'somestring',
b: 42,
c: false
};
console.log(Object.keys(object1));
// Array ["a", 'b", "c"]
(+) window.prompt()
사용자가 텍스트를 입력할 수 있도록 안내하는 선택적 메시지를 갖고 있는 대화 상자를 띄운다. DOM의 window 객체의 메소드이므로 앞의 window는 생략하여 사용해도 무방하다.
result = window.prompt(message, default);
매개변수
- message(optional) : 사용자에게 보여줄 문자열. 프롬프트 창에 표시할 메시지가 없다면 생략해도 좋다.
- default(optional) : 텍스트 입력 필드에 기본으로 채워 넣을 문자열. IE7, IE8에서는 이 값을 지정하지 않으면 문자열 "undefined"가 지정되니 유의하자.
반환값
- 사용자가 입력한 문자열, 혹은 null
예제
let sign = prompt('당신의 별자리는 무엇입니까?');
if (sign.toLowerCase() === '전갈자리') {
alert('와! 저도 전갈자리예요!');
}
// prompt 기능을 쓰는 다양한 방법
sign = window.prompt(); // 1. 빈 대화 상자를 연다.
sign = prompt(); // 2. 위와 동일하게 빈 대화 상자를 연다. window 객체 생략 가능.
sign = window.prompt('A'); // 3. 안내 문구에 A가 보이는 창을 띄운다.
sign = window.prompt('A', 'B'); // 4. 안내 문구에 A가 보이고 기본적으로 입력된 값은 B로 한다.
(+) request.on : req이벤트
request.on(event, listener);
이벤트 핸들러 종류
- 'data' : 요청의 body(데이터)를 stream 형식으로 받는다.
- 'end' : 요청의 body를 다 받은 후에 실행된다.
➡ data 이벤트를 통해 받는 데이터는 문자열이므로 JSON으로 만드는 JSON.parse과정이 필요하다.
Step 3. 서버 실행하기
$ node restServer
8082번 포트에서 서버 대기 중입니다.
웹 사이트에 들어가서 등록, 수정, 삭제를 해보면서 서버에 어떤 요청을 보내는지 확인해보자.
특히 Method 부분을 보면 어떤 http 요청 메소드를 받았는지 볼 수 있다.
1) Home 클릭 시
2) About 클릭 시
3) 서버에 보내는 요청들 확인하기
REST 방식으로 주소를 만들었으므로 주소와 메서드만 봐도 요청 내용을 유추할 수 있다.
Name은 요청 주소, Method는 요청 메서드, Status는 HTTP 응답 코드, Protocol은 통신 프로토콜, Type은 요청의 종류를 의미한다. 여기서 xhr(Xml Http Request)은 AJAX 요청이다.
POST user는 사용자를 등록하는 요청임을 알 수 있다.
PUT은 사용자 이름을 수정하는 요청이며,
DELETE 15055505861271618393127075 은 해당 키(Date.now())를 가진 사용자를 제거하는 요청임을 알 수 있다.
그리고 등록, 수정, 삭제가 발생할 때마다 GET /users로 갱신된 사용자 정보를 가져온다.
이렇게 REST 서버를 만들어보았다. 하지만 이는 데이터가 메모리에 저장되기 때문에 서버를 종료하면 데이터가 소실된다.
데이터를 영구적으로 저장하려면 데이터베이스를 사용해야 한다.
3. 쿠키와 세션 이해하기
로그인 기능을 구현하기 위해서 쿠키와 세션에 대해 알아야 한다.
우리가 웹 사이트에 방문하여 로그인을 할 때 내부적으로 쿠키와 세션을 사용하고 있다.
로그인 후 새로고침(=새로운 요청)을 해도 로그아웃이 되지 않는다는 것이 그 증거이다. 클라이언트가 서버에 우리가 누구인지 지속적으로 알려주고 있기 때문이다.
3.1 쿠키
- 유효 기간이 있다.
- 단순한 '키-값'의 쌍이다. (ex. name=nno3onn)
쿠키에 대한 이해
- 서버는 요청에 대한 응답을 할 때 '쿠키'라는 것을 같이 전송한다.
- 서버로부터 쿠키가 오면 웹 브라우저는 쿠키를 저장해두었다가 다음에 요청할 때마다 쿠키를 동봉해서 같이 보낸다.
- 서버는 요청에 들어 있는 쿠키를 읽어서 사용자가 누구인지 파악한다.
쿠키가 있다면 브라우저는 자동으로 동봉해서 보내준다. 따라서 쿠키를 따로 처리할 필요가 없다.
서버에서 브라우저로 쿠키를 보낼 때만 코드를 직접 작성하여 처리하면 된다.
따라서 서버는 미리 클라이언트에 요청자를 추정할 만한 정보를 쿠키를 만들어서 응답에 동봉하여 웹 브라우저에게 보내고,
그 다음부터는 클라이언트로부터 쿠키를 받아 요청자를 파악한다.
즉, 우리가 누구인지 쿠키가 추적하고 있는 것이다.
개인정보 유출 방지를 위해 쿠키를 주기적으로 지우라고 권고하는 것이 바로 이 이유 때문이다.
쿠키 전송 및 저장하기
- 쿠키는 요청의 헤더(Cookie)에 담겨 전송된다.
- 브라우저는 서버 응답의 헤더(Set-Cookie)에 따라 쿠키를 저장한다.
서버에서 직접 쿠키를 만들어서 요청자의 브라우저에 넣어보자.
// cookie.js
const http = require('http');
http.createServer((req, res) => {
console.log(req.url, req.headers.cookie); // (1)
res.writeHead(200, { // (2)
'Set-Cookie': 'mycookie=test'
});
res.end('Hello Cookie');
})
.listen(8083, () => {
console.log('8083번 포트에서 서버 대기 중입니다!');
});
- 콘솔 출력
$ node cookie
8083번 포트에서 서버 대기 중입니다!
쿠키는 name=nno3onn;year=1997 처럼 문자열 형식이다. 쿠키 간에는 세미콜론(;)으로 구분한다.
아까 (1) 쿠키는 요청의 헤더(req.headers)에 담겨 전송되고,
(2) 브라우저는 응답의 헤더(Set-Cookie)에 따라 쿠키를 저장한다고 했다.
이 사실을 생각하면서 위 코드를 살펴보자.
(1) createServer 메소드의 콜백에서는 req 객체에 담겨 있는 쿠키(=req.headers.cookie)를 가져온다.
브라우저가 처음 요청을 하는 경우에는 헤더에 쿠키가 없다. 따라서 req.headers.cookie를 콘솔하면 undefined라고 뜬다.
(2) 브라우저의 요청에 서버가 응답할 때, 응답(res) 객체의 헤더에 쿠키를 기록하기 위해 res.writeHead 메소드를 사용한다.
Set-Cookie는 브라우저에게 다음과 같은 값의 쿠키를 저장하라는 의미이다.
그리고 첫 번째 요청에 따른 응답을 받은 브라우저는 mycookie=test라는 쿠키를 저장한다.
localhost:8083에 접속하여 콘솔 출력을 확인해보자.
요청에 따른 응답으로(=res.end()) 인해 화면에는 'Hello Cookie'가 뜨고, 콘솔은 아래와 같이 출력된다.
/ undefined
/favicon.ico { mycookie: 'test' }
분명 요청은 한 번만 보냈는데, 두 개가 기록되어 있다.
첫 번째 요청(/)에서는 이전에 예상했듯이 쿠키에 대한 정보가 없다고 뜨고,
두 번째 요청(/favicon.ico)에서는 { mycookie: 'test' }가 기록되었다.
여기서 파비콘은 요청한 적이 없는데 왜 콘솔로 뜨는 걸까?
HTML에 파비콘에 대한 정보를 넣지 않으면 브라우저가 파비콘이 뭔지 유추할 수가 없어서
서버에 파비콘 정보에 대한 요청을 보낸다.
따라서 위의 경우에도 파비콘 정보를 추가하지 않았으므로 브라우저가 추가로 /favicon.ico를 서버에 요청한 것이다.
이렇게 한 번의 요청을 했지만 실제로 두 번의 요청이 이뤄졌으므로 서버에 제대로 쿠키가 심어진다.
쿠키로 사용자를 식별하기
하지만 아직은 단순한 쿠키만 심었을 뿐, 그 쿠키가 나인지 식별하지 못한다.
따라서 이제 쿠키로 사용자를 식별하는 방법을 알아보자.
'Back-End > Node.js' 카테고리의 다른 글
시퀄라이즈(Sequelize) (0) | 2021.04.12 |
---|---|
[Node.js 교과서] 6. 익스프레스 웹 서버 만들기 (0) | 2021.04.12 |
[Node.js 교과서] 3. 노드 기능 알아보기 (0) | 2021.04.12 |
AJAX (0) | 2021.04.11 |
[Node.js 교과서] 2. 알아두어야 할 자바스크립트 (0) | 2021.04.08 |