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

go·2021년 7월 28일
0

React

목록 보기
12/15
post-thumbnail

1. 소개하기

2. 작업 환경 준비

1) Node 설치 확인

$ node --version

2) 프로젝트 생성

$ mkdir blog
$ cd blog
$ mkdir blog-backend
$ cd blog-backend
$ yarn init -y

이 작업을 하고 나면 디렉터리에 package.json 파일이 생성됩니다.

다음 명령어를 통해 파일이 잘 만들어졌는지 확인해 볼 수 있습니다.

$ cat package.json

Koa 웹 프레임워크를 설치합니다.

$ yarn add koa

3) ESLint와 Prettier 설정

$ yarn add --dev eslint
$ yarn run eslint --init
$ yarn add eslint-config-prettier

3. Koa 기본 사용법

1) 서버 띄우기

const Koa = require('koa');

const app = new Koa();

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

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

기존 index.js 파일에 다음 코드를 입력한 뒤, 서버를 포트 4000번으로 열고, 웹 브라우저로 http://localhose:4000/ 에 접속합니다.

2. 미들웨어

Koa 애플리케이션은 미들웨어의 배열로 구성되어 있습니다. 아까 코드에서 사용한 app.use 함수는 미들웨어 함수를 애플리케이션에 등록합니다.

미들웨어 함수는 다음과 같은 구조로 이루어져 있습니다.

(ctx, next) => {
}

Koa의 미들웨어 함수는 두 개의 파라미터를 받습니다.

ctx는 Context의 줄임말로 웹 요청과 응답에 관한 정보를 지니고 있습니다.

next는 현재 처리 중인 미들웨어의 다음 미들웨어를 호출하는 함수입니다. 미들웨어를 등록하고 next 함수를 호출하지 않으면, 그 다음 미들웨어를 처리하지 않습니다.

다음 미들웨어를 처리할 필요가 없는 라우트 미들웨어를 나중에 설정할 때는 ctx ⇒ { } 와 같은 형태로 next를 생략하여 미들웨어를 작성합니다.

미들웨어는 app.use를 사용하여 등록되는 순서대로 처리됩니다.

const Koa = require('koa');

const app = new Koa();

app.use((ctx, next) => {
  console.log(ctx.url);
  console.log(1);
  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');
});

실행 중인 서버를 종료한 뒤, 다시 실행해보면 터미널에 다음과 같은 결과물이 나타날 것입니다.

❯ node src
Listening to port 4000
/
1
2
/favicon.ico
1
2

크롬 브라우저는 사용자가 웹 페이지에 들어가면 해당 사이트의 아이콘 파일인 /favicon.ico 파일을 서버에 요청하기 때문에 결과에 /경로도 나타나고 /favicon.ico 경로도 나타납니다.

첫 번째 미들웨어에서 호출하던 next 함수를 주석처리 해보면 첫 번째 미들웨어까지만 실행하고 그 아래에 있는 미들웨어는 모두 무시되어 다음과 같은 결과물이 나타날 것입니다.

❯ node src
Listening to port 4000
/
1
/favicon.ico
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.state = 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 gkatnsms Promise를 반환

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

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.state = 401; // Unauthorized
    return;
  }
  next().then(() => {
    console.log('END');
  });
});

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

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

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

서버를 재시작한 뒤 다시 접속해보면 END가 나타납니다.

❯ node src
Listening to port 4000
/
1
/?authorized=1
1
2
END

라우트 파라미터와 쿼리

파라미터와 쿼리는 둘 다 주소를 통해 특정 값을 받아 올 때 사용하지만, 용도가 서로 조금씩 다릅니다.

파라미터 : 처리할 작업의 카테고리를 받아 오거나, 고유 ID 혹은 이름으로 특정 데이터를 조회할 때 사용합니다.

쿼리 : 옵션에 관련된 정보를 받아옵니다.

// 파라미터
router.get('/about/:name?', ctx => {
    const { name } = ctx.params;
    // name의 존재 유무에 따라 다른 결과 출력
    ctx.body = name ? `${name}의 소개` : '소개';
});

// 쿼리
router.get('/posts', ctx => {
    const { id } = ctx.query;
    // id의 존재 유무에 따라 다른 결과 출력
    ctx.body = id ? `포스트 #${id}` : '포스트 아이디가 없습니다.';
});

REST API

웹 애플리케이션을 만들려면 데이터베이스에 정보를 입력하고 읽어 와야 합니다. 클라이언트가 서버에 자신이 데이터를 조회, 생성, 삭제, 업데이트하겠다고 요청하면, 서버는 필요한 로직에 따라 데이터베이스에 접근하여 작업을 처리합니다.

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

HTTP 메서드의 종류

블로그 포스트용 REST API

4. nodemon 사용하기

nodemon이라는 도구를 사용하면 코드를 변경할 때마다 서버를 자동으로 재시작해 줍니다.

$ yarn add --dev nodemon

5. 라우트 모듈화

프로젝트를 진행하다 보면 여러 종류의 라우트를 만들게 됩니다. 하지만 각 라우트를 index.js 파일에 모두 작성할 수 없으니 라우터를 여러 파일에 분리시켜 작성하고, 이를 불러와 적용합니다.

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

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

/**
 * 포스트 작성
 * POST /api/posts
 * { title, body }
 */ 
export const 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
 */
export const list = ctx => {
    ctx.body = posts;
};

/**
 * 특정 포스트 조회
 * GET /api/posts/:id
 */
export const 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
 */
export const 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 }
 */
export const replace = ctx => {
    // PUT 메서드는 전체 포스트 정보를 입력하여 데이터를 통째로 교체할 때 사용합니다.
    const { id } = ctx.params;
    // 해당 id를 가진 post가 몇 번쩨인지 확인합니다.
    const index = posts.findImdex(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 }
 */
export const 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];
};

0개의 댓글