안녕하세요, 코린이의 코딩 학습기 채니 입니다.
[Node.js 교과서]의 책을 참고하여 포스팅한 개인 공부 내용입니다.
http 모듈로 서버 만들기
요청과 응답 이해하기
클라이언트에서 서버로 요청(request)을 보내고, 서버에서는 요청의 내용을 읽고 처리한 뒤 클라이언트에 응답(response)을 보냅니다.
따라서 서버에는 요청을 받는 부분과 응답을 보내는 부분이 있어야 합니다. (이벤트 방식)
따라서 클라이언트로부터 요청이 왔을 때 어떤 작업을 수행할지 이벤트 리스너를 미리 등록해야 합니다.
createServer.js
const http = require("http");
http.createServer((req, res) => {
});
http 서버가 있어야 웹 브라우저 요청을 처리할 수 있기 때문에 http 모듈을 사용하였습니다.
인수로 요청에 대한 콜백 함수를 넣을 수 있으며, 요청이 들어올 때마다 매번 콜백 함수가 실행됩니다.
- req : 요청에 관한 정보들을 담은 객체
- res : 응답에 관한 정보들을 담은 객체
server1.js
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 part4/server1
8080번 포트에서 서버 대기 중입니다!
첫 서버가 잘 실행되었습니다. 만일 서버를 종료하고 싶다면 콘솔에서 Ctrl + C를 입력합니다.
createServer 메소드 뒤에 listen 메소드를 붙이고 포트 번호와 포트 연결 완료 후 실행될 콜백 함수를 넣습니다.
파일을 실행하게 되면 서버는 8080 포트에서 요청이 오기를 기다립니다.
- res.writeHead
- 응답에 대한 정보를 기록
- 첫 번째 인수로 성공적인 요청을 의미하는 200을, 두 번째 인수로는 응답에 대한 정보를 보냄 (HTML 형식, 한글 표시)
- 헤더
- res.write
- 첫 번째 인수는 클라이언트로 보낼 데이터
- HTML 모양의 문자열 뿐만 아니라 버퍼를 보낼 수도 있음
- 여러 번 호출해서 데이터를 여러 개 보낼 수도 있음
- 본문 (데이터 기록)
- res.end
- 응답을 종료하는 메소드
- 인수가 있다면 해당 데이터도 클라이언트로 보내고 응답을 종료
listen 메소드에 콜백 함수를 넣는 대신, 서버에 listening 이벤트 리스너를 붙여도 됩니다.
server1-1.js
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);
});
@콘솔출력값
$ node part4/server1-1
8080 포트에서 기다리는 중
한 번에 여러 서버를 실행할 수도 있습니다.
server1-2.js
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 포트에서 기다릴게");
});
@콘솔출력값
$ node part4/server1-2
8080에서 기다린당
8081 포트에서 기다릴게
실무에서 서버를 여러 개 띄우는 일은 드물지만, 여러 개 띄울 수도 있습니다.
또한 하나하나 HTML을 적는 것은 매우 비효율적이므로 HTML파일을 만들어 사용하는 것이 바람직 합니다.
server2.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Node.js 웹 서버</title>
</head>
<body>
<h1>Node.js 웹 서버</h1>
<p>만들 준비되셨나요?</p>
</body>
</html>
server2.js
const http = require("http");
const fs = require("fs").promises;
http
.createServer(async (req, res) => {
try {
const data = await fs.readFile(__dirname + "/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(8081, () => {
console.log("8081 포트에서 서버 대기 중입니다.");
});
@콘솔출력값
$ node part4/server2
8081 포트에서 서버 대기 중입니다.
REST와 라우팅 사용하기
- 서버에 요청을 보낼 때는 주소를 통해 요청의 내용을 표현
- REST (REpresentational State Transfer)
- 서버의 자원을 정의하고 자원에 대한 주소를 지정하는 방법
- 주소는 의미를 명확히 전달하기 위해 명사로 구성
- 주소 외에도 HTTP 요청 메소드를 사용
- GET
: 서버 자원을 가져오고자 할 때 사용, 쿼리스트링 사용
: 브라우저에서 캐싱(기억)할 수도 있어 같은 주소로 GET 요청을 할 때 서버에서 가져오는 것이 아니라 캐쉬에서 가져올 수 있음 - POST
: 서버에 자원을 새로 등록하고자 할 때 사용, 요청의 본문에 새로 등록할 데이터를 넣어 보냄 - PUT
: 서버의 자원을 요청에 들어 있는 자원으로 치환하고자 할 때 사용, 요청의 본문에 치환할 데이터를 넣어 보냄 - PATCH
: 서버 자원의 일부만 수정하고자 할 때 사용, 요청의 본문에 일부 수정할 데이터를 넣어 보냄 - DELETE
: 서버의 자원을 삭제하고자 할 때 사용, 요청의 본문에 데이터를 넣지 않음 - OPTIONS
: 요청을 하기 전에 통신 옵션을 설명하기 위해 사용
- GET
- 주소와 메소드만 보고 요청의 내용을 알아볼 수 있다는 것이 장점
- 클라이언트가 누구든 상관없이 같은 방식으로 서버와 소통할 수 있음 (서버와 클라이언트 분리)
restFront.css
a {
color: blue;
text-decoration: none;
}
restFront.html
<!DOCTYPE html>
<html lang="ko">
<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="http://unpkg.com/axios/dist/axios.min.js"></script>
<script src="./restFront.js"></script>
</body>
</html>
restFront.js
// 로딩 시 사용자 정보를 가져오는 함수
async function getUser() {
try {
const res = await axios.get("/users");
console.log(res);
const users = res.data;
const list = document.querySelector("#list");
list.innerHTML = "";
// 사용자마다 반복적으로 화면 표시 및 이벤트 연결
Object.keys(users).map((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 {
console.log(key, { name });
await axios.put("/user/" + key, { name });
getUser();
} catch (error) {
console.error(error);
}
});
const remove = document.createElement("button");
remove.textContent = "삭제";
remove.addEventListener("click", async () => {
try {
await axios.delete("/user/" + key);
getUser();
} catch (error) {
console.error(error);
}
});
userDiv.appendChild(span);
userDiv.appendChild(edit);
list.appendChild(userDiv);
console.log(res.data);
});
} catch (error) {
console.error(error);
}
}
// 화면 로딩 시 getUser 호출
window.onload = getUser;
// 폼 제출 시 실행
document.querySelector("#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 (error) {
console.error(error);
}
e.target.username.value = ""; // 초기화
});
about.html
<!DOCTYPE html>
<html lang="ko">
<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>
restServer.js
const http = require("http");
const fs = require("fs").promises;
const path = require("path");
http
.createServer(async (req, res) => {
try {
console.log(req.method, req.url);
if (req.method === "GET") {
// 메인 페이지 요청이 들어왔을 때
if (req.url === "/") {
const data = fs.readFile(path.join(__dirname, "restFront.html"));
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
return res.end(data);
}
// about 페이지 요청 왔을 때
else if (req.url === "/about") {
const data = fs.readFile(path.join(__dirname, "about.html"));
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
return res.end(data);
}
// 주소가 /와 /about 둘 다 아닐 때
try {
const data = fs.readFile(path.join(__dirname, req.url));
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
return res.end(data);
} catch (error) {
// 주소에 해당하는 라우터를 못 찾은 경우 404 에러 발생
}
}
res.writeHead(404);
return res.end("NOT FOUND");
} catch (error) {
console.error(error);
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
res.end(err.message);
}
})
.listen(8082, () => {
console.log("8082 포트에서 대기 중입니다.");
});
req.method로 HTTP 요청 메소드를 구분하고, 해당 메소드가 GET이라면 req.url로 주소를 구분하여 처리합니다.
만일 존재하지 않는 파일을 요청하거나 GET 메소드가 아니라면 404 에러가 발생되고, 이 외 에러가 발생한 경우 500 에러로 응답합니다.
이어서 코드를 완성 시켜보겠습니다.
restServer.js
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 = fs.readFile(path.join(__dirname, "restFront.html"));
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
return res.end(data);
}
// about 페이지 요청 왔을 때
else if (req.url === "/about") {
const data = 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 = fs.readFile(path.join(__dirname, req.url));
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
return res.end(data);
} catch (error) {
// 주소에 해당하는 라우터를 못 찾은 경우 404 에러 발생
}
} else if (req.method === "POST") {
if (req.url === "/user") {
let body = "";
// 요청의 body를 stream 형식으로 받음
req.on("data", (data) => {
console.log(data);
body += data;
});
// 요청의 body를 다 받은 후 실행
return req.on("end", () => {
console.log("POST 본문(Body): ", body);
const { name } = JSON.parse(body);
const id = Data.now();
users[id] = name;
console.log(users);
res.writeHead(201, { "Content-Type": "text/html; charset=utf-8" });
res.end("등록 성공");
});
}
} else if (req.method === "PUT") {
if (req.url.startsWith("/user/")) {
console.log(req.url.split("/"));
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/html; 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/html; charset=utf-8" });
return res.end(JSON.stringify(users));
}
}
res.writeHead(404);
return res.end("NOT FOUND");
} catch (error) {
console.error(error);
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
res.end(err.message);
}
})
.listen(8082, () => {
console.log("8082 포트에서 대기 중입니다.");
});
POST와 PUT 요청 처리에 req.on('data')와 req.on('end')를 사용하는 것을 확인할 수 있는데, 이는 요청의 본문에 들어 있는 데이터를 꺼내기 위한 작업입니다.
req와 res도 내부적으로는 스트림으로 되어 있으므로 요청/응답의 데이터가 스트림 형식으로 전달됩니다.
다만, 받은 데이터는 단순 문자열이므로 이를 JSON으로 만드는 JSON.parse 작업이 필요합니다.
- Name : 요청 주소
- Status : HTTP 응답 코드
- Protocol : 통신 프로토콜
- Type : 요청 종류
'JavaScript > Node.js' 카테고리의 다른 글
Node) 패키지 매니저 (0) | 2022.12.28 |
---|---|
Node) http 모듈로 서버 만들기 - 쿠키와 세션, https와 http2, cluster (0) | 2022.12.28 |
Node) 노드 기능 알아보기 - 이벤트 이해하기, 예외 처리하기 (0) | 2022.12.27 |
Node) 노드 기능 알아보기 - 노드 내장 모듈 사용하기 (1) | 2022.12.27 |
Node) 노드 기능 알아보기 - 노드 내장 객체 알아보기 (0) | 2022.12.27 |