ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Node.js 스터디 3주차
    BackEnd/Node.js 2022. 2. 5. 00:33

    Node.js 교과서 4장 Http모듈로 서버 만들기를 공부하고 정리한 글 


    4.1 요청과 응답 이해하기

    서버는 클라이언트의 요청을 받아 내용을 읽고 처리한 뒤, 응답을 보내준다. 

    따라서 요청을 받는 부분과 응답을 보내는 부분이 필요하다. 어떤 요청에서 어떤 작업을 수행할지도 등록해두어야 한다. 

     

    const http = require('http');
    http.createServer((req, res) => {
    	// 보낼 응답 
    });

    http 모듈을 사용해서 웹 브라우저 요청을 처리한다. 요청이 들어올 때마다 콜백함수가 실행된다.

     


     

    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번 포트에서 서버 대기 중입니다!');
        });

    실행 결과는 아래처럼 나온다.

    웹페이지(프론트)측에서 응답인 Hello Node! Hello Server을 확인할 수 있고, vscode상에는 listen에 작성한 8080번 포트에서 서버 대기중입니다 콘솔 출력을 확인할 수 있다. 

     

    listening 리스너를 붙이는 방식도 가능하다. 

    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);
    })

    잘 쓰이진 않지만, createServer을 여러 번 호출하면 서버를 여러개 동시 실행할 수 있다. (단 포트 번호는 달라야 함)

    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('8080번 포트에서 서버 대기 중입니다!');
        });

    write와 end에 일일이 HTML적는 방식말고 미리 만들어둔 파일을 저번 단원에서 공부한 fs모듈을 사용해 읽어서 전송할 수 있다. 

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="utf-8"/>
            <title>Node.js 웹 서버</title>
        </head>
        <body>
            <h1>Node.js 웹 서버</h1>
            <p>만들 준비되셨나요?</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 '}); //html 파일
            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번 포트에서 서버 대기중입니다. ');
        })

    data 변수에 fs모듈의 readFile메소드로 server2.html파일을 버퍼로 읽고, 그대로 클라이언트에 전달해준다. 

     

     


    4.2 REST와 라우팅 사용하기

    - REST란 무엇인가? 

    ✔ REpresentational State Transfer의 줄임말로, 서버의 자원을 정의하고 이 자원에 대한 주소를 지정하는 약속을 의미

    ✔ 자원을 이름(표현)으로 구분하여 자원의 상태(정보)를 주고 받는 것이다. 

    ✔ HTTP 프로토콜을 활용하기에 웹의 장점 활용이 가능하다. 

     

    - 자원 / 행위 / 표현 3가지로 구성이 된다

    자원 : URI (인터넷 상의 자원을 식별하기 위한 문자열의 구성)

    행위 : HTTP Method (EX : GET, POST, PUT, DELETE등..)

    표현 : 클라이언트 - 서버가 주고받는 데이터 형식(xml, json등)

     

    - REST의 특징? 

    ✔ 서버-클라이언트 구조 (자원을 요청하는 쪽이 Client, 자원을 제공하는 쪽이 Server)

    ✔ HTTP 프로토콜이 Stateless함 -> REST역시 무상태성 

    -> statelss는 클라이언트의 상태 정보(세션, 쿠키정보 등을)를 별도로 기록하지 않는다. 들어오는 요청만 단순히 처리함.

    ✔ 캐싱 기능 사용 가능 - 캐싱 처리 가능 여부를 명시해야 함

    ✔ 인터페이스 일관성

    ✔ 계층 구조 (REST서버는 다중 계층으로 구성될 수 있으며 클라이언트는 직접 통신하는지 중간서버(프록시)와 통신하는지 알 수 없습니다)

     

    참고한 블로그 : https://dev-coco.tistory.com/97

     

    REST란? REST API 와 RESTful API의 차이점?

    참고 REST(REpresentational State Transfer)란? REST의 정의 "REpresentational State Transfer" 의 약자로, 자원을 이름(자원의 표현)으로 구분해 해당 자원의 상태(정보)를 주고 받는 모든 것을 의미합니다. 즉..

    dev-coco.tistory.com


    REST를 따르는 서버를 RESTful 하다고 표현한다. REST에 기반한 서버 주소 구조를 미리 설계하고 시작하면 체계적으로 프로그래밍 할 수 있다. 

     

    * 소문자, 명사를 사용하여 주소를 명시하며, /(슬래쉬) 기호로 계층을 구분한다. /로 끝내면 혼용의 여지가 있으니 /로 끝내지 말 것

    메소드 주소 역할
    GET / restFront.html파일 제공
    GET /about about.html파일 제공
    GET /users 사용자 목록 제공
    GET   기타 정적 파일
    POST /user 사용자 등록
    PUT /user/:id 해당 id 사용자 수정
    DELETE /user/:id 해당 id 사용자 제거

     

    프론트 html 코드도 있지만 생략하고 .. 서버 코드는 아래와 같다

     

    async function getUser() { //사용자 정보를 가져오는 함수 
        try {
            const res = await axios.get('/users'); //users 주소로 GET 요청 왔을 때 처리.
            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}); // /user/:id 로 PUT(해당 ID 사용자 수정)
                        getUser(); //다시 유저 정보 불러옴 
                    }
                    catch(err) {
                        console.error(err);
                    }
                });
    
                //삭제 버튼 클릭시 이벤트
                const remove = document.createElement('button');
                remove.textContent = '삭제';
                remove.addEventListener('click', async () => {
                    try {
                        await axios.delete('/user/' + key); // /user/:id 로 DELETE(해당 ID 사용자 삭제)
                        getUser();
                    } catch(error) {
                        console.error(error);
                    }
                });
                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}); // /user로 POST(해당 사용자 새로 등록)
            getUser();
        } catch(err) {
            console.error(err);
        }
        e.target.username.value = '';
    });

    아래 코드에서 메소드와 URL에 따라 파일을 제공한다. 

    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):', 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):', 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(8082, () => {
            console.log('8082번 포트에서 서버 대기 중입니다.');
        });

    - Express로 라우터 분리가 안되어 있어서 if문의 중첩이 연결되어 있다. 뒤에서 배울 Express모듈과 라우팅을 사용하면 좀 더 깔끔해진다. 

     

    - 아래는 실행화면이다. 

    +) 데이터를 영구적으로 저장하려면 DB사용을 해야한다. 

     


    4.3 쿠키와 세션 이해하기

    HTTP는 connectionless하고 stateless하다. 서버가 적절한 응답을 하고 나면 연결이 끊어지고, 클라이언트의 정보를 저장해두지 않는다. 그렇다면 로그인 후 인증 상태를 어떻게 유지할 수 있는가?

     

    - 쿠키 ? 

    ✔ 요청을 보내는 클라이언트에 대한 정보. 

    ✔ 서버의 경우 처음 요청이 들어왔을 때 요청자를 추정할만한 정보를 쿠키로 만들어 보내면 클라이언트는 쿠키 정보를 저장해놨다 다음 요청부터 쿠키를 함께 서버에 전송함으로써 사용자를 구분할 수 있다. 

     

    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번 포트에서 서버 대기 중입니다!');
    });

    실행하면 아래와 같은 결과를 확인할 수 있다. 

     

    로그인 상태를 유지해보는 코드를 만들어보자! 

    const http = require('http');
    const fs = require('fs').promises;
    const url = require('url');
    const qs = require('querystring');
    
    const parseCookies = (cookie='') => 
        cookie //쿠키는 문자열 형태 parseCookies로 객체 형태로 변경
            .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);
    
        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);
            res.writeHead(302, {
                Location : '/',
                'Set-Cookie' : `name=${encodeURIComponent(name)}; Expires = ${expires.toGMTString()}; HttpOnly: Path=/`,
            });
            res.end();
        } 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(8080, () => {
        console.log('8080번 포트에서 서버 대기중입니다!');
    });

    +) url.parse와 qs.parse가 deprecated되었는데... host부분없이 pathname만 오는 주소라서 whatwg방식이 처리할 수 없는듯 하다. 

     

    실행결과는 아래와 같다. 

    새로고침을 해도 로그인 상태가 유지된다. 

    - 쿠키명=쿠키값 : 기본적인 쿠키의 값

    - Expires = 날짜 : 만료 기한, 쿠키가 이시간이 지나면 자동 제거.

    - max-age = 초 : 해당 초가 지나면 쿠키가 제거

     


    - 세션? 

    ✔ 세션또한 쿠키처럼 클라이언트 정보를 저장할 수 있으나, 쿠키는 브라우저의 쿠키 저장소에 저장되는 반면 세션은 서버에 저장된다. 

    ✔ 서버에 사용자 정보를 저장하고 세션아이디로 클라이언트와 소통

    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 : '/',
                'Set-Cookie' : `session=${uniqueInt}; Expires = ${expires.toGMTString()}; HttpOnly: Path=/`,
            }); //uniqueInt : 쿠키에 이름 대신 숫자 값 보냄
            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번 포트에서 서버 대기중입니다!');
    });

    보통 보안상의 문제 등으로 직접 구현하기보단 npm에 있는 모듈 등을 사용하는 경우가 많다! 


    추가로 참고한 블로그 : https://hyuntaeknote.tistory.com/3

     

    HTTP 는 Stateless 한데 로그인은 어떻게 구현할 수 있을까? (세션/쿠키를 이용한 인증)

    개요  소셜 네트워크 서비스 'AGORA'를 제작하면서 가장 먼저 개발한 기능은 회원가입/로그인 입니다. 혼자 사용하거나 모든 정보가 공개된 웹 어플리케이션이 아니라면 대부분의 웹 어플리케이

    hyuntaeknote.tistory.com


    4.4 https와 http2

    - https : 기존의 http 프로토콜을 암호화시킨 버전이다. SSL/TLS등을 통해 전송되는 정보들을 암호화시켜 보안 측면에서 뛰어나다. 

    서버에 암호화를 적용하려면 https모듈을 사용하며, 인증서 발급이 필요하다. 인증서가 있는 경우에는 아래와 같이 작성하면 된다. 기존 작성 코드 중, createServer의 첫 번째 인수에 인증서 관련 옵션을 넣는다고 생각하면 된다. 

    const http = require('https');
    
    http.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번 포트에서 서버 대기 중입니다!');
        }); //실제 서버에서는 443포트

     

    - http2 : 최신 HTTP 프로토콜

    http/1.1 : 하나의 연결 당 하나의 요청을 처리한다. 3-way handshacking이 반복적으로 발생하며 RTT(round-trip time, 전송하고 응답이 돌아올 때까지 걸리는 시간)가 증가하게 된다. 

    -> http2는 이를 개선해서, 속도 상향을 위해 하나의 커넥션으로 동시에 여러개 주고받을 수 있으며, 클라이언트의 요청을 최소화하기 위해 요청하지 않은 정보도 보내줄 수 있다. (server-push)

    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번 포트에서 서버 대기 중입니다!');
        });

    사용법은 https모듈과 거의 유사하다. 

     


    4.5 cluster

    - cluster 모듈이란?

    싱글 프로세스로 동작하는 노드가 CPU코어를 모두 사용할 수 있게끔 만들어 주는 모듈

     

    const cluster = require('cluster');
    const http = require('http');
    const numCPUs = require('os').cpus().length; //CPU 코어 개수
    
    if(cluster.isMaster) {
        console.log(`마스터 프로세스 아이디 : ${process.pid}`);
        for (let i = 0; i<numCPUs; i+= 1) {
            cluster.fork(); //CPU코어 개수만큼 fork
        }
    
        cluster.on('exit', (worker, code, signal) => {
            console.log(`${worker.process.pid}번 워커가 종료되었습니다.`);
            console.log('code', code, 'signal', signal);
            cluster.fork(); //워커 하나 종료 시 새로운 워커 생성, 서버 종료를 막는다. 
        });
    } else {
        http.createServer(async (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(() => {
                process.exit(1);
            }, 1000);
        }).listen(8086);
        console.log(`${process.pid}번 워커 실행`);
    }

    본인의 cpu코어 개수만큼 fork했기 때문에 실행결과를 확인하면 출력된 워커 갯수가 코어 개수와 동일하다. 나는 i7 그램을 사용하는데, 8개의 워커가 출력되어 있음을 확인할 수 있다. 

    - 위 코드처럼 새로운 워커 생성으로 막기보단 오류 자체의 원인을 해결해야함. 

    - 클러스터링으로 예기치 못한 에러로 서버의 종료를 막을 순 있음

    - 실무에서는 주로 pm2모듈을 사용해서 cluster 기능을 사용한다. 

    -> 추가로 pm2는 이렇게 생겼다. pm2 status 쳤을 때 나오는 화면인데, 에러가 발생하면 status error로 바뀌지만 서버 자체가 멈추진 않는다. 

    'BackEnd > Node.js' 카테고리의 다른 글

    Node.js 스터디 5주차  (0) 2022.02.15
    Node.js 스터디 4주차  (0) 2022.02.07
    Node.js 스터디 2주차  (0) 2022.01.25
    Node.js 스터디 1주차  (0) 2022.01.18
    Node.js에서 로그인 기능 구현하기(+github 위키)  (0) 2021.08.28
Designed by Tistory.