관리 메뉴

거니의 velog

(11) 백엔드 프로그래밍: Node.js의 Koa 프레임워크 5 본문

React/React_백엔드 프로그래밍

(11) 백엔드 프로그래밍: Node.js의 Koa 프레임워크 5

Unlimited00 2024. 2. 20. 15:31

(3) REST API

* 웹 애플리케이션을 만들려면 데이터베이스에 정보를 입력하고 읽어 와야 한다. 그런데 웹 브라우저에서 데이터베이스에 직접 접속하여 데이터를 변경한다면 보안상 문제가 되지 않을까? 그래서 REST API를 만들어서 사용한다.

* 클라이언트가 서버에 자신이 데이터를 조회, 생성, 삭제, 업데이트 하겠다고 요청하면, 서버는 필요한 로직에 따라 데이터베이스에 접근하여 작업을 처리한다.

* REST API는 요청 종류에 따라 다른 HTTP 메서드를 사용한다. HTTP 메서드는 여러 종류가 있으며, 주로 사용하는 메서드는 다음과 같다.

< HTTP 메서드의 종류 >

메서드 설명
GET 데이터를 조회할 때 사용한다.
POST 데이터를 등록할 때 사용한다. 인증 작업을 거칠 때 사용하기도 한다.
DELETE 데이터를 지울 때 사용한다.
PUT 데이터를 새 정보로 통째로 교체할 때 사용한다.
PATCH 데이터의 특정 필드를 수정할 때 사용한다.

* 메서드의 종류에 따라 get, post, delete, put, patch를 사용하여 라우터에서 각 메서드의 요청을 처리한다. 이전에 작성한 라우트에는 router.get이라고 입력했는데, 여기서 get이 바로 HTTP 메서드 GET이다. POST 요청을 받고 싶다면 route.post( ... ) 을 하면 된다.

* REST API를 설계할 때는 API 주소와 메서드에 따라 어떤 역할을 하는지 쉽게 파악할 수 있도록 작성해야 한다. 블로그 포스트용 REST API를 예시로 살펴보면 다음과 같다.

< 블로그 포스트용 REST API >

종류 기능
POST /posts 포스트 작성
GET /posts 포스트 목록 조회
GET /posts/:id 특정 포스트 조회
DELETE /posts/:id 특정 포스트 삭제
PATCH /posts/:id 특정 포스트 업데이트(구현 방식에 따라 PUT으로도 사용 가능)
POST /posts/:id/comments 특정 포스트에 덧글 등록
GET /posts/:id/comments 특정 포스트의 덧글 목록 조회
DELETE /posts/:id/comments/:commentId 특정 포스트의 특정 덧글 삭제

(4) 라우트 모듈화

* 프로젝트를 진행하다 보면 여러 종류의 라우트를 만들게 된다. 하지만 각 라우트를 index.js 파일 하나에 모두 작성하면, 코드가 너무 길어질 뿐 아니라 유지 보수하기도 힘들어진다. 여기서는 라우터를 여러 파일에 분리시켜서 작성하고, 이를 불러와 적용하는 방법을 알아보자.

* 우선 src 디렉터리에 api 디렉터리를 생성하고, 그 안에 index.js 파일을 만들자.

const Router = require('koa-router');
const api = new Router();

api.get('/test', (ctx) => {
  ctx.body = 'test 성공';
});

// 라우터를 내보낸다.
module.exports = api;

* 그 다음에는 이 api 라우트를 src/index.js 파일에 불러와서 기존 라우터에 /api 경로로 적용하자. 기존에 만들었던 라우트를 제거하겠다.

const Koa = require('koa');
const Router = require('koa-router');

const api = require('./api');

const app = new Koa();
const router = new Router();

// 라우터 설정
router.use('/api', api.routes()); // api 라우트 적용

// app 인스턴스에 라우터 적용
app.use(router.routes()).use(router.allowedMethods());

app.listen(4000, () => {
  console.log('Listening to port 4000');
});

* 우리가 만든 api 라우터를 서버의 메인 라우터의 /api 경로로 설정했다. 따라서 /api/test 경로로 요청하면 조금 전에 준비했던 'test 성공' 문자열이 나타날 것이다.

* 코드를 저장하고, http://localhost:4000/api/test 를 웹 브라우저로 띄워 보자.

* /api/test 라우트가 제대로 잘 작동하고 있다.


(5) posts 라우트 생성

* 이번에는 api 라우트 내부에 posts 라우트를 만들어 보겠다. api 디렉터리에 posts 디렉터리를 만들고, 그 내부에 index.js 파일을 만들자.

* 그리고 다음 코드를 입력하자.

const Router = require('koa-router');
const posts = new Router();

const printInfo = (ctx) => {
  ctx.body = {
    method: ctx.method,
    path: ctx.path,
    params: ctx.params,
  };
};

posts.get('/', printInfo);
posts.post('/', printInfo);
posts.get('/:id', printInfo);
posts.delete('/:id', printInfo);
posts.put('/:id', printInfo);
posts.patch('/:id', printInfo);
module.exports = posts;

* posts 라우트에 여러 종류의 라우트를 설정한 후 모두 printInfo 함수를 호출하도록 설정했다. 문자열이 아닌 JSON 객체를 반환하도록 설정하고, 이 객체에는 현재 요청의 메서드, 경로, 파라미터를 담았다.

* 코드를 완성한 후 api 라우트에 posts 라우트를 연결하자. 연결하는 방법은 서버의 메인 파일에 api 라우트를 적용하는 방법과 비슷하다.

[src/api/index.js]

const Router = require('koa-router');
const posts = require('./posts');

const api = new Router();

api.use('/posts', posts.routes());

// 라우터를 내보낸다.
module.exports = api;

* 기존 test 라우트는 지우고, posts 라우트를 불러와서 설정해 주었다. 우선 GET /api/posts 라우트부터 테스트해 보겠다. http://localhost:4000/api/posts 를 웹 브라우저로 띄워 보자.

* 잘 나타났는가? 이제 나머지 API도 테스팅해 볼 텐데, GET 메서드를 사용하는 API는 웹 브라우저에서 주소를 입력하여 테스팅할 수 있지만, POST, DELETE, PUT, PATCH 메서드를 사용하는 API는 자바스크립트로 호출해야 한다.

* 우리가 만든 API를 자바스크립트로 호출하는 대신, 편의상 REST API 요청 테스팅을 쉽게 할 수 있는 Postman이라는 프로그램을 설치해서 사용해 볼 것이다.

[1] Postman의 설치 및 사용

* Postman은 macOS, Windows, 리눅스에서 모두 사용할 수 있는 프로그램이다. 이 프로그램은 공식 사이트에서 인스톨러로 설치할 수 있다.

https://www.postman.com/

 

Postman API Platform | Sign Up for Free

Postman is an API platform for building and using APIs. Postman simplifies each step of the API lifecycle and streamlines collaboration so you can create better APIs—faster.

www.postman.com


* 공식 사이트에서 운영체제별로 내려받을 수 있는 페이지로 이동한다.

* Postman을 설치한 뒤 실행하면 다음과 같은 창이 나타난다.

* GET 셀렉트 박스를 클릭하여 메서드를 클릭한 후 오른쪽 텍스트 박스에 주소를 입력하고 Send 버튼을 누르면 요청할 수 있다.

POST /api/posts 에 한번 요청해 보자.

* 잘 작동하고 있는가? 이 외에 다른 API도 입력하여 테스트해 보자.

(1) PATCH : http://localhost:4000/api/posts/10

(2) PUT : http://localhost:4000/api/posts/10

(3) DELETE : http://localhost:4000/api/posts/10

[2] 컨트롤러 파일 지정

* 라우트를 작성하는 과정에서 특정 경로에 미들웨어를 등록할 때는 다음과 같이 두 번째 인자에 함수를 선언해서 바로 넣어 줄 수 있다.

router.get('/', ctx => {

});

* 하지만 각 라우트 처리 함수의 코드가 길면 라우터 설정을 한눈에 보기 힘들 것이다. 그렇기 땜누에 이 라우트 처리 함수들을 다른 파일로 따로 분리해서 관리할 수도 있다. 이 라우트 처리 함수만 모아 놓은 파일을 컨트롤러라고 한다.

* 지금은 아직 데이터베이스를 연결하지 않았으므로 자바스크립트의 배열 기능만 사용하여 임시로 기능을 구현해 보겠다.

* API 기능을 본격적으로 구현하기 전에 먼저 koa-bodyparser 미들웨어를 적용해야 한다. 이 미들웨어는 POST/PUT/PATCH 같은 메서드의 Request Body에 JSON 형식으로 데이터를 넣어 주면, 이를 파싱하여 서버에서 사용할 수 있게 한다.

* 다음 명령어를 실행하여 패키지를 설치하자.

$ yarn add koa-bodyparser

* 이어서 미들웨어를 불러와 적용해 보자. 이 때 주의할 점은 router를 적용하는 코드의 윗부분에서 해야 한다는 것이다.

[src/index.js]

const Koa = require('koa');
const Router = require('koa-router');
const bodyParser = require('koa-bodyparser');

const api = require('./api');

const app = new Koa();
const router = new Router();

// 라우터 설정
router.use('/api', api.routes()); // api 라우트 적용

// 라우터 적용 전에 bodyParser 적용
app.use(bodyParser());

// app 인스턴스에 라우터 적용
app.use(router.routes()).use(router.allowedMethods());

app.listen(4000, () => {
  console.log('Listening to port 4000');
});

* 그리고 posts 경로에 posts.ctrl.js 파일을 만든 후 주석을 참고하면서 다음 코드를 입력해 보자.

let postId = 1; // id의 초깃값입니다.

// posts 배열 초기 데이터
const posts = [
  {
    id: 1,
    title: '제목',
    body: '내용',
  },
];

/* 포스트 작성
POST /api/posts
{ title, body }
*/
exports.write = (ctx) => {
  // REST API의 request body는 ctx.request.body에서 조회할 수 있습니다.
  const { title, body } = ctx.request.body;
  postId += 1; // 기존 postId 값에 1을 더합니다.
  const post = { id: postId, title, body };
  posts.push(post);
  ctx.body = post;
};

/* 포스트 목록 조회
GET /api/posts
*/
exports.list = (ctx) => {
  ctx.body = posts;
};

/* 특정 포스트 조회
GET /api/posts/:id
*/
exports.read = (ctx) => {
  const { id } = ctx.params;
  // 주어진 id 값으로 포스트를 찾습니다.
  // 파라미터로 받아 온 값은 문자열 형식이니 파라미터를 숫자로 변환하거나,
  // 비교할 p.id 값을 문자열로 변경해야 합니다.
  const post = posts.find((p) => p.id.toString() === id);
  // 포스트가 없으면 오류를 반환합니다.
  if (!post) {
    ctx.status = 404;
    ctx.body = {
      message: '포스트가 존재하지 않습니다.',
    };
    return;
  }
  ctx.body = post;
};

/* 특정 포스트 제거
DELETE /api/posts/:id
*/
exports.remove = (ctx) => {
  const { id } = ctx.params;
  // 해당 id를 가진 post가 몇 번째인지 확인합니다.
  const index = posts.findIndex((p) => p.id.toString() === id);
  // 포스트가 없으면 오류를 반환합니다.
  if (index === -1) {
    ctx.status = 404;
    ctx.body = {
      message: '포스트가 존재하지 않습니다.',
    };
    return;
  }
  // index번째 아이템을 제거합니다.
  posts.splice(index, 1);
  ctx.status = 204; // No Content
};

/* 포스트 수정(교체)
PUT /api/posts/:id
{ title, body }
*/
exports.replace = (ctx) => {
  // PUT 메서드는 전체 포스트 정보를 입력하여 데이터를 통째로 교체할 때 사용합니다.
  const { id } = ctx.params;
  // 해당 id를 가진 post가 몇 번째인지 확인합니다.
  const index = posts.findIndex((p) => p.id.toString() === id);
  // 포스트가 없으면 오류를 반환합니다.
  if (index === -1) {
    ctx.status = 404;
    ctx.body = {
      message: '포스트가 존재하지 않습니다.',
    };
    return;
  }
  // 전체 객체를 덮어씌웁니다.
  // 따라서 id를 제외한 기존 정보를 날리고, 객체를 새로 만듭니다.
  posts[index] = {
    id,
    ...ctx.request.body,
  };
  ctx.body = posts[index];
};

/* 포스트 수정(특정 필드 변경)
PATCH /api/posts/:id
{ title, body }
*/
exports.update = (ctx) => {
  // PATCH 메서드는 주어진 필드만 교체합니다.
  const { id } = ctx.params;
  // 해당 id를 가진 post가 몇 번째인지 확인합니다.
  const index = posts.findIndex((p) => p.id.toString() === id);
  // 포스트가 없으면 오류를 반환합니다.
  if (index === -1) {
    ctx.status = 404;
    ctx.body = {
      message: '포스트가 존재하지 않습니다.',
    };
    return;
  }
  // 기존 값에 정보를 덮어씌웁니다.
  posts[index] = {
    ...posts[index],
    ...ctx.request.body,
  };
  ctx.body = posts[index];
};

* 컨트롤러를 만들면서 exports.이름 = ... 형식으로 함수를 내보내 주었다. 이렇게 내보낸 코드는 다음 형식으로 불러올 수 있다.

const 모듈이름 = require('파일이름');
모듈이름.이름();

* require('./posts.ctrl')을 입력하여 방금 만든 posts.ctrl.js 파일을 불러온다면 다음 객체를 불러오게 된다.

{
	write : Function,
    list : Function,
    read : Function,
    remove : Function,
    replace : Function,
    update : Function
};

* 우리가 만든 컨트롤러 함수들을 한번 각 라우트에 연결시켜 보자.

const Router = require('koa-router');
const postsCtrl = require('./posts.ctrl');

const posts = new Router();

posts.get('/', postsCtrl.list);
posts.post('/', postsCtrl.write);
posts.get('/:id', postsCtrl.read);
posts.delete('/:id', postsCtrl.remove);
posts.put('/:id', postsCtrl.replace);
posts.patch('/:id', postsCtrl.update);

module.exports = posts;

* 이제 posts 라우터가 완성되었다.

* list, read, remove를 제외한 API들은 요청할 때 Request Body가 필요한데, Postman에서 이 값을 어떻게 넣는지 알아보자.

* Postman에서 POST를 선택하면 다음과 같이 Body 부분이 활성화된다. Body 탭을 선택하여 raw 옵션을 클릭한 후 주황색으로 나타나는 데이터 타입을 JSON으로 설정하자.

* 그리고 하단 텍스트 박스에 다음 JSON을 입력하자.

{
	"title" : "테스팅",
    "body" : "테스팅"
}

* 모두 입력한 뒤 Send 버튼을 눌러 보자. POST 요청에 성공하면 다음과 같이 서버가 응답할 것이다.

* 결과가 잘 나타났는가? 포스트가 정말 성공적으로 등록되었는지 확인하고 싶다면? GET /api/posts에 요청해 보자.

* 등록에 성공했다면 방금 등록한 포스트가 list 함수로 조회될 것이다.

* 우리가 구현한 update와 replace 함수는 용도는 비슷하지만 구현 방식이 다르다. update(PATCH)는 기존 값은 유지하면서 새 값을 덮어 씌우는 반면, replace(PUT)은 Request Body로 받은 값이 id를 제외한 모든 값을 대체한다.

* 직접 한 번 호출해 보자. 다음 요청을 Postman으로 실행해 보자.

* PATCH로 했을 때는 기존 body 내용을 유지하며, Request Body로 전달한 title 값만 변경했다. 반면 PUT으로 하면 어떨까?

* PUT 메서드를 사용하니 기존 body가 사라져 버린다. 따라서 포스트 수정 API를 PUT으로 구현해야 할 때는 모든 필드가 다 있는지 검증하는 작업이 필요하다.


6. 정리

* 이 장에서는 Koa를 사용하여 백엔드 서버를 만드는 기본 개념에 대해 알아보았다. 먼저 REST API를 살펴본 후 어떻게 작동하는지를 자바스크립트 배열을 사용하여 구현하면서 알아보았다. 자바스크립트 배열을 사용하여 구현하면 서버를 재시작할 때 당연히 데이터가 소멸된다. 물론 이 데이터를 로컬 파일에 저장하는 방법도 있지만, 실제 프로젝트에서는 권장하지 않는다. 그 대신 MySQL, MongoDB 등의 데이터베이스에 정보를 저장하여 관리한다.

* 데이터베이스를 사용하면 다양하고 효율적인 방식으로 많은 양의 데이터를 읽고 쓸 수 있다. 우리는 그 중 MongoDB를 사용하여 백엔드를 구현해 볼 것이다.