우리는 지난 시간 node.js에서 제공하는 http 모듈에서 벗어나 express가 웹프레임워크로서 제공하는 기능들을 도입하였습니다. 이 때 새롭게 등장한 개념이 있는데 그 중 가장 중요한 요청과 응답의 내용을 담아내는 객체를 먼저 살펴보겠습니다. 아래는 Express가 간단히 적용된 서버 동작을 확인하는 소스 코드입니다.
const express = require('express')
const cors = require('cors')
const app = express()
app.use(cors())
app.get('/ping', function (req, res, next) {
response.json({message : 'pong'})
})
app.listen(3000, function () {
'listening on port 3000'
})
Express를 도입하며 자주 등장하게 될 req | res 이 두 단어는 ‘요청’과 ‘응답’으로 쉽게 번역될 수 있는 request와 request를 의미하는 변수명입니다.
여기서 req는 HTTP 통신시 요청에 대한 정보를 담는 객체를 의미합니다. 그리고 이 req에 상응하여 응답에 대한 정보를 담는 객체가 바로 res라고 표현될 수 있습니다. 그리고 백엔드 서버 입장에서 클라이언트측에서 전달한 여러 메타 정보를 확인해야할 때는 req 객체에, 백엔드 서버로서 내보내야하는 다양한 메타 정보들을 담아내야 할 때는 res 객체에 접근하게 됩니다.
이제부터 이 둘이 품고 있는 다양한 속성 및 메소드들을 살펴보며 세부 역할과 기능을 파악해보겠습니다.
1-1-2. req - 요청 객체 (Request)
- req.params : 이름 요청시 넘겨져온 path 파라미터를 담습니다.
- req.query : GET 방식으로 넘어오는 query string 파라미터를 담고 있습니다.
- req.body : POST 방식으로 넘어오는 파라미터를 담고있다.
HTTP의 BODY 부분에 담겨져있는데, 이 부분을 파싱하기 위해 body-parser와 같은 패키지가 필요하다. 과거에는 body-parser를 꼭 별도로 설정했어야 했으나 express에 그 기본 기능이 내장되어 이제는 불필요하다. app.use(express.json()) 코드가 실행되면 저절로 parsing 해주는 기능이 있습니다.
- req.route : 현재 라우트에 관한 정보를 담고 있습니다.
- req.headers : HTTP의 Header 정보를 가지고 있습니다.
req.ip : 클라이언트의 IP Address를 호출합니다.
req.path : 클라이언트가 요청한 경로. 프로토콜, 호스트, 포트, 쿼리스트링을 제외한 순수 요청 경로입니다.
req.host : 요청 호스트 이름을 반환하는 간단한 메서드. 조작될 수 있으므로 보안 목적으로는 사용되어서는 안됩니다.
req.protocol : 현재 요청의 프로토콜 (http / https 등)
1-1-3. res - 응답 객체 (Response)
- res.status(code) : HTTP 응답 코드를 설정합니다.
- res.set(name, value) : 응답 헤더를 설정합니다. 일반적으로 직접 사용자가 쓸 일은 드문 편입니다.
- res.send(body), res.send(status, body) : 클라이언트에 응답을 보내며 상태 코드에 대한 추가 여부는 선택사항입니다. 기본 콘텐츠 타입은 text/html이므로 text/plain을 보내려면 res.set(‘Content-Type’, ‘text/plain’)을 먼저 호출 해야합니다. JSON 형태의 전송은 바로 다음 이어지는 res.json을 사용합니다.
- res.json(json), res.json(status, json) : 클라이언트로 JSON 형태의 데이터를 보냅니다.
- res.type(type) : Contents-Type 헤더를 설정할 수 있는 간단한 메서드입니다.
- res.sendFile(path, [options], [callback]) : path의 파일을 읽고 해당 내용을 클라이언트로 전송한다.
1-2. [app.xxx( )] 의 정체
‘app’은 express가 프레임워크로서 기본으로 제공하는 다양한 내부 기능 (Application: 공식문서 내 영문명칭)을 담아내기 위해 사용하는 객체입니다.
그리고 변수명 네이밍 컨벤션에 의거, Application의 가장 세 앞글자를 본 따 ‘app’이라는 명칭으로 부르게 되었습니다. 소스코드 내에서 이 ‘app’이 가장 먼저 등장하는 시점은 바로 최상위단에서 함수 형태의 express를 불러오는 순간으로, 이는 곧 express가 직접 실행되는 시점을 의미합니다.
const express = require('express'); // --- (1)
const app = express(); // --- (2)
‘express()’ 라는 일종의 클래스 기능을 ‘app’이라는 새로운 변수안에 담아 객체 형태로 선언하면, 위와 같이 선언한 객체는 그 내부에 존재하는 다양한 메소드를 활용할 때 빈번히 쓰이게 된다.
왜냐하면 express()라는 함수를 통해서 생성된 app이라는 객체는, 전체 API 서버의 기능을 정의하고 서버를 실행시키는 주 객체로 활용되기 때문입니다. 즉, app 객체와 상호작용 하는 과정에서 우리가 의도하는 API 기능이 실행/구현되는 것이라고 정의할 수 있습니다.
express가 제공하는 Application 기능은 대표적으로 아래와 같습니다.
- HTTP 요청 라우팅 기능
- 미들웨어 상세설정
- HTML 렌더링 기능
- 템플릿 엔진 렌더링 기능
1-2-2. app.use()
Express는 미들웨어 함수의 호출/실행으로 이루어지는 프레임워크라고 표현할 수 있습니다.
여기서 미들웨어 함수란, ‘req’ 객체, ‘res’ 객체, 그리고 미들웨어 함수를 호출시키는 ’next’를 매개변수로 받는 함수를 의미합니다. “함수와 함수 사이에 존재하여 이 둘을 연결 짓는 또 다른 함수” 정도로 이해한다면 이어지는 내용을 이해하는데 큰 문제는 없을 것 입니다.
우리가 이곳에서 다룰 app.use()는 일종의 미들웨어를 추가하는 함수 입니다. 매개변수의 형태로 들어오는 다양한 함수들을 받아 app에 middleware로 추가해줍니다.
app.use() 의 소괄호 내부에는 외부 요청시 경로로 활용되는 path가 먼저 자리하고, 이후 기타 callback 함수들이 매개변수로 놓입니다.
path에 명시되어 있는 경로 내용 중 ‘/' 이후의 경로를 일부분이라도 공유하고 있는 메소드는 무조건 호출되게 됩니다.
예를들어, app.use('/drinks', ...) 는 ‘/drinks’, ‘/drinks/coffee’, ‘/drinks/wine’’, ‘/drinks/coffee/coldbrew’ 등의 경로들과 매칭되어서 해당 경로를 품고 있는 메소드들을 활성화 시키는 효과를 줍니다.
app.use() 의 기본 디폴트 path는 ‘/‘ 입니다. 따라서 만일 app.use() 내부 path 부분에 아무것도 적혀 있지 않았다면, 클라이언트가 그 어떠한 요청을 보내더라도 app.use는 매번 호출되게 됩니다.
이 때 초기 세팅 시 app.use()에 우리가 무심코 추가하였던 morgan, express 등의 동작원리에 대한 힌트를 얻을 수 있습니다. 바로 path를 따로 지정하지 않음으로써 런서버 환경의 모든 요청에 필히 동작할 수 있게 설정하는 목적이 있었던 것 입니다.
1-2-3. app.httpMethod()
app.httpMethod()는 외부에서 들어오는 HTTP 네트워크 요청을 라우팅합니다.
HTTP 네트워크 요청이란, CRUD 기능에 상응하는 세부 HTTP 메서드 (GET, POST, PUT, DELETE) 들을 의미합니다.
이를 express 소스코드 내에서 사용하려면 [app.get(), app.post(), app.put(), app.delete()]등 의 형태로 작성하면 됩니다.
app.use()로 수렴되는 모든 http 메소드를, 각각의 요청에 맞게 의도한 별도의 callback 함수만이 동작하도록 아래와 같이 분기 처리하는 방법을 사용합니다.
예를들어, 클라이언트가 GET에 해당하는 데이터 조회 요청을 ' / ' path로 보낸다면 app.get(' / ' , functionForGet) 요청만을 처리하게 되는 것 입니다.
(예시)
app.post('/', functionForPost); // 클라이언트의 post요청
app.get('/', functionForGet); // 클라이언트의 get요청
app.put('/', functionForPut; // 클라이언트의 put요청
app.delete('/', functionForDelete); // 클라이언트의 delete요청
※ app.xxx 형태의 Application 메소드가 Express에 무수히 많이 내장되어있습니다. 다음의 공식문서 링크를 보고 참고하여 보다 넓고 다양한 형태의 메소드를 탐구해보세요.
여러분은 지금까지 express 프레임워크를 이용하여 구조화된 app을 만들었습니다. application 코드를 더욱 유지보수 하기 쉬운 구조화된 형태로 발전시키기는 했지만, 아직 데이터베이스와 연결되지 않은 상태로 데이터를 영구적으로 저장할 수 없습니다.
이제 TypeORM에서 제공하는 연결(Pooling) 기능을 사용하여, 데이터베이스에 Create, Read, Update, Delete 작업을 수행할 수 있는 API로 발전시켜 작은 프로젝트 앱을 완성시킬 차례입니다.
2-1. 초기 환경 세팅
다음의 두 링크를 참고하여, 본격적인 미니앱 생성에 앞서 초기 환경 세팅을 완료합니다.
참고 링크 (1): Express 초기 세팅 가이드 (https://study.wecode.co.kr/session/content/309)
참고 링크 (2): TypeORM & DB connection (https://study.wecode.co.kr/session/content/310)
2-2. 데이터베이스 테이블 생성
2-3. CRUD API 작성하기
2-3-1. 책 정보 생성 엔드포인트 구현
API 상세설명
Express의 app.post() 기능을 활용, “http://localhost:3000/” 라는 URI를 기준으로 클라이언트가 원하는 자원에 POST 접근을 할 수 있게 설정합니다. 그 후 의도한 자원이 ‘책임'을 명시하는 리소스 target인 /books를, ‘path' 매개변수를 작성해주어야 하는 자리에 문자열 형태로 추가합니다.
req, res 객체를 통해서 post 메소드 전송시 함께 들어온 body 에 담긴 내용을 구조 분해하여 각각의 변수에 담아줍니다. 이 때 mysql 테이블 컬럼의 snake_case와 자바스크립트 상 사용되는 camelCase 사이의 차이점에 주의합니다.
이후 클라이언트가 db 에 입력 및 생성하기를 원하는 내용을 테이블에 반영할 수 있게 SQL Raw Query 문을 작성해줍니다.
app.post('/books', async (req, res) => {
const { title, description, coverImage} = req.body
await myDataSource.query(
`INSERT INTO books(
title,
description,
cover_image
) VALUES (?, ?, ?);
`,
[ title, description, coverImage ]
);
res.status(201).json({ message : "successfully created" });
})
2-3-2. 책 + 작가 정보 조회 엔드포인트 구현
API 상세설명
Express의 app.get() 기능을 이용, “http://localhost:3000/” 라는 URI를 기준으로 클라이언트가 원하는 자원에 GET 접근을 할 수 있게 설정합니다. 그 후 의도한 자원이 ‘책’이라는 것을 명시하는 엔드포인트 /books를, ‘path' 매개변수를 기입하는 자리에 문자열 형태로 추가합니다.
이후 클라이언트가 db 에서 조회를 원하는 내용을 테이블에서 꺼내어 조회할 수 있게 SQL Raw Query 문을 작성해줍니다. 이 때, 중간 테이블을 가지고 있는 책과 작가 테이블의 관계를 유심히 관찰하며 다대다(many to many) 관계에서의 각 테이블간 조합을 어떻게 SQL Raw Query문으로 표현할지 깊게 고민해봅니다.
2-3-3. 책 정보 수정 엔드포인트 구현
API 상세설명
(1) Express의 app.put() 기능을 활용, “http://localhost:3000/” 라는 URI를 기준으로 클라이언트가 원하는 자원에 PUT 접근을 할 수 있게 설정합니다. 그 후 의도한 자원이 ‘책’임을 명시하는 엔드포인트 /books를, ‘path' 매개변수를 기입하는 자리에 문자열 형태로 추가합니다.
(2) req, res 객체를 통해서 post 메소드 전송시 함께 들어온 body 에 담긴 내용을 구조 분해하여 각각의 변수에 담아줍니다. 이 때 mysql 테이블 컬럼의 snake_case와 자바스크립트 상 사용되는 camelCase 사이의 차이점을 주의합니다.
(3) 이후 클라이언트가 db 에 입력 및 수정하기를 원하는 내용을 테이블에 반영할 수 있게 SQL Raw Query 문을 작성해줍니다.
2-3-4. 책 정보 삭제 엔드포인트 구현
API 상세설명
(1) Express의 app.delete() 라우터 기능을 활용하여 “http://localhost:3000/” 라는URI를 기준으로 클라이언트가 원하는 자원에 DELETE 접근을 허용해줍니다.
그 후 내부 설정으로 /books/:bookId로 타겟 엔드포인트를 설정합니다.
이때 새롭게 등장한 :bookId 의 개념은 클라이언트 선에서 뒷 부분에 책의 id값을 의도대로 기입할 수 있게 설정하는 방법입니다. (변수)
(2) req 객체에 담겨 전송된 path parameter 정수 값을 읽어올 수 있게 req.params 내부의 정수값을 bookId 변수에 할당해 줍니다.
(3) 이후 클라이언트가 db 에 입력 및 삭제하기를 원하는 내용을 테이블에 반영할 수 있게 SQL Raw Query 문을 작성해줍니다.