요청과 응답
클라이언트에서 서버로 요청(request)을 보내고, 서버에서는 요청의 내용을 읽고 처리한 뒤 클라이언트에 응답(respoense)을 보낸다.
따라서 서버에는 요청을 받는 부분과 응답을 보내는 부분이 있어야 한다. 요청과 응답은 이벤트 방식이라 생각하면 된다. 클라이언트로부터 요청이 왔을 때 어떤 작업을 수행할지 이벤트 리스너를 미리 등록해두어야 한다.
const http = require('http');
http.createServer( (req, res) => {
//응답 내용
});
http 서버가 있어야 웹 브라우저의 요청을 처리할 수 있다. createServer 메서드를 사용하여 콜백 함수에 요청에 대한 응답 내용을 적으면 요청이 들어올 때마다 콜백 함수가 실행된다.
createServer의 콜백 부분을 보면 req와 res 매개변수가 있다. req가 요청, res가 응답이다.
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(9000, () => {
console.log('9000 포트 서버 대기...');
});
9000 포트를 통해 요청이 들어오면 header정보를 적고 h1, p 태그를 적어 응답을 보낸다.
localhost는 현재 커퓨터의 내부 주소를 가리키니다. 외부에서는 접근할 수 없고 자신의 컴퓨터에서만 접근할 수 있어 서버 개발시 테스트용으로 많이 사용된다. (localhost = 127.0.0.1)
포트(Port)는 서버 내에서 프로세스를 구분하는 번호이다. 서버는 HTTP 요청을 대기하는 것 외에도 다양한 작업을 한다.
데이터베이스와 통신, FTP 요청 처리 등 많은 일을 한다. 따라서 서버는 프로세스에 포트를 다르게 할당하여 들어오는 요청을 구분한다.
80번 포트를 사용하면 주소에 포트를 생략할 수 있다. (https는 443)
서버를 종료하려면 ctrl+c를 입력하여 종료하면 된다.
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(9000);
server.on('listening', () => {
console.log('9000 포트 서버 대기...');
});
server.on('error', (err) => {
console.log(err);
});
listening 이벤트로도 처리가 가능하다. 또한 한 번에 여러 서버를 실행할 수도 있다.
html을 일일이 적는 것은 비효율적이다. 따라서 html 파일을 만들어 fs 모듈로 읽어서 전송할 수 있다.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Node.js 웹 서버</title>
</head>
<body>
<h1>Node.js 웹 서버</h1>
<p>Hello World</p>
</body>
</html>
const http = require('http');
const fs = require('fs').promises;
http.createServer(async(req, res) => {
try{
const data = await fs.readFile('./server2.html');
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8'});
res.end(data);
}catch(err){
console.error(err);
res.writeHead(500,{ 'Content-Type': 'text/plain; charset=utf-8'});
res.end(err.message);
}
})
.listen(9000, () => {
console.log('9000 포트 서버 대기...');
});
HTTP 상태 코드는 다음과 같은 의미가 있다.
- 2XX: 성공을 알리는 코드, 200(성공), 201(작성됨)
- 3XX: 리다이렉션을 알리는 코드, 301(영구 이동), 302(임시 이동)
- 4XX: 요청 오류를 알리는 코드, 400(잘못된 요청), 401(권한 없음), 403(금지됨), 404(찾을 수 없음)
- 5XX: 서버 오류를 알리는 코드, 500(내부 서버 오류), 502(불량 게이트웨이), 503(서비스 사용 불가)
REST와 라우팅
서버에 요청을 보낼 때는 주소를 통해 요청의 내용을 표현한다. 주소가 /index.html이면 서버의 index.html을 보내달라는 뜻이고, /about.html이면 about.html을 보내달라는 뜻이다.
항상 html만 요청할 필요는 없고 css나 js 또는 이미지 같은 파일을 요청할 수도 있다.
여기서 REST가 등장한다.
REST는 REpresentational State Transfer의 줄임말이다. 서버의 자원을 정의하고 자원에 대한 주소를 지정하는 방법을 가리킨다. 자원은 꼭 파일일 필요는 없고 서버가 행할 수 있는 것들을 통틀어서 의미한다.
REST API에는 많은 규칙이 있는데 모든 규칙을 지키는 것은 현실적으로 어렵다.
주소의 의미를 명확히 전달하기 위해 명사로 구성된다. /user이면 사용자 정보에 관련된 자원, /post라면 게시글에 관련된 자원을 요청하는 것이라고 추측할 수 있다. 또한 그것과 관련된 무슨 동작을 행하는지는 메서드로 구분할 수 있다.
- GET: 서버 자원을 가져오고자 할 때 사용
- POST: 서버에 자원들 새로 등록하고자 할 때 사용
- PUT: 서버의 자원을 변경하고자 할 때 사용
- PATCH: 서버 자원의 일부만 수정하고자 할 때 사용
- DELETE: 서버의 자원을 삭제하고자 할 때 사용
- OPTIONS: 요청을 하기 전에 통신 옵션을 설명하기 위해 사용
GET 메서드 같은 경우에는 브라우저에서 캐싱할 수도 있으므로 같은 주소로 GET 요청을 할 때 서버에서 가져오는 것이 아니라 캐시에서 가져올 수도 있다. 캐싱이 되면 성능이 좋아진다.
이제 REST를 사용한 주소 체계로 RESTful 한 웹 서버를 만들어 보자. REST를 따르는 서버를 'RESTful 하다'라고 표현한다.
a { color: blue; text-decoration: none;}
restFront.css
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>RESTful SERVER</title>
<link rel="stylesheet" href="./restFront.css" />
</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="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="./restFront.js"></script>
</body>
</html>
restFront.html
async function getUser(){
try{
const res = await axios.get('/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');
span.textContent = users[key];
const edit = document.createElement('button');
edit.textContent = '수정';
edit.addEventListener('click', async () => {
const name = prompt('바꿀 이름을 입력하세요');
if(!name){
return alert('이름을 반드시 입력하세요');
}
try{
await axios.put('/user/' + key, { name });
getUser();
}catch(err){
console.error(err);
}
});
const remove = document.createElement('button');
remove.textContent = '삭제';
remove.addEventListener('click', async () => {
try{
await axios.delete('/user/'+ key);
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호출
document.getElementById('form').addEventListener('submit', async (e) => { //제출 실행
e.preventDefault();
const name = e.target.username.value;
if(!name){
return alert('이름을 입력하세요');
}
try{
await axios.post('/user', { name });
getUser();
}catch(err){
console.error(err);
}
e.target.username.value = '';
});
restFront.js
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>RESTful SERVER</title>
<link rel="stylesheet" href="./restFront.css" />
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
<div>
<h2>소개 페이지</h2>
<p>사용자 이름을 등록하세요</p>
</div>
</body>
</html>
about.html
const http = require('http');
const fs = require('fs').promises;
http.createServer(async(req, res) => {
try{
console.log(req.method, req.url);
if(req.method ==='GET'){
if(req.url === '/'){
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'){
const data = await fs.readFile('./about.html');
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8'});
return res.end(data);
}
try{
const data = await fs.readFile(`.${req.url}`);
return res.end(data);
}catch(err){
}
}
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(9000, () => {
console.log('9000 포트 서버 대기...');
});
restServer.js
restServer.js를 보면 req.method로 HTTP 요청 메서드를 구분하고 있다. 메서드가 GET이면 다시 req.url로 요청 주소를 구분한다. /일 때는 restFront.html을 제공하고, 주소가 /about이면 about.html 파일을 제공한다.
이외의 경우에는 주소에 적힌 파일을 제공한다. 만약 존재하지 않는 파일을 요청했거나 GET메서드 요청이 아닌 경우에는 404 NOT FOUND 에러가 응답으로 전송된다.
나머지 메서드를 추가하여 완성하자
const http = require('http');
const fs = require('fs').promises;
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('./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('./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(users));
}
try{
const data = await fs.readFile(`.${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);
const { name } = JSON.parse(body);
const id = Date.now();
users[id] = name;
res.writeHead(201);
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);
users[key] = JSON.parse(body).name;
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];
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(9000, () => {
console.log('9000 포트 서버 대기...');
});
REST 방식으로 주소를 만들었으므로 주소와 메서드만 봐도 요청 내용을 유추할 수 있다.
Protocol은 통신 프로토콜을, Type은 요청의 종류를 의미한다. xhr은 AJAX 요청이다.