본문 바로가기

react

[리액트를 다루는 기술] 21장 백엔드 프로그래밍 : Node.js의 Koa 프레임워크

21.1 작업 환경 준비 

 

21.1.1 프로젝트 생성 

 

- 블로그 서비스와 연동할 서버 

- blog 디렉터리를 만들고, 그 내부에 blog-backend 디렉터리 만듬

- npm init -y 명령어를 통해 패키지 정보 생성 

- koa 웹 프레임워크 설치 

npm install koa

 

 

21.2 koa 기본 사용법 

 

21.2.1 서버 띄우기 

 

index.js

- 서버를 포트 4000번으로 열고, 서버에 접속하면 'hello world'라는 텍스트를 반환하도록 설정

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

 

21.2.2 미들웨어 

 

- Koa 애플리케이션은 미들웨어의 배열로 구성되어 있음

- app.use 함수는 미들웨어 함수를 애플리케이션에 등록

 

미들웨어 함수 

ctx - 웹 요청과 응답에 관한 정보를 지님
next - 현재 처리중인 미들웨어의 다음 미들웨어를 호출하는 함수 
(ctx, next) => {
}

 

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

 

index.js 

- 현재 요청을 받은 주소와 우리가 정해 준 숫자를 기록하는 두 개의 미들웨어 

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');
});

 

index.js

- 조건부로 다음 미들웨어 처리를 무시하게 만들 수 있음

- 요청 경로에 authorized=1이라는 쿼리 파라미터가 포함되어 있으면 이후 미들웨어를 처리해 주고, 그렇지 않으면 이후 미들웨어를 처리하지 않음 

- 쿼리 파라미터는 문자열이기 때문에 비교할 때는 문자열 형태로 비교 

 

 

 

21.2.2.1 next 함수는 Promise를 반환

 

- next 함수를 호출하면 Promise를 반환 

- next 함수가 반환하는 Promise는 다음에 처리해야 할 미들웨어가 끝나야 완료

 

index.js

- next 함수 호출 이후에 then을 사용하여 Promise가 끝난 다음 콘솔에 END를 기록하도록 수정 

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().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');
});

 

 

 

21.2.2.2 async/await 사용하기 

 

- Koa는 async/await를 정식으로 지원

 

 

21.3 nodemon 사용하기 

 

- nodemon을 이용하면 코드를 변경할 때마다 서버를 자동으로 재시작 

npm install nodemon --save-dev

 

 

- start 스크립트에는 서버를 시작하는 명령어

npm start #재시작이 필요없을때

- start:dev 스크립트에는 nodemon을 통해 서버를 실행해 주는 명령어 

npm start:dev #재시작이 필요할 떄

 

 

 

21.4 koa-router 사용하기 

 

- Koa를 사용할 때도 다른 주소로 요청이 들어올 경우 다른 작업을 처리할 수 있도록 라우터를 사용 

npm install koa-router

 

 

21.4.1 기본 사용법 

 

index.js

const Koa = require('koa');
const Router = require('koa-router'); //Router 인스턴스 

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

//라우터 설정
router.get('/', ctx=> {
    ctx.body='홈';
});

router.get('/about', ctx => {
    ctx.body='소개';
});

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


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

 

- koa-router를 불러온 뒤 이를 사용하여 Router 인스턴스를 만듬 

- / 경로로 들어오면 '홈'을 띄우고, /about 경로로 들어오면 '소개' 텍스트 나타나도록 설정

- 라우트를 설정할 때 route.get의 첫 번째 파라미터는 라우트의 경로를 넣고, 

두번쨰 파라미터에는 해당 라우트에 적용할 미들웨어 함수를 넣음

 

 

 

21.4.2 라우트 파라미터와 쿼리 

 

라우트 파라미터 

 

- 처리할 작업의 카테고리를 받아오거나, 고유 ID 혹은 이름으로 특정 데이터를 조회할 때 사용 

- 라우터의 파라미터를 설정할 때는 /about/:name 형식으로 콜론(:)을 사용하여 라우트 경로를 설정

- 파라미터가 있을 수도 있고 없을수도 있다면 /about/:name? 같은 형식으로 파라미터 이름 뒤에 물음표를 사용 

- 설정한 파라미터는 함수의 ctx.params 객체에서 조회 가능 

 

 

라우트 쿼리 

 

- 옵션에 관련된 정보를 받아옴

 ex) 여러 항목을 리스팅하는 API라면, 어떤 조건을 만족하는 항목을 보여 줄지 또는 어떤 기준으로 정렬할지를 정해야할 떄 쿼리 사용 

- post/?id=10 같은 형식으로 요청했다면 해당 값을 ctx.query에서 조회 가능 

- 쿼리 문자열을 자동으로 객체 형태로 파싱해 주므로 별도로 파싱 함수를 돌릴 필요 없음 

 

index.js

const Koa = require('koa');
const Router = require('koa-router'); //Router 인스턴스 

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

//라우터 설정
router.get('/', ctx=> {
    ctx.body='홈';
});

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}`:`포스트 아이디가 없습니다.`;
});

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


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

 

 

21.4.3 Rest API

 

- 웹 애플리케이션을 만들려면 데이터베이스에 정보를 입력하고 읽어 와야 함 

- 보안 문제로 REST API를 만들어서 사용 

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

 

- REST API는 요청 종류에 따라 다른 HTTP 메서드를 사용 

- 메서드의 종류에 따라 get,post,delete,put, patch를 사용하여 라우터에서 각 메서드의 요청을 처리 

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

 

 

- 블로그 포스트용 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 특정 포스트의 특정 댓글 삭제 

 

 

21.4.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;

 

 

src/index.js

- api 라우트를 src/index.js 파일에 불러와서 기존 라우터에 /api 경로로 적용

const Koa = require('koa');
const Router = require('koa-router'); //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');
});

 

 

21.4.5 posts 라우트 생성

 

- api 라우트 내부에 posts 라우트 만들기 

 

src/api/posts/index.js

- posts 라우트에 여러 종류의 라우트를 설정한 후 모두 printInfo 함수를 호출하도록 설정

- 문자열이 아닌 JSON 객체를 반환하도록 설정하고, 이 객체에는 현재 요청의 메서드, 경로, 파라미터를 담음

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;

 

 

src/api/index.js

- api 라우트에 posts 라우트를 연결

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

const api = new Router();

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

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

 

 

- GET 메서드를 사용하는 API는 웹 브라우저에서 주소를 입력하여 테스팅할 수 있지만 POST, DELETE, PUT, PATCH 메서드를 사용하는 API는 자바스크립트로 호출해야 함 

 

21.4.5.1 Postman의 설치 및 사용 

 

- POST/api/posts 요청 

 

 

21.4.5.2 컨트롤러 파일 작성

 

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

router.get('/', ctx => {
});

 

- 각 라우트 처리 함수의 코드가 길면 라우터 설정을 한눈에 보기 힘듬

 -> 라우트 처리 함수들을 다른 파일로 따로 분리해서 관리 가능

 -> 컨트롤러 = 라우트 처리 함수만 모아놓은 파일 

 

- API 기능을 본격적으로 구현하기 전 먼저 koa-bodyparser 미들웨어를 적용해야 함 

 

koa-bodyparser 미들웨어 

 

npm install koa-bodyparser

 

- POST/PUT/PATCH 같은 메서드의 Request Body에 JSON 형식으로 데이터를 넣어 주면, 이를 파싱하여 서버에서 사용할 수 있게 함 

 

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

 

- 컨트롤러를 만들면서 exports.이름 = ...형식으로 함수를 내보내줌

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

 

- 내보낸 코드는 다음 형식으로 불러올 수 있음

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

 

- require('./posts/ctrl')을 입력하여 posts.ctrl.js 파일을 불러온다면 다음 객체를 불러오게 됨

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

 

src/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;

 

- list,read,remove를 제외한 API들은 요청할 때 Request Body가 필요