본문 바로가기

Programming/Node.js

5. http 모듈로 서버 만들기 (2) - REST와 라우팅 [7]

node js logo image

 

 

 

 

우선 앞서 작성했던 RESTful 서버 구현 관련 코드를 전부 다시 살펴보겠습니다. 

 

 

 

 

[restFront.css]

a {
    color: blue;
    text-decoration: none;
}

 

 


[restFront.html]

<!DOCTYPE html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>RESTful SERVER EXAMPLE</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);
    }
}

// 화면이 로드될 때 getUser 호출
window.onload = getUser;

// form 제출(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 = '';
});



 

 

[about.html]

<!DOCTYPE html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <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');


// 유저 데이터 저장용
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': 'application/json; 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 (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('등록 성공');
                });
            }
        } 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);
                    user[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 (err) {
        console.error(err);
        res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8'});
        res.end(err.message);
    }
})
    .listen(8082, () => {
        console.log('8082번 포트 대기 중');
    });

 

 

그리고 REST에 기반한 서버 주소의 구조도 다시 상기해 보겠습니다. 

 

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

 

 

 

 


 

 

 

전체적인 코드를 모두 분석하고 설명하는 것은 현실적으로 어려우니, 핵심이라고 할 수 있는 [restServer.js]를 중심으로 살펴보겠습니다. 우선 코드에서 req.method로 HTTP 요청 메서드를 구분하죠. 여기서 다시 GET일 경우에는 req.rul로 요청 주소를 분류하게 됩니다.

 

/일 때는 resFront.html, /about이면 about.html을 제공합니다. 이외의 경우에는 주소에 적힌 파일을 제공하게 됩니다. /restFront.js라면 restFront.js 파일을 제공하고, /restFront.css라면 restFront.css 파일을 제공하는 방식입니다. 만일 없는 파일에 대한 요청이 이루어지면 주소에 해당하는 라우트를 찾지 못했다는 404 NOT FOUND가 전송됩니다. 

 

 

 


 

 

코드 중간에 return res.end(data)와 같은 형식의 리턴문이 있습니다. res.end( ) 역시 자바스크립트 문법에 비춰본다면, 단순히 이를 실행한다고 해서 함수가 종료되지 않습니다. 기본적인 자바스크립트의 문법에 따라 함수를 종료하기 위해서 return을 붙이는 것입니다. 만일 res.end에 return을 붙이지 않는 경우 Error: Can't render headers after they are sent to the client. 에러가 발생합니다. 

 

 

데이터베이스 저장, 즉 POST와 PUT 등의 메서드 사용에 대해서는 다음 아티클에서 살펴보겠습니다.