4 http 모듈로 서버 만들기
source: categories/study/nodejs/nodejs4.md
4.1 HTTP 서버 만들기
4.1.1 요청과 응답 이해하기
4.1.1.1 서버와 클라이언트
노드는 자바스크립트 실행기이지 서버가 아닙니다.
다만, 자바스크립트로 서버를 돌릴 수 있는 코드를 작성해놓으면 노드가 그 서버를 실행을 해줍니다.
-
서버와 클라이언트의 관계
가장 중요한 개념입니다.
서버와 클라이언트의 관계를 이해하셔야됩니다.- 클라이언트가 서버로 요청(request)을 보냄
웹 브라우저에www.naver.com
을 입력하면, 웹 브라우저가 클라이언트가 되는거고 요청을 받는 것은 네이버 서버입니다.
네이버 서버는 어떤 컴퓨터라고 생각하시면 됩니다.
네이버 회사에 있겠죠? 그게 서버가돼서 저희의 웹 브라우저에서www.naver.com
을 입력해서 네이버 서버에가서 네이버 사이트를 받아와야됩니다.
네이버 사이트는 저희 컴퓨터에 저장된게 아니라 네이버 서버에 저장되어있는 것을 받아오는 거거든요?
그래서 이 웹브라우저가 클라이언트면 네이버 서버로 메인화면을 주세요 라고 요청을 보내고 네이버 서버는 저희 웹브라우저(클라이언트)로 응답을 보내줍니다. - 서버는 요청을 처리
-
처리 후 클라이언트로 응답(response)을 보냄
요청과 응답은
http
라는 프로토콜, 예를 들어 저희가 서버한테 메인 페이지를 주세요 라고 요청을 했는데 서버가 말을 못알아들으면 안돼잖아요?
이건 마치 언어와도 같습니다.
예를 들어 구글, 구글은 미국 서버잖아요?
만약 구글 서버에 한국말로 구글 메인 페이지좀 보내주세요 라고 하면 못알아 들을거 아니에요?즉, 공통된 언어가 필요하겠죠?
그게
http
입니다. 서버쪽 프로그래밍 하실 때는http
지식을 반드시 익혀두셔야합니다.
- 클라이언트가 서버로 요청(request)을 보냄
4.1.1.2 노드로 http 서버 만들기
위의 제목에도 아예 http
서버 만들기라고 해놨죠?
http
프로토콜이므로 규약? 이라고 생각하시면 됩니다.
-
http 요청에 응답하는 노드 서버
createServer
로 요청 이벤트에 대기-
req
객체는 요청에 관한 정보가,res
객체는 응답에 관한 정보가 담겨 있음// createServer.js const http = require("http"); http.createServer((req, res) => { // 여기에 어떻게 응답할지 적습니다. })
4.1.1.3 8080 포트에 연결하기
-
res
메소드로 응답 보냄write
로 응답 내용을 적고end
로 응답 마무리(내용을 넣어도 됨)
-
listen(포트)
메소드로 특정 포트에 연결// server1.js const http = require("http"); http.createServer((req, res) => { res.writeHead(200, {"Content-Type": "text/html; charset=utf-8"}); rew.write("<h1>Hello Node!</h1>"); res.end("<p>Hello Server!</p>"); }) .listen(8080, () => { // 서버 연결 console.log("8080번 포트에서 서버 대기 중입니다!"); })
4.1.1.4 8080 포트로 접속하기
- 스크립트를 실행하면 8080 포트에 연결됨
-
localhost:8080
또는http://127.0.0.1:8080
에 접속// server1.js const http = require("http"); // 저희가 만든 서버로 요청이 올겁니다. http.createServer((req, res) => { // 요청이 왔을 때 실행할 코드를 여기에 작성합니다. // 그리고 응답은 항상 보내야하는게 아니라 거부할 수 도 있습니다. // 요청한 너가 악성 유저로 판단이 되면 응답을 거부할 수 있습니다. // 그러한 코딩은 여기에 적어주시면 됩니다. // 응답은 아래와 같이 chunk 데이터로 html 태그로도 보낼 수 있습니다. // chunk 데이터라함은 스트림 방식이라는 거거든요? // 아래처럼 스트림 방식으로 보낼 수 있다는 뜻입니다. res.write("<h1>Hello Node!</h1>"); res.write("<p>Hello server</p>"); res.end("<p>Hello hyungju-lee</p>"); }) // 여기까지만 작성하면 안돼고 아래 코드로 process로 올려야되거든요? // 서버도 프로그램이기 때문에 노드가 이 코드를 실행하는 순간 서버를 process로 올려줘야됩니다. // 보통 process로 올릴 땐 port라는걸 하나 잡아먹거든요? // 저희는 이 서버를 8080이라는 포트 process에 올릴겁니다. .listen(8080, () => { console.log("8080번 포트에서 서버 대기 중입니다."); })
원래는
node 파일.js
를 실행하면 해당 파일을 실행하고 종료까지 자동으로 했는데 서버를 실행하니까 종료되지않고 계속 실행되어있죠?
서버를 실행한 경우,.listen()
을 한 경우에는 터미널 하나를 잡아먹습니다.
위 스크린샷의 터미널은 8080번 포트랑 연결되어서 다른 동작을 더 이상 할 수 없게됩니다.즉, 위에서 만든 첫번째 서버는 8080번 포트에 연결이 되어있는 거고, 이거를 이제 직접 들어가서 접근을 할 수 있습니다.
어떻게 접근하느냐.
저희는 아직www.naver.com
처럼 도메인을 구입하지 않았잖아요?
이런 경우를 위해서localhost
라는 도메인을 지원합니다.이렇게 8080번 포트에 process를 띄우고 클라이언트(웹브라우저)에서 서버로 요청을 보내서 응답까지 받아봤습니다.
서버쪽 프로그래밍을 안하신 분들은 위와 같이:8080
이렇게 붙어있는 것을 처음 보셨을 수도 있는데 여러분이 아시는 모든 서버들은 이렇게 포트가 존재합니다.www.naver.com
도 뒤에:443
이라는 포트가 붙어있습니다.
위와 같이 뒤에:443
포트 번호를 붙이시고 접속하셔도 똑같이 네이버로 접속됩니다.
이게 어떤 규칙이 있냐면위와 같이
https
적용되어있는 데는 기본적으로 포트가443
이어서443
포트번호를 생략할 수 있습니다.
반대로https
가 아닌http
로 되어있는 사이트는 뒤에80
번 포트번호가 숨겨져있습니다.
대부분의 웹사이트는https
-443
이나http
-80
을 사용하고 있기 때문에 생략을 해서 여러분들이 못 보신거지 실제로는 포트번호가 하나씩은 존재합니다..listen()
의 포트 범위는 아무거나 쓰실 수 있는데 쓸 수 있는 포트 번호가65535
번까지 있습니다.-
보통은 1024 ~ 49151 번호를 등록을 많이 해주시고 실제 서비스할 땐
80
이나443
으로 등록을 많이 합니다.
-
4.1.1.5 localhost와 포트
-
localhost는 컴퓨터 내부 주소
- 외부에서는 접근 불가능
-
포트는 서버 내에서 프로세스를 구분하는 번호
- 기본적으로
http
서버는80
번 포트 사용(생략가능,https
는443
) - 예)
www.gilbut.com:80
->www.gilbut.com
-
다른 포트로 데이터베이스나 다른 서버 동시에 연결 가능
포트가 좋은 점이
www.naver.com
이 여러분이 제공하는 서비스라고 생각을 해보시면 도메인 하나에다가 메인 페이지 하나밖에 연결을 못하잖아요?
그러면 다른 페이지를 보여주려면 도메인을 또 새로 사야돼나?
이렇게 생각이 드실 수도 있는데, 예를 들어443
말고444
포트 번호에서 다른걸 보여줄 수 있고445
에서 또 다른걸 보여줄 수가 있습니다.즉, 하나의 도메인 또는 호스트라고 불리는 곳에서 포트번호만 다르게하면, 다양한 페이지, 프로그램을 연결하고 보여줄 수 있는 겁니다.
포트 하나가 하나의 프로그램이라고 보시면됩니다.그래서 위 이미지처럼 보통은
80
번에http
를 연결해놓고21
번에ftp
,23
번에telnet
그 다음에3306
에다가mysql
,27017
에다가mongoDB
이런식으로 연결을 해서 하나의 주소에 달려있는 여러개의 포트들에 여러개의 프로그램을 연결해서 띄워 놓는겁니다.// server1.js const http = require("http"); http.createServer((req, res) => { res.write("<h1>Hello Node!</h1>"); res.write("<p>Hello server</p>"); res.end("<p>Hello hyungju-lee</p>"); }) .listen(8080, () => { console.log("8080번 포트에서 서버 대기 중입니다."); })
참고로 아까 위에서 위 코드로
localhost
로 서버를 만들었잖아요?
localhost
는 다른 컴퓨터에선 접근이 안돼고 여러분의 컴퓨터에서만 접근하실 수 있습니다.
즉, 개발자용 서버라고 보시면 되는데 이 로컬호스트를 다른 사람들에게 공개를 하고싶다면, 내 서버를 공개하고 싶으시다면 공개를 하기위한 웹서버 설정을 따로 하셔야됩니다.
그거엔 여러가지 방법이 있는데, 진짜 여러분의 컴퓨터 또는 노트북, 심지어 핸드폰도 됩니다.
그런 각종 기기를 서버로 만들 수도 있고.. 그런데 보통 그렇겐 안하죠? 항상 켜놔야하기 때문에 여러분의 PC, 노트북, 핸드폰이 과열돼서 고장날 수도 있죠.
그래서 보통은 남의 PC, 클라우드 이런데서 PC를 빌려가지고 거기다가localhost
서버를 만든다음에 그거를 남들에게 공개하는식으로, 그런식으로 많이 하게되는데 그와 관련된 내용은 15장에서 직접 배포를 하면서 진행해보도록 하겠습니다.
그러한 행위를 배포라고 합니다. 남들에게 제 서버를 공개하는 행위
- 기본적으로
4.1.1.6 이벤트 리스너 붙이기
-
listening
과error
이벤트를 붙일 수 있음// 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); })
// server1.js const http = require("http"); const server = http.createServer((req, res) => { res.write("<h1>Hello Node!</h1>"); res.write("<p>Hello server</p>"); res.end("<p>Hello hyungju-lee</p>"); }) .listen(8080) // listening 이벤트가 있어서 위에 .listen 함수에서 실행했던 콜백을 아래와 같이 뺄 수도 있습니다. server.on("listening", () => { console.log("8080번 포트에서 서버 대기 중입니다."); }) // 에러처리를 해줍니다. server.on("error", (error) => { console.error(error); })
위의
http
서버에서http.createServer(콜백함수)
,.listen(8080, 콜백함수)
콜백함수 이 부분 다 비동기거든요?
비동기여서 에러가 날 수 있기 때문에 항상 위와 같이 에러처리를 해야됩니다.
그리고 코드를 수정하시면 서버에 바로 자동 반영이 되는게 아니기 때문에 항상 종료하고 다시 실행해주셔야합니다.
4.1.1.7 한 번에 여러 개의 서버 실행하기
-
createServer
를 여러 번 호출하면 됨- 단, 두 서버의 포트를 다르게 지정해야 함
-
같게 지정하면
EADDRINUSE
에러 발생// 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번 포트에서 서버 대기 중입니다!"); })
위와 같이 작성하면 서버가 동시에 2개가 실행돼서
localhost:8080
도 접속돼고localhost:8081
도 접속됩니다.
그런데 굳이 이렇게 할 필요는 없긴합니다.
-
두번째 서버는 포트 번호를
8081
로 설정- 첫번째
8080
포트 번호 서버를 종료하면 두번째 서버도8080
번 포트를 써도 됨 - 종료하지 않을 경우 같은 포트를 쓰면 충돌이나서 에러 발생
- 첫번째
4.2 fs로 HTML 읽어 제공하기
// server1.js
const http = require("http");
http.createServer((req, res) => {
res.write("<h1>Hello Node!</h1>");
res.end("<p>Hello Server!</p>");
})
.listen(8080, () => { // 서버 연결
console.log("8080번 포트에서 서버 대기 중입니다!");
})
위에서 write
, end
메소드에 html 코드를 넣어서 응답을 했었는데, 어떤 브라우저는 해당 부분을 html인지 아니면 그냥 일반 문자열인지 구분을 못하는 경우도 있습니다.
예를 들어, 사파리 같은 브라우저가 모르더라구요.
사파리가 <h1>Hello Node!</h1>
이게 html인지 문자열인지 구분을 못해가지고, 그런 경우에는 저희가 직접 이게 html이다 라는 걸 알려줘야되는데,
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
이럴 때는 위 코드를 추가해줍니다.
// server1.js
const http = require("http");
http.createServer((req, res) => {
// html 태그를 알아보기 위해, 그리고 한글도 알아볼 수 있도록 charset=utf-8도 입력
// 이 부분은 http 관련 내용이거든요?
// 이 부분에 대한 설명은 조금 이 따 더 자세하게 말씀드리도록 하겠습니다.
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번 포트에서 서버 대기 중입니다!");
})
이렇게 수정을 했으면 항상 종료했다가 키셔야됩니다.
그래야 저희가 수정한 부분이 반영이 됩니다.
보통 다른 서버에서 코드를 수정했는데 바로 반영되는 것은 위 server1.js
파일을 감시하고 있다가 (fs.watch()
)
뭔가 변화가 감지되면 알아서 서버를 한번 재시작해주는거지 실제로 자동으로 바뀌거나 그런 마법은 프로그래밍에 없습니다.
다만, 노드는.. 노드도 실제로 코드를 바꿨을 때 서버가 재시작되게 할 수는 있긴한데 아직까진 그 부분에 대해 살펴보진 않을거고 기본적으로 원칙은 코드가 바뀌면 서버를 재시작 해줘야된다는 것.
그리고 나중에 저희가 배포를 할 때는 위 포트번호를 80
으로 바꿔서 배포하면 굳이 뒤에 포트번호를 붙일 필요가 없고(생략 가능하니까) 여러분이 구매하신 도메인으로 바로 접속이 가능하겠죠?
아마 여러분이 실습하실 때는 80
으로 하면 보통 80
번은 다른 서비스가 이미 잡고있는 경우가 많거든요?
그래서 에러가 날 수도 있으니까 위 코드에서 제가 8080
으로 한거구요, 만약 80
포트번호로 된다고 하면 localhost
만 입력해서 접속하셔도 동작할겁니다.
대부분의 컴퓨터는 80
번 포트쓰면 에러가 나기 때문에 8080
번으로 한겁니다.
// 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>");
res.end("<p>Hello Server!</p>");
res.end("<p>Hello Server!</p>");
res.end("<p>Hello Server!</p>");
res.end("<p>Hello Server!</p>");
res.end("<p>Hello Server!</p>");
res.end("<p>Hello Server!</p>");
})
.listen(8080, () => { // 서버 연결
console.log("8080번 포트에서 서버 대기 중입니다!");
})
-
위와 같이
write
와end
에 문자열을 넣는 것은 비효율적fs
모듈로html
을 읽어서 전송하자-
write
가 버퍼도 전송 가능<!-- server2.html --> <!doctype html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <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) => { // async await를 사용하실 땐 항상 에러처리! 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); // 아래 plain은 뭐냐면 일반 문자열이라는 뜻입니다. // 위의 text/html은 html이란걸 뜻하는 것이고 plain은 일반 문자열임을 알려주는 것! res.writeHead(500, {"Content-Type": "text/plain; charset=utf-8"}); res.end(err.message); } }) .listen(8080, () => { console.log("8080번 포트에서 서버 대기 중입니다!"); })
위 코드를 아래와 같이 작성할 수도 있습니다.
// server2.js const http = require("http"); const fs = require("fs").promises; const server = http.createServer(async (req, res) => { // async await를 사용하실 땐 항상 에러처리! 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); // 아래 plain은 뭐냐면 일반 문자열이라는 뜻입니다. // 위의 text/html은 html이란걸 뜻하는 것이고 plain은 일반 문자열임을 알려주는 것! res.writeHead(500, {"Content-Type": "text/plain; charset=utf-8"}); res.end(err.message); } }) .listen(8080); server.on("listening", () => { console.log("8080번 포트에서 서버 대기 중입니다."); }) server.on("error", (error) => { console.error(error); })
HTML을 읽어서 응답하는 방법이 훨씬 편하다는 것!
4.3 REST API 서버 만들기
https://ko.wikipedia.org/wiki/TCP/UDP%EC%9D%98_%ED%8F%AC%ED%8A%B8_%EB%AA%A9%EB%A1%9D
위의 주소에서 https://ko.wikipedia.org/
이 부분이 localhost
라면 wiki/TCP/UDP%EC%9D%98_%ED%8F%AC%ED%8A%B8_%EB%AA%A9%EB%A1%9D
이렇게 뒤에 주소가 붙잖아요?
이런 주소를 만드는 것도 가능합니다.
여기서 REST API가 나옵니다.
- REST API와 라우팅 -
4.3.1 REST API
-
서버에 요청을 보낼 때는 주소를 통해 요청의 내용을 표현
/index.html
이면index.html
을 보내달라는 뜻- 항상
html
을 요구할 필요는 없음,css
,js
,image
등등 이런 파일이 아니어도됨, 추상적인 것도 됨.
예를 들어 id가 hyungju-lee인 사용자 정보를 갖다달라.
누구한테 돈을 보내달라.
이런 요구도 서버에 할 수 있음.
물론 이러한 요구를 받아들이는 것은 서버가 결정. 즉, 서버에 주도권이 있음.
보통 서버가 클라이언트에 "야, 너는 나한테 이런걸 요청할 수가 있어" 이렇게 미리 알려주고 클라이언트는 서버가 알려준 주소중에 몇 개를 골라서 요청을 보내는 식임.
서버에서 정해준대로 보통 클라이언트가 따라감. - 서버가 이해하기 쉬운 주소가 좋음
-
REST API(Representational State Transfer)
주소 정할 때의 규칙 - 보통 REST API를 많이 따라감.
REST API는 일종의 주소 정하는 규칙이라고 보시면 됨.
REST API 규칙대로 주소를 정하면 사용자들이 찾기 쉬우니까 추측을 할 수 있겠죠?
REST API로 구조화를 해놓으시면 클라이언트가 서버쪽 자원들 파악하는데 도움이 됨.
다만, REST API로 구조를 잘 잡아놓으면 단점도 있는게 해커들이 추측을 하기가 쉬움. "아, 어떤 자원은 이렇게 주소입력하면 나오겠네."
이렇게 해킹을 할 수도 있으니까 보안을 철저히 해야됨.
예측 가능하다는 것 - 보안에 위험이 있다라는 뜻도됨.- 서버의 자원을 정의하고 자원에 대한 주소를 지정하는 방법
/user
이면 사용자 정보에 관한 정보를 요청하는 것/post
면 게시글에 관련된 자원을 요청하는 것
-
HTTP 요청 메소드
게시글 이라는 명사가 있으면 해당 게시글을 "가져와라", "등록해라", "삭제해라" 등등 여러개의 동사가 붙을 수 있음.
실제로 HTTP는 아래와 같은 동사들을 지원함.
아래 동사들과 주소를 조합을함.
예를 들어, GET/user면 사용자를 가져와라.
POST/user면 사용자를 등록해라.
PUT or PATCH/user면 사용자를 수정해라.
DELETE/post면 게시글을 삭제해라.
이런식으로 할 수 있다는 것.
그리고 "어떤 사람에게 돈을 보내라" 이런 것도 할 수 있음.
은행 같은 경우는 이런걸 만들어 놨겠죠?
돈을 보내라 라는 동사는 아래에 없잖아요?
그래서 애매할 때는POST
를 쓰시면 됩니다.
예를 들어 "로그인해라"도 애매하죠? 그럴 땐POST
쓰시면 됩니다.그리고 많이 궁금해하시는
PUT
,PATCH
이건 사람마다 다르게 생각할 수도 있는데, 저는 보통PUT
은 전체 수정,PATCH
는 부분 수정으로 씁니다.
예를 들어 hyungju-lee라는 데이터가 있는데, 그거를 아예 다른 사람으로 통채로 대체를 해버릴 때는PUT
.
hyungju-lee라는 데이터가 있는데 이 데이터의 나이가 33에서 34로 올랐을 때는 부분 수정이잖아요?
hyungju-lee라는 데이터에 나이만 수정한 거. 그런거는PATCH
저는
PUT
과PATCH
도 엄격하게 구분하는 편.- GET: 서버 자원을 가져오려고할 때 사용
- POST: 서버에 자원을 새로 등록하고자 할 때 사용(또는 뭘 써야할지 애매할 때)
- PUT: 서버의 자원을 요청에 들어있는 자원으로 치환하고자할 때 사용
- PATCH: 서버 자원의 일부만 수정하고자할 때 사용
- DELETE: 서버의 자원을 삭제하고자할 때 사용
4.3.2 HTTP 프로토콜
-
클라이언트가 누구든 서버와 HTTP 프로토콜로 소통 가능
HTTP 프로토콜은 언어 상관 없이 모든 서버간의 약속이라 HTTP 프로토콜(규약)대로만 전송을 하면 소통이 가능함.- iOS, 안드로이드, 웹이 모두 같은 주소로 요청 보낼 수 있음
-
서버와 클라이언트의 분리
-
RESTful
- REST API를 사용한 주소 체계를 이용하는 서버
-
GET/user는 사용자를 조회하는 요청, POST/user는 사용자를 등록하는 요청
위의 HTTP 메소드, 그리고 주소, 이런 것들은 다 약속되어있는 것이기 때문에 어떤 서버든지 http 서버라면 다 알아들을 수 있습니다.
REST API 방식으로 서버 주소를 체계적으로 정리했다면 그 서버는 RESTful한 서버라고 말을 함.
그런데 사실 RESTful한 서버는 저는 거의 본적은 없음.
REST API가 실제로 더 지켜야되는 규칙이 많거든요?
예를 들어 주소에다 동사를 쓰면 안된다던지.. 그런데 막상 로그인만 생각해봐도 주소에 login은 동사거든요?
login을 안 쓰고 어떻게 login을 표현할지 애매합니다.
여기서도 사람들간의 논쟁이 많은데 RESTful하게 서버를 만들려고 노력하시기 보다는 주소는 깔끔하게 만든다고 생각하시면되고 위의 HTTP 메소드와 주소를 조합했을 때 의미 전달만 잘 되면 충분히 훌륭한 서버라고 생각합니다.아직까진 REST API를 지켰다고 한 서버 중에 REST API를 제대로 지킨 서버는 본 적이 없습니다.
4.3.3 REST 서버 만들기
- GitHub 저장소 (https://github.com/hyungju-lee/node-study/blob/master/ch4/4.2/restServer.js) ch4 소스 참조
-
restServer.js에 주목
-
GET 메소드에서
/
,/about
요청 주소는 페이지를 요청하는 것이므로 HTML 파일을 읽어서 전송합니다.
AJAX 요청을 처리하는/users
에서는 users 데이터를 전송합니다.
JSON 형식으로 보내기 위해JSON.stringify
를 해주었습니다.
그 외의 GET 요청은 CSS나 JS 파일을 요청하는 것이므로 찾아서 보내주고, 없다면 404 NOT FOUND 에러를 응답합니다. -
POST와 PUT 메소드는 클라이언트로부터 데이터를 받으므로 특별한 처리가 필요합니다.
req.on("data", 콜백)
과req.on("end", 콜백)
부분인데요.
3강의 버퍼와 스트림에서 배웠던readStream
입니다.
readStream
으로 요청과 같이 들어오는 요청 본문을 받을 수 있습니다.
단, 문자열이므로 JSON으로 만드는JSON.parse
과정이 한 번 필요합니다. - DELETE 메소드로 요청이 오면 주소에 들어있는 키에 해당하는 사용자를 제거합니다.
- 해당하는 주소가 없을 경우 404 NOT FOUND 에러를 응답합니다.
-
<!-- about.html -->
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<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>
/* restFront.css */
a { color: blue; text-decoration: none; }
<!-- restFront.html -->
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<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.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 (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 호출
// 폼 제출(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 (err) {
console.error(err);
}
e.target.username.value = '';
});
// restServer.js
const http = require('http');
const fs = require('fs').promises;
const users = {}; // 데이터 저장용
// 서버를 만듭니다.
http.createServer(async (req, res) => {
try {
// 요청이 오면 서버는 응답을 해야됩니다.
// 응답을 안하면 브라우저는 계속 기다리다가 30초 정도 응답을 안하면 포기를 합니다.
// 웬만하면 30초 안에 응답을 해주는 것이 좋습니다.
// 요청이 왔을 때 서버가 어떤 식으로 응답할 건지 적어줍니다.
// req.method - 지금까지 저희는 res만 썼었는데 req가 request 요청에 관한 것이고 res가 response 응답에 관한 겁니다.
// req에서 요청에 관련된 정보를 얻어낼 수 있습니다.
// 주소창에 "localhost:8082"를 입력하고 엔터를 누르면 서버로 요청을 보내는겁니다. 보통 이런건 GET 요청을 보내는 겁니다.
// 주소창에 "localhost:8082"를 입력하는 것은 페이지를 가져오는 것이죠? 즉, GET 요청을 보내는 겁니다.
// "localhost:8082" 입력하면 사실상 뒤에 "/"를 생략한 거거든요?
// 즉, "localhost:8082" 하면 GET 요청에다가 url은 "/" 슬래쉬
// 즉, HTTP 메소드가 GET이고
if (req.method === 'GET') {
// url이 "/" 슬래쉬일 때
if (req.url === '/') {
// 이때는 restFront.html을 읽어서 전달해줘라. 이 의미가 되는 겁니다.
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': 'application/json; charset=utf-8'});
return res.end(JSON.stringify(users));
}
// /도 /about도 /users도 아니면
try {
const data = await fs.readFile(`.${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('ok');
});
}
} 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('ok');
});
}
} 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('ok');
}
}
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);
}
})
// 해당 서버는 포트 8082에 연결했습니다.
.listen(8082, () => {
console.log('8082번 포트에서 서버 대기 중입니다');
});
4.3.4 REST 서버 실행하기
-
localhost:8082
에 접속
4.3.5 REST 요청 확인하기
-
개발자도구(F12) Network 탭에서 요청 내용 실시간 확인 가능
- Name은 요청 주소, Method는 요청 메소드, Status는 HTTP 응답 코드
-
Protocol은 HTTP 프로토콜, Type은 요청 종류(xhr은 AJAX 요청)
개발자창의 NetWork 탭에서 실제 요청에 대한 정보를 볼 수 있습니다.
새로고침을 누르는 것도 다시 "GET", "/"를 보내는 겁니다.
메소드는 GET 그리고 status는 200이라고 뜹니다.
위 코드의 res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
이 부분에서 숫자 200이 쓰여있었죠?
즉, .writeHead()
메소드가 대충 어떤건지를 이해하실 수도 있으실거 같긴 한데 숫자 200은 "성공"을 뜻합니다.
너가 보낸 "GET", "/" 요청은 성공했어. 우린 성공적으로 응답했어. 라는 걸 알려주는 겁니다. 그리고 같이 전달한 Content-Type
관련 정보도 보낸 그대로 들어있습니다.
.writeHead()
메소드는 이 Headers 부분을 작성하는 메소드였다는 것을 알 수 있습니다.
그럼 그 외 다른 것들은?
다른 것들은 알아서 넣어주는 것들입니다.
알아서 들어가게 할 수도 있고 아니면 .writeHead()
메소드로 저희가 넣을 수도 있고.
Request Header는 요청을 보낼 때 들어가는 헤더들인데 이런거는 브라우저가 알아서 넣어줬기 때문에 들어가는 겁니다.
그럼 Headers가 뭐냐?
헤더는 데이터들에 대한 데이터이거든요?
즉, 위와 같이 GET, / 요청을 보내면 위와 같이 응답이 오잖아요?
html에 대한 응답이 오는데 그 html에 대한 데이터를 Headers에 담아서 보내줍니다.
text/html
을 통해 html이란걸 알려주고 charset=utf-8
로 인코딩 방법을 알려주고 Date
를 통해 몇시에 응답했는지 알려주고 Status Code
를 통해 성공했는지 실패했는지 알려주고 Request URL
과 Request Method
도 알려주고..
즉, 데이터에 대한 데이터라고 보시면 됩니다.
메타 데이터라고도 합니다.
눈에 보이지는 않지만 요청 응답하면서 데이터들을 주고받으면서 이 Headers를 이용하는 겁니다.
이 Request Headers도 살펴보시면 요청에 대한 데이터가 담겨져있습니다.
저희가 GET
요청을 보냈고 /
란 url을 보냈잖아요?
그런 요청에 대한 부가적인 데이터들을 Request Headers에 담습니다.
- Host는
localhost:8082
이고 - Cache-Control 캐쉬는 사용하지 않을 거고
no-cache
- Accept-Language 언어는 한글이고
- Accept-Encoding은 해당 방식(
gzip
,deflate
,br
)으로만 전달해주면 압축해서 보내줘도 해당 형식이면 우리가 풀겠다. -
Accept는 응답으로 html이나 xhtml+xml이나 image나 그런거 보내주면 우리가 받겠다.
이게 브라우저도 응답을 거부할 권한이 있습니다.
예를 들어 요청을 보냈는데 서버가 엉뚱한걸 보내주잖아요?
그럼 그것도 보안에 있어서 위협적이거든요.
즉, Accept라고 적혀져있는 범위 내에서만 응답을 받을거야 라는 겁니다.
Accept에 기재된 형식대로 서버가 안 보내주면 브라우저도 화면 표시를 거부할 수가 있습니다.서버쪽도 보안적인 권한이 있고 브라우저도 어느정도 권한이 있는거죠.
- 많이 들어보셨을법한 Cookie 관련해서도 들어있습니다.
- User-Agent를 통해 내 브라우저가 어떤건지도 알려줍니다.
Headers 관련한 것도 너무 많기 때문에 외우시지 마시고 그때그때 찾아보시는게 좋습니다.
저도 중요한 것 위주로만 알려드릴겁니다.
지금은 그냥 이렇게 많은 정보들을 주고받고 그러는구나! 그정도만 알아두시면됩니다.
4.4 POST, PUT, DELETE 요청 보내기
a태그도 GET 요청입니다.
즉 위 a태그를 누르면 GET/about
요청을하는데 해당 버튼을 누르면
위 about.html
페이지로 들어가집니다.
해당 부분도 위 코드에 작성해놨었죠?
GET/about
요청을 하면 위 코드가 실행되는 겁니다.
즉, 이렇게 css
와 js
파일을 프론트로 보내줍니다.
보통 웹 사이트를 구성할 때, html, css, javascript 이렇게 3개를 사용하는데, 이 3개를 직접 모두 코딩을 해서 전달을 해줘야됩니다.
"css
나 js
는 html
에 달려있으니까 자동으로 가겠지?" 라고 생각하시면 안됩니다.
위와 같이 페이지 요청 코드를 주석처리하고 http://localhost:8082/restFront.js
요청을 보내면, NOT FOUND
라고 뜬다.
뿐만아니라 link
, script
태그로 걸려있는 파일들에 대해서도 응답 실패가 뜬다.
NOT FOUND
라고 뜨는 이유는 코드에서 이렇게 뜨도록 했기 때문이다.
위와 같이 경로가 걸려있는 것들도 다 서버로 GET 요청을 보내는 거거든요?
GET/restFront.css
, GET/restFront.js
이렇게 요청을 보내는 겁니다.
요청을 보내면 서버는 반드시 그 요청에 대한 판단을 해서 응답을 해줘야됩니다.
응답 코드를 작성 안하면 위와 같이 모두 응답 실패의 의미로 빨간색으로 뜹니다.
GET/restFront.css
, GET/restFront.js
위의 요청이 빨강색 코드 영역에선 실행되지 않고(왜? 조건에 부합하는게 없으니깐) 주황 부분에서 실행됩니다.(req.url
)
이런 원리로 응답을 보내주는 겁니다.
그런데 클라이언트에서 요청을 할 때 위와 같이 짜놓아도 위 빨강, 주황 영역에서 실행 안될 때도 있거든요?
부합하는 조건이 없거나 그래서 실행이 안되면
위 부분이 실행됩니다.
res.writeHead(404)
, res.end("NOT FOUND")
.. 이번엔 숫자 200이 아니라 404입니다.
404 NOT FOUND는 다들 한번쯤은 보셨을 겁니다.
이는 요청을 했는데 서버거 그 요청에 대한 정보를 찾지 못했을 때 뜹니다.
이 8082포트에 연결된 서버는 hyungju-lee
라는 요청을 모르는 겁니다.
그럴 때 이런 404 에러가 뜹니다.
이렇게 input에 "123123"을 입력하고 등록 버튼을 누르면
위 코드가 실행됩니다.
이러면 서버에서도 POST/user
가 있어야겠죠?
POST
요청은 데이터가 들어갑니다.
axios.post("/user", { name });
에서 이 부분 {...}
이렇게 데이터도 같이 보냅니다.
이 데이터를 서버에서 받아서 처리를 해줘야되는데 그럴 땐 위의 코드처럼 받아줘야됩니다.
위의 코드 형식을 사용하시면 된다고 생각하시면 됩니다.
이 예제 코드는 const users = {}
에 담는 거라, 즉, 메모리에 저장되는 것이라 서버를 재시작하면 정보가 지워집니다.
위와 같이 사용자를 등록했으면 사용자를 가져오는 것도 필요한데,
이 부분입니다.
위 부분 보시면 application/json
이라고 되어있는데 html이 아니라 JSON 형식으로 보내겠다는 뜻이고 JSON으로 보낼 때는 대신에 JSON.stringify()
를 해주셔야됩니다.
저희가 응답할 때 보낸 번호가 200
, 201
, 404
가 있는데, 이런 번호들 있잖아요?
이 번호들은 http status code
라고 부릅니다.
위 사이트에서 내가 어떤 상태코드로 응답을 해줄 것인가.를 찾으시면 됩니다.
보통 200
, 201
, 404
, 403
, 500
, 503
이 정도가 많이 쓰이고 409
번 같은 경우는 극히 드물게 쓰이는데, 만약에 여러분의 상황이 해당 상태코드의 의미와 맞다면 그 코드를 쓰시면 됩니다.
414
에러처럼 클라이언트가 요청한 URI 주소의 길이가 너무 긴 경우에 쓰는 것도 있습니다. 418
과 같은 이스터에그같은 코드도 있습니다.
무슨 의도로 만들었는지는 모르겠지만..
POST
에서 user 생성하고 GET
해서 user를 가져오고.
간단하진 않죠?
아까 http server 예제하던거에 비해서 난이도가 확 올라갔다고 느끼실텐데, 단순한 페이지만 제공하는 것이 아니라, css
, js
, image
등과 같은 파일도 제공해야돼고.. 아래와 같은 식으로.
그리고 POST
, PUT
, PATCH
, DELETE
이러한 요청들 있죠?
그러한 요청들에 대한 처리, 처리하면서 요청에 들어있는 데이터도 꺼내와야되고..
PUT
도 요청 처리하면서 바꿀 user 대한 데이터를 꺼내와서 실제로 바꿔줘야되고..
DELETE
할 때도 user id를 가져와서 delete를 해줘야되고..
그래서 상당히 복잡해지는데..
그리고 현재 위의 예시 코드 자체도 지저분합니다.
if
문을 계속 사용한 것도 그렇고..
그래서 난 이런식으로 서버만들기 불편하다 하시면 보통 남들이 만들어둔 코드를 써서 좀 더 깔끔하게 만드는게 있거든요?
그거는 6장.
6장에서 express
를 쓰면서 같이 알아봅시다.
기본적으로 http
만 써서 만들 때는 위와 같은 식으로 만들어야됩니다.
코드가 상당히 지저분할 수밖에 없어요.
처음에 이 REST API 서버 해보시면서 직접 여러분들이 코드 수정을 해보세요.
수정을 직접 해보시면서 페이지도 더 추가를 해보시고.. 현재는 restFront.html
, about.html
페이지가 2개있죠?
/users
는 JSON을 가져오는 요청이기 때문에 제외. 페이지는 아니니깐.
페이지를 더 늘리는 것도 해보시고 user를 넣었다면 게시글 기능도.. 포스팅하는 것과 가져오는 것과 PATCH
, DELETE
까지 여러분들이 코드를 직접 더 추가를 해보십시오.
이런걸 하다보면 여러분들이 구조에 대한 감도 잡히시고 어떤 공통된 규칙, "아, 이거는 이런식으로 만들어나가면 편하겠구나." 이런 감이 잡히실겁니다.
눈으로만 보시는 것은 도움이 아마 하나도 안될겁니다.
아직 로그인이나 로그아웃은 구현할 생각하지 마시고 단순한거부터. 페이지들이 조금 있고 데이터 등록하고 데이터 제거하고 수정하고. 요정도?
로그인은 원리를 알아야하기 때문에 지금은 아직 좀 벅찹니다.
다음절에서 쿠키와 세션에 대해 공부하면서 로그인과 로그아웃이 어떻게 구현되는지를 보여드리도록 하겠습니다.
여튼 페이지 더 추가 + 게시글 기능 만들기! + 댓글도!
4.5 쿠키 이해하기
이번 시간은 쿠키와 세션에 대해 다뤄보도록 하겠습니다.
쿠키와 세션은 저희가 로그인, 로그아웃할 때 필수로 알아두셔야하는 개념입니다.
지난 시간에 REST API를 하면서 기본적으로 여러개의 페이지들,
- 주소별로 페이지들도 제공을 하고,
- 그 다음에 파일 - js나 css도 제공을 하고
- 그리고 POST, PUT, DELETE 같이 페이지나 파일은 아니지만 서버쪽 자원을 요청하는 거 해봤었죠?
- 사용자 등록해보고 이름도 수정해보고 삭제도해보고
- 그 다음에 사용자 리스트 가져오고.
그리고 지난시간에 배운걸 활용해서 게시글이든 댓글이든 여러분들이 직접 추가해보시라고 말씀드렸었죠?
그러지 않으면 그 코드를 이해하지 못하실겁니다.
공부하실 땐 항상 코드를 여러분들 것으로 소화시켜야됩니다.
여튼 공부하시다가 로그인/로그아웃/회원가입 같은 것도 구현해보고 싶다는 생각을 많이 해보셨을텐데요,
해당 기능을 구현하시려면 쿠키와 세션에 대해 아셔야됩니다.
- 쿠키와 세션 이해하기 -
4.5.1 쿠키의 필요성
저희가 매번 요청을 보내고, 그에따른 응답을 보내주고 이 사이클을 반복을 많이 하는데 요청에는 한 가지 단점이 있습니다.
-
요청에는 한 가지 단점이 있음
- 누가 요청을 보냈는지 모름(IP 주소와 브라우저 정보 정도만 앎)
IP만 알아도 다 아는거 아닌가요? 라고 하실 수도 있는데, PC방 같은 곳은 모든 컴퓨터의 IP가 다 똑같습니다.
이럴 때는 그 사람이 누군지 알 수 없겠죠?
이럴 때 로그인 기능을 구현해서 요청을 한 사람이 누군지 알아내면됨 - 로그인을 구현하면 됨
- 쿠키와 세션이 필요
로그인을 구현할 때 필요!!!
- 누가 요청을 보냈는지 모름(IP 주소와 브라우저 정보 정도만 앎)
-
쿠키: 키 = 값의 쌍
쿠키는 기본적으로 로그인에만 쓰이진 않습니다.
요청을 보낼 때 같이 정보를 보내주는 그런 개념입니다.
그 정보에키=값
이런 형식으로 데이터를 넣어줄 수 있으니까 요청을 보낼 때 쿠키에다 제가 누군지를 넣어서 보내주면 서버가 "아, 너 누군지 알겠어" 이렇게 쿠키를 읽어서 누군지를 알 수가 있겠죠?- name=hyungjulee
- 매 요청마다 서버에 동봉해서 보냄
- 서버는 쿠키를 읽어 누구인지 파악
이러한 쿠키는 브라우저가 알아서 보내줍니다.
즉 저희가 할 일은 쿠키에다가 그 사람이 누군지만 적어놓으면 되는겁니다.
그럼 브라우저가 알아서 서버로 요청을 보낼 때 쿠키를 넣어서 보내줍니다.
위 이미지를 보시면 처음에 브라우저가 요청을 보낼 때 서버에서 먼저 쿠키를 넣어서 응답을 합니다.
그럼 브라우저는 그 쿠키를 저장하고있다가 나중에 요청을 보낼 때 그 쿠키와 함께 요청을 보내면 서버에서 요청을 받고 쿠키를 읽어서 그 요청이 누구로부터 왔는지 알 수 있겠죠?
처음 한번 서버로부터 쿠키를 받으면 그 다음부터 쿠키를 브라우저에서 서버로 보내고 그에 따라서 서버는 요청을 누가 보냈는지 알고 이렇게 할 수 있습니다.
여튼 처음엔 서버에서 브라우저로 쿠키를 보내줘야합니다.
그것을 저희가 직접 구현할 수 있습니다.
4.5.2 쿠키 서버 만들기
-
쿠키 넣는 것을 직접 구현
writeHead
: 요청 헤더에 입력하는 메소드Set-Cookie
: 브라우저에게 쿠키를 설정하라고 명령
-
쿠키: 키=값의 쌍
- name=hyungjulee
-
매 요청마다 서버에 동봉해서 보냄
// cookie.js 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번 포트에서 서버 대기 중입니다!"); })
저희가 이전에
writeHead
메소드를 사용했었죠?
writeHead
에서Content-Type
을 전달했었습니다.res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
위와 같이
Content-Type
이라고 보냈더니 개발자 창에서 Headers 부분에 content type이 들어있었던 것처럼 헤더에Set-Cookie
라고 넣어 보낼 수 있습니다.
그리고mycookie=test
,키=값
형태로 보낼 수 있습니다.
키=값
형태로 보내야 나중에 객체로 전환하면 mycookie가 속성명이되고 test가 속성값이 되겠죠.
이런식으로Set-Cookie
를 서버에서 보내주면 브라우저는Set-Cookie
를 파악해서 브라우저에다가mycookie=test
라는 걸 넣습니다.그리고 이 다음 요청부터는 알아서 브라우저가 브라우저에 있는 Cookie를 전달을 해줍니다.
그럼 위 코드의console.log(req.url, req.headers.cookie);
의req.headers.cookie
이렇게 Cookie를 읽을 수 있게됩니다.
요청의 헤더에 들어있는 쿠키를 읽는 것이죠.
4.5.3 쿠키 서버 실행하기
req.headers.cookie
: 쿠키가 문자열로 담겨있음-
req.url
: 요청 주소 -
localhost:포트번호
에 접속- 요청이 전송되고 응답이 왔을 때 쿠키가 설정됨
- favicon.ico는 브라우저가 자동으로 보내는 요청
-
두번째 요청인 favicon.ico에 쿠키가 넣어짐
실습
// cookie.js
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번 포트에서 서버 대기 중입니다!');
});
여튼 처음 서버에서 Set-Cookie
로 쿠키를 브라우저로 보내주면, 다음 브라우저가 서버로 요청보낼 때는 그 쿠키를 알아서 같이 넣어서 요청을 보내니까 서버쪽에서는 내가 누군지 알 수 있는 것.
4.5.4 헤더와 본문
-
http
요청과 응답은 헤더와 본문을 가짐- 헤더는 요청 또는 응답에 대한 정보를 가짐
- 본문은 주고받는 실제 데이터
-
쿠키는 부가적인 정보이므로 헤더에 저장
위의 Request Payload 기억나시나요?
이 부분이 요청 보낼 때의 데이터라고 했습니다.
이를 다른말로 본문 또는 Body 또는 요청 데이터라고 합니다.
그리고 위 부분을 응답의 데이터 또는 응답의 본문 또는 응답의 Body라고 합니다.
그럼 위 Headers는 뭐라고 그랬죠?
데이터들의 데이터라고 했습니다.
그래서 요청은 Headers, Body로 구성이 되어있고 응답도 Headers, Body로 구성이 되어있습니다.
본문, Body는 실제 데이터이고 Headers는 그런 데이터들의 데이터라고 그랬잖아요?
쿠키도 화면에 보이는 그런게아니라 부가적인 정보이므로 헤더에 저장을 하게됩니다.
4.5.5 http 상태 코드
-
writeHead
메소드에 첫 번째 인수로 넣은 값- 요청이 성공했는지 실패했는지를 알려줌
- 2XX: 성공을 알리는 상태코드입니다. 대표적으로 200(성공), 201(작성됨)이 많이 사용됩니다.
- 3XX: 리다이렉션(다른 페이지로 이동)을 알리는 상태코드입니다.
어떤 주소를 입력했는데 다른 주소의 페이지로 넘어갈 때 이 코드가 사용됩니다.
대표적으로 301(영구 이동), 302(임시 이동)가 있습니다. - 4XX: 요청 오류를 나타냅니다. 요청 자체에 오류가 있을 때 표시됩니다. 대표적으로 401(권한없음), 403(금지됨), 404(찾을 수 없음)가 있습니다.
- 5XX: 서버 오류를 나타냅니다. 요청은 제대로 왔지만 서버에 오류가 생겼을 때 발생합니다.
이 오류가 뜨지 않게 주의해서 프로그래밍해야 합니다.
이 오류를 클라이언트로res.writeHead
로 직접 보내는 경우는 없고, 예기치 못한 에러 발생 시 서버가 알아서 5XX대 코드를 보냅니다.
500(내부 서버 오류), 502(불량 게이트웨이), 503(서비스를 사용할 수 없음)이 자주 사용됩니다.
4.5.6 쿠키로 나를 식별하기
아까 위의 예제 코드에선 mycookie=test
만 해서 부족했는데 이번엔 진짜 실전 예제로 제가 누군지 파악하는 예제로 가보겠습니다.
-
쿠키에 내 정보를 입력
parseCookie
: 쿠키 문자열을 객체로 변환- 주소가
/login
인 경우와/
인 경우로 나뉨 /login
인 경우 쿼리스트링으로 온 이름을 쿠키로 저장-
그 외의 경우 쿠키가 있는지 없는지 판단
- 있으면 환영 인사
- 없으면 로그인 페이지로 리다이렉트
<!-- cookie2.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>쿠키&세션 이해하기</title>
</head>
<body>
<form action="/login">
<input id="name" name="name" placeholder="이름을 입력하세요" />
<button id="login">로그인</button>
</form>
</body>
</html>
// cookie2.js
const http = require('http');
const fs = require('fs').promises;
const url = require('url');
const qs = require('querystring');
// req.headers.cookie에 다음과 같이 담겨서 온다면..
// Webstorm-12d1a888=49bc4788-1696-4c39-9544-9e16969bfd05; mycookie=test
const parseCookies = (cookie = '') =>
cookie
.split(';') // ['Webstorm-12d1a888=49bc4788-1696-4c39-9544-9e16969bfd05', 'mycookie=test']
.map(v => v.split('=')) // [ ['Webstorm-12d1a888', '49bc4788-1696-4c39-9544-9e16969bfd05'], ['mycookie', 'test'] ]
.reduce((acc, [k, v]) => {
acc[k.trim()] = decodeURIComponent(v);
return acc;
}, {}); // {Webstorm-12d1a888: '49bc4788-1696-4c39-9544-9e16969bfd05', mycookie: 'test'}
http.createServer(async (req, res) => {
const cookies = parseCookies(req.headers.cookie); // { mycookie: 'test' }
// 주소가 /login으로 시작하는 경우
if (req.url.startsWith('/login')) {
const { query } = url.parse(req.url);
const { name } = qs.parse(query);
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();
// name이라는 쿠키가 있는 경우
} else if (cookies.name) {
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end(`${cookies.name}님 안녕하세요`);
} else {
try {
const data = await fs.readFile('./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번 포트에서 서버 대기 중입니다!');
});
- Expires : 쿠키 유효기간, 설정안하면 Sessions
- HttpOnly : 이건 자바스크립트로 쿠키에 접근하지 못하게.
쿠키들을 자바스크립트로 접근할 수 있다면 위험하기 때문에.
보안에 위협이 되기 때문에HttpOnly
라고 설정.
특히 로그인에 사용되는 쿠키는HttpOnly
가 필수라고 보시면 됨.
자바스크립트로 접근할 수 있게 해놓으면 해커들이 악성 자바스크립트를 심어서 쿠키도 탈취를 해서 보내버릴 수도 있기 때문에 로그인과 같은 쿠키는 자바스크립트로 접근할 수 없게 설정해야됨. - Path=/ :
localhost:8084/
여기서/
아래있는 주소는 쿠키가 다 유효하다 라는 뜻.
위와 같이 쿠키엔 안전장치들이 다 마련되어있음.
즉, 그렇기 때문에 로그인 기능을 구현하실 땐 이렇게 쿠키를 사용하시는게 안전함.
안그러면 직접 데이터를 전송/응답 형식으로 처리하셔야되는데, 그러면 이런 안전장치들을 직접 구현하셔야됨.
그럴바엔 이렇게 쿠키로해서 브라우저가 마련해준 안전장치 사용하는게 좀 더 편하겠죠? 특히 초보자 입장에선.
그래서 같은 /
슬래쉬라도 쿠키가 있냐 없냐에 따라서 화면을 다르게 그려줄 수 있습니다.
이렇게 쿠키가 있냐없냐에 따라 분기 처리를 해주게되면
이전에 예제 코드로 보여줬던 restServer.js
같은 서버쪽 코드에도 분기 처리를 해줘야되니까 코드가 점점 더 복잡해지겠죠?
if
문도 계속 사용하게될거고..
if (req.method === "GET") {
if (req.url === "/") {
if (cookies.name) {
// ...
// 이렇게 if문이 계속계속 중첩되고 많아질 겁니다.
}
}
}
이렇게 코드 지저분해지는걸 방지하시려면 6강을 참조해주시고.. 일단은 이렇게 만들어야 된다는 것.
여튼 이렇게 쿠키를 알아봤습니다.
쿠키 유효기간이 아직 안 끝난 상태라면 새로고침을해도 로그인 상태가 유지됩니다.
유효기간이 끝나면 다시 로그인이 해제될겁니다.
가끔 여러분들이 로그인을 하셨다가 일정시간 지나면 로그인이 풀린거 경험하셨을텐데 그런게 쿠키의 유효기간이 끝나서 로그인이 저절로 끝나버린거라고 생각하시면 됩니다.
4.5.7 쿠키 옵션
-
Set-Cookie
시 다양한 옵션이 있음- 쿠키명=쿠키값: 기본적인 쿠키의 값입니다. mycookie=test 또는 name=hyungjulee 같이 설정합니다.
- Expires=날짜: 만료 기한입니다. 이 기한이 지나면 쿠키가 제거됩니다. 기본값은 Sessions(클라이언트가 종료될 때까지입니다.)
Expires를 할 땐 날짜를 정확하게 입력해줘야됩니다.
위의 코드에서 봤듯이expires.setMinutes(expires.getMinutes() + 5)
이렇게 설정한 다음에
Expires=${expires.toGMTString()}
이런식으로 날짜를 정확하게 입력 - Max-age=초: Expires와 비슷하지만 날짜 대신 초를 입력할 수 있습니다. 해당 초가 지나면 쿠키가 제거됩니다. Expires보다 우선합니다.
- Domain=도메인명: 쿠키가 전송될 도메인을 특정할 수 있습니다. 기본값은 현재 도메인입니다.
- Path=URL: 쿠키가 전송될 URL을 특정할 수 있습니다. 기본값은
/
이고 이 경우 모든 URL에서 쿠키를 전송할 수 있습니다. - Secure: HTTPS일 경우에만 쿠키가 전송됩니다.
- HttpOnly: 설정시 자바스크립트에서 쿠키에 접근할 수 없습니다. 쿠키 조작을 방지하기 위해 설정하는 것이 좋습니다.
4.5.8 쿠키 서버 실행하기
-
localhost:8084 포트에 접속
- Application 탭(F12) 열기
-
로그인을 하면 쿠키가 생성됨
4.6 세션 사용하기
중요한 정보를 브라우저로 보내지 않고 중요한 정보는 서버가 들고있고 그 중요한 정보에 접속할 수 있는 key같은 거만 브라우저에 보내줘서 브라우저에서는 중요한 정보를 알아낼 수 없게 그렇게 만드는 것 중에 세션 방법이 있습니다.
4.6.1 세션 사용하기
-
쿠키의 정보는 노출되고 수정되는 위험이 있음
- 중요한 정보는 서버에서 관리하고 클라이언트에는 세션 키만 제공
- 서버에 세션 객체(session) 생성 후, uniqueInt(키)를 만들어 속성명으로 사용
- 속성 값에 정보 저장하고 uniqueInt를 클라이언트에 보냄
// session.js
const http = require('http');
const fs = require('fs').promises;
const url = require('url');
const qs = require('querystring');
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 { query } = url.parse(req.url);
const { name } = qs.parse(query);
const expires = new Date();
expires.setMinutes(expires.getMinutes() + 5);
const uniqueInt = Date.now();
session[uniqueInt] = {
name,
expires,
};
res.writeHead(302, {
Location: '/',
// 아래와 같이 uniqueInt 값 전달
'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('./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번 포트에서 서버 대기 중입니다!');
});
여튼 서버쪽에서 위의 const session = {}
객체를 활용해 데이터를 저장해놓을 수 있습니다.
이러한 객체 하나를 마련해두시고, 위 코드를 보시면 login 하는 것까지는 코드가 똑같은데
const uniqueInt = Date.now();
이 부분이 조금 다릅니다.
저희가 session
에다가 uniqueInt
라는 키. 이게 키입니다. 저희가 사용할 키
const uniqueInt = Date.now();
session[uniqueInt] = {
name,
expires,
};
위의 코드 중에서 위 부분이 있는데, 이 키는 여러분들이 마음대로 만들으셔도되는데, 다른 사람이랑 겹치면 분명 제가 로그인했는데 다른사람의 것으로 로그인되는 황당한 상황이 발생할 수도 있겠죠?
uniqueInt
이건 안 겹치게 만들어주시면 되고.. 저는 지금 현재시간(Date.now()
)을 사용했습니다.
이 uniqueInt
를 사용해서 session
에 name
과 expires
를 넣습니다.
name
엔 "이형주", expires
엔 5분, 이런 식으로 들어가겠죠?
res.writeHead(302, {
Location: '/',
'Set-Cookie': `session=${uniqueInt}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`,
});
그래서 위와 같이 브라우저엔 uniqueInt
를 보냅니다.
저의 name
이나 expires
를 보내는 것이 아니라 uniqueInt
라는 고유 키값만 보내는겁니다.
그럼 위의 value를 가지고 어떻게 "이형주"라는걸 알아내느냐.
그거는 아래와 같이..
if (req.url.startsWith('/login')) {
// ...
} 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}님 안녕하세요`);
}
session[cookies.session].expires > new Date()
이 부분은 원래는 브라우저에서 쿠키가 만료되었으면 세션 쿠키를 보내주지 않지만 저희가 노파심에 한번 더 안전 장치를 걸어놓은 겁니다.
한번 더 검사해서 손해볼 일은 없으니까요.
보안은 철저할 수록 좋습니다.
여튼 세션 쿠키가 존재하고 만료기간이 지나지 않았다면
res.end(`${session[cookies.session].name}님 안녕하세요`);
이 부분이 실행되면서 "이형주님 안녕하세요"가 응답됩니다.
브라우저는 서버로 세션 쿠키만 보내주고 서버쪽에서 그 세션에 접근해서 이름을 꺼내와서 쓰는거죠.
브라우저는 의미를 알 수 없는 키를 가지고 있는 거고 그 키를 통해서 서버의 실제로 중요한 데이터에 접근을 하는겁니다.
그런데 궁금한게
Date.now()
값으로 전송한session
이야 만료되면 없어지는데,const session = {};
에 담긴session[uniqueInt] = { name, expires, };
이 값은 없어지나..? 안 없어지는거 아닌가?
그럼 아직 보안상 문제가 될 여지가 남은거아냐?
흠..
4.6.2 세션 서버 실행하기
방금 위에서 세션 예시 코드를 보여드렸는데, 위에 쓰인 방식은 정말 여러분들의 이해를 돕기 위해서 간단하게 만들은거고, 실제로 위와 같은 방식으로하면 보안상 위협이 있을겁니다.
6장에서 실제 실무에서 세션을 어떻게 활용할 수 있는지 보여드리겠습니다.
-
localhost:8085
-
실 서버에서는 세션을 직접 구현하지 말자
- 6장에서 나오는 expression-session 사용하기
4.7 https, http2
4강에서 저희가 서버 만들고 REST API에 따라서 if
문으로 분기 처리했었죠?
그리고 로그인도 간단하게 어떤식으로 개념이 돌아가는지 그것도 알려드렸는데 이 https
는 조금 부가적인 거라고 생각하시면 됩니다.
요즘 https
가 개인정보 보호법 같은 것 때문에 거의 필수가 되어가고 있거든요?
4.7.1 https
-
웹 서버에 SSL 암호화를 추가하는 모듈
- 오고 가는 데이터를 암호화해서 중간에 다른 사람이 요청을 가로채더라도 내용을 확인할 수 없음
-
요즘에는
https
적용이 필수 (개인 정보가 있는 곳은 특히)https
가 적용되면 위와 같이 좌물쇠가 채워집니다.
이걸하면 뭐가 좋냐면,https
를 왜 해야되냐면요청 보낼 때 Headers 부분에 너무 많은 것들이 담겨있잖아요?
주소도 나오고 세션 쿠키 값도 나오고 쿼리스트링도 들어있고..
이러한 정보들이 탈취가 될 수 있습니다.특히 로그인을 할 때 서버쪽으로 이메일이나 아이디나 비밀번호 등을 같이 보내줘야하잖아요?
그럼 queryString에 이메일, 비밀번호가 나와있거든요.
그럼 해커들이 이http
요청을 탈취해버리면 비밀번호, 아이디, 이메일을 그대로 빼앗겨버리죠.그런데
https
를 적용해놓으면 이 요청들 자체가 암호화되어서 전달이 됩니다.
그러면 중간에 해커가 그거를 가로채더라도 모르겠죠?
암호화되어있으니까 풀 수가 없겠죠.
실제로 그 암호는 강력해서 웬만한 경우가 아니고선 풀리지가 않습니다.
실무에서는 항상https
를 적용해두셔야 합니다.
안그러면 해커들이 중간에 요청과 응답을 가로채서 털어갈 수도 있습니다.그래서
https
는 실무에서 거의 필수라고 보시면돼고 당연히 노드에서도https
를 하는 방법을 지원을 합니다.
4.7.2 https 서버
-
http 서버를 https 서버로
- 암호화를 위해 인증서가 필요한데 발급받아야함
-
createServer가 인자 두 개 받음
- 첫번째 인자는 인증서와 관련된 옵션 객체
- pem, crt, key 등 인증서를 구입할 때 얻을 수 있는 파일 넣기
-
두번째 인자는 서버 로직
// 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번 포트에서 서버 대기 중입니다!'); });
위와 같은
http
서버를// server1-3.js const https = require('https'); const fs = require('fs'); https.createServer({ cert: fs.readFileSync('도메인 인증서 경로'), key: fs.readFileSync('도메인 비밀키 경로'), ca: [ fs.readFileSync('상위 인증서 경로'), fs.readFileSync('상위 인증서 경로'), ], }, (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(443, () => { console.log('443번 포트에서 서버 대기 중입니다!'); });
위와 같이
https
로 바꿔주실 수가 있습니다.
http
와https
모듈, 서로 다릅니다.
그리고createServer
로 해서 2번째 인자는 이전 예제들 처럼 똑같이 만드시면 되는데 첫번째 인자가 추가됩니다.
위 코드를 보시면cert
,key
,ca
이렇게 3개를 넣어주셔야되거든요?그런데
https
는 그냥 막 할 수 있는게 아니라 아까 암호화하는 거 있죠?
암호화해서 보내는데 인증서를 인증 기관에서 얻어와야합니다.
그래서 나중에https
적용할 때는 인증서를 인증 기관에서 얻어오는 절차를 가지는데 만약zerocho.com
을 인증기관에 신청했다고 하면, 인증 기관에서 "어 너네 인증됐어"하면서cert
,key
,ca
파일들을 줍니다.
그 파일들을 주면 폴더에 저장을 해주시고fs.readFileSync()
로 읽어서 위와 같이 넣어주시면 됩니다.서버에서는 sync 쓰지 말라고했는데 위에 보시면
readFileSync
를 썼잖아요?
sync 써도 되는 경우 말씀드렸잖아요?
딱 한번 실행하거나 아니면 서버의 경우엔 초기화할 때.
서버를 시작하기 전에 서버를 초기화할 때는 sync를 써도됩니다.만약
cert
,key
,ca
를 안넣고https
서버를 돌리면, 좌물쇠 모양은 뜨지만, 빨갛게 인증서에 문제가 있습니다. 라고 뜰겁니다.
즉, 공식 인증기관에서 받은걸 꼭 넣어주셔야됩니다.공식 인증 기관 중에서 가장 유명하고 무료인 것이
letsencrypt
라고 있습니다.
https://letsencrypt.org/ko/
여기서 받으시면 됩니다.실무에선 반드시
https
.
https
하면 포트번호가443
이라는 거.
물론443
번 말고 다른 번호로도 할 수 있는데,443
으로 해야만 생략이 가능하죠?https
인 경우에는?
www.naver.com:443
네이버도 이렇게 포트번호가443
인데https
인 경우443
은 생략 가능하니까www.naver.com
로도 접속 가능.
4.7.3 http2
-
SSL 암호화와 더불어 최신 HTTP 프로토콜인 http/2를 사용하는 모듈
- 요청 및 응답 방식이 기존 http/1.1보다 개선됨
-
웹의 속도도 개선됨
위에서 저희가 한거는
http/1.1
입니다. (그냥http
로 하면 버전이 1.1)
http2
는 버전이 2.
그리고 지금은http3
까지 나왔는데http3
는 아직 많이 쓰이진 않음.
http2
는 암호화까지 같이 포함이돼서 요청도http
보다 더 빨리 보낼 수 있음.위 그림과 같이
http
에선 요청을index.html
,styles.css
.. 이렇게 파일 하나씩 따로따로 보냈다면,http2
에선 자원들 요청을 동시에 여러개보낼 수 있게.
이렇게 동시성을 늘려서 요청을 동시에 여러개 보내서 더 빠르게 받아오는.. 특히 이미지, 작은 이미지가 여러개 있는 경우에는http
에 비해서http2
가 엄청나게 빠릅니다.작은 이미지가 수백개 있다고 하면
http
에선 하나하나씩 받아오고 있어야하거든요?
실제로http
(http1.1)도 하나하나씩은 아니고 8개씩? 이렇게 묶어서 받아오긴 하는데http2
에서는 더 많이 동시에 받아오고 그럽니다.
그래서http2
도 실무에서 적용하실 수 있으면 적용하시는게 좋습니다.
그리고http2
할 때는https
처럼 인증 부분도 같이 적용해서 하기 때문에http2
만 하면 속도도 챙기고 보안도 챙길 수 있음.
4.7.4 http2 적용 서버
-
https 모듈을 http2로, createServer 메소드를 createSecureServer 메소드로
// server1-4.js const http2 = require('http2'); const fs = require('fs'); http2.createSecureServer({ cert: fs.readFileSync('도메인 인증서 경로'), key: fs.readFileSync('도메인 비밀키 경로'), ca: [ fs.readFileSync('상위 인증서 경로'), fs.readFileSync('상위 인증서 경로'), ], }, (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(443, () => { console.log('443번 포트에서 서버 대기 중입니다!'); });
개발시에는
http
모듈로 하시고 실무에 배포할 때는http2
4.8 cluster
4.8.1 cluster
이거는 http2
적용하시면서 실무에 cluster
도 같이 적용해서 배포하시면 좋습니다.
왜냐면 서버가 기본적으로 싱글 스레드라 코어를 1개밖에 차지하지 않죠? 나머지 7개의 코어가 놀고있다고 그랬죠? (코어가 8개라면)
그러면 여러가지 방법이 있는데, 서버를 기본적으로 코어 갯수만큼 실행을 하면, 코어가 8개라면 서버 8개를 동시에 실행을하면 성능면에서 좋겠죠?
성능면에서 아주 효율적이게 될것입니다.
이를 하려면 cluster
라는 걸 사용해야됩니다.
-
기본적으로 싱글 스레드인 노드가 CPU 코어를 모두 사용할 수 있게 해주는 모듈
- 포트를 공유하는 노드 프로세스를 여러 개 둘 수 있음
- 요청이 많이 들어왔을 때 병렬로 실행된 서버의 개수만큼 요청이 분산됨
- 서버에 무리가 덜 감
- 코어가 8개인 서버가 있을 때: 보통은 코어 하나만 활용
- cluster로 코어 하나당 노드 프로세스 하나를 배정 가능
- 단, 서버를 8개를 돌린다고 해서 성능이 8배가 되는 것은 아니지만 개선됨
이 부분은worker_threads
랑 스레드풀 할 때 보여드렸었죠?
멀티 스레드를 한다고해서 코어 갯수만큼 정비례해서 좋아지는 것은 아닙니다.
왜냐면 멀티 스레드를 관리하는데도 비용이 들고 그러기 때문에. - 단점: 컴퓨터 자원(메모리, 세션 등) 공유 못함
그래서cluster
는 당연하게 해야되는 것처럼 보이지만 이와 같은 단점도 있음.
cluster
는 프로세스를 여러개 띄우는 거라서 메모리나 세션 등을 공유를 못해서 로그인 같은거 구현하실 때 아마 애를 먹으실 거에요.
예를 들어 지금 서버 8개를 동시에 띄워놨으면, 저희가 그 중 서버 1개에다가 로그인 기능을 구현해놔요.
그럼 새로고침해도 그 서버 1개에서 로그인이 유지되어야하는데, 새로고침할 때마다 8개 서버중에 랜덤으로 접속이 되거든요?
그럼 어떤 1개의 서버에만 로그인이 되어있고 다른 서버에는 로그인이 안되어있고, 그런식으로 하면 새로고침 할 때마다 로그인이 풀렸다가 되었다가 풀렸다가 되었다가 이러겠죠? - Redis 등 별도 서버로 해결
위와 같은 문제는Redis
같은 별도의 메모리 서버로 해결을 하는데, 이는 15장에서 알아보도록 하겠습니다.
일단은 서버를 8개.. 여러분들의 컴퓨터 코어 갯수만큼 동시에 띄울 수 있는 방법을 소개해드리겠습니다.
4.8.2 서버 클러스터링
이거는 worker_threads
랑 비슷하거든요?
worker_threads
는 스레드를 여러개 만드는 거였다면 cluster
는 프로세스를 여러개 만드는 거입니다.
-
마스터 프로세스와 워커 프로세스
- 마스터 프로세스는 CPU 개수만큼 워커 프로세스를 만듦(
worker_threads
랑 구조 비슷) - 요청이 들어오면 워커 프로세스에 고르게 분배
-
// cluster.js const cluster = require('cluster'); const http = require('http'); // 현태 컴퓨터의 코어 갯수 체크 const numCPUs = require('os').cpus().length; // cluster.isMaster - worker_threads의 isMainThread와 비슷하죠? if (cluster.isMaster) { console.log(`마스터 프로세스 아이디: ${process.pid}`); // CPU 개수만큼 워커를 생산 // master 스래드에서 fork를 해서 woker 프로세스들을 만듭니다. 워커스레드가 아니라 워커프로세스입니다. 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); // cluster.fork(); // fork는 워커프로세스를 만드는 메소드. 여기선 테스트를 위해 이 코드는 잠시 주석처리. }); } else { // 이 부분은 워커 프로세스겠죠? // 워커 프로세스에서 아래와 같이 서버를 띄웁니다. 포트번호 8086 // 그럼 현재 컴퓨터 코어 갯수만큼 서버가 실행됨. 위의 마스커 프로세스에서 코어 갯수만큼 fork함 // 워커들이 포트에서 대기 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}번 워커 실행`); }
위와 같이하면 현재 컴퓨터 코어 갯수만큼 서버를 띄웁니다.
이cluster
방식이 좋은게, 서버를 여러개를 띄우면.. process는 보통 프로그램당 하나씩 차지한다고 했었죠?
그리고 그 process들은 각각 포트번호를 하나씩 차지한다고 했잖아요?
그런데cluster
를 사용하면 좋은게 여러개(해당 컴퓨터의 코어갯수)의 서버를 하나의 포트에 같이 묶어놓을 수가 있습니다.
하나의 포트에서 여러개의 서버를 동시에 띄울 수가 있습니다.여튼 워커프로세스마다 다른 포트를 쓰지 않고 하나의 포트로 서버를 여러개 띄울 수 있어서 알아서 워커프로세스들한테 요청을 랜덤으로 분배를 해줍니다.
정확히는 랜덤은 아니고 라운드 로빈이라는 알고리즘에 따라서 분배를 해줍니다.
거의 고르게 분배를 해준다고 생각하시면 됩니다.여튼 지금부터 실제로 컴퓨터의 코어 갯수만큼 서버가 띄워지는지 실습해보도록 하겠습니다.
확인은 어떻게 할까요?
확인하는 방법은 쉽습니다.
서버를 하나씩 접속할 때마다 하나씩 종료하는 겁니다.
그럼 저희가 6번 접속하면 6개의 서버가 다 종료돼서 더 이상 남아있는 서버가 없어서 접속이 안되겠죠?그래서 위 코드 보시면
setTimeout(() => {process.exit(1)}, 1000))
코드를 넣어놨거든요?
이 코드를 실행하면 서버가 종료(노드 프로세스 종료)됩니다.
3장에서 배웠던 것들을 실제 서버를 만들면서 사용해보는 겁니다.setTimeout(() => {process.exit(1)}, 1000))
이 코드가 실행되어 서버가 종료되면 마스터 프로세스에서cluster.on('exit', 콜백)
이벤트 핸들러가 실행이 됩니다.
- 마스터 프로세스는 CPU 개수만큼 워커 프로세스를 만듦(
4.8.3 워커 프로세스 갯수 확인하기
-
요청이 들어올 때마다 서버 종료되도록 설정
-
실행한 컴퓨터의 코어가 8개이면 8번 요청을 받고 종료됨
프로세스는 총 33개가 실행되었지만 서버는 32개라는 것.
나머지 마스터 프로세스는 라운드 로빈 알고리즘에 의해 워커 프로세스(서버)들로 고르게 요청을 분배하는 역할이라고 보시면 됨.이렇게 32번 요청을 보내면 모든 서버들이 종료되었죠? 위에 작성해놓은 코드가 실행되면서.
이렇게 정확하게 해당 컴퓨터의 코어 갯수만큼 서버가 실행된다는 것을 확인할 수 있었습니다.
-
4.8.4 워커 프로세스 다시 살리기
-
워커가 죽을 때마다 새로운 워커를 생성
- 이 방식은 좋지 않음
- 오류 자체를 해결하지 않는 한 계속 문제가 발생
-
하지만 서버가 종료되는 현상을 막을 수 있어 참고할만함
그런데 실무에선 위와 같이 서버를 강제로 종료하는 경우는 없고 혹시나 에러가 발생해서 서버가 종료되더라도 다시 실행시키거든요?
위에서 서버 테스트 때문에 주석처리한cluster.fork()
코드있죠?// cluster.js 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); cluster.fork(); // 서버가 뜻하지않게 종료되더라도 다시 생성하는 코드를 작성합니다. // cluster.fork(); 실행하면 새로운 워커프로세스가 실행되는데, 어떤 워커프로세스가 뜻하지않게 종료되었을 때 이렇게 다시 생성해줍니다. // 하나 종료되었을 때 하나를 다시 만들어주면 서버 갯수는 계속 똑같잖아요? }); } 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); console.error("무슨 에러인지 여기에 기록"); // 원래는 이렇게 에러나서 종료될 때 무슨 에러인지 기록하고 }, 1000); }).listen(8086); console.log(`${process.pid}번 워커 실행`); }
위에 보시면 워커프로세스(서버)가 종료되도 바로 다시 새 워커프로세스(서버)를 만들죠?
즉, 실무에선http
서버 코딩을 하는 것 뿐만 아니라, 더 나아가http2
도 적용을 해놓고 거기다가cluster
까지 같이 적용을 해서 서버가 혹시나 에러가나서 꺼지더라도 다시 살리는거.
그리고cluster
를 적용하면 해당 컴퓨터의 코어 갯수만큼 여러개의 서버도 돌릴 수 있고.
이런 장치까지 다 해줘야지 실제 서비스를 할 수 있는겁니다.
4강에선 이렇게 HTTP관련해서 실습을 해봤는데, 실제로도 위와 같이 만드시면 되기는 하는데 솔직히 복잡합니다.restServer.js
실습할 때부터 코드가 복잡해져서 이미 좌절하신 분들도 많을겁니다.
“아.. 서버 코딩하는게 이렇게 지저분하고 복잡한건가?” 라는 생각이 드셨을 수도 있는데 아닙니다.
많은 사람들이 http
, http2
모듈과 cluster
모듈을 직접 사용해서 코드를 작성해보니 너무 코드가 복잡하고 지저분해져서.. 보기도 안좋고 유지보수도 잘 안돼고해서 이런걸 편하게 해주는 코드를 이미 많이 만들어놨거든요?
그 코드를 사용하면 코드 작성도 깔끔하고 유지보수도 쉽고 관리하기도 편하게 만들 수 있으니까 그 내용에 관해선 5,6장에서 같이 보시면 될 것 같습니다.
어쨌든 NodeJS 자체에서 제공하는 방식은 4장에서 공부한 내용대로 만든다는 것. 그렇게 알아두시면 될 것 같습니다.
resetServer.js
와 session.js
, cluster.js
이거까지해서 여러분만의 서버를 한번 만들어보세요.
단, https
랑 http2
는 아직 적용을 못하실거에요.
왜냐하면 도메인도 없고 인증서도 없기 때문에.
그래서 이거는 적용하지마시고 http
서버로 session.js
, cluster.js
까지해서 여러분만의 서버를 만들어보시고 localhost로 계속 접속을 해보시면서 테스트를 해보십시오.