백엔드 프로그래밍 (Node.js)

나혜수·2023년 4월 3일
0

리액트

목록 보기
20/23

Koa를 사용하여 웹서버 만들기

Koa

Node.js 환경에서 웹 서버를 구축할 땐 보통 Express, Koa, Hapi 등 웹 프레임워크를 사용한다. Koa는 Express 개발팀이 개발한 새로운 프레임워크이다.

작업 환경 준비

  1. Node.js 설치 확인
$ node --version
> v18.15.0
  1. 프로젝트 생성
$ mkdir blog
$ cd blog
$ mkdir blog-backend
$ cd blog-backend
$ yarn init -y  // package.json 파일 생성 
$ yarn add koa  // koa 프레임워크 설치 


koa 사용법

서버 띄우기

먼저 서버를 여는 방법부터 알아보자. src 디렉토리를 생성하여 index.js 파일을 만든다.

🏷️src/index.js

  • Koa 프레임워크를 require( )로 가져온 후, app을 생성한다.
  • use( )로 앱이 실행되었을 때 동작을 지정한다. ( 'hello world' 텍스트를 반환 )
  • listen( )을 실행해 4000번 포트로 서버를 동작시킨다. 콜백을 전달해서 서버가 실행된 후 동작을 지정해줄 수도 있다. 여기에서는 console에 로그를 찍는 동작을 수행했다.
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를 통해 자바스크립트 파일을 실행할 때는 node src/index.js 와 같이 전체 경로를 입력하는 것이 맞지만 index.js는 예외로 디렉토리까지만 입력해도 실행할 수 있다.

// node를 통해 자바스크립트 파일을 실행

PS C:\Users\naban\blog> node src
listening to port 4000

웹 브라우저로 http://localhost:4000/에 접속한다.

미들웨어

미들웨어란?
웹에서 말하는 미들웨어란 요청과 응답 중간에서 동작하는 것들을 뜻한다. 라우터 등이 대표적이 미들웨어이다.


Koa 애플리케이션은 여러 미들웨어가 연결되어 구성된다. app.use( ) 함수는 사용할 미들웨어 함수를 애플리케이션에 등록한다.

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

(ctx, next) => {  
}

미들웨어 함수는 2개의 파라미터를 받는다.

  • ctx : context의 줄임말로 웹 요청과 응답에 대한 정보를 지니고 있다.
  • next : 현재 처리 중인 미들웨어의 다음 미들웨어를 호출하는 함수

미들웨어는 등록하는 순서대로 실행된다.

const Koa = require('koa');
const app = new Koa();

app.use(ctx => {
    console.log(1);
});


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


app.use(ctx => {
    ctx.body = 'Hello Koa';
});

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

실행 중인 서버를 cmd 창에서 ctr + c를 눌러 종료한 뒤, 다시 node src 명령어를 입력하여 서버를 실행해준다. 다시 http://localhost:4000/에 들어가게 된다면, Not Found가 뜨고 터미널에는 다음과 같이 기록된다.
next( ) 를 호출하지 않았기 때문에 그 다음 미들웨어를 실행하지 않고, 첫번째 미들웨어에서 멈춰버렸다. 함수의 파라미터 부분에 next를 받아와서 호출해보자.

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

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

이런 next의 속성을 사용해 조건부로 미들웨어 처리를 무시하게 만들 수 있다. 다음 코드에서는 요청 경로에 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 Koa';
});

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

각 링크에 따라 결과가 다르게 나타난다.

  • localhost:4000/
  • localhost:4000/?authorized=1

next 함수는 promise를 반환

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

console.log(1) → console.log(2) → console.log('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 Koa';
});

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

async / await

Koa는 async/await를 정식으로 지원하기 때문에 해당 문법을 바로 사용할 수 있다. 방금 작성했던 미들웨어를 async/await을 통해 재작성해보자.

const Koa = require('koa');
const app = new Koa();

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


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

app.use(ctx => {
    ctx.body = 'Hello Koa';
});

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

이전과 똑같이 작동한다.

console.log(1) → console.log(2) → console.log('end')

nodemon 사용하기

서버 코드를 변경할 때마다 서버를 재시작하는 것은 꽤 번거롭다. nodemon을 사용하면 코드를 변경할 때마다 서버를 자동으로 재시작해준다.

이 도구를 개발용 의존 모듈로 설치한다.

$ yarn add --dev nodemon

그 후 package.json에 srcipt를 다음과 같이 입력한다.

{
  "name": "blog-backend",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "koa": "^2.14.1"
  },
  "devDependencies": {
    "nodemon": "^2.0.22"
  },
  "scripts": {
    "start": "node src",
    "start:dev": "nodemon --watch src/ src/index.js"
  }
}

start 스크립트에는 서버를 시작하는 명령어를 넣고, start:dev 스크립트에는 nodemon을 통해 서버를 실행하는 명령어를 넣었다. nodemon은 src 파일을 주시하고 있다가 해당 디렉토리 내부의 어떤 파일이 변경되면 이를 감지해 src/index.js 파일을 재시작해준다.

이제부터 다음 명령어를 통해 서버를 시작할 수 있다.

yarn start       // 재시작 필요 x
yarn start:dev   // 재시작 필요할 때 

기존 실행 중이던 서버를 종료한 뒤 yarn start:dev 명령어를 실행해보자. 그 후 index.js에서 기존 미들웨어를 모두 제거한다.

const Koa = require('koa');
const app = new Koa();

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


koa-router 사용하기

앞서 웹 브라우저의 라우팅을 돕는 리액트 라우터 라이브러리를 사용해보았다.
koa를 사용할 때도 다른 주소로 요청이 들어올 경우 다른 작업을 처리할 수 있도록 라우터를 사용해야 한다. koa 자체에 이 기능이 내장되어 있지 않으므로 koa-router 모듈을 설치한다.

$ yarn add koa-router

기본 사용법

index.js에 라우터를 불러와 적용한다.

koa-router를 불러온 뒤 이를 사용해 Router 인스턴스를 만든다. '/' 경로로 들어오면 홈을 띄우고, '/about' 경로로 들어오면 소개 텍스트가 나타나도록 설정한다.

라우터를 설정할 때는 router.get 첫번째 파라미터에 경로를, 두번째 파라미터에는 해당 라우트에 적용할 미들웨어 함수를 넣는다. get 키워드는 해당 라우트에서 사용할 HTTP 메소드를 의마하며 get 대신 post, put, delete 등을 넣을 수 있다.

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

라우트 파라미터와 쿼리

파라미터와 쿼리는 둘 다 주소를 통해 특정 값을 받아올 때 사용한다. 정해진 규칙은 없지만 일반적으로 파라미터는 처리할 작업의 카테고리를 받아오거나, 고유 id or 이름으로 특정 데이터를 조회할 때 사용한다. 쿼리는 옵션에 관련된 정보를 받아온다.

  • 파라미터
    라우터의 파라미터를 설정할 때는 /about/:name 형식으로 콜론 :을 사용하여 라우트 경로를 살정한다. 파라미터가 있을 수도 없을 수도 있다면 /about/:name? 형식으로 파라미터 이름 뒤에 ?를 붙인다.
    이렇게 설정한 파라미터는 함수의 ctx.params에서 조회할 수 있다.

  • 쿼리
    URL 쿼리의 경우, 예를 들어 /post/?id=10 형식으로 요청했다면 해당 값을 ctx.query에서 조회할 수 있다. 쿼리 문자열을 자동으로 객체 형식으로 파싱해주기 때문에 파싱 함수를 돌릴 필요가 없다. 문자열 형태의 쿼리스트링을 조회할 때는 ctx.quertstring을 이용한다.

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

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


// 라우터 설정 
router.get('/about/:name?',ctx => {
    const {name} = ctx.params
    ctx.body = name ? `${name}의 소개` : `소개`
})

router.get('/posts',ctx => {
    const {id} = ctx.query
    ctx.body = id ? `포스트 #${id}` : `포스트 아이디가 없습니다.`
})


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


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

REST API

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

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

REST API는 요청의 종류에 따라 다른 HTTP 메소드를 사용한다. HTTP 메소드는 여러 종류가 있는데 그 중 주로 사용되는것들은 다음과 같다.

  • GET : 데이터를 조회할 때 사용
  • POST : 데이터를 등록할 때 or 인증 작업을 거칠때 사용
  • DELETE : 데이터를 지울 때 사용
  • PUT : 데이터를 새 정보로 통째로 교체 할 때 사용
  • PATCH : 데이터의 특정 필드를 수정할 때 사용

REST API를 설계할 때는 api 주소와 메소드에 따라 어떤 역할을 하는지 쉽게 파악할 수 있게 작성해야 한다.

라우트 모듈화

프로젝트에는 여러 종류의 라우트를 만들게된다. 각 라우트를 index.js 파일에 다 작성한다면 코드가 너무 길어지고 유지보수도 하기 어려워진다. 라우터를 여러 파일에 분리시켜 작성하고 이를 불러와 적용해보자.

우선 src 디렉토리에 api 디렉토리를 만들고 이 내부에 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

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())

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


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

post 라우트 생성

api 디렉토리에 posts 디렉토리를 만들고 그 내부에 index.js 파일을 만든다.

🏷️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, put, patch 메소드를 사용하는 api는 자바스크립트로 호출해야 한다. 우리가 만든 api를 자바스크립트로 호출하는 대신 REST API 테스트를 쉽게할 수 있는 Postman 프로그램을 사용해보자.

Postman

Postman 다운로드

컨트롤러 파일 작성

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

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

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

koa bodyparser

  • 먼저 Parser는 무엇을 의미하는가?
    가지고 있는 데이터를 내가 원하는 형태의 데이터로 가공하는 과정을 parsing이라 하며 그 과정을 수행하는 모듈 혹은 메소드를 parser라 일컫는다.
    내가 원하는 형식에 맞춰 해석하는 용도이므로 bodyParser 뿐만 아니라 cookieParser, JSON.parse, JSON.stringify 등 파서의 종류는 셀 수 없이 많다.

    여기서 기억해야 할 것은 parser는 구문 해석을 할 뿐 번역을 하진 않는다. 번역의 역할은 compiler에서 담당한다. 예를 들어 외국어를 번역할 때 어떤 언어인지 정하고 해당 언어에 맞게 구문을 해석해주는 것을 parser, 그에 따라 실제 번역하는 것을 compiler라고 할 수 있다.

    파싱을 하지 않는다는 것은 구글 번역기에 프랑스어를 선택하고 한국어를 쓴 후에 영어로 번역하라는 것과 같으니 필요한 상황에서는 그에 맞는 파싱을 해주어야 한다.

  • 그렇다면 bodyParser는 무엇을 파싱하는가?
    express 문서에 따르면 미들웨어 없이 req.body에 접근하는 경우에는 기본으로 undefined가 설정되어 있으므로 bodyParser, multer 와 같은 미들웨어를 사용하여 요청 데이터 값에 접근해야 한다고 나와있다. 클라이언트 측에서 API post, put 메소드로 요청시 body를 포함하여 보낼 수 있는데 이 값을 서버 측에서 받는다고 그대로 사용할 수 있는 것이 아니고, 서버 내에서 해석 가능한 형태로 변형해야 사용할 수 있다.
    이때 API 요청에서 받은 body 값을 파싱하는 역할을 수행하는 것이 bodyParser 라는 미들웨어이다.


$ yarn add koa-bodyparser

🏷️src/index.js
❗라우터를 적용하기 전에 bodyparser를 먼저 적용해야 한다.

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())

// 라우트 적용 전에 bodyparser 적용
app.use(bodyParser())

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


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

🏷️src/api/posts/posts.ctrl.js 컨트롤러 파일

let postId = 1; // id의 초기값

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

// 1. 포스트 작성 : POST /api/posts
// {title, body}
exports.write = (ctx) => {
  const { title, body } = ctx.request.body; // REST API의 Request body는 ctx.request.body에서 조회
  postId += 1;
  const post = { id: postId, title, body };
  posts.push(post);
  ctx.body = post;
};

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

// 3. 특정 포스트 조회 : GET /api/posts/:id
exports.read = (ctx) => {
  const { id } = ctx.params;
  // 파라미터로 받아온 값이 문자열 형식이므로 id 값을 문자열로 변경함
  const post = posts.find((p) => p.id.toString() === id);

  // 포스트가 없으면 오류 반환
  if (!post) {
    ctx.status = 404;
    ctx.body = {
      message: "포스트가 존재하지 않습니다.",
    };
    return;
  }
  ctx.body = post;
};

// 4. 특정 포스트 제거 : 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
  }
  posts.splice(index,1)
  ctx.status = 204 // No Content 
}

// 5. 포스트 수정 : PUT /api/posts/:id 
// {title, body}
// PUT 메소드는 전체 포스트 정보를 입력하여 데이터를 통째로 교체할 때 사용!
exports.replace = 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
  }

  // 전체 객체를 덮어 씌운다. id를 제외한 기존 정보를 날리고 객체를 새로 만든다. 
  posts[index] = {
    id, 
    ... ctx.request.body
  }
  ctx.body = posts[index]
}

// 6. 포스트 수정 (특정 필드) : PATCH /api/posts/:id 
// {title, body}
exports.update = 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
  }

  // 기존 값에 정보를 덮어씌움 
  posts[index] = {
    ...posts[index], 
    ... ctx.request.body
  }
  ctx.body = posts[index]
}

🏷️src/api/posts/index.js

require('./posts/ctrl')을 입력하여 위의 post.ctrl.js 파일을 불러오면 다음 객체를 불러오게 된다.

{
  write: Function,
  list: Function,
  read: Function,
  remove: Function,
  replace: Function,
  update: Function  
}
const Router = require('koa-router')
const postsCtrl = require('./post.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 라우터가 완성되었다.
post, put, patch api들을 요청할 때 request body가 필요하다. postman에서 이 값을 어떻게 넣는지 알아보자.

  • 포스트 등록
  • 포스트 등록 확인

put, patch 차이

  • patch는 기존 값을 유지하면서 새 값을 덮어씌운다.
  • put은 requestbody로 받은 값이 id를 제외한 모든 값을 대체한다.
    put 메소드를 사용하니 기존 body가 사라졌다. 따라서 포스트 수정을 put으로 구현할 때는 모든 필드가 다 있는지 검정하는 작업이 필요하다.

profile
오늘도 신나개 🐶

0개의 댓글