쌓고 쌓다

[Node.js] REST와 REST API 본문

프로그래밍/node.js

[Node.js] REST와 REST API

승민아 2023. 2. 1. 01:05

서버에 요청을 보낼 때는 주소를 통해 요청의 내용을 표현한다.

주소가 /index.html이면 서버의 index.html을 보내달라는 뜻이다. 물론 html이나 css, 이미지 같은 파일도 요청이 가능하고, 특정 동작을 행하는 것을 요청할 수도 있다.

서버의 요청이 주소로 표현되니 서버가 이해하기 쉽게 주소를 사용하는것이 좋다. 여기서 REST가 등장한다.

 

REST(REpresentational State Transfer)는 서버의 자원을 정의하고 자원에 대한 주소를 지정하는 방법을 뜻한다.

자원이라 해서 파일만 뜻하는 것이 아니라 서버가 행할 수 있는 것들을 통 들어서 의미한다.

REST를 따르는 서버를 RESTful하다라고 한다...

 

REST API?

  • API(Application Programming Interface) ?
    • 데이터와 기능의 집합을 제공해 컴퓨터 프로그램 간 상호작용을 도우며, 서로 정보 교환이 가능하도록 함.
  • REST API ?
    • REST 기반으로 서비스 API를 구현한 것

REST API에 많은 규칙들이 있지만 간단하게만 살펴보자.

주소는 의미를 명확히 전달하기 위해 명사를 주로 사용함.

/user라면 사용자 정보에 관련된 자원을 요청하는 것, /post라면 게시글에 관련된 자원을 요청하는 것이라고 추측이 가능함.

여기서, 단순히 명사만 있으면 무슨 동작을 행하라는 것인지 알기 어려우니

REST에서는 주소 외에도 HTTP 요청 메소드를 사용함.

폼 데이터를 전송할 때 GET, POST 메소드를 지정하는데 이것이 요청 메소드이다.

(html의 <form> 태그에 대해 공부해 봐야겠다...)

 

HTTP 요청 메소드

주소 하나가 요청 메소드를 여러 개 가질 수 있다. GET 메소드의 /user 주소로 요청을 보내면

사용자 정보를 가져오는 요청이라는 것을 알 수 있고. POST 메소드의 /user 주소로 요청을 보내면

새로운 사용자를 등록하려 한다는 것을 알 수 있다.

( 로그인 같이 메소드로 표현하기 애매한 동작이 있다면 POST를 사용하면 된다. )

 

  • GET: 서버 자원을 가져오고자 할 때 사용함. 요청의 본문(body)에 데이터를 넣지 않는다. 데이터를 서버에 보내야 한             다면 쿼리스트링을 사용함.
  • POST: 서버에 자원을 새로 등록하고자 할 때 사용함. 요청의 본문에 새로 등록할 데이터를 넣어 보냄.
  • PUT: 서버의 자원을 요청에 들어 있는 자원으로 치환하고자 할 때 사용함. 요청의 본문에 치환할 데이터를 넣어 보냄.
  • PATCH: 서버 자원의 일부만 수정할 때 사용함. 요청의 본문에 일부 수정할 데이터를 넣어 보냄.
  • DELETE: 서버의 자원을 삭제하고자 할 때 사용함. 요청의 본문에 데이터를 넣지 않음.
  • OPTIONS: 요청을 하기 전에 통신 옵션을 설명하기 위해 사용함.

주소와 메소드만 부고 요청의 내용을 알 수 있다는 것이 장점이다.

(GET 메소드 같은 경우 브라우저에서 캐싱(기억)할 수 있어 같은 주소로 GET 요청할 떄 서버에서 가져오는 것이 아니라 캐시에서도 가져올 수 있다고 한다...)

 

출처: Node.js 교과서 - 조현영

 

 

 


이제 RSET 서버를 구축해 보자.

대략적인 주소를 먼저 설계한다.

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

 

restFront.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>RESTful SERVER</title>
    <link rel="stylesheet" type="text/css" 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="./restFront.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> 
</body>
</html>

 

id가 list인 곳에 사용자들을 가져와 목록을 출력할 예정임.

1. <link rel="stylesheet" type="text/css" href="./restFront.css">  이것 또한 서버에 GET 요청을 보내는 것이다.

2. <a> 태그도 GET 요청을 보냄...

3. <script src="./restFront.js"></script> 이것 또한 GET 요청.

서버 주소 구조에서 기타 주소에 위와 같은 기타 정적 파일이 들어가는 거임.

<a> 태그

 

 

 

restFront.css

a {
    text-decoration: none;
}

 

about.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>RESTful SERVER</title>
</head>
<body>
<nav>
    <a href="/">HOME</a>
    <a href="/about">ABOUT</a>
</nav>
<div>
    <h2>소개 페이지</h2>
    <p>사용자 이름을 등록할 수 있습니다.</p>
</div>
</body>
</html>

restFront와 동일하나 css를 제외했습니다.

 

restFront.js

const { off } = require("process");

async function getUser() { // 사용자 정보를 가져오는 함수
    try {
        const res = await axios.get('/users'); // 서버로부터 사용자 목록을 가져옴
        const users = res.data;
        const list = document.getElementById('list'); // html에서 list 태그를 가져옴.
        list.innerHTML = ''; // list 태그의 내용을 초기화시킴 수정, 삭제, 추가시 내용이 달라지니깐(세가지 작업후 getUser 호출하여 갱신하여 보여줄것임)
        
        // Object.keys() - 주어진 객체의 속성 이름들을 일반적인 반복문과 동일한 순서로 순회되는 열거할 수 있는 배열로 반환함.
        // users에 key: 시각, value: 이름으로 들어가 있음. map에 키들이 들어감
        // 배열의 map 메소드는 콜백 함수에서 리턴한 값들을 가지고 새로운 배열을 만드는 함수.
        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' 에 치환할 데이터를 함께 보냄.
                    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' 해당id 사용자 서버에서 제거
                    getUser();
                } catch (err) {
                    console.error(err);
                }
            });

            // userDiv에 이름, 수정, 삭제 버튼을 달아줌
            userDiv.append(span);
            userDiv.append(edit);
            userDiv.append(remove);
            
            // list에 userDiv를 달아줌.
            list.append(userDiv);

            // 이 과정을 모든 users에 대해 적용하고 list에 붙여나감
        })
    } catch (err) {
        console.error(err);
    }
} // getUser()

window.onload = getUser; // 화면(restFront.html) 로딩이 끝나면 getUser 호출

// 폼 제출(submit)시 실행
document.getElementById('form').addEventListener('submit', async (e) => {
    e.preventDefault(); // form안에 submit 버튼을 눌러도 새로고침 안되게 (submit은 실행됨)
    const name = e.target.username.value; /* console.log(e.target) */
    if (!name) {
        return alert('이름을 반드시 입력하세요.');
    }
    try {
        await axios.post('/user', { name }); // 새로 등록할 이름과 함께 요청
        getUser();
    } catch (err) {
        console.error(err);
    }
    e.target.username.value = '';
})

서버에서 추가, 수정, 삭제가 이루어지고 다시 서버에서 /users를 가져와 목록을 보여줌. (list 태그만 갱신)

e.target을 찍어봤다.

e.target

e.target.username.value 방법이 가능하다...

 

restServer.js

const http = require('http');
const fs = require('fs').promises;
const path = require('path');

const users = {}; // 데이터 저장용(DB)

http.createServer(async (req, res) => {
    try {
        /* console.log(req) */
        if (req.method === 'GET') { // 요청의 method 확인
            if(req.url === '/') { // localhost:8082 = localhost:8082/와 동일 생략된거임.
                /* console.log(req.method, 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); // .end()에 청크도 가능.
            } 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') {
                // 일반적인 문자열은 'text/plain'을 사용한다.
                res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8'});
                return res.end(JSON.stringify(users)); // JSON 문자열로 바꿔 반환.
            }

            // /도 /about도 /users도 아니면 주소 '기타'라고 했음.
            try { //restFront.css와 restFront.js 또한 GET 가져오는것임!
                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 = '';

                req.on('data', (data) => { // 청크들을 모아
                    body += data;
                })
            
                // 요청의 body를 다 받은뒤 end 이벤트 실행됨.
                return req.on('end', () => {
                    console.log('POST 본문(body):', body); // {"name": "홍길동"} 요청과 응답은 JSON 형태로 자동 변경해줌.
                    const { name } = JSON.parse(body); // JSON 문자열을 자바스크립트 객체로 변환
                    const id = Date.now(); // id 생성
                    users[id] = name; // users 객체에 id 속성과 name 값을 추가해줌.
                    res.writeHead(201, { 'Content-Type': 'text/html; charset=utf-8' });
                    res.end('등록 성공');
                })
            }
        } else if (req.method === 'PUT') {
            if (req.url.startsWith('/user/')) {
                const key = req.url.split('/')[2]; // /user/사용자id 형태
                let body = '';

                req.on('data', (data) => {
                    body += data;
                });

                return req.on('end', () => {
                    console.log('PUT 본문(body):', body);
                    users[key] = JSON.parse(body).name; // 해당key의 속성값 수정
                    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]; // 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 (err) {
        console.error(err);
        res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8'});
        res.end(err);
    }
})
    .listen(8082, () => {
        console.log('8082번 포트에서 서버 대기중.');
    });

req.method로 HTTP 요청 메소드를 구분하고 있다.

메소드

 

메소드가 GET이면 다시 req.url로 요청 주소를 구분할 수 있다.

주소가 /일 때는 restFront.html을 제공하고, 주소가 /about이면 about.html 파일을 제공함.

 

이외의 경우(기타)에는 주소에 적힌 파일을 제공함

/restFront.js라면 restFront.js 파일을 제공하고 /restFront.css라면 restFront.css 파일을 제공함.

만약 존재하지 않는 파일을 요청하면 404 NOT FOUND 에러를 발생시킴.

 

응답 과정에서 예기치 못한 에러가 발생한 경우에는 500 에러가 응답으로 전송했음.

 

+ res.end 앞에 return은 왜 붙었는가?

res.end를 호출하면 함수가 종료된다고 생각하는데, 노드는 일반적인 자바스크립트 문법을 따르므로

return을 붙이지 않는 한 함수가 종료되지 않는다. 다음에 코드가 이어지는 경우에는 return으로 명시적으로 함수를 종료하자. return을 붙이지 않아 res.end 같은 메서드가 여러 번 실행되면 Error: Can't render headers after they are sent to the client 에러가 발생함.

 

+ POST 처리 형식

POST

post하면서 데이터를 보내는데 서버에서 받아 처리를 이런 식으로 형식을 하자...

 

+ POST와 PUT 요청을 처리할 때

조금 특이한 것이 보이는데 req.on('data')와 res.on('end)의 사용인데.

요청의 본문에 들어 있는 데이터를 꺼내기 위한 작업으로 보면 된다.

req와 res도 내부적으로는 스트림(각각 readStream과 writeStream)으로 되어 있으므로 요청/응답의 데이터가 스트림 형식으로 전달된다. on과 같이 이벤트도 달려 있다.

 

1. { name }이랑 { name: name }이랑 같은 겁니다. 그리고 json 객체랑 js 객체랑 일반적으로 같다.

2. axios는 알아서 json이라고 판단되는 문자열을 js 객체로 바꿔줍니다

JSON에 대해서, axios에서 객체와 json을 어떻게 처리하는지에 대해서 공부를 해봐야겠다.

Comments