서버에 요청을 보낼 때는 주소를 통해 요청의 내용을 표현합니다.
주소가 /index.html 이면 서버의 index.html을 보내달라는 뜻이고, /about.html 이면 about.html을 보내달라는 뜻입니다.
그리고 항상 html 만 요청할 필요는 없습니다. css나 js 그리고 이미지 같은 파일을 요청할 수도 있고, 특정 동작을 행하는 것을 요청할 수 도 있습니다. 요청의 내용이 주소를 통해 표현되므로 서버가 이해하기 쉬운 주소를 사용하는 것이 좋습니다. 여기서 REST라는 개념이 등장합니다.
REST(Representational State Transfer)는 웹 기반 시스템에서 자원을 정의하고, 그 자원에 대한 상태를 주고 받는 방법을 기술한 아키텍처 스타일입니다. REST는 클라이언트와 서버 간의 통신을 위한 규칙을 제공하여 분산 시스템을 구축하는 데 사용됩니다.
- 자원(Resource) 지향: 모든 자원에는 고유한 식별자(URI)가 있으며, 클라이언트는 이를 통해 자원을 식별하고 조작합니다. 예를 들어, 웹 페이지의 URI는 해당 자원의 위치를 나타내며 클라이언트는 이를 통해 해당 페이지를 요청할 수 있습니다.
- 상태를 주고받는 구조(Stateful): REST는 상태를 저장하지 않고, 요청 간의 상태 전환을 통해 데이터를 주고 받습니다. 이는 서버가 클라이언트의 상태를 관리하지 않고, 각 요청이 독립적으로 처리되도록 합니다.
- 표준화된 인터페이스: REST는 자원에 대한 표준화된 인터페이스를 제공합니다. 주요 HTTP 메서드(GET, POST, PUT, DELETE)를 사용하여 자원을 조작하고, HTTP 상태 코드를 통해 요청의 성공 또는 실패를 알려줍니다.
- 계층화(Layered): REST 아키텍처는 서버와 클라이언트 사이에 중간 계층을 두어 구조를 단순화하고 확장성을 향상시킵니다. 이는 서버와 클라이언트 간의 결합도를 줄이고, 시스템의 확장성과 보안성을 향상시킵니다.
REST에서는 주소 외에도 HTTP 요청 메서드라는 것을 사용합니다.
REST에서 사용되는 주요 HTTP 메서드는 다음과 같습니다.
- GET: 리소스의 조회를 요청할 때 사용됩니다. 서버로부터 데이터를 가져오는 요청에 사용되며, 요청한 URI의 정보를 반환합니다. GET 요청은 클라이언트가 서버에게 어떤 리소스의 상태를 조회하기 위해 사용됩니다.
- POST: 리소스를 생성하기 위해 사용됩니다. 클라이언트가 서버에 새로운 데이터를 제출할 때 사용되며, 서버는 해당 데이터를 처리하고 새로운 리소스를 생성합니다. 예를 들어, 사용자가 폼을 제출하여 새로운 블로그 게시물을 작성하는 경우 POST 요청을 사용할 수 있습니다.
- PUT: 리소스의 전체적인 업데이트를 요청할 때 사용됩니다. 클라이언트가 서버에게 리소스의 전체적인 업데이트를 요청하고자 할 때 사용됩니다. 즉, 클라이언트가 요청한 리소스의 모든 내용을 제공하여 서버에 새로운 내용으로 업데이트합니다.
- DELETE: 리소스의 삭제를 요청할 때 사용됩니다. 클라이언트가 서버에게 특정 리소스를 삭제하도록 요청하는 메서드입니다. 서버는 해당 리소스를 삭제하고 삭제에 성공했는지에 대한 상태 코드를 반환합니다.
- PATCH: 리소스의 일부를 업데이트하기 위해 사용됩니다. PUT과 유사하지만, PATCH는 리소스의 일부분만을 업데이트할 때 사용됩니다. 즉, 클라이언트가 리소스의 일부만을 제공하여 해당 부분을 업데이트하도록 요청합니다.
💡 실전예제
프로젝트를 아래와 같이 구성하고 각 파일마다 코드를 아래와 같이 작성합니다.
📘 restFront.css
* CSS 부분과 HTML 부분은 중요하지 않으므로 복사해서 사용하는 것을 권장합니다.
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
nav {
background-color: #333;
color: #fff;
padding: 10px;
}
nav a {
color: #fff;
text-decoration: none;
margin-right: 10px;
}
nav a:hover {
text-decoration: underline;
}
div {
max-width: 600px;
margin: 20px auto;
padding: 20px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
form {
display: flex;
margin-bottom: 20px;
}
input[type="text"] {
flex: 1;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 16px;
}
button[type="submit"] {
padding: 10px 20px;
background-color: #333;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
}
button[type="submit"]:hover {
background-color: #555;
}
#list {
border-top: 1px solid #ccc;
padding-top: 20px;
}
📘 restFront.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<title>RESTful 서버 예제</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>
📘 about.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>RESTful 서버 예제</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>
📘 restFront.js
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 (e) {
console.error(e);
}
});
const remove = document.createElement('button');
remove.textContent = '삭제';
remove.addEventListener('click', async () => { // 삭제 버튼 클릭
try {
await axios.delete('/user/' + key);
getUser();
} catch (e) {
console.error(e);
}
});
userDiv.appendChild(span);
userDiv.appendChild(edit);
userDiv.appendChild(remove);
list.appendChild(userDiv);
console.log(res.data);
});
} catch (e) {
console.error(e);
}
}
window.onload = getUser; // 화면 로딩 시 getUser 호출
// 폼 제출(submit) 시 실행하기
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 (e) {
console.error(e);
}
e.target.username.value = '';
});
📘 restServer.js (핵심 부분)
- req.method로 HTTP 요청 메서드를 구분하고 있습니다.
- 메서드가 GET 이면 다시 req.url로 요청 주소를 구분합니다.
- 주소가 '/' 일 때는 restFront.html을 제공하고, 주소가 '/about' 이면 about.html 파일을 제공합니다.
- 만약 존재하지 않는 파일을 요청했거나 GET 메서드 요청이 아닌 경우라면 404 not found 에러가 응답으로 전송됩니다.
- 데이터베이스 대용으로 users 라는 객체를 선언해 사용자 정보를 저장하였습니다.
- POST와 PUT 요청을 처리할 때에는 req.on('data')와 req.on('end')의 사용인데 요청의 본문에 들어 있는 데이터를 꺼내기 위한 작업이라고 볼 수 있습니다.
- req와 res도 내부적으로는 스트림(readStream과 writeStream)으로 되어 있으므로 요청/응답의 데이터가 스트림 형식으로 전달됩니다.
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': 'text/html; charset=utf-8'});
return res.end(JSON.stringify(users));
}
// 주소가 / 도 /about 도 아니면?
try {
const data = await fs.readFile(path.join(__dirname, req.url));
return res.end(data);
} catch (e) {
// 주소에 해당하는 라우트를 찾지 못했다는 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': 'application/json; 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 (e) {
console.error(e);
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end(e.message);
}
})
.listen(8082, () => {
console.log('8082번 포트에서 서버 대기중 입니다.');
});
💡 서버 실행하기
프로젝트 파일이 있는 폴더에서 터미널을 실행하고 'node restServer' 명령어를 입력하여 서버를 실행합니다.
http://localhost:8082로 접속하면 아래와 같은 화면이 보입니다.