React

[React] 백엔드 프로그래밍 : Node.js의 Koa 프레임워크

옝옹 2023. 5. 3. 16:08

Node.js

자바스크립트 엔진을 기반으로 웹 브라우저뿐만 아니라 서버에서도 자바스크립트를 사용할 수 있는 런타임

Koa

  • Node.js 환경에서 웹 서버를 구축할 때 보통 Express, Hapi, Koa등의 웹 프레임워크를 사용
  • Koa는 Express의 기존 개발 팀이 개발한 프레임워크
    • Express는 미들웨어, 라우팅, 템플릿, 파일 호스팅 등과 같은 다양한 기능이 자체적으로 내장되어 있음
    • Koa는 미들웨어 기능만 갖추고 있으며 나머지는 다른 라이브러리를 적용하여 사용
    • 즉, Koa는 우리가 필요한 기능들만 붙여서 서버를 만들 수 있기 때문에 Express보다 훨씬 가벼움
  • Koa는 async/await 문법을 정식으로 지원하기 때문에 비동기 작업을 더 편하게 관리

미들웨어

  • Koa 애플리케이션은 미들웨어의 배열로 구성되어 있음
  • 미들웨어 함수 구조
    • ctx : Context의 줄임말로 웹 요청과 응답에 관한 정보를 지니고 있음
    • next : 현재 처리 중인 미들웨어의 다음 미들웨어를 호출하는 함수
      • 미들웨어를 등록하고 next 함수를 호출하지 않으면, 그 다음 미들웨어를 처리하지 않음
      • 다음 미들웨어를 처리할 필요가 없는 라우트 미들웨어를 나중에 설정할 때 ctx => {} 구조로 미들웨어를 작성
(ctx, next) => {
}
  • 미들웨어는 app.use를 사용하여 등록되는 순서대로 처리된다.

첫번째 미들웨어까지만 실행되고 아래 미들웨어는 모두 무시됨

  • 아래 코드는 http://localhost:4000/?authorized=1 와 나머지 일때 결과가 다르다.
    • authorized=1이라는 쿼리 파라미터가 포함되어 있으면 이후 미들웨어를 처리해줌
const Koa = require('koa');

const app = new Koa();

app.use((ctx, next) => {
  console.log(ctx.url);
  console.log(1);
  if (ctx.query.authorized !== '1') {
    ctx.status = 401; // Unauthorized
    return;
  }
  next();
});

app.use((ctx, next) => {
  console.log(2);
  next();
});

app.use((ctx) => {
  ctx.body = 'hello world';
});

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

next 함수는 Promise를 반환

  • next 함수를 호출하면 Promise를 반환한다. 이는 Koa가 Express와 차별화되는 부분이다.
  • next 함수가 반환하는 Promise는 다음에 처리해야 할 미들웨어가 끝나야 완료된다.

koa-router 사용하기

다른 주소로 요청이 들어올 경우 다른 작업을 처리할 수 있도록 라우터를 사용해야 한다.  → koa-router 모듈 설치

  • 파라미터는 처리할 작업의 카테고리를 받아오거나, 고유 ID 혹은 이름으로 특정 데이터를 조회할 때 사용
  • 쿼리는 옵션에 관련된 정보를 받아온다.
    • 어떤 조건을 만족하는 항목을 보여 줄지 또는 어떤 기준으로 정렬할지를 정해야할 때 쿼리를 사용

REST API 

  • 웹 브라우저에서 데이터베이스에 직접 접속하여 데이터를 변경하면 보안상 문제가 되기 때문에 REST API를 만들어 사용한다

  • 클라이언트가 서버에 자신이 데이터를 조회, 생성, 삭제, 업데이트하겠다고 요청하면, 서버는 필요한 로직에 따라 데이터베이스에 접근하여 작업을 처리한다.
  • HTTP 메서드
메서드 설명
GET 데이터를 조회할 때 사용한다
POST 데이터를 등록할 때 사용한다. 인증 작업을 거칠 때 사용하기도 한다
DELETE 데이터를 지울 때 사용한다
PUT 데이터를 새 정보로 통째로 교체할 때 사용한다
PATCH 데이터의 특정 필드를 수정할 때 사용한다

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

api/posts/index.js

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;