상세 컨텐츠

본문 제목

[Node.js] 4장 http 모듈로 서버 만들기

21-22/21-22 Node.js

by Kimpeep 2021. 11. 8. 18:46

본문

728x90

1. 요청과 응답 이해하기

클라이언트와 서버의 관계(Node.js 교과서 개정2판, 조현영 저)

  • 클라이언트가 서버로 요청(request)을 보냄
  • 서버에서는 요청의 내용을 읽고 처리한 뒤 클라이언트에 응답(response)을 보냄
  • 클라이언트로부터 들어온 요청을 받을 이벤트 리스너를 등록해야 함
const http = require('http'); 
http.createServer((req, res) => { // 응답 처리 }
  • http 모듈에 있는 createServer 메서드를 사용
  • 인수로 요청에 대한 콜백 함수를 넣을 수 있으며 요청이 들어올 때마다 콜백 함수가 실행됨
  • 콜백 함수에 대한 응답을 적으면 됨
  • 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(8080, () => { // 클라이언트에 공개할 포트번호, 포트 연결 후 실행되는 콜백함수 
// 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, () => { // 8081번 서버 연결 console.log('8081번 포트에서 서버 대기 중입니다!'); });

실행결과

  1. 위처럼 8080번 포트가 열려 대기 중
  2. http://127.0.0.1:8080(로컬 호스트)에 접속
  3. http.createServer의 콜백 함수가 실행되면서 홈페이지 출력

* 참고로 포트 연결은 안전하게 1024 이후의 숫자로 연결하는 것이 좋음(포트 충돌 방지)

  • res.writeHead(성공을 나타내는 http 상태코드, {응답에 대한 정보의 형식, 한글 표시를 위한 utf-8) : 응답에 대한 정보 기록, 이 정보가 기록되는 부분을 헤더(Header)라고 함
  • res.write(데이터) : 클라이언트로 보낼 데이터를 기록, 이 부분을 본문(Body)라고 함
  • res.end() : 응답을 종료하는 메서드, 데이터를 인수로 보내면 데이터 전송 후 종료
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>'); 
}); 
server.listen(8080);
server.on('listening', () => { 
	// listening 이벤트 리스너 
    console.log('8080번 포트에서 서버 대기 중입니다!') }); 
    
    server.on('error', (error) => { 
    // error 리스너 
    console.error(error); 
});

 

서버 HTML 파일을 따로 만들어 연결하기
const http = require('http'); 
const fs = require('fs').promises; 
http.createServer(async (req, res) =>{ 
	try { // 요청이 들어오면 fs모듈로 HTML파일을 읽음 
    	const data = await fs.readFile(렌더링할 html 파일); 
        res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
        res.end(data); // 읽은 HTML 파일을 보냄 
        } catch(err) { 
        console.error(err); // text/plain은 일반 문자열을 의미 
        res.writeHead(500, {'Content-Type': 'text/plain; charset=utf-8'}); 
        res.end(err.message); } 
     }).listen(8081, () => { 
     	console.log('8081번 포트에서 서버 대기 중입니다!'
     ); 
});

위처럼 오류가 났을 때 예외처리는 응답에 에러코드를 담아 보내는 것이다.
오류가 났다고 응답을 보내지 않으면 서버는 응답이 오기를 기다리다가 Timeout(시간 초과) 처리한다.

2. REST와 라우팅 사용하기

서버에 요청을 보낼 때 그것이 어떠한 요청인지는 주소에 있다.
(/index.html 은 index.html 파일을 가져오라는 뜻이다. 그 외에도 css, js를 불러오는 것도 가능하다.)
서버가 이해하기 쉬운 주소를 표현하는 방법 -> REST

REST (Representational State Transfer)

  • 서버의 자원을 정의하고 자원에 대한 주소를 지정하는 방법을 가리킴
  • 명확한 의미 전달을 위해 몇 가지 규칙으로 이루어짐
    • 명사로 구성
    • HTTP 요청 메소드를 사용(GET, POST, PUT, PATCH, DELETE, OPTIONS)
      • GET : 서버 자원을 가져올 때 사용, 요청의 본문에 데이터를 넣지 않음(보내야 한다면 쿼리스트링 사용)
      • POST : 서버에 자원을 새로 등록할 때 사용, 요청의 본문에 새로 등록할 데이터를 넣어 보냄
      • PUT : 서버의 자원을 요청에 들어있는 자원으로 치환하고자 할 떄 사용, 요청의 본문에 치환할 데이터를 넣어 보냄
      • PATCH :  서버 자원의 일부만 수정하고자 할 때 사용, 요청의 본문에 일부 수정할 데이터를 넣어 보냄
      • DELETE : 서버의 자원을 삭제하고자 할 때 사용, 요청의 본문에 데이터를 넣지 않음
      • OPTIONS : 요청을 하기 전에 통신 옵션을 설명하기 위해 사용
    • 주소 하나에 여러 개의 요청 메소드가 들어갈 수 있음
      • GET 메소드 /user 주소로 요청을 보내면 사용자 정보를 가져오는 요청
      • POST 메소드 /user 주소로 요청을 보내면 새로운 사용자를 등록하려는 요청
      • 로그인 같은 애매한 메서드는 그냥 POST 사용하면 됨
const http = request('http'); 
const fs = require('fs').pormises; 
http.createServer(async (req,res) => { 
	try { 
		console.log(req.method, req.url); // GET 요청이 들어왔을 때 
        if (req.method === 'GET') { 
        	if (req.url === '/') { 
            	//자바스크립트는 await 키워드를 만나면 프라미스가 처리(settled)될 때까지 기다림 
                const data = await fs.readFile(로드할 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) { 
             	// 주소에 해당하는 라우트를 못 찾았다는 404 에러 발생 
             } else if (req.method === 'POST') { 
             	// POST /user 사용자를 새로 저장 
                if (req.url === '/user') { 
                	let body = ''; // 요청의 body를 stream 형식으로 받음 
                    // req.on() -> 요청의 본문에 들어 있는 데이터를 꺼내기 위한 작업 
                    req.on('data', (data) => { body += data; }); // 요청의 body를 다 받은 후 실행됨 
                    // req.on() -> 요청의 본문에 들어 있는 데이터를 꺼내기 위한 작업 
                    return req.on('end', () => { console.log('POST 본문(Body):', body); 
                    const { name } = JSON.parse(body); 
                    const id = Date.now(); 
                    user[id] = name; 
                    res.writeHead(201); 
                    res.end('등록 성공'); 
                }); 
            } 
        } else if (req.method === 'PUT') { 
            // PUT 요청이 들어왔을 때 
            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; carset=utf-8' }); 
        res.end(err.message); } 
        }) 
        .listen(8080, () => { 
        console.log('8082번 포트 서버 대기중'); 
    });

 

3. 쿠키와 세션 이해하기

  • 클라이언트에서 보내는 요청은 누가 보냈는지를 알 수 없음
  • 이를 해결하기 위한 '로그인'의 개념은 쿠키와 세션에 기반함
  • 서버는 요청에 대한 응답 시 쿠키를 동봉 -> 쿠키를 받은 웹 브라우저는 다음 요청 때 그 쿠키를 동봉해 요청
  • 서버에서 브라우저로 쿠키를 보낼 때만 코드를 작성하여 처리하고 브라우저에서는 따로 코딩 X
const http = require('http'); 
http.createServer((req, res) => { 
	//req.headers.cookie -> req 객체의 쿠키 
    console.log(req.url, req.headers.cookie); 
    res.writeHead(200, { 
    	//res의 헤더에 쿠키 기록 
    	// 쿠키의 형태는 문자열, 아래와 같은 형태로 쿠키 지정 
        'Set-Cookie': 'mycookie=test' 
    }); res.end('Hello Cookie'); 
}) .listen(8083, () => { 
	console.log('8083번 포트 대기중'); 
})

결과

가장 처음에는 req.headers.cookie에 아무 값도 들어가 있지 않다가 한번 response가 돌아온 이후에는 쿠키가 test로 설정된 것을 알 수 있다.

favicon이란 웹 사이트 상단의 아이콘을 말한다. 이 아이콘 정보를 불러오기 위해 요청을 보내는 것이다.

로그인하고 5분간 유지하기
<!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>
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; }, {}); http.createServer(async (req,res) => { // 쿠키를 자바스크립트 객체로 바꿈 const cookies = parseCookies(req.headers.cookie); // 주소가 /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': `session=${uniqueInt}; Expires= ${expires.toGMTString()}; HttpOnly; Path=/`, 'Set-Cookie': `name=${encodeURIComponent(name)}; Expires= ${expires.toGMTString()}; HttpOnly; Path=/`, }); res.end(); // cookies.session && session[cookies.session].expires > new Date() } else if(cookies.name) { // name이라는 쿠키가 있는 경우 res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'}); // res.end(`${session[cookies.session].name}); res.end(`${cookies.name}님 반가워요`); } else { // 처음 방문했을 때는 로그인하는 페이지로 이동 try { const data = await fs.readFile('./4장/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번 포트 열림'); })
  • encodeURIComponent(name) : name이 한글이기 때문에 영어로 인코딩하여 url로 사용

Set-Cookie 옵션

  • 쿠키명=쿠키값 : 기본 쿠키값을 나타냄
  • Expires=날짜 : 만료 기한. 기한이 지나면 쿠키가 제거됨
  • Max-age=초 : Expires과는 다르게 날짜 대신 초를 입력. 초가 지나면 쿠키가 제거됨
  • Domain=도메인명 : 쿠키가 전송될 도메인을 특정. 기본값은 현재 도메인
  • Path=URL : 쿠키가 전송될 URL을 특정. 기본값 '/'
  • Secure : HTTPS일 경우에만 쿠키 전송
  • HttpOnly : 설정 시 자바스크립트에서 쿠키에 접근할 수 없음. 쿠키 조작을 방지하기 위해 설정. 
    값에 쿠키가 그대로 노출되어 있음

쿠키가 그대로 노출되어 있는 것을 방지하기 위해 '세션'을 사용한다. 그러면 값에 쿠키 대신 세션 아이디가 들어가게 되고 쿠키 대신 세션 아이디를 주고받으면서 본인을 확인한다. 이렇게 세션을 위해 사용하는 쿠키를 '세션 쿠키'라고 한다.

4. https와 http2

 

  • https 모듈 : 웹서버에 SSL 암호화 추가. GET, POST 요청 시에 오가는 데이터를 암호화하여 중간에 가로채더라도 내용을 확인할 수 없게 함
  • https 모듈은 아무나 사용할 수 없음. 발급받았을 때 사용하는 방법은 아래와 같음
const https = require('https'); 
const fs = require('fs'); 
https.createServer({ 
	cert: fs.readFielSync('도메인 인증서 경로'), 
	key: fs.readFileSync('도메인 비밀키 경로'), 
	ca: [ fs.readFileSync('상위 인증서 경로'), 
	fs.readFileSync('상위 인증서 경로'), ], }, 
    (req, res) => { 
		res.writeHead(200, {'Content-Type': 'text/html; carset=utf-8'}); 
		res.write('<h1>Hello Node!</h1>');
		res.end('<p>Hello Server!</p>'); 
	}) .listen(443, () => { console.log('443번 포트 대기중'); });
  • http2 모듈 : SSL 암호화와 더불어 최신 HTTP 프로토콜인 http/2 제공. 웹의 속도도 개선됨.

HTTP/1.1과 HTTP/2의 비교(Node.js 교과서 개정2판, 조현영 저, 186p)

5. cluster

  • cluster 모듈 : 싱글 프로세스로 동작하는 노드가 CPU 코어를 모두 사용할 수 있도록 하는 모듈
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(); 
	}); 
} 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>'); 
}).listen(8086); console.log(`${process.pid}번 워커 실행`); }

결과

코어 개수에 맞게 워커 프로세스가 실행된 것을 확인할 수 있다.
워커가 실행된 개수만큼 서버에 오류가 나서 종료되더라도 서버가 정상작동할 수 있다. cluster를 통해 서버가 완전히 다운되는 걸 방지할 수 있으나, 오류를 고치는 것이 우선적으로 진행되어야 한다. 워커가 다운되면 다시 새로운 워커를 생성하도록 하여 무한으로 서버가 종료되는 걸 방지할 수도 있다.

728x90

관련글 더보기