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

unzinzanda·2025년 2월 22일
0
post-thumbnail

1 ~ 20장까지 학습하며 리액트의 기본 개념을 대부분 습득하였습니다. 따라서 이제는 실습을 해볼 차례입니다! 하지만 프론트엔드 기술만으로는 필요한 기능을 모두 구현할 수 없는 경우가 흔합니다. 따라서 서버를 만들어 데이터를 여러 사람과 공유하기 위해 백엔드에 대해 알아보겠습니다.

21.1. 소개하기

21.1.1. 백엔드

서버 프로그래밍 또는 백엔드(back-end) 프로그래밍이란 어떤 종류의 데이터를 몇 개씩 보여 줄지, 그리고 또 어떻게 보여 줄지 등에 관한 로직을 만드는 것을 말합니다.

백엔드 프로그래밍은 언어에 구애 받지 않기 때문에 PHP, 파이썬, Golang, Java, Javascript, 루비 등과 같은 다양한 언어로 구현할 수 있습니다. 리액트를 다루는 기술에선 그중 자바스크립트로 서버를 구현할 수 있는 Node.js를 사용했습니다.

21.1.2. Node.js

구글의 V8이라는 자바스크립트 엔진을 기반으로 **웹 브라우저뿐만 아니라 서버에서도 자바스크립트를 사용할 수 있는 런타임**을 개발했고 그것이 바로 **Node.js**입니다.

21.1.3. Koa

Node.js 환경에서 웹 서버를 구축할 때, Express, Hapi, Koa 등의 웹 프레임워크를 사용합니다. 리액트를 다루는 기술 20장에선 Express를 사용했으니 이번엔 Koa를 써봅시다.

Express는 미들웨어, 라우팅, 템플릿, 파일 호스팅 등 다양한 기능이 자체적으로 내장되어 있지만 Koa미들웨어 기능만 갖추며 필요한 기능들만 붙여서 서버를 만들 수 있기 때문에 Express보다 훨씬 가볍습니다.

또한 Koa는 async/await 문법을 정식으로 지원하기 때문에 비동기 작업을 더 편하게 관리할 수 있습니다.


21.2. 작업 환경 준비

21.2.1. Node 설치 확인

$ node --version

21.2.2. 프로젝트 생성

이 서버는 뒤에서 다룰 블로그 서비스와 연동할 서버입니다.

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

저는 터미널을 사용하지 않고 VS Code에서 New Floder로 만들었습니다.
디렉토리에 package.json 파일이 생성되었음을 확인하였다면 Koa를 설치합니다.

$ yarn add koa

21.2.3. ESLint와 Prettier 설정

자바스크립트 문법을 검사하고 깔끔한 코드를 작성하기 위해 ESLint와 Prettier를 사용합니다. 두 기능을 사용하기 위해 VS Code에서 Prettier-Code formatter와 ESLint 확장 프로그램을 설치해야 합니다.

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

위와 같이 교재대로 eslint를 설치했는데 eslint.config.mjs 파일이 생성되어 당황했습니다. 알고 보니 위와 같이 eslint를 설치하면 9버전이 설치되기 때문이었습니다.

ESLint 9

ESLint가 버전 9로 업데이트되면서 기존 .eslintrc 또는 .eslintrc.json 설정 파일은 지원되지 않고 flat config라는 새로운 config 포맷이 등장하며 eslint.config.mjs 설정 파일을 지원합니다.

기존의 ESLint 8 버전이 EOL 상태로 전환되며 일부 프로젝트의 경우, ESLint 9로 마이그레이션이 필요합니다.

Configuration Migration Guide
이 공식 문서에서 기존 파일을 eslint.config.mjs 파일로 마이그레이션하는 방법을 알려줍니다.

하지만 저는 아직 ESLint 9에 대한 지식도 부족하기 때문에 리액트를 다루는 기술의 실습에서는 그냥 기존의 ESLint 8 버전을 사용하기로 결정하였습니다. 나중에 ESLint 9 버전을 학습하고 포스트를 해봐야겠습니다.ㅎㅎ

다시 교재의 내용을 이어서 .prettierrc 파일을 만들고 코드 스타일은 ESLint에서 관리하지 않도록

$ yarn add eslint-config-prettier

eslint-config-prettier를 설치하고 .eslintrc.json 파일에

...
"extends": ["eslint:recommended", "prettier"]
...
"rule": {
	"no-unused-vars": "warn",
    "no-console": "off"
}

를 추가합니다.


21.3. Koa 기본 사용법

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

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

$ node src
Listening to port 4000

서버를 실행하면

따라란~

21.3.2. 미들웨어

Koa 애플리케이션은 미들웨어의 배열로 구성되어 있습니다. 미들웨어 함수는 use 함수를 사용하여 애플리케이션에 등록합니다. 미들웨어 함수의 구조는 다음과 같습니다.

(ctx, next) => {
}

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

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')
})
next 호출함next 호출 안 함

이런 속성을 사용하여 조건부로 다음 미들웨어 처리를 무시하게 만들 수 있습니다.

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

아까의 코드에서 위처럼 수정하게 되면

이런 결과를 얻을 수 있습니다.
단순하게는 주소의 쿼리 파라미터를 사용하여 조건부로 처리할 수 있고 웹 요청의 쿠키 혹은 헤더를 통해 처리할 수도 있습니다.

21.3.2.1. next 함수는 Promise를 반환

next 함수를 호출하면 Promise를 반환합니다. 이 Promise다음에 처리해야 할 미들웨어가 끝나야 완료됩니다.

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

위처럼 코드를 수정하면

로그에서 볼 수 있듯이 1을 출력하는 미들웨어의 다음 미들웨어인 2를 출력하는 미들웨어가 끝난 후에 'END'가 출력됩니다.

21.3.2.2. async/await 사용하기

Koa는 async/await을 정식으로 지원합니다. 기존 코드를 async/await으로 수정해보면

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

이렇게 되겠네요. 결과도 동일합니다!


21.4. nodemon 사용하기

nodemon은 코드를 변경할 때마다 서버를 자동으로 재시작해주는 도구입니다.

$ yarn add --dev nodemon

으로 설치할 수 있습니다. 그다음 package.jsonscripts를 추가합니다.

"scripts": {
	"start": "node src",
	"start:dev": "nodemon --watch src/ src/index.js"
}
  • start : 서버를 시작하는 명령어
  • start:dev : nodemon을 통해 서버를 실행해 주는 명령어

nodemon으로 서버를 실행하게 되면 src 디렉토리를 주시하고 있다가 해당 디렉토리 내부의 어떤 파일이 변경되면, 이를 감지하여 src/index.js 파일을 재시작해 줍니다.


21.5. koa-router 사용하기

Koa를 사용할 때 다른 주소로 요청이 들어올 경우 다른 작업을 처리할 수 있도록 라우터를 사용하는 데, Koa 자체에 기능이 내장되어 있지 않기 때문에 koa-router 모듈을 설치하여 사용합니다.

$ yarn add koa-router

21.5.1. 기본 사용법

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')
})
//about

라우터를 설정할 때, router.get첫 번째 파라미터에는 라우트의 경로를 넣고, 두 번째 파라미터에는 해당 라우트에 적용할 미들웨어 함수를 넣습니다.

여기서 get 키워드는 해당 라우트에서 사용할 HTTP 메서드를 의미합니다. 따라서 get, post, put, delete 등 다양한 키워드를 넣을 수 있습니다.

21.5.2. 라우트 파라미터와 쿼리

라우터 파라미터

라우터 파라미터를 설정할 때는 콜론(:)을 사용하여 라우트 경로를 설정합니다. 만약 파라미터가 있을 수도, 없을 수도 있다면 파라미터 이름 뒤에 물음표(?)를 사용합니다.

/about/:name
/about/:name?

하지만 버전에 따라 ?가 아닌 경로를 명확하게 분리해야 정의해야 하는 경우도 있습니다. 전 그대로 사용했더니 오류가 났습니다.

이렇게 설정한 파라미터는 함수의 ctx.params 객체에서 조회할 수 있습니다.

URL 쿼리

/posts/?id=10

처럼 요청했다면, 해당 값을 ctx.query 에서 조회할 수 있습니다. 쿼리 문자열을 자동으로 객체 형태로 파싱해주므로 별도로 파싱 함수 사용하지 않아도 됩니다. 만약 문자열 형태로 쿼리 문자열을 조회하고 싶다면 ctx.querystring을 사용합니다.

용도

둘 다 주소를 통해 특정 값을 받아 올 때 사용하지만, 용도가 서로 조금씩 다릅니다.

파라미터

  • 처리할 작업의 카테고리를 받아올 때
  • 고유 ID 혹은 이름으로 특정 데이터를 조회할 때

쿼리

  • 옵션에 관련된 정보
  • 예를 들어 여러 항목을 리스팅하는 API라면, 어던 조건을 만족하는 항목을 보여 줄지 또는 어떤 기준으로 정렬할지를 정해야 할 때 사용

21.5.3. REST API

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

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

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

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

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

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

21.5.4. 라우트 모듈화

여러 종류의 라우트를 파일 하나에 작성하면, 코드가 너무 길어질 뿐 아니라 유지 보수하기도 힘들어집니다. 따라서 라우터를 여러 파일에 분리시켜 작성하고, 이를 불러와 적용하는 방법을 사용하면 좋습니다!

// 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()) // api 라우트 적용

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

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

이렇게 라우트를 분리하여 생성하고 불러와 사용할 수 있습니다. /api/test에 접속하면

따라란~

21.5.5. posts 라우트 생성

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

여러 종류의 라우트를 설정한 후, 모두 printInfo 함수를 호출하도록 설정했습니다. 이제 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

/api/posts로 접속해보면

이런 결과를 얻을 수 있습니다. 하지만 POST, DELETE, PUT, PATCH 메서드를 사용하는 API는 자바스크립트로 호출해야 합니다. 따라서 테스트를 위해 Postman을 사용해봅시다.

21.5.5.1. Postman의 설치 및 사용

Postman 공식 사이트
에서 운영 체제에 맞게 다운로드하고

PATCH : http://localhost:4000/api/posts/10
PUT : http://localhost:4000/api/posts/10
DELETE : http://localhost:4000/api/posts/10

를 테스트해보면

PATCHPUTDELETE

매우 잘 동작합니다!

21.5.5.2. 컨트롤러 파일 작성

컨트롤러라우트 처리 함수만 모아 놓은 파일을 말합니다.

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

라우트를 작성하는 과정에서 특정 경로에 미들웨어를 등록할 때, 두 번째 인자로 함수를 선언하여 넣는데, 각 라우트 처리 함수의 코드가 길면 라우터 설정을 한눈에 보기 힘들어집니다.

따라서 라우트 처리함수들을 다른 파일로 따로 분리하여 관리하는 것입니다.

아직 데이터베이스를 연결하지 않았기 때문에 자바스크립트의 배열 기능만으로 임시 기능을 구현해보겠습니다.

그 전에 koa-bodyparser 미들웨어를 적용해야 합니다.

$ yarn add koa-bodyparser

이 미들웨어는 POST/PUT/PATCH 같은 메서드의 Request Body에 JSON 형식으로 데이터를 넣어 주면, 이를 파싱하여 서버에서 사용할 수 있게 합니다.

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

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

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

이를 적용할 때 주의할 점은 router를 적용하는 코드 윗부분에서 적용해야 한다는 것입니다.

이제 컨트롤러 파일을 만들어 봅시다.

// src/api/posts/posts.ctrl.js

exports.이름 = ...

// REST API의 Request Body는 ctx.request.body에서 조회할 수 있습니다.

이런 형식으로 함수를 내보냅니다. 그럼 내보낸 코드는

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

이런 형식으로 불러와 사용합니다.

// 초기 데이터
{
    id: 1,
    title: '제목',
    body: '내용',
}
update(PATCH)replace(PUT)

PATCH 메서드를 사용한 update 함수와 PUT 메서드를 사용한 replace는 용도가 비슷하지만 update기존 값은 유지하면서 새 값을 덮어 씌우는 반면, replaceRequest Body로 받은 값이 id를 제외한 모든 값을 대체합니다. 따라서 포스트 수정 API를 PUT으로 구현해야 할 때는 모든 필드가 있는지 검증하는 작업이 필요합니다.

profile
안녕하세요 :)

0개의 댓글