1 ~ 20장까지 학습하며 리액트의 기본 개념을 대부분 습득하였습니다. 따라서 이제는 실습을 해볼 차례입니다! 하지만 프론트엔드 기술만으로는 필요한 기능을 모두 구현할 수 없는 경우가 흔합니다. 따라서 서버를 만들어 데이터를 여러 사람과 공유하기 위해 백엔드에 대해 알아보겠습니다.
서버 프로그래밍 또는 백엔드(back-end) 프로그래밍이란 어떤 종류의 데이터를 몇 개씩 보여 줄지, 그리고 또 어떻게 보여 줄지 등에 관한 로직을 만드는 것을 말합니다.
백엔드 프로그래밍은 언어에 구애 받지 않기 때문에 PHP, 파이썬, Golang, Java, Javascript, 루비 등과 같은 다양한 언어로 구현할 수 있습니다. 리액트를 다루는 기술에선 그중 자바스크립트로 서버를 구현할 수 있는 Node.js를 사용했습니다.
Node.js 환경에서 웹 서버를 구축할 때, Express, Hapi, Koa 등의 웹 프레임워크를 사용합니다. 리액트를 다루는 기술 20장에선 Express를 사용했으니 이번엔 Koa를 써봅시다.
Express는 미들웨어, 라우팅, 템플릿, 파일 호스팅 등 다양한 기능이 자체적으로 내장되어 있지만 Koa는 미들웨어 기능만 갖추며 필요한 기능들만 붙여서 서버를 만들 수 있기 때문에 Express보다 훨씬 가볍습니다.
또한 Koa는 async/await 문법을 정식으로 지원하기 때문에 비동기 작업을 더 편하게 관리할 수 있습니다.
$ node --version
이 서버는 뒤에서 다룰 블로그 서비스와 연동할 서버입니다.
$ mkdir blog
$ cd blog
$ mkdir blog-backend
$ cd blog-backend
$ yarn init -y
저는 터미널을 사용하지 않고 VS Code에서 New Floder로 만들었습니다.
디렉토리에 package.json 파일이 생성되었음을 확인하였다면 Koa를 설치합니다.
$ yarn add koa
자바스크립트 문법을 검사하고 깔끔한 코드를 작성하기 위해 ESLint와 Prettier를 사용합니다. 두 기능을 사용하기 위해 VS Code에서 Prettier-Code formatter와 ESLint 확장 프로그램을 설치해야 합니다.
$ yarn add --dev eslint
$ yarn run eslint --init
위와 같이 교재대로 eslint를 설치했는데 eslint.config.mjs
파일이 생성되어 당황했습니다. 알고 보니 위와 같이 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"
}
를 추가합니다.
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
서버를 실행하면
따라란~
Koa 애플리케이션은 미들웨어의 배열로 구성되어 있습니다. 미들웨어 함수는 use
함수를 사용하여 애플리케이션에 등록합니다. 미들웨어 함수의 구조는 다음과 같습니다.
(ctx, next) => {
}
Koa의 미들웨어 함수는 ctx
와 next
를 파라미터로 받습니다.
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()
})
아까의 코드에서 위처럼 수정하게 되면
![]() | ![]() |
이런 결과를 얻을 수 있습니다.
단순하게는 주소의 쿼리 파라미터를 사용하여 조건부로 처리할 수 있고 웹 요청의 쿠키 혹은 헤더를 통해 처리할 수도 있습니다.
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'가 출력됩니다.
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')
})
이렇게 되겠네요. 결과도 동일합니다!
nodemon
은 코드를 변경할 때마다 서버를 자동으로 재시작해주는 도구입니다.
$ yarn add --dev nodemon
으로 설치할 수 있습니다. 그다음 package.json
에 scripts
를 추가합니다.
"scripts": {
"start": "node src",
"start:dev": "nodemon --watch src/ src/index.js"
}
start
: 서버를 시작하는 명령어start:dev
: nodemon
을 통해 서버를 실행해 주는 명령어nodemon
으로 서버를 실행하게 되면 src
디렉토리를 주시하고 있다가 해당 디렉토리 내부의 어떤 파일이 변경되면, 이를 감지하여 src/index.js
파일을 재시작해 줍니다.
Koa를 사용할 때 다른 주소로 요청이 들어올 경우 다른 작업을 처리할 수 있도록 라우터를 사용하는 데, Koa 자체에 기능이 내장되어 있지 않기 때문에 koa-router
모듈을 설치하여 사용합니다.
$ yarn add koa-router
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
등 다양한 키워드를 넣을 수 있습니다.
라우터 파라미터를 설정할 때는 콜론(:)을 사용하여 라우트 경로를 설정합니다. 만약 파라미터가 있을 수도, 없을 수도 있다면 파라미터 이름 뒤에 물음표(?)를 사용합니다.
/about/:name
/about/:name?
하지만 버전에 따라 ?가 아닌 경로를 명확하게 분리해야 정의해야 하는 경우도 있습니다. 전 그대로 사용했더니 오류가 났습니다.
이렇게 설정한 파라미터는 함수의 ctx.params
객체에서 조회할 수 있습니다.
/posts/?id=10
처럼 요청했다면, 해당 값을 ctx.query
에서 조회할 수 있습니다. 쿼리 문자열을 자동으로 객체 형태로 파싱해주므로 별도로 파싱 함수 사용하지 않아도 됩니다. 만약 문자열 형태로 쿼리 문자열을 조회하고 싶다면 ctx.querystring
을 사용합니다.
둘 다 주소를 통해 특정 값을 받아 올 때 사용하지만, 용도가 서로 조금씩 다릅니다.
파라미터
- 처리할 작업의 카테고리를 받아올 때
- 고유 ID 혹은 이름으로 특정 데이터를 조회할 때
쿼리
- 옵션에 관련된 정보
- 예를 들어 여러 항목을 리스팅하는 API라면, 어던 조건을 만족하는 항목을 보여 줄지 또는 어떤 기준으로 정렬할지를 정해야 할 때 사용
웹 브라우저에서 데이터베이스로 직접 접속하여 데이터를 변경하면 보안상 문제가 되기 때문에 REST API를 만들어 사용합니다.
따라서 REST API로 클라이언트가 서버에 자신이 데이터를 조회ㆍ생성ㆍ삭제ㆍ업데이트하겠다고 요청하면, 서버는 필요한 로직에 따라 데이터베이스에 접근하여 작업을 처리합니다.
REST API는 요청 종류에 따라 다른 HTTP 메서드를 사용합니다.
메서드 | 설명 |
---|---|
GET | 데이터를 조회할 때 사용합니다. |
POST | 데이터를 등록할 때 사용합니다. 인증 작업을 거칠 때 사용하기도 합니다. |
DELETE | 데이터를 삭제할 때 사용합니다. |
PUT | 데이터를 새 정보로 통째로 교체할 때 사용합니다. |
PATCH | 데이터의 특정 필드를 수정할 때 사용합니다. |
메서드의 종류에 따라 get
, post
, delete
, put
, patch
를 사용하여 라우터에서 각 메서드의 요청을 처리합니다.
REST API를 설계할 때는 API 주소와 메서드에 따라 어떤 역할을 하는지 쉽게 파악할 수 있도록 작성해야 합니다.
여러 종류의 라우트를 파일 하나에 작성하면, 코드가 너무 길어질 뿐 아니라 유지 보수하기도 힘들어집니다. 따라서 라우터를 여러 파일에 분리시켜 작성하고, 이를 불러와 적용하는 방법을 사용하면 좋습니다!
// 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
에 접속하면
따라란~
// 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을 사용해봅시다.
Postman 공식 사이트
에서 운영 체제에 맞게 다운로드하고
PATCH : http://localhost:4000/api/posts/10
PUT : http://localhost:4000/api/posts/10
DELETE : http://localhost:4000/api/posts/10
를 테스트해보면
PATCH | PUT | DELETE |
---|---|---|
![]() | ![]() | ![]() |
매우 잘 동작합니다!
컨트롤러란 라우트 처리 함수만 모아 놓은 파일을 말합니다.
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
는 기존 값은 유지하면서 새 값을 덮어 씌우는 반면, replace
는 Request Body로 받은 값이 id를 제외한 모든 값을 대체합니다. 따라서 포스트 수정 API를 PUT으로 구현해야 할 때는 모든 필드가 있는지 검증하는 작업이 필요합니다.